Changeset View
Changeset View
Standalone View
Standalone View
src/applications/calendar/parser/ics/PhutilICSWriter.php
- This file was added.
| <?php | |||||
| final class PhutilICSWriter extends Phobject { | |||||
| public function writeICSDocument(PhutilCalendarRootNode $node) { | |||||
| $out = array(); | |||||
| foreach ($node->getChildren() as $child) { | |||||
| $out[] = $this->writeNode($child); | |||||
| } | |||||
| return implode('', $out); | |||||
| } | |||||
| private function writeNode(PhutilCalendarNode $node) { | |||||
| if (!$this->getICSNodeType($node)) { | |||||
| return null; | |||||
| } | |||||
| $out = array(); | |||||
| $out[] = $this->writeBeginNode($node); | |||||
| $out[] = $this->writeNodeProperties($node); | |||||
| if ($node instanceof PhutilCalendarContainerNode) { | |||||
| foreach ($node->getChildren() as $child) { | |||||
| $out[] = $this->writeNode($child); | |||||
| } | |||||
| } | |||||
| $out[] = $this->writeEndNode($node); | |||||
| return implode('', $out); | |||||
| } | |||||
| private function writeBeginNode(PhutilCalendarNode $node) { | |||||
| $type = $this->getICSNodeType($node); | |||||
| return $this->wrapICSLine("BEGIN:{$type}"); | |||||
| } | |||||
| private function writeEndNode(PhutilCalendarNode $node) { | |||||
| $type = $this->getICSNodeType($node); | |||||
| return $this->wrapICSLine("END:{$type}"); | |||||
| } | |||||
| private function writeNodeProperties(PhutilCalendarNode $node) { | |||||
| $properties = $this->getNodeProperties($node); | |||||
| $out = array(); | |||||
| foreach ($properties as $property) { | |||||
| $propname = $property['name']; | |||||
| $propvalue = $property['value']; | |||||
| $propline = array(); | |||||
| $propline[] = $propname; | |||||
| foreach ($property['parameters'] as $parameter) { | |||||
| $paramname = $parameter['name']; | |||||
| $paramvalue = $parameter['value']; | |||||
| $propline[] = ";{$paramname}={$paramvalue}"; | |||||
| } | |||||
| $propline[] = ":{$propvalue}"; | |||||
| $propline = implode('', $propline); | |||||
| $out[] = $this->wrapICSLine($propline); | |||||
| } | |||||
| return implode('', $out); | |||||
| } | |||||
| private function getICSNodeType(PhutilCalendarNode $node) { | |||||
| switch ($node->getNodeType()) { | |||||
| case PhutilCalendarDocumentNode::NODETYPE: | |||||
| return 'VCALENDAR'; | |||||
| case PhutilCalendarEventNode::NODETYPE: | |||||
| return 'VEVENT'; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| } | |||||
| private function wrapICSLine($line) { | |||||
| $out = array(); | |||||
| $buf = ''; | |||||
| // NOTE: The line may contain sequences of combining characters which are | |||||
| // more than 80 bytes in length. If it does, we'll split them in the | |||||
| // middle of the sequence. This is okay and generally anticipated by | |||||
| // RFC5545, which even allows implementations to split multibyte | |||||
| // characters. The sequence will be stitched back together properly by | |||||
| // whatever is parsing things. | |||||
| foreach (phutil_utf8v($line) as $character) { | |||||
| // If adding this character would bring the line over 75 bytes, start | |||||
| // a new line. | |||||
| if (strlen($buf) + strlen($character) > 75) { | |||||
| $out[] = $buf."\r\n"; | |||||
| $buf = ' '; | |||||
| } | |||||
| $buf .= $character; | |||||
| } | |||||
| $out[] = $buf."\r\n"; | |||||
| return implode('', $out); | |||||
| } | |||||
| private function getNodeProperties(PhutilCalendarNode $node) { | |||||
| switch ($node->getNodeType()) { | |||||
| case PhutilCalendarDocumentNode::NODETYPE: | |||||
| return $this->getDocumentNodeProperties($node); | |||||
| case PhutilCalendarEventNode::NODETYPE: | |||||
| return $this->getEventNodeProperties($node); | |||||
| default: | |||||
| return array(); | |||||
| } | |||||
| } | |||||
| 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(); | |||||
| $uid = $event->getUID(); | |||||
| if (!strlen($uid)) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Unable to write ICS document: event has no UID, but each event '. | |||||
| 'MUST have a UID.')); | |||||
| } | |||||
| $properties[] = $this->newTextProperty( | |||||
| 'UID', | |||||
| $uid); | |||||
| $created = $event->getCreatedDateTime(); | |||||
| if ($created) { | |||||
| $properties[] = $this->newDateTimeProperty( | |||||
| 'CREATED', | |||||
| $event->getCreatedDateTime()); | |||||
| } | |||||
| $dtstamp = $event->getModifiedDateTime(); | |||||
| if (!$dtstamp) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Unable to write ICS document: event has no modified time, but '. | |||||
| 'each event MUST have a modified time.')); | |||||
| } | |||||
| $properties[] = $this->newDateTimeProperty( | |||||
| 'DTSTAMP', | |||||
| $dtstamp); | |||||
| $dtstart = $event->getStartDateTime(); | |||||
| if ($dtstart) { | |||||
| $properties[] = $this->newDateTimeProperty( | |||||
| 'DTSTART', | |||||
| $dtstart); | |||||
| } | |||||
| $dtend = $event->getEndDateTime(); | |||||
| if ($dtend) { | |||||
| $properties[] = $this->newDateTimeProperty( | |||||
| 'DTEND', | |||||
| $event->getEndDateTime()); | |||||
| } | |||||
| $name = $event->getName(); | |||||
| if (strlen($name)) { | |||||
| $properties[] = $this->newTextProperty( | |||||
| 'SUMMARY', | |||||
| $name); | |||||
| } | |||||
| $description = $event->getDescription(); | |||||
| if (strlen($description)) { | |||||
| $properties[] = $this->newTextProperty( | |||||
| 'DESCRIPTION', | |||||
| $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); | |||||
| } | |||||
| } | |||||
| $rrule = $event->getRecurrenceRule(); | |||||
| if ($rrule) { | |||||
| $properties[] = $this->newRRULEProperty( | |||||
| 'RRULE', | |||||
| $rrule); | |||||
| } | |||||
| $recurrence_id = $event->getRecurrenceID(); | |||||
| if ($recurrence_id) { | |||||
| $properties[] = $this->newTextProperty( | |||||
| 'RECURRENCE-ID', | |||||
| $recurrence_id); | |||||
| } | |||||
| $exdates = $event->getRecurrenceExceptions(); | |||||
| if ($exdates) { | |||||
| $properties[] = $this->newDateTimesProperty( | |||||
| 'EXDATE', | |||||
| $exdates); | |||||
| } | |||||
| $rdates = $event->getRecurrenceDates(); | |||||
| if ($rdates) { | |||||
| $properties[] = $this->newDateTimesProperty( | |||||
| 'RDATE', | |||||
| $rdates); | |||||
| } | |||||
| return $properties; | |||||
| } | |||||
| private function newTextProperty( | |||||
| $name, | |||||
| $value, | |||||
| array $parameters = array()) { | |||||
| $map = array( | |||||
| '\\' => '\\\\', | |||||
| ',' => '\\,', | |||||
| "\n" => '\\n', | |||||
| ); | |||||
| $value = (array)$value; | |||||
| foreach ($value as $k => $v) { | |||||
| $v = str_replace(array_keys($map), array_values($map), $v); | |||||
| $value[$k] = $v; | |||||
| } | |||||
| $value = implode(',', $value); | |||||
| return $this->newProperty($name, $value, $parameters); | |||||
| } | |||||
| private function newDateTimeProperty( | |||||
| $name, | |||||
| PhutilCalendarDateTime $value, | |||||
| array $parameters = array()) { | |||||
| return $this->newDateTimesProperty($name, array($value), $parameters); | |||||
| } | |||||
| private function newDateTimesProperty( | |||||
| $name, | |||||
| array $values, | |||||
| array $parameters = array()) { | |||||
| assert_instances_of($values, 'PhutilCalendarDateTime'); | |||||
| if (head($values)->getIsAllDay()) { | |||||
| $parameters[] = array( | |||||
| 'name' => 'VALUE', | |||||
| 'values' => array( | |||||
| 'DATE', | |||||
| ), | |||||
| ); | |||||
| } | |||||
| $datetimes = array(); | |||||
| foreach ($values as $value) { | |||||
| $datetimes[] = $value->getISO8601(); | |||||
| } | |||||
| $datetimes = implode(';', $datetimes); | |||||
| return $this->newProperty($name, $datetimes, $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 newRRULEProperty( | |||||
| $name, | |||||
| PhutilCalendarRecurrenceRule $rule, | |||||
| array $parameters = array()) { | |||||
| $value = $rule->toRRULE(); | |||||
| return $this->newProperty($name, $value, $parameters); | |||||
| } | |||||
| private function newProperty( | |||||
| $name, | |||||
| $value, | |||||
| array $parameters = array()) { | |||||
| $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' => $writable_params, | |||||
| ); | |||||
| } | |||||
| } | |||||