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 @@ -143,6 +143,7 @@ 'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php', 'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php', 'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php', + 'PhutilCalendarUserNode' => 'parser/calendar/data/PhutilCalendarUserNode.php', 'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php', 'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php', 'PhutilChannel' => 'channel/PhutilChannel.php', @@ -726,6 +727,7 @@ 'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime', 'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode', + 'PhutilCalendarUserNode' => 'PhutilCalendarNode', 'PhutilCallbackFilterIterator' => 'FilterIterator', 'PhutilCallbackSignalHandler' => 'PhutilSignalHandler', 'PhutilChannel' => 'Phobject', 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 @@ -13,6 +13,8 @@ private $duration; private $createdDateTime; private $modifiedDateTime; + private $organizer; + private $attendees = array(); public function setUID($uid) { $this->uid = $uid; @@ -99,4 +101,28 @@ return $this->modifiedDateTime; } + public function setOrganizer(PhutilCalendarUserNode $organizer) { + $this->organizer = $organizer; + return $this; + } + + public function getOrganizer() { + return $this->organizer; + } + + public function setAttendees(array $attendees) { + assert_instances_of($attendees, 'PhutilCalendarUserNode'); + $this->attendees = $attendees; + return $this; + } + + public function getAttendees() { + return $this->attendees; + } + + public function addAttendee(PhutilCalendarUserNode $attendee) { + $this->attendees[] = $attendee; + return $this; + } + } diff --git a/src/parser/calendar/data/PhutilCalendarUserNode.php b/src/parser/calendar/data/PhutilCalendarUserNode.php new file mode 100644 --- /dev/null +++ b/src/parser/calendar/data/PhutilCalendarUserNode.php @@ -0,0 +1,40 @@ +name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setURI($uri) { + $this->uri = $uri; + return $this; + } + + public function getURI() { + return $this->uri; + } + + public function setStatus($status) { + $this->status = $status; + return $this; + } + + public function getStatus() { + return $this->status; + } + +} 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 @@ -193,6 +193,22 @@ $description); } + $organizer = $event->getOrganizer(); + if ($organizer) { + $properties[] = $this->newUserProperty( + 'ORGANIZER', + $organizer); + } + + $attendees = $event->getAttendees(); + if ($attendees) { + foreach ($attendees as $attendee) { + $properties[] = $this->newUserProperty( + 'ATTENDEE', + $attendee); + } + } + return $properties; } @@ -236,6 +252,46 @@ return $this->newProperty($name, $datetime, $parameters); } + private function newUserProperty( + $name, + PhutilCalendarUserNode $value, + array $parameters = array()) { + + $parameters[] = array( + 'name' => 'CN', + 'values' => array( + $value->getName(), + ), + ); + + $partstat = null; + switch ($value->getStatus()) { + case PhutilCalendarUserNode::STATUS_INVITED: + $partstat = 'NEEDS-ACTION'; + break; + case PhutilCalendarUserNode::STATUS_ACCEPTED: + $partstat = 'ACCEPTED'; + break; + case PhutilCalendarUserNode::STATUS_DECLINED: + $partstat = 'DECLINED'; + break; + } + + if ($partstat !== null) { + $parameters[] = array( + 'name' => 'PARTSTAT', + 'values' => array( + $partstat, + ), + ); + } + + // TODO: We could reasonably fill in "ROLE" and "RSVP" here too, but it + // isn't clear if these are important to external programs or not. + + return $this->newProperty($name, $value->getURI(), $parameters); + } + private function newProperty( $name, $value, @@ -257,7 +313,7 @@ // 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)) { + if (!preg_match('/^[A-Za-z0-9-]*\z/', $v)) { $v = '"'.$v.'"'; } 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 @@ -56,6 +56,37 @@ $this->assertICS('writer-christmas.ics', $ics_data); } + public function testICSWriterUsers() { + $event = id(new PhutilCalendarEventNode()) + ->setUID('office-party') + ->setName('Office Party') + ->setCreatedDateTime( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20161001T120000Z')) + ->setModifiedDateTime( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20161001T120000Z')) + ->setStartDateTime( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20161215T200000Z')) + ->setEndDateTime( + PhutilCalendarAbsoluteDateTime::newFromISO8601('20161215T230000Z')) + ->setOrganizer( + id(new PhutilCalendarUserNode()) + ->setName('Big Boss') + ->setURI('mailto:big.boss@example.com')) + ->addAttendee( + id(new PhutilCalendarUserNode()) + ->setName('Milton') + ->setStatus(PhutilCalendarUserNode::STATUS_INVITED) + ->setURI('mailto:milton@example.com')) + ->addAttendee( + id(new PhutilCalendarUserNode()) + ->setName('Nancy') + ->setStatus(PhutilCalendarUserNode::STATUS_ACCEPTED) + ->setURI('mailto:nancy@example.com')); + + $ics_data = $this->writeICSSingleEvent($event); + $this->assertICS('writer-office-party.ics', $ics_data); + } + private function writeICSSingleEvent(PhutilCalendarEventNode $event) { $calendar = id(new PhutilCalendarDocumentNode()) ->appendChild($event); diff --git a/src/parser/calendar/ics/__tests__/data/writer-office-party.ics b/src/parser/calendar/ics/__tests__/data/writer-office-party.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/writer-office-party.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Phacility//Phabricator//EN +BEGIN:VEVENT +UID:office-party +CREATED:20161001T120000Z +DTSTAMP:20161001T120000Z +DTSTART:20161215T200000Z +DTEND:20161215T230000Z +SUMMARY:Office Party +ORGANIZER;CN="Big Boss":mailto:big.boss@example.com +ATTENDEE;CN=Milton;PARTSTAT=NEEDS-ACTION:mailto:milton@example.com +ATTENDEE;CN=Nancy;PARTSTAT=ACCEPTED:mailto:nancy@example.com +END:VEVENT +END:VCALENDAR