diff --git a/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php b/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php --- a/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php +++ b/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php @@ -51,11 +51,30 @@ ->setHour((int)$matches['h']) ->setMinute((int)$matches['i']) ->setSecond((int)$matches['s']); + } else { + $datetime + ->setIsAllDay(true); } return $datetime; } + public static function newFromEpoch($epoch, $timezone = 'UTC') { + $date = new DateTime('@'.$epoch); + + $zone = new DateTimeZone($timezone); + $date->setTimezone($zone); + + return id(new self()) + ->setYear((int)$date->format('Y')) + ->setMonth((int)$date->format('m')) + ->setDay((int)$date->format('d')) + ->setHour((int)$date->format('H')) + ->setMinute((int)$date->format('i')) + ->setSecond((int)$date->format('s')) + ->setTimezone($timezone); + } + public function setYear($year) { $this->year = $year; return $this; diff --git a/src/parser/calendar/data/PhutilCalendarDateTime.php b/src/parser/calendar/data/PhutilCalendarDateTime.php --- a/src/parser/calendar/data/PhutilCalendarDateTime.php +++ b/src/parser/calendar/data/PhutilCalendarDateTime.php @@ -4,6 +4,7 @@ extends Phobject { private $viewerTimezone; + private $isAllDay = false; public function setViewerTimezone($viewer_timezone) { $this->viewerTimezone = $viewer_timezone; @@ -14,6 +15,15 @@ return $this->viewerTimezone; } + public function setIsAllDay($is_all_day) { + $this->isAllDay = $is_all_day; + return $this; + } + + public function getIsAllDay() { + return $this->isAllDay; + } + public function getEpoch() { $datetime = $this->newPHPDateTime(); return (int)$datetime->format('U'); @@ -22,7 +32,12 @@ public function getISO8601() { $datetime = $this->newPHPDateTime(); $datetime->setTimezone(new DateTimeZone('UTC')); - return $datetime->format('Ymd\\THis\\Z'); + + if ($this->getIsAllDay()) { + return $datetime->format('Ymd'); + } else { + return $datetime->format('Ymd\\THis\\Z'); + } } abstract protected function newPHPDateTimeZone(); diff --git a/src/parser/calendar/data/PhutilCalendarProxyDateTime.php b/src/parser/calendar/data/PhutilCalendarProxyDateTime.php --- a/src/parser/calendar/data/PhutilCalendarProxyDateTime.php +++ b/src/parser/calendar/data/PhutilCalendarProxyDateTime.php @@ -23,6 +23,15 @@ return $this->getProxy()->getViewerTimezone(); } + public function setIsAllDay($is_all_day) { + $this->getProxy()->setIsAllDay($is_all_day); + return $this; + } + + public function getIsAllDay() { + return $this->getProxy()->getIsAllDay(); + } + protected function newPHPDateTimezone() { return $this->getProxy()->newPHPDateTimezone(); } diff --git a/src/parser/calendar/ics/PhutilICSWriter.php b/src/parser/calendar/ics/PhutilICSWriter.php --- a/src/parser/calendar/ics/PhutilICSWriter.php +++ b/src/parser/calendar/ics/PhutilICSWriter.php @@ -56,7 +56,7 @@ foreach ($property['parameters'] as $parameter) { $paramname = $parameter['name']; - $paramvalue = 'TODO'; + $paramvalue = $parameter['value']; $propline[] = ";{$paramname}={$paramvalue}"; } @@ -95,14 +95,14 @@ // If adding this character would bring the line over 75 bytes, start // a new line. if (strlen($buf) + strlen($character) > 75) { - $out[] = $buf."\n"; + $out[] = $buf."\r\n"; $buf = ' '; } $buf .= $character; } - $out[] = $buf."\n"; + $out[] = $buf."\r\n"; return implode('', $out); } @@ -110,7 +110,7 @@ private function getNodeProperties(PhutilCalendarNode $node) { switch ($node->getNodeType()) { case PhutilCalendarDocumentNode::NODETYPE: - return array(); + return $this->getDocumentNodeProperties($node); case PhutilCalendarEventNode::NODETYPE: return $this->getEventNodeProperties($node); default: @@ -118,6 +118,21 @@ } } + private function getDocumentNodeProperties( + PhutilCalendarDocumentNode $event) { + $properties = array(); + + $properties[] = $this->newTextProperty( + 'VERSION', + '2.0'); + + $properties[] = $this->newTextProperty( + 'PRODID', + '-//Phacility//Phabricator//EN'); + + return $properties; + } + private function getEventNodeProperties(PhutilCalendarEventNode $event) { $properties = array(); @@ -208,6 +223,16 @@ PhutilCalendarDateTime $value, array $parameters = array()) { $datetime = $value->getISO8601(); + + if ($value->getIsAllDay()) { + $parameters[] = array( + 'name' => 'VALUE', + 'values' => array( + 'DATE', + ), + ); + } + return $this->newProperty($name, $datetime, $parameters); } @@ -216,12 +241,39 @@ $value, array $parameters = array()) { - // TODO: Actually handle parameters. + $map = array( + '^' => '^^', + "\n" => '^n', + '"' => "^'", + ); + + $writable_params = array(); + foreach ($parameters as $k => $parameter) { + $value_list = array(); + foreach ($parameter['values'] as $v) { + $v = str_replace(array_keys($map), array_values($map), $v); + + // If the parameter value isn't a very simple one, quote it. + + // RFC5545 says that we MUST quote it if it has a colon, a semicolon, + // or a comma, and that we MUST quote it if it's a URI. + if (!preg_match('/^[A-Za-z0-9]*\z/', $v)) { + $v = '"'.$v.'"'; + } + + $value_list[] = $v; + } + + $writable_params[] = array( + 'name' => $parameter['name'], + 'value' => implode(',', $value_list), + ); + } return array( 'name' => $name, 'value' => $value, - 'parameters' => array(), + 'parameters' => $writable_params, ); } diff --git a/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php b/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php --- a/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php +++ b/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php @@ -2,7 +2,7 @@ final class PhutilICSWriterTestCase extends PhutilTestCase { - public function testICSWriter() { + public function testICSWriterTeaTime() { $teas = array( 'earl grey tea', 'English breakfast tea', @@ -37,6 +37,25 @@ $this->assertICS('writer-tea-time.ics', $ics_data); } + public function testICSWriterAllDay() { + $event = id(new PhutilCalendarEventNode()) + ->setUID('christmas-day') + ->setName('Christmas 2016') + ->setDescription('A minor religious holiday.') + ->setCreatedDateTime( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20160901T232425Z')) + ->setModifiedDateTime( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20160901T232425Z')) + ->setStartDateTime( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20161225')) + ->setEndDateTime( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20161226')); + + $ics_data = $this->writeICSSingleEvent($event); + + $this->assertICS('writer-christmas.ics', $ics_data); + } + private function writeICSSingleEvent(PhutilCalendarEventNode $event) { $calendar = id(new PhutilCalendarDocumentNode()) ->appendChild($event); diff --git a/src/parser/calendar/ics/__tests__/data/writer-christmas.ics b/src/parser/calendar/ics/__tests__/data/writer-christmas.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/writer-christmas.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Phacility//Phabricator//EN +BEGIN:VEVENT +UID:christmas-day +CREATED:20160901T232425Z +DTSTAMP:20160901T232425Z +DTSTART;VALUE=DATE:20161225 +DTEND;VALUE=DATE:20161226 +SUMMARY:Christmas 2016 +DESCRIPTION:A minor religious holiday. +END:VEVENT +END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/writer-tea-time.ics b/src/parser/calendar/ics/__tests__/data/writer-tea-time.ics --- a/src/parser/calendar/ics/__tests__/data/writer-tea-time.ics +++ b/src/parser/calendar/ics/__tests__/data/writer-tea-time.ics @@ -1,14 +1,16 @@ -BEGIN:VCALENDAR -BEGIN:VEVENT -UID:tea-time -CREATED:20160915T070000Z -DTSTAMP:20160915T070000Z -DTSTART:20160916T150000Z -DTEND:20160916T160000Z -SUMMARY:Tea Time -DESCRIPTION:Tea and\, perhaps\, crumpets.\nYour presence is requested!\nThi - s is a long list of types of tea to test line wrapping: earl grey tea\, En - glish breakfast tea\, black tea\, green tea\, t-rex\, oolong tea\, mint te - a\, tea with milk. -END:VEVENT -END:VCALENDAR +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Phacility//Phabricator//EN +BEGIN:VEVENT +UID:tea-time +CREATED:20160915T070000Z +DTSTAMP:20160915T070000Z +DTSTART:20160916T150000Z +DTEND:20160916T160000Z +SUMMARY:Tea Time +DESCRIPTION:Tea and\, perhaps\, crumpets.\nYour presence is requested!\nThi + s is a long list of types of tea to test line wrapping: earl grey tea\, En + glish breakfast tea\, black tea\, green tea\, t-rex\, oolong tea\, mint te + a\, tea with milk. +END:VEVENT +END:VCALENDAR