Path: blob/master/src/applications/calendar/parser/ics/PhutilICSWriter.php
12262 views
<?php12final class PhutilICSWriter extends Phobject {34public function writeICSDocument(PhutilCalendarRootNode $node) {5$out = array();67foreach ($node->getChildren() as $child) {8$out[] = $this->writeNode($child);9}1011return implode('', $out);12}1314private function writeNode(PhutilCalendarNode $node) {15if (!$this->getICSNodeType($node)) {16return null;17}1819$out = array();2021$out[] = $this->writeBeginNode($node);22$out[] = $this->writeNodeProperties($node);2324if ($node instanceof PhutilCalendarContainerNode) {25foreach ($node->getChildren() as $child) {26$out[] = $this->writeNode($child);27}28}2930$out[] = $this->writeEndNode($node);3132return implode('', $out);33}3435private function writeBeginNode(PhutilCalendarNode $node) {36$type = $this->getICSNodeType($node);37return $this->wrapICSLine("BEGIN:{$type}");38}3940private function writeEndNode(PhutilCalendarNode $node) {41$type = $this->getICSNodeType($node);42return $this->wrapICSLine("END:{$type}");43}4445private function writeNodeProperties(PhutilCalendarNode $node) {46$properties = $this->getNodeProperties($node);4748$out = array();49foreach ($properties as $property) {50$propname = $property['name'];51$propvalue = $property['value'];5253$propline = array();54$propline[] = $propname;5556foreach ($property['parameters'] as $parameter) {57$paramname = $parameter['name'];58$paramvalue = $parameter['value'];59$propline[] = ";{$paramname}={$paramvalue}";60}6162$propline[] = ":{$propvalue}";63$propline = implode('', $propline);6465$out[] = $this->wrapICSLine($propline);66}6768return implode('', $out);69}7071private function getICSNodeType(PhutilCalendarNode $node) {72switch ($node->getNodeType()) {73case PhutilCalendarDocumentNode::NODETYPE:74return 'VCALENDAR';75case PhutilCalendarEventNode::NODETYPE:76return 'VEVENT';77default:78return null;79}80}8182private function wrapICSLine($line) {83$out = array();84$buf = '';8586// NOTE: The line may contain sequences of combining characters which are87// more than 80 bytes in length. If it does, we'll split them in the88// middle of the sequence. This is okay and generally anticipated by89// RFC5545, which even allows implementations to split multibyte90// characters. The sequence will be stitched back together properly by91// whatever is parsing things.9293foreach (phutil_utf8v($line) as $character) {94// If adding this character would bring the line over 75 bytes, start95// a new line.96if (strlen($buf) + strlen($character) > 75) {97$out[] = $buf."\r\n";98$buf = ' ';99}100101$buf .= $character;102}103104$out[] = $buf."\r\n";105106return implode('', $out);107}108109private function getNodeProperties(PhutilCalendarNode $node) {110switch ($node->getNodeType()) {111case PhutilCalendarDocumentNode::NODETYPE:112return $this->getDocumentNodeProperties($node);113case PhutilCalendarEventNode::NODETYPE:114return $this->getEventNodeProperties($node);115default:116return array();117}118}119120private function getDocumentNodeProperties(121PhutilCalendarDocumentNode $event) {122$properties = array();123124$properties[] = $this->newTextProperty(125'VERSION',126'2.0');127128$properties[] = $this->newTextProperty(129'PRODID',130self::getICSPRODID());131132return $properties;133}134135public static function getICSPRODID() {136return '-//Phacility//Phabricator//EN';137}138139private function getEventNodeProperties(PhutilCalendarEventNode $event) {140$properties = array();141142$uid = $event->getUID();143if (!strlen($uid)) {144throw new Exception(145pht(146'Unable to write ICS document: event has no UID, but each event '.147'MUST have a UID.'));148}149$properties[] = $this->newTextProperty(150'UID',151$uid);152153$created = $event->getCreatedDateTime();154if ($created) {155$properties[] = $this->newDateTimeProperty(156'CREATED',157$event->getCreatedDateTime());158}159160$dtstamp = $event->getModifiedDateTime();161if (!$dtstamp) {162throw new Exception(163pht(164'Unable to write ICS document: event has no modified time, but '.165'each event MUST have a modified time.'));166}167$properties[] = $this->newDateTimeProperty(168'DTSTAMP',169$dtstamp);170171$dtstart = $event->getStartDateTime();172if ($dtstart) {173$properties[] = $this->newDateTimeProperty(174'DTSTART',175$dtstart);176}177178$dtend = $event->getEndDateTime();179if ($dtend) {180$properties[] = $this->newDateTimeProperty(181'DTEND',182$event->getEndDateTime());183}184185$name = $event->getName();186if (phutil_nonempty_string($name)) {187$properties[] = $this->newTextProperty(188'SUMMARY',189$name);190}191192$description = $event->getDescription();193if (phutil_nonempty_string($description)) {194$properties[] = $this->newTextProperty(195'DESCRIPTION',196$description);197}198199$organizer = $event->getOrganizer();200if ($organizer) {201$properties[] = $this->newUserProperty(202'ORGANIZER',203$organizer);204}205206$attendees = $event->getAttendees();207if ($attendees) {208foreach ($attendees as $attendee) {209$properties[] = $this->newUserProperty(210'ATTENDEE',211$attendee);212}213}214215$rrule = $event->getRecurrenceRule();216if ($rrule) {217$properties[] = $this->newRRULEProperty(218'RRULE',219$rrule);220}221222$recurrence_id = $event->getRecurrenceID();223if ($recurrence_id) {224$properties[] = $this->newTextProperty(225'RECURRENCE-ID',226$recurrence_id);227}228229$exdates = $event->getRecurrenceExceptions();230if ($exdates) {231$properties[] = $this->newDateTimesProperty(232'EXDATE',233$exdates);234}235236$rdates = $event->getRecurrenceDates();237if ($rdates) {238$properties[] = $this->newDateTimesProperty(239'RDATE',240$rdates);241}242243return $properties;244}245246private function newTextProperty(247$name,248$value,249array $parameters = array()) {250251$map = array(252'\\' => '\\\\',253',' => '\\,',254"\n" => '\\n',255);256257$value = (array)$value;258foreach ($value as $k => $v) {259$v = str_replace(array_keys($map), array_values($map), $v);260$value[$k] = $v;261}262263$value = implode(',', $value);264265return $this->newProperty($name, $value, $parameters);266}267268private function newDateTimeProperty(269$name,270PhutilCalendarDateTime $value,271array $parameters = array()) {272273return $this->newDateTimesProperty($name, array($value), $parameters);274}275276private function newDateTimesProperty(277$name,278array $values,279array $parameters = array()) {280assert_instances_of($values, 'PhutilCalendarDateTime');281282if (head($values)->getIsAllDay()) {283$parameters[] = array(284'name' => 'VALUE',285'values' => array(286'DATE',287),288);289}290291$datetimes = array();292foreach ($values as $value) {293$datetimes[] = $value->getISO8601();294}295$datetimes = implode(';', $datetimes);296297return $this->newProperty($name, $datetimes, $parameters);298}299300private function newUserProperty(301$name,302PhutilCalendarUserNode $value,303array $parameters = array()) {304305$parameters[] = array(306'name' => 'CN',307'values' => array(308$value->getName(),309),310);311312$partstat = null;313switch ($value->getStatus()) {314case PhutilCalendarUserNode::STATUS_INVITED:315$partstat = 'NEEDS-ACTION';316break;317case PhutilCalendarUserNode::STATUS_ACCEPTED:318$partstat = 'ACCEPTED';319break;320case PhutilCalendarUserNode::STATUS_DECLINED:321$partstat = 'DECLINED';322break;323}324325if ($partstat !== null) {326$parameters[] = array(327'name' => 'PARTSTAT',328'values' => array(329$partstat,330),331);332}333334// TODO: We could reasonably fill in "ROLE" and "RSVP" here too, but it335// isn't clear if these are important to external programs or not.336337return $this->newProperty($name, $value->getURI(), $parameters);338}339340private function newRRULEProperty(341$name,342PhutilCalendarRecurrenceRule $rule,343array $parameters = array()) {344345$value = $rule->toRRULE();346return $this->newProperty($name, $value, $parameters);347}348349private function newProperty(350$name,351$value,352array $parameters = array()) {353354$map = array(355'^' => '^^',356"\n" => '^n',357'"' => "^'",358);359360$writable_params = array();361foreach ($parameters as $k => $parameter) {362$value_list = array();363foreach ($parameter['values'] as $v) {364$v = str_replace(array_keys($map), array_values($map), $v);365366// If the parameter value isn't a very simple one, quote it.367368// RFC5545 says that we MUST quote it if it has a colon, a semicolon,369// or a comma, and that we MUST quote it if it's a URI.370if (!preg_match('/^[A-Za-z0-9-]*\z/', $v)) {371$v = '"'.$v.'"';372}373374$value_list[] = $v;375}376377$writable_params[] = array(378'name' => $parameter['name'],379'value' => implode(',', $value_list),380);381}382383return array(384'name' => $name,385'value' => $value,386'parameters' => $writable_params,387);388}389390}391392393