1*a1a3b679SAndreas Boehler<?php 2*a1a3b679SAndreas Boehler 3*a1a3b679SAndreas Boehlernamespace Sabre\VObject\ITip; 4*a1a3b679SAndreas Boehler 5*a1a3b679SAndreas Boehleruse Sabre\VObject\Component\VCalendar; 6*a1a3b679SAndreas Boehleruse Sabre\VObject\DateTimeParser; 7*a1a3b679SAndreas Boehleruse Sabre\VObject\Reader; 8*a1a3b679SAndreas Boehleruse Sabre\VObject\Recur\EventIterator; 9*a1a3b679SAndreas Boehler 10*a1a3b679SAndreas Boehler/** 11*a1a3b679SAndreas Boehler * The ITip\Broker class is a utility class that helps with processing 12*a1a3b679SAndreas Boehler * so-called iTip messages. 13*a1a3b679SAndreas Boehler * 14*a1a3b679SAndreas Boehler * iTip is defined in rfc5546, stands for iCalendar Transport-Independent 15*a1a3b679SAndreas Boehler * Interoperability Protocol, and describes the underlying mechanism for 16*a1a3b679SAndreas Boehler * using iCalendar for scheduling for for example through email (also known as 17*a1a3b679SAndreas Boehler * IMip) and CalDAV Scheduling. 18*a1a3b679SAndreas Boehler * 19*a1a3b679SAndreas Boehler * This class helps by: 20*a1a3b679SAndreas Boehler * 21*a1a3b679SAndreas Boehler * 1. Creating individual invites based on an iCalendar event for each 22*a1a3b679SAndreas Boehler * attendee. 23*a1a3b679SAndreas Boehler * 2. Generating invite updates based on an iCalendar update. This may result 24*a1a3b679SAndreas Boehler * in new invites, updates and cancellations for attendees, if that list 25*a1a3b679SAndreas Boehler * changed. 26*a1a3b679SAndreas Boehler * 3. On the receiving end, it can create a local iCalendar event based on 27*a1a3b679SAndreas Boehler * a received invite. 28*a1a3b679SAndreas Boehler * 4. It can also process an invite update on a local event, ensuring that any 29*a1a3b679SAndreas Boehler * overridden properties from attendees are retained. 30*a1a3b679SAndreas Boehler * 5. It can create a accepted or declined iTip reply based on an invite. 31*a1a3b679SAndreas Boehler * 6. It can process a reply from an invite and update an events attendee 32*a1a3b679SAndreas Boehler * status based on a reply. 33*a1a3b679SAndreas Boehler * 34*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/). 35*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/) 36*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License 37*a1a3b679SAndreas Boehler */ 38*a1a3b679SAndreas Boehlerclass Broker { 39*a1a3b679SAndreas Boehler 40*a1a3b679SAndreas Boehler /** 41*a1a3b679SAndreas Boehler * This setting determines whether the rules for the SCHEDULE-AGENT 42*a1a3b679SAndreas Boehler * parameter should be followed. 43*a1a3b679SAndreas Boehler * 44*a1a3b679SAndreas Boehler * This is a parameter defined on ATTENDEE properties, introduced by RFC 45*a1a3b679SAndreas Boehler * 6638. This parameter allows a caldav client to tell the server 'Don't do 46*a1a3b679SAndreas Boehler * any scheduling operations'. 47*a1a3b679SAndreas Boehler * 48*a1a3b679SAndreas Boehler * If this setting is turned on, any attendees with SCHEDULE-AGENT set to 49*a1a3b679SAndreas Boehler * CLIENT will be ignored. This is the desired behavior for a CalDAV 50*a1a3b679SAndreas Boehler * server, but if you're writing an iTip application that doesn't deal with 51*a1a3b679SAndreas Boehler * CalDAV, you may want to ignore this parameter. 52*a1a3b679SAndreas Boehler * 53*a1a3b679SAndreas Boehler * @var bool 54*a1a3b679SAndreas Boehler */ 55*a1a3b679SAndreas Boehler public $scheduleAgentServerRules = true; 56*a1a3b679SAndreas Boehler 57*a1a3b679SAndreas Boehler /** 58*a1a3b679SAndreas Boehler * The broker will try during 'parseEvent' figure out whether the change 59*a1a3b679SAndreas Boehler * was significant. 60*a1a3b679SAndreas Boehler * 61*a1a3b679SAndreas Boehler * It uses a few different ways to do this. One of these ways is seeing if 62*a1a3b679SAndreas Boehler * certain properties changed values. This list of specified here. 63*a1a3b679SAndreas Boehler * 64*a1a3b679SAndreas Boehler * This list is taken from: 65*a1a3b679SAndreas Boehler * * http://tools.ietf.org/html/rfc5546#section-2.1.4 66*a1a3b679SAndreas Boehler * 67*a1a3b679SAndreas Boehler * @var string[] 68*a1a3b679SAndreas Boehler */ 69*a1a3b679SAndreas Boehler public $significantChangeProperties = array( 70*a1a3b679SAndreas Boehler 'DTSTART', 71*a1a3b679SAndreas Boehler 'DTEND', 72*a1a3b679SAndreas Boehler 'DURATION', 73*a1a3b679SAndreas Boehler 'DUE', 74*a1a3b679SAndreas Boehler 'RRULE', 75*a1a3b679SAndreas Boehler 'RDATE', 76*a1a3b679SAndreas Boehler 'EXDATE', 77*a1a3b679SAndreas Boehler 'STATUS', 78*a1a3b679SAndreas Boehler ); 79*a1a3b679SAndreas Boehler 80*a1a3b679SAndreas Boehler /** 81*a1a3b679SAndreas Boehler * This method is used to process an incoming itip message. 82*a1a3b679SAndreas Boehler * 83*a1a3b679SAndreas Boehler * Examples: 84*a1a3b679SAndreas Boehler * 85*a1a3b679SAndreas Boehler * 1. A user is an attendee to an event. The organizer sends an updated 86*a1a3b679SAndreas Boehler * meeting using a new iTip message with METHOD:REQUEST. This function 87*a1a3b679SAndreas Boehler * will process the message and update the attendee's event accordingly. 88*a1a3b679SAndreas Boehler * 89*a1a3b679SAndreas Boehler * 2. The organizer cancelled the event using METHOD:CANCEL. We will update 90*a1a3b679SAndreas Boehler * the users event to state STATUS:CANCELLED. 91*a1a3b679SAndreas Boehler * 92*a1a3b679SAndreas Boehler * 3. An attendee sent a reply to an invite using METHOD:REPLY. We can 93*a1a3b679SAndreas Boehler * update the organizers event to update the ATTENDEE with its correct 94*a1a3b679SAndreas Boehler * PARTSTAT. 95*a1a3b679SAndreas Boehler * 96*a1a3b679SAndreas Boehler * The $existingObject is updated in-place. If there is no existing object 97*a1a3b679SAndreas Boehler * (because it's a new invite for example) a new object will be created. 98*a1a3b679SAndreas Boehler * 99*a1a3b679SAndreas Boehler * If an existing object does not exist, and the method was CANCEL or 100*a1a3b679SAndreas Boehler * REPLY, the message effectively gets ignored, and no 'existingObject' 101*a1a3b679SAndreas Boehler * will be created. 102*a1a3b679SAndreas Boehler * 103*a1a3b679SAndreas Boehler * The updated $existingObject is also returned from this function. 104*a1a3b679SAndreas Boehler * 105*a1a3b679SAndreas Boehler * If the iTip message was not supported, we will always return false. 106*a1a3b679SAndreas Boehler * 107*a1a3b679SAndreas Boehler * @param Message $itipMessage 108*a1a3b679SAndreas Boehler * @param VCalendar $existingObject 109*a1a3b679SAndreas Boehler * @return VCalendar|null 110*a1a3b679SAndreas Boehler */ 111*a1a3b679SAndreas Boehler public function processMessage(Message $itipMessage, VCalendar $existingObject = null) { 112*a1a3b679SAndreas Boehler 113*a1a3b679SAndreas Boehler // We only support events at the moment. 114*a1a3b679SAndreas Boehler if ($itipMessage->component !== 'VEVENT') { 115*a1a3b679SAndreas Boehler return false; 116*a1a3b679SAndreas Boehler } 117*a1a3b679SAndreas Boehler 118*a1a3b679SAndreas Boehler switch($itipMessage->method) { 119*a1a3b679SAndreas Boehler 120*a1a3b679SAndreas Boehler case 'REQUEST' : 121*a1a3b679SAndreas Boehler return $this->processMessageRequest($itipMessage, $existingObject); 122*a1a3b679SAndreas Boehler 123*a1a3b679SAndreas Boehler case 'CANCEL' : 124*a1a3b679SAndreas Boehler return $this->processMessageCancel($itipMessage, $existingObject); 125*a1a3b679SAndreas Boehler 126*a1a3b679SAndreas Boehler case 'REPLY' : 127*a1a3b679SAndreas Boehler return $this->processMessageReply($itipMessage, $existingObject); 128*a1a3b679SAndreas Boehler 129*a1a3b679SAndreas Boehler default : 130*a1a3b679SAndreas Boehler // Unsupported iTip message 131*a1a3b679SAndreas Boehler return null; 132*a1a3b679SAndreas Boehler 133*a1a3b679SAndreas Boehler } 134*a1a3b679SAndreas Boehler 135*a1a3b679SAndreas Boehler return $existingObject; 136*a1a3b679SAndreas Boehler 137*a1a3b679SAndreas Boehler } 138*a1a3b679SAndreas Boehler 139*a1a3b679SAndreas Boehler /** 140*a1a3b679SAndreas Boehler * This function parses a VCALENDAR object and figure out if any messages 141*a1a3b679SAndreas Boehler * need to be sent. 142*a1a3b679SAndreas Boehler * 143*a1a3b679SAndreas Boehler * A VCALENDAR object will be created from the perspective of either an 144*a1a3b679SAndreas Boehler * attendee, or an organizer. You must pass a string identifying the 145*a1a3b679SAndreas Boehler * current user, so we can figure out who in the list of attendees or the 146*a1a3b679SAndreas Boehler * organizer we are sending this message on behalf of. 147*a1a3b679SAndreas Boehler * 148*a1a3b679SAndreas Boehler * It's possible to specify the current user as an array, in case the user 149*a1a3b679SAndreas Boehler * has more than one identifying href (such as multiple emails). 150*a1a3b679SAndreas Boehler * 151*a1a3b679SAndreas Boehler * It $oldCalendar is specified, it is assumed that the operation is 152*a1a3b679SAndreas Boehler * updating an existing event, which means that we need to look at the 153*a1a3b679SAndreas Boehler * differences between events, and potentially send old attendees 154*a1a3b679SAndreas Boehler * cancellations, and current attendees updates. 155*a1a3b679SAndreas Boehler * 156*a1a3b679SAndreas Boehler * If $calendar is null, but $oldCalendar is specified, we treat the 157*a1a3b679SAndreas Boehler * operation as if the user has deleted an event. If the user was an 158*a1a3b679SAndreas Boehler * organizer, this means that we need to send cancellation notices to 159*a1a3b679SAndreas Boehler * people. If the user was an attendee, we need to make sure that the 160*a1a3b679SAndreas Boehler * organizer gets the 'declined' message. 161*a1a3b679SAndreas Boehler * 162*a1a3b679SAndreas Boehler * @param VCalendar|string $calendar 163*a1a3b679SAndreas Boehler * @param string|array $userHref 164*a1a3b679SAndreas Boehler * @param VCalendar|string $oldCalendar 165*a1a3b679SAndreas Boehler * @return array 166*a1a3b679SAndreas Boehler */ 167*a1a3b679SAndreas Boehler public function parseEvent($calendar = null, $userHref, $oldCalendar = null) { 168*a1a3b679SAndreas Boehler 169*a1a3b679SAndreas Boehler if ($oldCalendar) { 170*a1a3b679SAndreas Boehler if (is_string($oldCalendar)) { 171*a1a3b679SAndreas Boehler $oldCalendar = Reader::read($oldCalendar); 172*a1a3b679SAndreas Boehler } 173*a1a3b679SAndreas Boehler if (!isset($oldCalendar->VEVENT)) { 174*a1a3b679SAndreas Boehler // We only support events at the moment 175*a1a3b679SAndreas Boehler return array(); 176*a1a3b679SAndreas Boehler } 177*a1a3b679SAndreas Boehler 178*a1a3b679SAndreas Boehler $oldEventInfo = $this->parseEventInfo($oldCalendar); 179*a1a3b679SAndreas Boehler } else { 180*a1a3b679SAndreas Boehler $oldEventInfo = array( 181*a1a3b679SAndreas Boehler 'organizer' => null, 182*a1a3b679SAndreas Boehler 'significantChangeHash' => '', 183*a1a3b679SAndreas Boehler 'attendees' => array(), 184*a1a3b679SAndreas Boehler ); 185*a1a3b679SAndreas Boehler } 186*a1a3b679SAndreas Boehler 187*a1a3b679SAndreas Boehler $userHref = (array)$userHref; 188*a1a3b679SAndreas Boehler 189*a1a3b679SAndreas Boehler if (!is_null($calendar)) { 190*a1a3b679SAndreas Boehler 191*a1a3b679SAndreas Boehler if (is_string($calendar)) { 192*a1a3b679SAndreas Boehler $calendar = Reader::read($calendar); 193*a1a3b679SAndreas Boehler } 194*a1a3b679SAndreas Boehler if (!isset($calendar->VEVENT)) { 195*a1a3b679SAndreas Boehler // We only support events at the moment 196*a1a3b679SAndreas Boehler return array(); 197*a1a3b679SAndreas Boehler } 198*a1a3b679SAndreas Boehler $eventInfo = $this->parseEventInfo($calendar); 199*a1a3b679SAndreas Boehler if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) { 200*a1a3b679SAndreas Boehler // If there were no attendees on either side of the equation, 201*a1a3b679SAndreas Boehler // we don't need to do anything. 202*a1a3b679SAndreas Boehler return array(); 203*a1a3b679SAndreas Boehler } 204*a1a3b679SAndreas Boehler if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) { 205*a1a3b679SAndreas Boehler // There was no organizer before or after the change. 206*a1a3b679SAndreas Boehler return array(); 207*a1a3b679SAndreas Boehler } 208*a1a3b679SAndreas Boehler 209*a1a3b679SAndreas Boehler $baseCalendar = $calendar; 210*a1a3b679SAndreas Boehler 211*a1a3b679SAndreas Boehler // If the new object didn't have an organizer, the organizer 212*a1a3b679SAndreas Boehler // changed the object from a scheduling object to a non-scheduling 213*a1a3b679SAndreas Boehler // object. We just copy the info from the old object. 214*a1a3b679SAndreas Boehler if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) { 215*a1a3b679SAndreas Boehler $eventInfo['organizer'] = $oldEventInfo['organizer']; 216*a1a3b679SAndreas Boehler $eventInfo['organizerName'] = $oldEventInfo['organizerName']; 217*a1a3b679SAndreas Boehler } 218*a1a3b679SAndreas Boehler 219*a1a3b679SAndreas Boehler } else { 220*a1a3b679SAndreas Boehler // The calendar object got deleted, we need to process this as a 221*a1a3b679SAndreas Boehler // cancellation / decline. 222*a1a3b679SAndreas Boehler if (!$oldCalendar) { 223*a1a3b679SAndreas Boehler // No old and no new calendar, there's no thing to do. 224*a1a3b679SAndreas Boehler return array(); 225*a1a3b679SAndreas Boehler } 226*a1a3b679SAndreas Boehler 227*a1a3b679SAndreas Boehler $eventInfo = $oldEventInfo; 228*a1a3b679SAndreas Boehler 229*a1a3b679SAndreas Boehler if (in_array($eventInfo['organizer'], $userHref)) { 230*a1a3b679SAndreas Boehler // This is an organizer deleting the event. 231*a1a3b679SAndreas Boehler $eventInfo['attendees'] = array(); 232*a1a3b679SAndreas Boehler // Increasing the sequence, but only if the organizer deleted 233*a1a3b679SAndreas Boehler // the event. 234*a1a3b679SAndreas Boehler $eventInfo['sequence']++; 235*a1a3b679SAndreas Boehler } else { 236*a1a3b679SAndreas Boehler // This is an attendee deleting the event. 237*a1a3b679SAndreas Boehler foreach($eventInfo['attendees'] as $key=>$attendee) { 238*a1a3b679SAndreas Boehler if (in_array($attendee['href'], $userHref)) { 239*a1a3b679SAndreas Boehler $eventInfo['attendees'][$key]['instances'] = array('master' => 240*a1a3b679SAndreas Boehler array('id'=>'master', 'partstat' => 'DECLINED') 241*a1a3b679SAndreas Boehler ); 242*a1a3b679SAndreas Boehler } 243*a1a3b679SAndreas Boehler } 244*a1a3b679SAndreas Boehler } 245*a1a3b679SAndreas Boehler $baseCalendar = $oldCalendar; 246*a1a3b679SAndreas Boehler 247*a1a3b679SAndreas Boehler } 248*a1a3b679SAndreas Boehler 249*a1a3b679SAndreas Boehler if (in_array($eventInfo['organizer'], $userHref)) { 250*a1a3b679SAndreas Boehler return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); 251*a1a3b679SAndreas Boehler } elseif ($oldCalendar) { 252*a1a3b679SAndreas Boehler // We need to figure out if the user is an attendee, but we're only 253*a1a3b679SAndreas Boehler // doing so if there's an oldCalendar, because we only want to 254*a1a3b679SAndreas Boehler // process updates, not creation of new events. 255*a1a3b679SAndreas Boehler foreach($eventInfo['attendees'] as $attendee) { 256*a1a3b679SAndreas Boehler if (in_array($attendee['href'], $userHref)) { 257*a1a3b679SAndreas Boehler return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); 258*a1a3b679SAndreas Boehler } 259*a1a3b679SAndreas Boehler } 260*a1a3b679SAndreas Boehler } 261*a1a3b679SAndreas Boehler return array(); 262*a1a3b679SAndreas Boehler 263*a1a3b679SAndreas Boehler } 264*a1a3b679SAndreas Boehler 265*a1a3b679SAndreas Boehler /** 266*a1a3b679SAndreas Boehler * Processes incoming REQUEST messages. 267*a1a3b679SAndreas Boehler * 268*a1a3b679SAndreas Boehler * This is message from an organizer, and is either a new event 269*a1a3b679SAndreas Boehler * invite, or an update to an existing one. 270*a1a3b679SAndreas Boehler * 271*a1a3b679SAndreas Boehler * 272*a1a3b679SAndreas Boehler * @param Message $itipMessage 273*a1a3b679SAndreas Boehler * @param VCalendar $existingObject 274*a1a3b679SAndreas Boehler * @return VCalendar|null 275*a1a3b679SAndreas Boehler */ 276*a1a3b679SAndreas Boehler protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null) { 277*a1a3b679SAndreas Boehler 278*a1a3b679SAndreas Boehler if (!$existingObject) { 279*a1a3b679SAndreas Boehler // This is a new invite, and we're just going to copy over 280*a1a3b679SAndreas Boehler // all the components from the invite. 281*a1a3b679SAndreas Boehler $existingObject = new VCalendar(); 282*a1a3b679SAndreas Boehler foreach($itipMessage->message->getComponents() as $component) { 283*a1a3b679SAndreas Boehler $existingObject->add(clone $component); 284*a1a3b679SAndreas Boehler } 285*a1a3b679SAndreas Boehler } else { 286*a1a3b679SAndreas Boehler // We need to update an existing object with all the new 287*a1a3b679SAndreas Boehler // information. We can just remove all existing components 288*a1a3b679SAndreas Boehler // and create new ones. 289*a1a3b679SAndreas Boehler foreach($existingObject->getComponents() as $component) { 290*a1a3b679SAndreas Boehler $existingObject->remove($component); 291*a1a3b679SAndreas Boehler } 292*a1a3b679SAndreas Boehler foreach($itipMessage->message->getComponents() as $component) { 293*a1a3b679SAndreas Boehler $existingObject->add(clone $component); 294*a1a3b679SAndreas Boehler } 295*a1a3b679SAndreas Boehler } 296*a1a3b679SAndreas Boehler return $existingObject; 297*a1a3b679SAndreas Boehler 298*a1a3b679SAndreas Boehler } 299*a1a3b679SAndreas Boehler 300*a1a3b679SAndreas Boehler /** 301*a1a3b679SAndreas Boehler * Processes incoming CANCEL messages. 302*a1a3b679SAndreas Boehler * 303*a1a3b679SAndreas Boehler * This is a message from an organizer, and means that either an 304*a1a3b679SAndreas Boehler * attendee got removed from an event, or an event got cancelled 305*a1a3b679SAndreas Boehler * altogether. 306*a1a3b679SAndreas Boehler * 307*a1a3b679SAndreas Boehler * @param Message $itipMessage 308*a1a3b679SAndreas Boehler * @param VCalendar $existingObject 309*a1a3b679SAndreas Boehler * @return VCalendar|null 310*a1a3b679SAndreas Boehler */ 311*a1a3b679SAndreas Boehler protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null) { 312*a1a3b679SAndreas Boehler 313*a1a3b679SAndreas Boehler if (!$existingObject) { 314*a1a3b679SAndreas Boehler // The event didn't exist in the first place, so we're just 315*a1a3b679SAndreas Boehler // ignoring this message. 316*a1a3b679SAndreas Boehler } else { 317*a1a3b679SAndreas Boehler foreach($existingObject->VEVENT as $vevent) { 318*a1a3b679SAndreas Boehler $vevent->STATUS = 'CANCELLED'; 319*a1a3b679SAndreas Boehler $vevent->SEQUENCE = $itipMessage->sequence; 320*a1a3b679SAndreas Boehler } 321*a1a3b679SAndreas Boehler } 322*a1a3b679SAndreas Boehler return $existingObject; 323*a1a3b679SAndreas Boehler 324*a1a3b679SAndreas Boehler } 325*a1a3b679SAndreas Boehler 326*a1a3b679SAndreas Boehler /** 327*a1a3b679SAndreas Boehler * Processes incoming REPLY messages. 328*a1a3b679SAndreas Boehler * 329*a1a3b679SAndreas Boehler * The message is a reply. This is for example an attendee telling 330*a1a3b679SAndreas Boehler * an organizer he accepted the invite, or declined it. 331*a1a3b679SAndreas Boehler * 332*a1a3b679SAndreas Boehler * @param Message $itipMessage 333*a1a3b679SAndreas Boehler * @param VCalendar $existingObject 334*a1a3b679SAndreas Boehler * @return VCalendar|null 335*a1a3b679SAndreas Boehler */ 336*a1a3b679SAndreas Boehler protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null) { 337*a1a3b679SAndreas Boehler 338*a1a3b679SAndreas Boehler // A reply can only be processed based on an existing object. 339*a1a3b679SAndreas Boehler // If the object is not available, the reply is ignored. 340*a1a3b679SAndreas Boehler if (!$existingObject) { 341*a1a3b679SAndreas Boehler return null; 342*a1a3b679SAndreas Boehler } 343*a1a3b679SAndreas Boehler $instances = array(); 344*a1a3b679SAndreas Boehler $requestStatus = '2.0'; 345*a1a3b679SAndreas Boehler 346*a1a3b679SAndreas Boehler // Finding all the instances the attendee replied to. 347*a1a3b679SAndreas Boehler foreach($itipMessage->message->VEVENT as $vevent) { 348*a1a3b679SAndreas Boehler $recurId = isset($vevent->{'RECURRENCE-ID'})?$vevent->{'RECURRENCE-ID'}->getValue():'master'; 349*a1a3b679SAndreas Boehler $attendee = $vevent->ATTENDEE; 350*a1a3b679SAndreas Boehler $instances[$recurId] = $attendee['PARTSTAT']->getValue(); 351*a1a3b679SAndreas Boehler if (isset($vevent->{'REQUEST-STATUS'})) { 352*a1a3b679SAndreas Boehler $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue(); 353*a1a3b679SAndreas Boehler list($requestStatus) = explode(';', $requestStatus); 354*a1a3b679SAndreas Boehler } 355*a1a3b679SAndreas Boehler } 356*a1a3b679SAndreas Boehler 357*a1a3b679SAndreas Boehler // Now we need to loop through the original organizer event, to find 358*a1a3b679SAndreas Boehler // all the instances where we have a reply for. 359*a1a3b679SAndreas Boehler $masterObject = null; 360*a1a3b679SAndreas Boehler foreach($existingObject->VEVENT as $vevent) { 361*a1a3b679SAndreas Boehler $recurId = isset($vevent->{'RECURRENCE-ID'})?$vevent->{'RECURRENCE-ID'}->getValue():'master'; 362*a1a3b679SAndreas Boehler if ($recurId==='master') { 363*a1a3b679SAndreas Boehler $masterObject = $vevent; 364*a1a3b679SAndreas Boehler } 365*a1a3b679SAndreas Boehler if (isset($instances[$recurId])) { 366*a1a3b679SAndreas Boehler $attendeeFound = false; 367*a1a3b679SAndreas Boehler if (isset($vevent->ATTENDEE)) { 368*a1a3b679SAndreas Boehler foreach($vevent->ATTENDEE as $attendee) { 369*a1a3b679SAndreas Boehler if ($attendee->getValue() === $itipMessage->sender) { 370*a1a3b679SAndreas Boehler $attendeeFound = true; 371*a1a3b679SAndreas Boehler $attendee['PARTSTAT'] = $instances[$recurId]; 372*a1a3b679SAndreas Boehler $attendee['SCHEDULE-STATUS'] = $requestStatus; 373*a1a3b679SAndreas Boehler // Un-setting the RSVP status, because we now know 374*a1a3b679SAndreas Boehler // that the attende already replied. 375*a1a3b679SAndreas Boehler unset($attendee['RSVP']); 376*a1a3b679SAndreas Boehler break; 377*a1a3b679SAndreas Boehler } 378*a1a3b679SAndreas Boehler } 379*a1a3b679SAndreas Boehler } 380*a1a3b679SAndreas Boehler if (!$attendeeFound) { 381*a1a3b679SAndreas Boehler // Adding a new attendee. The iTip documentation calls this 382*a1a3b679SAndreas Boehler // a party crasher. 383*a1a3b679SAndreas Boehler $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, array( 384*a1a3b679SAndreas Boehler 'PARTSTAT' => $instances[$recurId] 385*a1a3b679SAndreas Boehler )); 386*a1a3b679SAndreas Boehler if ($itipMessage->senderName) $attendee['CN'] = $itipMessage->senderName; 387*a1a3b679SAndreas Boehler } 388*a1a3b679SAndreas Boehler unset($instances[$recurId]); 389*a1a3b679SAndreas Boehler } 390*a1a3b679SAndreas Boehler } 391*a1a3b679SAndreas Boehler 392*a1a3b679SAndreas Boehler if(!$masterObject) { 393*a1a3b679SAndreas Boehler // No master object, we can't add new instances. 394*a1a3b679SAndreas Boehler return null; 395*a1a3b679SAndreas Boehler } 396*a1a3b679SAndreas Boehler // If we got replies to instances that did not exist in the 397*a1a3b679SAndreas Boehler // original list, it means that new exceptions must be created. 398*a1a3b679SAndreas Boehler foreach($instances as $recurId=>$partstat) { 399*a1a3b679SAndreas Boehler 400*a1a3b679SAndreas Boehler $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid); 401*a1a3b679SAndreas Boehler $found = false; 402*a1a3b679SAndreas Boehler $iterations = 1000; 403*a1a3b679SAndreas Boehler do { 404*a1a3b679SAndreas Boehler 405*a1a3b679SAndreas Boehler $newObject = $recurrenceIterator->getEventObject(); 406*a1a3b679SAndreas Boehler $recurrenceIterator->next(); 407*a1a3b679SAndreas Boehler 408*a1a3b679SAndreas Boehler if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue()===$recurId) { 409*a1a3b679SAndreas Boehler $found = true; 410*a1a3b679SAndreas Boehler } 411*a1a3b679SAndreas Boehler $iterations--; 412*a1a3b679SAndreas Boehler 413*a1a3b679SAndreas Boehler } while($recurrenceIterator->valid() && !$found && $iterations); 414*a1a3b679SAndreas Boehler 415*a1a3b679SAndreas Boehler // Invalid recurrence id. Skipping this object. 416*a1a3b679SAndreas Boehler if (!$found) continue; 417*a1a3b679SAndreas Boehler 418*a1a3b679SAndreas Boehler unset( 419*a1a3b679SAndreas Boehler $newObject->RRULE, 420*a1a3b679SAndreas Boehler $newObject->EXDATE, 421*a1a3b679SAndreas Boehler $newObject->RDATE 422*a1a3b679SAndreas Boehler ); 423*a1a3b679SAndreas Boehler $attendeeFound = false; 424*a1a3b679SAndreas Boehler if (isset($newObject->ATTENDEE)) { 425*a1a3b679SAndreas Boehler foreach($newObject->ATTENDEE as $attendee) { 426*a1a3b679SAndreas Boehler if ($attendee->getValue() === $itipMessage->sender) { 427*a1a3b679SAndreas Boehler $attendeeFound = true; 428*a1a3b679SAndreas Boehler $attendee['PARTSTAT'] = $partstat; 429*a1a3b679SAndreas Boehler break; 430*a1a3b679SAndreas Boehler } 431*a1a3b679SAndreas Boehler } 432*a1a3b679SAndreas Boehler } 433*a1a3b679SAndreas Boehler if (!$attendeeFound) { 434*a1a3b679SAndreas Boehler // Adding a new attendee 435*a1a3b679SAndreas Boehler $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, array( 436*a1a3b679SAndreas Boehler 'PARTSTAT' => $partstat 437*a1a3b679SAndreas Boehler )); 438*a1a3b679SAndreas Boehler if ($itipMessage->senderName) { 439*a1a3b679SAndreas Boehler $attendee['CN'] = $itipMessage->senderName; 440*a1a3b679SAndreas Boehler } 441*a1a3b679SAndreas Boehler } 442*a1a3b679SAndreas Boehler $existingObject->add($newObject); 443*a1a3b679SAndreas Boehler 444*a1a3b679SAndreas Boehler } 445*a1a3b679SAndreas Boehler return $existingObject; 446*a1a3b679SAndreas Boehler 447*a1a3b679SAndreas Boehler } 448*a1a3b679SAndreas Boehler 449*a1a3b679SAndreas Boehler /** 450*a1a3b679SAndreas Boehler * This method is used in cases where an event got updated, and we 451*a1a3b679SAndreas Boehler * potentially need to send emails to attendees to let them know of updates 452*a1a3b679SAndreas Boehler * in the events. 453*a1a3b679SAndreas Boehler * 454*a1a3b679SAndreas Boehler * We will detect which attendees got added, which got removed and create 455*a1a3b679SAndreas Boehler * specific messages for these situations. 456*a1a3b679SAndreas Boehler * 457*a1a3b679SAndreas Boehler * @param VCalendar $calendar 458*a1a3b679SAndreas Boehler * @param array $eventInfo 459*a1a3b679SAndreas Boehler * @param array $oldEventInfo 460*a1a3b679SAndreas Boehler * @return array 461*a1a3b679SAndreas Boehler */ 462*a1a3b679SAndreas Boehler protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) { 463*a1a3b679SAndreas Boehler 464*a1a3b679SAndreas Boehler // Merging attendee lists. 465*a1a3b679SAndreas Boehler $attendees = array(); 466*a1a3b679SAndreas Boehler foreach($oldEventInfo['attendees'] as $attendee) { 467*a1a3b679SAndreas Boehler $attendees[$attendee['href']] = array( 468*a1a3b679SAndreas Boehler 'href' => $attendee['href'], 469*a1a3b679SAndreas Boehler 'oldInstances' => $attendee['instances'], 470*a1a3b679SAndreas Boehler 'newInstances' => array(), 471*a1a3b679SAndreas Boehler 'name' => $attendee['name'], 472*a1a3b679SAndreas Boehler 'forceSend' => null, 473*a1a3b679SAndreas Boehler ); 474*a1a3b679SAndreas Boehler } 475*a1a3b679SAndreas Boehler foreach($eventInfo['attendees'] as $attendee) { 476*a1a3b679SAndreas Boehler if (isset($attendees[$attendee['href']])) { 477*a1a3b679SAndreas Boehler $attendees[$attendee['href']]['name'] = $attendee['name']; 478*a1a3b679SAndreas Boehler $attendees[$attendee['href']]['newInstances'] = $attendee['instances']; 479*a1a3b679SAndreas Boehler $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend']; 480*a1a3b679SAndreas Boehler } else { 481*a1a3b679SAndreas Boehler $attendees[$attendee['href']] = array( 482*a1a3b679SAndreas Boehler 'href' => $attendee['href'], 483*a1a3b679SAndreas Boehler 'oldInstances' => array(), 484*a1a3b679SAndreas Boehler 'newInstances' => $attendee['instances'], 485*a1a3b679SAndreas Boehler 'name' => $attendee['name'], 486*a1a3b679SAndreas Boehler 'forceSend' => $attendee['forceSend'], 487*a1a3b679SAndreas Boehler ); 488*a1a3b679SAndreas Boehler } 489*a1a3b679SAndreas Boehler } 490*a1a3b679SAndreas Boehler 491*a1a3b679SAndreas Boehler $messages = array(); 492*a1a3b679SAndreas Boehler 493*a1a3b679SAndreas Boehler foreach($attendees as $attendee) { 494*a1a3b679SAndreas Boehler 495*a1a3b679SAndreas Boehler // An organizer can also be an attendee. We should not generate any 496*a1a3b679SAndreas Boehler // messages for those. 497*a1a3b679SAndreas Boehler if ($attendee['href']===$eventInfo['organizer']) { 498*a1a3b679SAndreas Boehler continue; 499*a1a3b679SAndreas Boehler } 500*a1a3b679SAndreas Boehler 501*a1a3b679SAndreas Boehler $message = new Message(); 502*a1a3b679SAndreas Boehler $message->uid = $eventInfo['uid']; 503*a1a3b679SAndreas Boehler $message->component = 'VEVENT'; 504*a1a3b679SAndreas Boehler $message->sequence = $eventInfo['sequence']; 505*a1a3b679SAndreas Boehler $message->sender = $eventInfo['organizer']; 506*a1a3b679SAndreas Boehler $message->senderName = $eventInfo['organizerName']; 507*a1a3b679SAndreas Boehler $message->recipient = $attendee['href']; 508*a1a3b679SAndreas Boehler $message->recipientName = $attendee['name']; 509*a1a3b679SAndreas Boehler 510*a1a3b679SAndreas Boehler if (!$attendee['newInstances']) { 511*a1a3b679SAndreas Boehler 512*a1a3b679SAndreas Boehler // If there are no instances the attendee is a part of, it 513*a1a3b679SAndreas Boehler // means the attendee was removed and we need to send him a 514*a1a3b679SAndreas Boehler // CANCEL. 515*a1a3b679SAndreas Boehler $message->method = 'CANCEL'; 516*a1a3b679SAndreas Boehler 517*a1a3b679SAndreas Boehler // Creating the new iCalendar body. 518*a1a3b679SAndreas Boehler $icalMsg = new VCalendar(); 519*a1a3b679SAndreas Boehler $icalMsg->METHOD = $message->method; 520*a1a3b679SAndreas Boehler $event = $icalMsg->add('VEVENT', array( 521*a1a3b679SAndreas Boehler 'UID' => $message->uid, 522*a1a3b679SAndreas Boehler 'SEQUENCE' => $message->sequence, 523*a1a3b679SAndreas Boehler )); 524*a1a3b679SAndreas Boehler if (isset($calendar->VEVENT->SUMMARY)) { 525*a1a3b679SAndreas Boehler $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue()); 526*a1a3b679SAndreas Boehler } 527*a1a3b679SAndreas Boehler $event->add(clone $calendar->VEVENT->DTSTART); 528*a1a3b679SAndreas Boehler $org = $event->add('ORGANIZER', $eventInfo['organizer']); 529*a1a3b679SAndreas Boehler if ($eventInfo['organizerName']) $org['CN'] = $eventInfo['organizerName']; 530*a1a3b679SAndreas Boehler $event->add('ATTENDEE', $attendee['href'], array( 531*a1a3b679SAndreas Boehler 'CN' => $attendee['name'], 532*a1a3b679SAndreas Boehler )); 533*a1a3b679SAndreas Boehler $message->significantChange = true; 534*a1a3b679SAndreas Boehler 535*a1a3b679SAndreas Boehler } else { 536*a1a3b679SAndreas Boehler 537*a1a3b679SAndreas Boehler // The attendee gets the updated event body 538*a1a3b679SAndreas Boehler $message->method = 'REQUEST'; 539*a1a3b679SAndreas Boehler 540*a1a3b679SAndreas Boehler // Creating the new iCalendar body. 541*a1a3b679SAndreas Boehler $icalMsg = new VCalendar(); 542*a1a3b679SAndreas Boehler $icalMsg->METHOD = $message->method; 543*a1a3b679SAndreas Boehler 544*a1a3b679SAndreas Boehler foreach($calendar->select('VTIMEZONE') as $timezone) { 545*a1a3b679SAndreas Boehler $icalMsg->add(clone $timezone); 546*a1a3b679SAndreas Boehler } 547*a1a3b679SAndreas Boehler 548*a1a3b679SAndreas Boehler // We need to find out that this change is significant. If it's 549*a1a3b679SAndreas Boehler // not, systems may opt to not send messages. 550*a1a3b679SAndreas Boehler // 551*a1a3b679SAndreas Boehler // We do this based on the 'significantChangeHash' which is 552*a1a3b679SAndreas Boehler // some value that changes if there's a certain set of 553*a1a3b679SAndreas Boehler // properties changed in the event, or simply if there's a 554*a1a3b679SAndreas Boehler // difference in instances that the attendee is invited to. 555*a1a3b679SAndreas Boehler 556*a1a3b679SAndreas Boehler $message->significantChange = 557*a1a3b679SAndreas Boehler $attendee['forceSend'] === 'REQUEST' || 558*a1a3b679SAndreas Boehler array_keys($attendee['oldInstances']) != array_keys($attendee['newInstances']) || 559*a1a3b679SAndreas Boehler $oldEventInfo['significantChangeHash']!==$eventInfo['significantChangeHash']; 560*a1a3b679SAndreas Boehler 561*a1a3b679SAndreas Boehler foreach($attendee['newInstances'] as $instanceId => $instanceInfo) { 562*a1a3b679SAndreas Boehler 563*a1a3b679SAndreas Boehler $currentEvent = clone $eventInfo['instances'][$instanceId]; 564*a1a3b679SAndreas Boehler if ($instanceId === 'master') { 565*a1a3b679SAndreas Boehler 566*a1a3b679SAndreas Boehler // We need to find a list of events that the attendee 567*a1a3b679SAndreas Boehler // is not a part of to add to the list of exceptions. 568*a1a3b679SAndreas Boehler $exceptions = array(); 569*a1a3b679SAndreas Boehler foreach($eventInfo['instances'] as $instanceId=>$vevent) { 570*a1a3b679SAndreas Boehler if (!isset($attendee['newInstances'][$instanceId])) { 571*a1a3b679SAndreas Boehler $exceptions[] = $instanceId; 572*a1a3b679SAndreas Boehler } 573*a1a3b679SAndreas Boehler } 574*a1a3b679SAndreas Boehler 575*a1a3b679SAndreas Boehler // If there were exceptions, we need to add it to an 576*a1a3b679SAndreas Boehler // existing EXDATE property, if it exists. 577*a1a3b679SAndreas Boehler if ($exceptions) { 578*a1a3b679SAndreas Boehler if (isset($currentEvent->EXDATE)) { 579*a1a3b679SAndreas Boehler $currentEvent->EXDATE->setParts(array_merge( 580*a1a3b679SAndreas Boehler $currentEvent->EXDATE->getParts(), 581*a1a3b679SAndreas Boehler $exceptions 582*a1a3b679SAndreas Boehler )); 583*a1a3b679SAndreas Boehler } else { 584*a1a3b679SAndreas Boehler $currentEvent->EXDATE = $exceptions; 585*a1a3b679SAndreas Boehler } 586*a1a3b679SAndreas Boehler } 587*a1a3b679SAndreas Boehler 588*a1a3b679SAndreas Boehler // Cleaning up any scheduling information that 589*a1a3b679SAndreas Boehler // shouldn't be sent along. 590*a1a3b679SAndreas Boehler unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); 591*a1a3b679SAndreas Boehler unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); 592*a1a3b679SAndreas Boehler 593*a1a3b679SAndreas Boehler foreach($currentEvent->ATTENDEE as $attendee) { 594*a1a3b679SAndreas Boehler unset($attendee['SCHEDULE-FORCE-SEND']); 595*a1a3b679SAndreas Boehler unset($attendee['SCHEDULE-STATUS']); 596*a1a3b679SAndreas Boehler 597*a1a3b679SAndreas Boehler // We're adding PARTSTAT=NEEDS-ACTION to ensure that 598*a1a3b679SAndreas Boehler // iOS shows an "Inbox Item" 599*a1a3b679SAndreas Boehler if (!isset($attendee['PARTSTAT'])) { 600*a1a3b679SAndreas Boehler $attendee['PARTSTAT'] = 'NEEDS-ACTION'; 601*a1a3b679SAndreas Boehler } 602*a1a3b679SAndreas Boehler 603*a1a3b679SAndreas Boehler } 604*a1a3b679SAndreas Boehler 605*a1a3b679SAndreas Boehler } 606*a1a3b679SAndreas Boehler 607*a1a3b679SAndreas Boehler $icalMsg->add($currentEvent); 608*a1a3b679SAndreas Boehler 609*a1a3b679SAndreas Boehler } 610*a1a3b679SAndreas Boehler 611*a1a3b679SAndreas Boehler } 612*a1a3b679SAndreas Boehler 613*a1a3b679SAndreas Boehler $message->message = $icalMsg; 614*a1a3b679SAndreas Boehler $messages[] = $message; 615*a1a3b679SAndreas Boehler 616*a1a3b679SAndreas Boehler } 617*a1a3b679SAndreas Boehler 618*a1a3b679SAndreas Boehler return $messages; 619*a1a3b679SAndreas Boehler 620*a1a3b679SAndreas Boehler } 621*a1a3b679SAndreas Boehler 622*a1a3b679SAndreas Boehler /** 623*a1a3b679SAndreas Boehler * Parse an event update for an attendee. 624*a1a3b679SAndreas Boehler * 625*a1a3b679SAndreas Boehler * This function figures out if we need to send a reply to an organizer. 626*a1a3b679SAndreas Boehler * 627*a1a3b679SAndreas Boehler * @param VCalendar $calendar 628*a1a3b679SAndreas Boehler * @param array $eventInfo 629*a1a3b679SAndreas Boehler * @param array $oldEventInfo 630*a1a3b679SAndreas Boehler * @param string $attendee 631*a1a3b679SAndreas Boehler * @return Message[] 632*a1a3b679SAndreas Boehler */ 633*a1a3b679SAndreas Boehler protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) { 634*a1a3b679SAndreas Boehler 635*a1a3b679SAndreas Boehler if ($this->scheduleAgentServerRules && $eventInfo['organizerScheduleAgent']==='CLIENT') { 636*a1a3b679SAndreas Boehler return array(); 637*a1a3b679SAndreas Boehler } 638*a1a3b679SAndreas Boehler 639*a1a3b679SAndreas Boehler // Don't bother generating messages for events that have already been 640*a1a3b679SAndreas Boehler // cancelled. 641*a1a3b679SAndreas Boehler if ($eventInfo['status']==='CANCELLED') { 642*a1a3b679SAndreas Boehler return array(); 643*a1a3b679SAndreas Boehler } 644*a1a3b679SAndreas Boehler 645*a1a3b679SAndreas Boehler $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? 646*a1a3b679SAndreas Boehler $oldEventInfo['attendees'][$attendee]['instances'] : 647*a1a3b679SAndreas Boehler array(); 648*a1a3b679SAndreas Boehler 649*a1a3b679SAndreas Boehler $instances = array(); 650*a1a3b679SAndreas Boehler foreach($oldInstances as $instance) { 651*a1a3b679SAndreas Boehler 652*a1a3b679SAndreas Boehler $instances[$instance['id']] = array( 653*a1a3b679SAndreas Boehler 'id' => $instance['id'], 654*a1a3b679SAndreas Boehler 'oldstatus' => $instance['partstat'], 655*a1a3b679SAndreas Boehler 'newstatus' => null, 656*a1a3b679SAndreas Boehler ); 657*a1a3b679SAndreas Boehler 658*a1a3b679SAndreas Boehler } 659*a1a3b679SAndreas Boehler foreach($eventInfo['attendees'][$attendee]['instances'] as $instance) { 660*a1a3b679SAndreas Boehler 661*a1a3b679SAndreas Boehler if (isset($instances[$instance['id']])) { 662*a1a3b679SAndreas Boehler $instances[$instance['id']]['newstatus'] = $instance['partstat']; 663*a1a3b679SAndreas Boehler } else { 664*a1a3b679SAndreas Boehler $instances[$instance['id']] = array( 665*a1a3b679SAndreas Boehler 'id' => $instance['id'], 666*a1a3b679SAndreas Boehler 'oldstatus' => null, 667*a1a3b679SAndreas Boehler 'newstatus' => $instance['partstat'], 668*a1a3b679SAndreas Boehler ); 669*a1a3b679SAndreas Boehler } 670*a1a3b679SAndreas Boehler 671*a1a3b679SAndreas Boehler } 672*a1a3b679SAndreas Boehler 673*a1a3b679SAndreas Boehler // We need to also look for differences in EXDATE. If there are new 674*a1a3b679SAndreas Boehler // items in EXDATE, it means that an attendee deleted instances of an 675*a1a3b679SAndreas Boehler // event, which means we need to send DECLINED specifically for those 676*a1a3b679SAndreas Boehler // instances. 677*a1a3b679SAndreas Boehler // We only need to do that though, if the master event is not declined. 678*a1a3b679SAndreas Boehler if (isset($instances['master']) && $instances['master']['newstatus'] !== 'DECLINED') { 679*a1a3b679SAndreas Boehler foreach($eventInfo['exdate'] as $exDate) { 680*a1a3b679SAndreas Boehler 681*a1a3b679SAndreas Boehler if (!in_array($exDate, $oldEventInfo['exdate'])) { 682*a1a3b679SAndreas Boehler if (isset($instances[$exDate])) { 683*a1a3b679SAndreas Boehler $instances[$exDate]['newstatus'] = 'DECLINED'; 684*a1a3b679SAndreas Boehler } else { 685*a1a3b679SAndreas Boehler $instances[$exDate] = array( 686*a1a3b679SAndreas Boehler 'id' => $exDate, 687*a1a3b679SAndreas Boehler 'oldstatus' => null, 688*a1a3b679SAndreas Boehler 'newstatus' => 'DECLINED', 689*a1a3b679SAndreas Boehler ); 690*a1a3b679SAndreas Boehler } 691*a1a3b679SAndreas Boehler } 692*a1a3b679SAndreas Boehler 693*a1a3b679SAndreas Boehler } 694*a1a3b679SAndreas Boehler } 695*a1a3b679SAndreas Boehler 696*a1a3b679SAndreas Boehler // Gathering a few extra properties for each instance. 697*a1a3b679SAndreas Boehler foreach($instances as $recurId=>$instanceInfo) { 698*a1a3b679SAndreas Boehler 699*a1a3b679SAndreas Boehler if (isset($eventInfo['instances'][$recurId])) { 700*a1a3b679SAndreas Boehler $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART; 701*a1a3b679SAndreas Boehler } else { 702*a1a3b679SAndreas Boehler $instances[$recurId]['dtstart'] = $recurId; 703*a1a3b679SAndreas Boehler } 704*a1a3b679SAndreas Boehler 705*a1a3b679SAndreas Boehler } 706*a1a3b679SAndreas Boehler 707*a1a3b679SAndreas Boehler $message = new Message(); 708*a1a3b679SAndreas Boehler $message->uid = $eventInfo['uid']; 709*a1a3b679SAndreas Boehler $message->method = 'REPLY'; 710*a1a3b679SAndreas Boehler $message->component = 'VEVENT'; 711*a1a3b679SAndreas Boehler $message->sequence = $eventInfo['sequence']; 712*a1a3b679SAndreas Boehler $message->sender = $attendee; 713*a1a3b679SAndreas Boehler $message->senderName = $eventInfo['attendees'][$attendee]['name']; 714*a1a3b679SAndreas Boehler $message->recipient = $eventInfo['organizer']; 715*a1a3b679SAndreas Boehler $message->recipientName = $eventInfo['organizerName']; 716*a1a3b679SAndreas Boehler 717*a1a3b679SAndreas Boehler $icalMsg = new VCalendar(); 718*a1a3b679SAndreas Boehler $icalMsg->METHOD = 'REPLY'; 719*a1a3b679SAndreas Boehler 720*a1a3b679SAndreas Boehler $hasReply = false; 721*a1a3b679SAndreas Boehler 722*a1a3b679SAndreas Boehler foreach($instances as $instance) { 723*a1a3b679SAndreas Boehler 724*a1a3b679SAndreas Boehler if ($instance['oldstatus']==$instance['newstatus'] && $eventInfo['organizerForceSend'] !== 'REPLY') { 725*a1a3b679SAndreas Boehler // Skip 726*a1a3b679SAndreas Boehler continue; 727*a1a3b679SAndreas Boehler } 728*a1a3b679SAndreas Boehler 729*a1a3b679SAndreas Boehler $event = $icalMsg->add('VEVENT', array( 730*a1a3b679SAndreas Boehler 'UID' => $message->uid, 731*a1a3b679SAndreas Boehler 'SEQUENCE' => $message->sequence, 732*a1a3b679SAndreas Boehler )); 733*a1a3b679SAndreas Boehler $summary = isset($calendar->VEVENT->SUMMARY)?$calendar->VEVENT->SUMMARY->getValue():''; 734*a1a3b679SAndreas Boehler // Adding properties from the correct source instance 735*a1a3b679SAndreas Boehler if (isset($eventInfo['instances'][$instance['id']])) { 736*a1a3b679SAndreas Boehler $instanceObj = $eventInfo['instances'][$instance['id']]; 737*a1a3b679SAndreas Boehler $event->add(clone $instanceObj->DTSTART); 738*a1a3b679SAndreas Boehler if (isset($instanceObj->SUMMARY)) { 739*a1a3b679SAndreas Boehler $event->add('SUMMARY', $instanceObj->SUMMARY->getValue()); 740*a1a3b679SAndreas Boehler } elseif ($summary) { 741*a1a3b679SAndreas Boehler $event->add('SUMMARY', $summary); 742*a1a3b679SAndreas Boehler } 743*a1a3b679SAndreas Boehler } else { 744*a1a3b679SAndreas Boehler // This branch of the code is reached, when a reply is 745*a1a3b679SAndreas Boehler // generated for an instance of a recurring event, through the 746*a1a3b679SAndreas Boehler // fact that the instance has disappeared by showing up in 747*a1a3b679SAndreas Boehler // EXDATE 748*a1a3b679SAndreas Boehler $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); 749*a1a3b679SAndreas Boehler // Treat is as a DATE field 750*a1a3b679SAndreas Boehler if (strlen($instance['id']) <= 8) { 751*a1a3b679SAndreas Boehler $recur = $event->add('DTSTART', $dt, array('VALUE' => 'DATE')); 752*a1a3b679SAndreas Boehler } else { 753*a1a3b679SAndreas Boehler $recur = $event->add('DTSTART', $dt); 754*a1a3b679SAndreas Boehler } 755*a1a3b679SAndreas Boehler if ($summary) { 756*a1a3b679SAndreas Boehler $event->add('SUMMARY', $summary); 757*a1a3b679SAndreas Boehler } 758*a1a3b679SAndreas Boehler } 759*a1a3b679SAndreas Boehler if ($instance['id'] !== 'master') { 760*a1a3b679SAndreas Boehler $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); 761*a1a3b679SAndreas Boehler // Treat is as a DATE field 762*a1a3b679SAndreas Boehler if (strlen($instance['id']) <= 8) { 763*a1a3b679SAndreas Boehler $recur = $event->add('RECURRENCE-ID', $dt, array('VALUE' => 'DATE')); 764*a1a3b679SAndreas Boehler } else { 765*a1a3b679SAndreas Boehler $recur = $event->add('RECURRENCE-ID', $dt); 766*a1a3b679SAndreas Boehler } 767*a1a3b679SAndreas Boehler } 768*a1a3b679SAndreas Boehler $organizer = $event->add('ORGANIZER', $message->recipient); 769*a1a3b679SAndreas Boehler if ($message->recipientName) { 770*a1a3b679SAndreas Boehler $organizer['CN'] = $message->recipientName; 771*a1a3b679SAndreas Boehler } 772*a1a3b679SAndreas Boehler $attendee = $event->add('ATTENDEE', $message->sender, array( 773*a1a3b679SAndreas Boehler 'PARTSTAT' => $instance['newstatus'] 774*a1a3b679SAndreas Boehler )); 775*a1a3b679SAndreas Boehler if ($message->senderName) { 776*a1a3b679SAndreas Boehler $attendee['CN'] = $message->senderName; 777*a1a3b679SAndreas Boehler } 778*a1a3b679SAndreas Boehler $hasReply = true; 779*a1a3b679SAndreas Boehler 780*a1a3b679SAndreas Boehler } 781*a1a3b679SAndreas Boehler 782*a1a3b679SAndreas Boehler if ($hasReply) { 783*a1a3b679SAndreas Boehler $message->message = $icalMsg; 784*a1a3b679SAndreas Boehler return array($message); 785*a1a3b679SAndreas Boehler } else { 786*a1a3b679SAndreas Boehler return array(); 787*a1a3b679SAndreas Boehler } 788*a1a3b679SAndreas Boehler 789*a1a3b679SAndreas Boehler } 790*a1a3b679SAndreas Boehler 791*a1a3b679SAndreas Boehler /** 792*a1a3b679SAndreas Boehler * Returns attendee information and information about instances of an 793*a1a3b679SAndreas Boehler * event. 794*a1a3b679SAndreas Boehler * 795*a1a3b679SAndreas Boehler * Returns an array with the following keys: 796*a1a3b679SAndreas Boehler * 797*a1a3b679SAndreas Boehler * 1. uid 798*a1a3b679SAndreas Boehler * 2. organizer 799*a1a3b679SAndreas Boehler * 3. organizerName 800*a1a3b679SAndreas Boehler * 4. attendees 801*a1a3b679SAndreas Boehler * 5. instances 802*a1a3b679SAndreas Boehler * 803*a1a3b679SAndreas Boehler * @param VCalendar $calendar 804*a1a3b679SAndreas Boehler * @return array 805*a1a3b679SAndreas Boehler */ 806*a1a3b679SAndreas Boehler protected function parseEventInfo(VCalendar $calendar = null) { 807*a1a3b679SAndreas Boehler 808*a1a3b679SAndreas Boehler $uid = null; 809*a1a3b679SAndreas Boehler $organizer = null; 810*a1a3b679SAndreas Boehler $organizerName = null; 811*a1a3b679SAndreas Boehler $organizerForceSend = null; 812*a1a3b679SAndreas Boehler $sequence = null; 813*a1a3b679SAndreas Boehler $timezone = null; 814*a1a3b679SAndreas Boehler $status = null; 815*a1a3b679SAndreas Boehler $organizerScheduleAgent = 'SERVER'; 816*a1a3b679SAndreas Boehler 817*a1a3b679SAndreas Boehler $significantChangeHash = ''; 818*a1a3b679SAndreas Boehler 819*a1a3b679SAndreas Boehler // Now we need to collect a list of attendees, and which instances they 820*a1a3b679SAndreas Boehler // are a part of. 821*a1a3b679SAndreas Boehler $attendees = array(); 822*a1a3b679SAndreas Boehler 823*a1a3b679SAndreas Boehler $instances = array(); 824*a1a3b679SAndreas Boehler $exdate = array(); 825*a1a3b679SAndreas Boehler 826*a1a3b679SAndreas Boehler foreach($calendar->VEVENT as $vevent) { 827*a1a3b679SAndreas Boehler 828*a1a3b679SAndreas Boehler if (is_null($uid)) { 829*a1a3b679SAndreas Boehler $uid = $vevent->UID->getValue(); 830*a1a3b679SAndreas Boehler } else { 831*a1a3b679SAndreas Boehler if ($uid !== $vevent->UID->getValue()) { 832*a1a3b679SAndreas Boehler throw new ITipException('If a calendar contained more than one event, they must have the same UID.'); 833*a1a3b679SAndreas Boehler } 834*a1a3b679SAndreas Boehler } 835*a1a3b679SAndreas Boehler 836*a1a3b679SAndreas Boehler if (!isset($vevent->DTSTART)) { 837*a1a3b679SAndreas Boehler throw new ITipException('An event MUST have a DTSTART property.'); 838*a1a3b679SAndreas Boehler } 839*a1a3b679SAndreas Boehler 840*a1a3b679SAndreas Boehler if (isset($vevent->ORGANIZER)) { 841*a1a3b679SAndreas Boehler if (is_null($organizer)) { 842*a1a3b679SAndreas Boehler $organizer = $vevent->ORGANIZER->getNormalizedValue(); 843*a1a3b679SAndreas Boehler $organizerName = isset($vevent->ORGANIZER['CN'])?$vevent->ORGANIZER['CN']:null; 844*a1a3b679SAndreas Boehler } else { 845*a1a3b679SAndreas Boehler if ($organizer !== $vevent->ORGANIZER->getNormalizedValue()) { 846*a1a3b679SAndreas Boehler throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); 847*a1a3b679SAndreas Boehler } 848*a1a3b679SAndreas Boehler } 849*a1a3b679SAndreas Boehler $organizerForceSend = 850*a1a3b679SAndreas Boehler isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ? 851*a1a3b679SAndreas Boehler strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) : 852*a1a3b679SAndreas Boehler null; 853*a1a3b679SAndreas Boehler $organizerScheduleAgent = 854*a1a3b679SAndreas Boehler isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ? 855*a1a3b679SAndreas Boehler strtoupper((string)$vevent->ORGANIZER['SCHEDULE-AGENT']) : 856*a1a3b679SAndreas Boehler 'SERVER'; 857*a1a3b679SAndreas Boehler } 858*a1a3b679SAndreas Boehler if (is_null($sequence) && isset($vevent->SEQUENCE)) { 859*a1a3b679SAndreas Boehler $sequence = $vevent->SEQUENCE->getValue(); 860*a1a3b679SAndreas Boehler } 861*a1a3b679SAndreas Boehler if (isset($vevent->EXDATE)) { 862*a1a3b679SAndreas Boehler foreach ($vevent->select('EXDATE') as $val) { 863*a1a3b679SAndreas Boehler $exdate = array_merge($exdate, $val->getParts()); 864*a1a3b679SAndreas Boehler } 865*a1a3b679SAndreas Boehler sort($exdate); 866*a1a3b679SAndreas Boehler } 867*a1a3b679SAndreas Boehler if (isset($vevent->STATUS)) { 868*a1a3b679SAndreas Boehler $status = strtoupper($vevent->STATUS->getValue()); 869*a1a3b679SAndreas Boehler } 870*a1a3b679SAndreas Boehler 871*a1a3b679SAndreas Boehler $recurId = isset($vevent->{'RECURRENCE-ID'})?$vevent->{'RECURRENCE-ID'}->getValue():'master'; 872*a1a3b679SAndreas Boehler if ($recurId==='master') { 873*a1a3b679SAndreas Boehler $timezone = $vevent->DTSTART->getDateTime()->getTimeZone(); 874*a1a3b679SAndreas Boehler } 875*a1a3b679SAndreas Boehler if(isset($vevent->ATTENDEE)) { 876*a1a3b679SAndreas Boehler foreach($vevent->ATTENDEE as $attendee) { 877*a1a3b679SAndreas Boehler 878*a1a3b679SAndreas Boehler if ($this->scheduleAgentServerRules && 879*a1a3b679SAndreas Boehler isset($attendee['SCHEDULE-AGENT']) && 880*a1a3b679SAndreas Boehler strtoupper($attendee['SCHEDULE-AGENT']->getValue()) === 'CLIENT' 881*a1a3b679SAndreas Boehler ) { 882*a1a3b679SAndreas Boehler continue; 883*a1a3b679SAndreas Boehler } 884*a1a3b679SAndreas Boehler $partStat = 885*a1a3b679SAndreas Boehler isset($attendee['PARTSTAT']) ? 886*a1a3b679SAndreas Boehler strtoupper($attendee['PARTSTAT']) : 887*a1a3b679SAndreas Boehler 'NEEDS-ACTION'; 888*a1a3b679SAndreas Boehler 889*a1a3b679SAndreas Boehler $forceSend = 890*a1a3b679SAndreas Boehler isset($attendee['SCHEDULE-FORCE-SEND']) ? 891*a1a3b679SAndreas Boehler strtoupper($attendee['SCHEDULE-FORCE-SEND']) : 892*a1a3b679SAndreas Boehler null; 893*a1a3b679SAndreas Boehler 894*a1a3b679SAndreas Boehler 895*a1a3b679SAndreas Boehler if (isset($attendees[$attendee->getNormalizedValue()])) { 896*a1a3b679SAndreas Boehler $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = array( 897*a1a3b679SAndreas Boehler 'id' => $recurId, 898*a1a3b679SAndreas Boehler 'partstat' => $partStat, 899*a1a3b679SAndreas Boehler 'force-send' => $forceSend, 900*a1a3b679SAndreas Boehler ); 901*a1a3b679SAndreas Boehler } else { 902*a1a3b679SAndreas Boehler $attendees[$attendee->getNormalizedValue()] = array( 903*a1a3b679SAndreas Boehler 'href' => $attendee->getNormalizedValue(), 904*a1a3b679SAndreas Boehler 'instances' => array( 905*a1a3b679SAndreas Boehler $recurId => array( 906*a1a3b679SAndreas Boehler 'id' => $recurId, 907*a1a3b679SAndreas Boehler 'partstat' => $partStat, 908*a1a3b679SAndreas Boehler ), 909*a1a3b679SAndreas Boehler ), 910*a1a3b679SAndreas Boehler 'name' => isset($attendee['CN'])?(string)$attendee['CN']:null, 911*a1a3b679SAndreas Boehler 'forceSend' => $forceSend, 912*a1a3b679SAndreas Boehler ); 913*a1a3b679SAndreas Boehler } 914*a1a3b679SAndreas Boehler 915*a1a3b679SAndreas Boehler } 916*a1a3b679SAndreas Boehler $instances[$recurId] = $vevent; 917*a1a3b679SAndreas Boehler 918*a1a3b679SAndreas Boehler } 919*a1a3b679SAndreas Boehler 920*a1a3b679SAndreas Boehler foreach($this->significantChangeProperties as $prop) { 921*a1a3b679SAndreas Boehler if (isset($vevent->$prop)) { 922*a1a3b679SAndreas Boehler $propertyValues = $vevent->select($prop); 923*a1a3b679SAndreas Boehler 924*a1a3b679SAndreas Boehler $significantChangeHash.=$prop.':'; 925*a1a3b679SAndreas Boehler 926*a1a3b679SAndreas Boehler if ($prop === 'EXDATE') { 927*a1a3b679SAndreas Boehler 928*a1a3b679SAndreas Boehler $significantChangeHash.= implode(',', $exdate).';'; 929*a1a3b679SAndreas Boehler 930*a1a3b679SAndreas Boehler } else { 931*a1a3b679SAndreas Boehler 932*a1a3b679SAndreas Boehler foreach($propertyValues as $val) { 933*a1a3b679SAndreas Boehler $significantChangeHash.= $val->getValue().';'; 934*a1a3b679SAndreas Boehler } 935*a1a3b679SAndreas Boehler 936*a1a3b679SAndreas Boehler } 937*a1a3b679SAndreas Boehler } 938*a1a3b679SAndreas Boehler } 939*a1a3b679SAndreas Boehler 940*a1a3b679SAndreas Boehler } 941*a1a3b679SAndreas Boehler $significantChangeHash = md5($significantChangeHash); 942*a1a3b679SAndreas Boehler 943*a1a3b679SAndreas Boehler return compact( 944*a1a3b679SAndreas Boehler 'uid', 945*a1a3b679SAndreas Boehler 'organizer', 946*a1a3b679SAndreas Boehler 'organizerName', 947*a1a3b679SAndreas Boehler 'organizerScheduleAgent', 948*a1a3b679SAndreas Boehler 'organizerForceSend', 949*a1a3b679SAndreas Boehler 'instances', 950*a1a3b679SAndreas Boehler 'attendees', 951*a1a3b679SAndreas Boehler 'sequence', 952*a1a3b679SAndreas Boehler 'exdate', 953*a1a3b679SAndreas Boehler 'timezone', 954*a1a3b679SAndreas Boehler 'significantChangeHash', 955*a1a3b679SAndreas Boehler 'status' 956*a1a3b679SAndreas Boehler ); 957*a1a3b679SAndreas Boehler 958*a1a3b679SAndreas Boehler } 959*a1a3b679SAndreas Boehler 960*a1a3b679SAndreas Boehler} 961