diff options
author | redmatrix <git@macgirvin.com> | 2016-05-10 17:26:44 -0700 |
---|---|---|
committer | redmatrix <git@macgirvin.com> | 2016-05-10 17:26:44 -0700 |
commit | 0b02a6d123b2014705998c94ddf3d460948d3eac (patch) | |
tree | 78ff2cab9944a4f5ab3f80ec93cbe1120de90bb2 /vendor/sabre/vobject/lib/ITip/Broker.php | |
parent | 40b5b6e9d2da7ab65c8b4d38cdceac83a4d78deb (diff) | |
download | volse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.tar.gz volse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.tar.bz2 volse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.zip |
initial sabre upgrade (needs lots of work - to wit: authentication, redo the browser interface, and rework event export/import)
Diffstat (limited to 'vendor/sabre/vobject/lib/ITip/Broker.php')
-rw-r--r-- | vendor/sabre/vobject/lib/ITip/Broker.php | 989 |
1 files changed, 989 insertions, 0 deletions
diff --git a/vendor/sabre/vobject/lib/ITip/Broker.php b/vendor/sabre/vobject/lib/ITip/Broker.php new file mode 100644 index 000000000..effa74317 --- /dev/null +++ b/vendor/sabre/vobject/lib/ITip/Broker.php @@ -0,0 +1,989 @@ +<?php + +namespace Sabre\VObject\ITip; + +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Reader; +use Sabre\VObject\Recur\EventIterator; + +/** + * The ITip\Broker class is a utility class that helps with processing + * so-called iTip messages. + * + * iTip is defined in rfc5546, stands for iCalendar Transport-Independent + * Interoperability Protocol, and describes the underlying mechanism for + * using iCalendar for scheduling for for example through email (also known as + * IMip) and CalDAV Scheduling. + * + * This class helps by: + * + * 1. Creating individual invites based on an iCalendar event for each + * attendee. + * 2. Generating invite updates based on an iCalendar update. This may result + * in new invites, updates and cancellations for attendees, if that list + * changed. + * 3. On the receiving end, it can create a local iCalendar event based on + * a received invite. + * 4. It can also process an invite update on a local event, ensuring that any + * overridden properties from attendees are retained. + * 5. It can create a accepted or declined iTip reply based on an invite. + * 6. It can process a reply from an invite and update an events attendee + * status based on a reply. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Broker { + + /** + * This setting determines whether the rules for the SCHEDULE-AGENT + * parameter should be followed. + * + * This is a parameter defined on ATTENDEE properties, introduced by RFC + * 6638. This parameter allows a caldav client to tell the server 'Don't do + * any scheduling operations'. + * + * If this setting is turned on, any attendees with SCHEDULE-AGENT set to + * CLIENT will be ignored. This is the desired behavior for a CalDAV + * server, but if you're writing an iTip application that doesn't deal with + * CalDAV, you may want to ignore this parameter. + * + * @var bool + */ + public $scheduleAgentServerRules = true; + + /** + * The broker will try during 'parseEvent' figure out whether the change + * was significant. + * + * It uses a few different ways to do this. One of these ways is seeing if + * certain properties changed values. This list of specified here. + * + * This list is taken from: + * * http://tools.ietf.org/html/rfc5546#section-2.1.4 + * + * @var string[] + */ + public $significantChangeProperties = [ + 'DTSTART', + 'DTEND', + 'DURATION', + 'DUE', + 'RRULE', + 'RDATE', + 'EXDATE', + 'STATUS', + ]; + + /** + * This method is used to process an incoming itip message. + * + * Examples: + * + * 1. A user is an attendee to an event. The organizer sends an updated + * meeting using a new iTip message with METHOD:REQUEST. This function + * will process the message and update the attendee's event accordingly. + * + * 2. The organizer cancelled the event using METHOD:CANCEL. We will update + * the users event to state STATUS:CANCELLED. + * + * 3. An attendee sent a reply to an invite using METHOD:REPLY. We can + * update the organizers event to update the ATTENDEE with its correct + * PARTSTAT. + * + * The $existingObject is updated in-place. If there is no existing object + * (because it's a new invite for example) a new object will be created. + * + * If an existing object does not exist, and the method was CANCEL or + * REPLY, the message effectively gets ignored, and no 'existingObject' + * will be created. + * + * The updated $existingObject is also returned from this function. + * + * If the iTip message was not supported, we will always return false. + * + * @param Message $itipMessage + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + function processMessage(Message $itipMessage, VCalendar $existingObject = null) { + + // We only support events at the moment. + if ($itipMessage->component !== 'VEVENT') { + return false; + } + + switch ($itipMessage->method) { + + case 'REQUEST' : + return $this->processMessageRequest($itipMessage, $existingObject); + + case 'CANCEL' : + return $this->processMessageCancel($itipMessage, $existingObject); + + case 'REPLY' : + return $this->processMessageReply($itipMessage, $existingObject); + + default : + // Unsupported iTip message + return; + + } + + return $existingObject; + + } + + /** + * This function parses a VCALENDAR object and figure out if any messages + * need to be sent. + * + * A VCALENDAR object will be created from the perspective of either an + * attendee, or an organizer. You must pass a string identifying the + * current user, so we can figure out who in the list of attendees or the + * organizer we are sending this message on behalf of. + * + * It's possible to specify the current user as an array, in case the user + * has more than one identifying href (such as multiple emails). + * + * It $oldCalendar is specified, it is assumed that the operation is + * updating an existing event, which means that we need to look at the + * differences between events, and potentially send old attendees + * cancellations, and current attendees updates. + * + * If $calendar is null, but $oldCalendar is specified, we treat the + * operation as if the user has deleted an event. If the user was an + * organizer, this means that we need to send cancellation notices to + * people. If the user was an attendee, we need to make sure that the + * organizer gets the 'declined' message. + * + * @param VCalendar|string $calendar + * @param string|array $userHref + * @param VCalendar|string $oldCalendar + * + * @return array + */ + function parseEvent($calendar = null, $userHref, $oldCalendar = null) { + + if ($oldCalendar) { + if (is_string($oldCalendar)) { + $oldCalendar = Reader::read($oldCalendar); + } + if (!isset($oldCalendar->VEVENT)) { + // We only support events at the moment + return []; + } + + $oldEventInfo = $this->parseEventInfo($oldCalendar); + } else { + $oldEventInfo = [ + 'organizer' => null, + 'significantChangeHash' => '', + 'attendees' => [], + ]; + } + + $userHref = (array)$userHref; + + if (!is_null($calendar)) { + + if (is_string($calendar)) { + $calendar = Reader::read($calendar); + } + if (!isset($calendar->VEVENT)) { + // We only support events at the moment + return []; + } + $eventInfo = $this->parseEventInfo($calendar); + if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) { + // If there were no attendees on either side of the equation, + // we don't need to do anything. + return []; + } + if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) { + // There was no organizer before or after the change. + return []; + } + + $baseCalendar = $calendar; + + // If the new object didn't have an organizer, the organizer + // changed the object from a scheduling object to a non-scheduling + // object. We just copy the info from the old object. + if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) { + $eventInfo['organizer'] = $oldEventInfo['organizer']; + $eventInfo['organizerName'] = $oldEventInfo['organizerName']; + } + + } else { + // The calendar object got deleted, we need to process this as a + // cancellation / decline. + if (!$oldCalendar) { + // No old and no new calendar, there's no thing to do. + return []; + } + + $eventInfo = $oldEventInfo; + + if (in_array($eventInfo['organizer'], $userHref)) { + // This is an organizer deleting the event. + $eventInfo['attendees'] = []; + // Increasing the sequence, but only if the organizer deleted + // the event. + $eventInfo['sequence']++; + } else { + // This is an attendee deleting the event. + foreach ($eventInfo['attendees'] as $key => $attendee) { + if (in_array($attendee['href'], $userHref)) { + $eventInfo['attendees'][$key]['instances'] = ['master' => + ['id' => 'master', 'partstat' => 'DECLINED'] + ]; + } + } + } + $baseCalendar = $oldCalendar; + + } + + if (in_array($eventInfo['organizer'], $userHref)) { + return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); + } elseif ($oldCalendar) { + // We need to figure out if the user is an attendee, but we're only + // doing so if there's an oldCalendar, because we only want to + // process updates, not creation of new events. + foreach ($eventInfo['attendees'] as $attendee) { + if (in_array($attendee['href'], $userHref)) { + return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); + } + } + } + return []; + + } + + /** + * Processes incoming REQUEST messages. + * + * This is message from an organizer, and is either a new event + * invite, or an update to an existing one. + * + * + * @param Message $itipMessage + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null) { + + if (!$existingObject) { + // This is a new invite, and we're just going to copy over + // all the components from the invite. + $existingObject = new VCalendar(); + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + } else { + // We need to update an existing object with all the new + // information. We can just remove all existing components + // and create new ones. + foreach ($existingObject->getComponents() as $component) { + $existingObject->remove($component); + } + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + } + return $existingObject; + + } + + /** + * Processes incoming CANCEL messages. + * + * This is a message from an organizer, and means that either an + * attendee got removed from an event, or an event got cancelled + * altogether. + * + * @param Message $itipMessage + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null) { + + if (!$existingObject) { + // The event didn't exist in the first place, so we're just + // ignoring this message. + } else { + foreach ($existingObject->VEVENT as $vevent) { + $vevent->STATUS = 'CANCELLED'; + $vevent->SEQUENCE = $itipMessage->sequence; + } + } + return $existingObject; + + } + + /** + * Processes incoming REPLY messages. + * + * The message is a reply. This is for example an attendee telling + * an organizer he accepted the invite, or declined it. + * + * @param Message $itipMessage + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null) { + + // A reply can only be processed based on an existing object. + // If the object is not available, the reply is ignored. + if (!$existingObject) { + return; + } + $instances = []; + $requestStatus = '2.0'; + + // Finding all the instances the attendee replied to. + foreach ($itipMessage->message->VEVENT as $vevent) { + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + $attendee = $vevent->ATTENDEE; + $instances[$recurId] = $attendee['PARTSTAT']->getValue(); + if (isset($vevent->{'REQUEST-STATUS'})) { + $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue(); + list($requestStatus) = explode(';', $requestStatus); + } + } + + // Now we need to loop through the original organizer event, to find + // all the instances where we have a reply for. + $masterObject = null; + foreach ($existingObject->VEVENT as $vevent) { + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + if ($recurId === 'master') { + $masterObject = $vevent; + } + if (isset($instances[$recurId])) { + $attendeeFound = false; + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $instances[$recurId]; + $attendee['SCHEDULE-STATUS'] = $requestStatus; + // Un-setting the RSVP status, because we now know + // that the attendee already replied. + unset($attendee['RSVP']); + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee. The iTip documentation calls this + // a party crasher. + $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $instances[$recurId] + ]); + if ($itipMessage->senderName) $attendee['CN'] = $itipMessage->senderName; + } + unset($instances[$recurId]); + } + } + + if (!$masterObject) { + // No master object, we can't add new instances. + return; + } + // If we got replies to instances that did not exist in the + // original list, it means that new exceptions must be created. + foreach ($instances as $recurId => $partstat) { + + $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid); + $found = false; + $iterations = 1000; + do { + + $newObject = $recurrenceIterator->getEventObject(); + $recurrenceIterator->next(); + + if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) { + $found = true; + } + $iterations--; + + } while ($recurrenceIterator->valid() && !$found && $iterations); + + // Invalid recurrence id. Skipping this object. + if (!$found) continue; + + unset( + $newObject->RRULE, + $newObject->EXDATE, + $newObject->RDATE + ); + $attendeeFound = false; + if (isset($newObject->ATTENDEE)) { + foreach ($newObject->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $partstat; + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee + $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $partstat + ]); + if ($itipMessage->senderName) { + $attendee['CN'] = $itipMessage->senderName; + } + } + $existingObject->add($newObject); + + } + return $existingObject; + + } + + /** + * This method is used in cases where an event got updated, and we + * potentially need to send emails to attendees to let them know of updates + * in the events. + * + * We will detect which attendees got added, which got removed and create + * specific messages for these situations. + * + * @param VCalendar $calendar + * @param array $eventInfo + * @param array $oldEventInfo + * + * @return array + */ + protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) { + + // Merging attendee lists. + $attendees = []; + foreach ($oldEventInfo['attendees'] as $attendee) { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => $attendee['instances'], + 'newInstances' => [], + 'name' => $attendee['name'], + 'forceSend' => null, + ]; + } + foreach ($eventInfo['attendees'] as $attendee) { + if (isset($attendees[$attendee['href']])) { + $attendees[$attendee['href']]['name'] = $attendee['name']; + $attendees[$attendee['href']]['newInstances'] = $attendee['instances']; + $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend']; + } else { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => [], + 'newInstances' => $attendee['instances'], + 'name' => $attendee['name'], + 'forceSend' => $attendee['forceSend'], + ]; + } + } + + $messages = []; + + foreach ($attendees as $attendee) { + + // An organizer can also be an attendee. We should not generate any + // messages for those. + if ($attendee['href'] === $eventInfo['organizer']) { + continue; + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $eventInfo['organizer']; + $message->senderName = $eventInfo['organizerName']; + $message->recipient = $attendee['href']; + $message->recipientName = $attendee['name']; + + if (!$attendee['newInstances']) { + + // If there are no instances the attendee is a part of, it + // means the attendee was removed and we need to send him a + // CANCEL. + $message->method = 'CANCEL'; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + $icalMsg->METHOD = $message->method; + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + ]); + if (isset($calendar->VEVENT->SUMMARY)) { + $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue()); + } + $event->add(clone $calendar->VEVENT->DTSTART); + if (isset($calendar->VEVENT->DTEND)) { + $event->add(clone $calendar->VEVENT->DTEND); + } elseif (isset($calendar->VEVENT->DURATION)) { + $event->add(clone $calendar->VEVENT->DURATION); + } + $org = $event->add('ORGANIZER', $eventInfo['organizer']); + if ($eventInfo['organizerName']) $org['CN'] = $eventInfo['organizerName']; + $event->add('ATTENDEE', $attendee['href'], [ + 'CN' => $attendee['name'], + ]); + $message->significantChange = true; + + } else { + + // The attendee gets the updated event body + $message->method = 'REQUEST'; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + $icalMsg->METHOD = $message->method; + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + // We need to find out that this change is significant. If it's + // not, systems may opt to not send messages. + // + // We do this based on the 'significantChangeHash' which is + // some value that changes if there's a certain set of + // properties changed in the event, or simply if there's a + // difference in instances that the attendee is invited to. + + $message->significantChange = + $attendee['forceSend'] === 'REQUEST' || + array_keys($attendee['oldInstances']) != array_keys($attendee['newInstances']) || + $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; + + foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) { + + $currentEvent = clone $eventInfo['instances'][$instanceId]; + if ($instanceId === 'master') { + + // We need to find a list of events that the attendee + // is not a part of to add to the list of exceptions. + $exceptions = []; + foreach ($eventInfo['instances'] as $instanceId => $vevent) { + if (!isset($attendee['newInstances'][$instanceId])) { + $exceptions[] = $instanceId; + } + } + + // If there were exceptions, we need to add it to an + // existing EXDATE property, if it exists. + if ($exceptions) { + if (isset($currentEvent->EXDATE)) { + $currentEvent->EXDATE->setParts(array_merge( + $currentEvent->EXDATE->getParts(), + $exceptions + )); + } else { + $currentEvent->EXDATE = $exceptions; + } + } + + // Cleaning up any scheduling information that + // shouldn't be sent along. + unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); + unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); + + foreach ($currentEvent->ATTENDEE as $attendee) { + unset($attendee['SCHEDULE-FORCE-SEND']); + unset($attendee['SCHEDULE-STATUS']); + + // We're adding PARTSTAT=NEEDS-ACTION to ensure that + // iOS shows an "Inbox Item" + if (!isset($attendee['PARTSTAT'])) { + $attendee['PARTSTAT'] = 'NEEDS-ACTION'; + } + + } + + } + + $icalMsg->add($currentEvent); + + } + + } + + $message->message = $icalMsg; + $messages[] = $message; + + } + + return $messages; + + } + + /** + * Parse an event update for an attendee. + * + * This function figures out if we need to send a reply to an organizer. + * + * @param VCalendar $calendar + * @param array $eventInfo + * @param array $oldEventInfo + * @param string $attendee + * + * @return Message[] + */ + protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) { + + if ($this->scheduleAgentServerRules && $eventInfo['organizerScheduleAgent'] === 'CLIENT') { + return []; + } + + // Don't bother generating messages for events that have already been + // cancelled. + if ($eventInfo['status'] === 'CANCELLED') { + return []; + } + + $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? + $oldEventInfo['attendees'][$attendee]['instances'] : + []; + + $instances = []; + foreach ($oldInstances as $instance) { + + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => $instance['partstat'], + 'newstatus' => null, + ]; + + } + foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) { + + if (isset($instances[$instance['id']])) { + $instances[$instance['id']]['newstatus'] = $instance['partstat']; + } else { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => null, + 'newstatus' => $instance['partstat'], + ]; + } + + } + + // We need to also look for differences in EXDATE. If there are new + // items in EXDATE, it means that an attendee deleted instances of an + // event, which means we need to send DECLINED specifically for those + // instances. + // We only need to do that though, if the master event is not declined. + if (isset($instances['master']) && $instances['master']['newstatus'] !== 'DECLINED') { + foreach ($eventInfo['exdate'] as $exDate) { + + if (!in_array($exDate, $oldEventInfo['exdate'])) { + if (isset($instances[$exDate])) { + $instances[$exDate]['newstatus'] = 'DECLINED'; + } else { + $instances[$exDate] = [ + 'id' => $exDate, + 'oldstatus' => null, + 'newstatus' => 'DECLINED', + ]; + } + } + + } + } + + // Gathering a few extra properties for each instance. + foreach ($instances as $recurId => $instanceInfo) { + + if (isset($eventInfo['instances'][$recurId])) { + $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART; + } else { + $instances[$recurId]['dtstart'] = $recurId; + } + + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->method = 'REPLY'; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $attendee; + $message->senderName = $eventInfo['attendees'][$attendee]['name']; + $message->recipient = $eventInfo['organizer']; + $message->recipientName = $eventInfo['organizerName']; + + $icalMsg = new VCalendar(); + $icalMsg->METHOD = 'REPLY'; + + $hasReply = false; + + foreach ($instances as $instance) { + + if ($instance['oldstatus'] == $instance['newstatus'] && $eventInfo['organizerForceSend'] !== 'REPLY') { + // Skip + continue; + } + + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + ]); + $summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : ''; + // Adding properties from the correct source instance + if (isset($eventInfo['instances'][$instance['id']])) { + $instanceObj = $eventInfo['instances'][$instance['id']]; + $event->add(clone $instanceObj->DTSTART); + if (isset($instanceObj->DTEND)) { + $event->add(clone $instanceObj->DTEND); + } elseif (isset($instanceObj->DURATION)) { + $event->add(clone $instanceObj->DURATION); + } + if (isset($instanceObj->SUMMARY)) { + $event->add('SUMMARY', $instanceObj->SUMMARY->getValue()); + } elseif ($summary) { + $event->add('SUMMARY', $summary); + } + } else { + // This branch of the code is reached, when a reply is + // generated for an instance of a recurring event, through the + // fact that the instance has disappeared by showing up in + // EXDATE + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('DTSTART', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('DTSTART', $dt); + } + if ($summary) { + $event->add('SUMMARY', $summary); + } + } + if ($instance['id'] !== 'master') { + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('RECURRENCE-ID', $dt); + } + } + $organizer = $event->add('ORGANIZER', $message->recipient); + if ($message->recipientName) { + $organizer['CN'] = $message->recipientName; + } + $attendee = $event->add('ATTENDEE', $message->sender, [ + 'PARTSTAT' => $instance['newstatus'] + ]); + if ($message->senderName) { + $attendee['CN'] = $message->senderName; + } + $hasReply = true; + + } + + if ($hasReply) { + $message->message = $icalMsg; + return [$message]; + } else { + return []; + } + + } + + /** + * Returns attendee information and information about instances of an + * event. + * + * Returns an array with the following keys: + * + * 1. uid + * 2. organizer + * 3. organizerName + * 4. organizerScheduleAgent + * 5. organizerForceSend + * 6. instances + * 7. attendees + * 8. sequence + * 9. exdate + * 10. timezone - strictly the timezone on which the recurrence rule is + * based on. + * 11. significantChangeHash + * 12. status + * @param VCalendar $calendar + * + * @return array + */ + protected function parseEventInfo(VCalendar $calendar = null) { + + $uid = null; + $organizer = null; + $organizerName = null; + $organizerForceSend = null; + $sequence = null; + $timezone = null; + $status = null; + $organizerScheduleAgent = 'SERVER'; + + $significantChangeHash = ''; + + // Now we need to collect a list of attendees, and which instances they + // are a part of. + $attendees = []; + + $instances = []; + $exdate = []; + + foreach ($calendar->VEVENT as $vevent) { + + if (is_null($uid)) { + $uid = $vevent->UID->getValue(); + } else { + if ($uid !== $vevent->UID->getValue()) { + throw new ITipException('If a calendar contained more than one event, they must have the same UID.'); + } + } + + if (!isset($vevent->DTSTART)) { + throw new ITipException('An event MUST have a DTSTART property.'); + } + + if (isset($vevent->ORGANIZER)) { + if (is_null($organizer)) { + $organizer = $vevent->ORGANIZER->getNormalizedValue(); + $organizerName = isset($vevent->ORGANIZER['CN']) ? $vevent->ORGANIZER['CN'] : null; + } else { + if ($organizer !== $vevent->ORGANIZER->getNormalizedValue()) { + throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); + } + } + $organizerForceSend = + isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ? + strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) : + null; + $organizerScheduleAgent = + isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ? + strtoupper((string)$vevent->ORGANIZER['SCHEDULE-AGENT']) : + 'SERVER'; + } + if (is_null($sequence) && isset($vevent->SEQUENCE)) { + $sequence = $vevent->SEQUENCE->getValue(); + } + if (isset($vevent->EXDATE)) { + foreach ($vevent->select('EXDATE') as $val) { + $exdate = array_merge($exdate, $val->getParts()); + } + sort($exdate); + } + if (isset($vevent->STATUS)) { + $status = strtoupper($vevent->STATUS->getValue()); + } + + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + if (is_null($timezone)) { + if ($recurId === 'master') { + $timezone = $vevent->DTSTART->getDateTime()->getTimeZone(); + } else { + $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); + } + } + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + + if ($this->scheduleAgentServerRules && + isset($attendee['SCHEDULE-AGENT']) && + strtoupper($attendee['SCHEDULE-AGENT']->getValue()) === 'CLIENT' + ) { + continue; + } + $partStat = + isset($attendee['PARTSTAT']) ? + strtoupper($attendee['PARTSTAT']) : + 'NEEDS-ACTION'; + + $forceSend = + isset($attendee['SCHEDULE-FORCE-SEND']) ? + strtoupper($attendee['SCHEDULE-FORCE-SEND']) : + null; + + + if (isset($attendees[$attendee->getNormalizedValue()])) { + $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [ + 'id' => $recurId, + 'partstat' => $partStat, + 'force-send' => $forceSend, + ]; + } else { + $attendees[$attendee->getNormalizedValue()] = [ + 'href' => $attendee->getNormalizedValue(), + 'instances' => [ + $recurId => [ + 'id' => $recurId, + 'partstat' => $partStat, + ], + ], + 'name' => isset($attendee['CN']) ? (string)$attendee['CN'] : null, + 'forceSend' => $forceSend, + ]; + } + + } + $instances[$recurId] = $vevent; + + } + + foreach ($this->significantChangeProperties as $prop) { + if (isset($vevent->$prop)) { + $propertyValues = $vevent->select($prop); + + $significantChangeHash .= $prop . ':'; + + if ($prop === 'EXDATE') { + + $significantChangeHash .= implode(',', $exdate) . ';'; + + } else { + + foreach ($propertyValues as $val) { + $significantChangeHash .= $val->getValue() . ';'; + } + + } + } + } + + } + $significantChangeHash = md5($significantChangeHash); + + return compact( + 'uid', + 'organizer', + 'organizerName', + 'organizerScheduleAgent', + 'organizerForceSend', + 'instances', + 'attendees', + 'sequence', + 'exdate', + 'timezone', + 'significantChangeHash', + 'status' + ); + + } + +} |