xref: /plugin/davcal/vendor/sabre/vobject/lib/ITip/Broker.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
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