1/*
2  Copyright (C) 2007-2020 Inverse inc.
3
4  This file is part of SOGo
5
6  SOGo is free software; you can redistribute it and/or modify it under
7  the terms of the GNU Lesser General Public License as published by the
8  Free Software Foundation; either version 2, or (at your option) any
9  later version.
10
11  SOGo is distributed in the hope that it will be useful, but WITHOUT ANY
12  WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14  License for more details.
15
16  You should have received a copy of the GNU Lesser General Public
17  License along with OGo; see the file COPYING.  If not, write to the
18  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19  02111-1307, USA.
20*/
21
22#import <Foundation/NSCalendarDate.h>
23#import <Foundation/NSTimeZone.h>
24#import <Foundation/NSValue.h>
25
26#import <NGObjWeb/NSException+HTTP.h>
27#import <NGObjWeb/WOContext+SoObjects.h>
28#import <NGObjWeb/WOResponse.h>
29#import <NGExtensions/NGCalendarDateRange.h>
30#import <NGExtensions/NSCalendarDate+misc.h>
31#import <NGExtensions/NSNull+misc.h>
32#import <NGExtensions/NSObject+Logs.h>
33#import <NGCards/iCalDateTime.h>
34#import <NGCards/iCalEvent.h>
35#import <NGCards/iCalToDo.h>
36#import <NGCards/NSCalendarDate+NGCards.h>
37#import <SaxObjC/XMLNamespaces.h>
38
39#import <NGCards/iCalDateTime.h>
40#import <NGCards/iCalTimeZone.h>
41#import <NGCards/iCalTimeZonePeriod.h>
42#import <NGCards/iCalToDo.h>
43#import <NGCards/NSString+NGCards.h>
44
45#import <SOGo/NSArray+Utilities.h>
46#import <SOGo/NSDictionary+Utilities.h>
47#import <SOGo/NSObject+DAV.h>
48#import <SOGo/NSString+Utilities.h>
49#import <SOGo/SOGoDateFormatter.h>
50#import <SOGo/SOGoUserManager.h>
51#import <SOGo/SOGoUser.h>
52#import <SOGo/SOGoUserSettings.h>
53#import <SOGo/SOGoDomainDefaults.h>
54#import <SOGo/WORequest+SOGo.h>
55
56#import "iCalCalendar+SOGo.h"
57#import "iCalEvent+SOGo.h"
58#import "iCalEventChanges+SOGo.h"
59#import "iCalEntityObject+SOGo.h"
60#import "iCalPerson+SOGo.h"
61#import "NSArray+Appointments.h"
62#import "SOGoAppointmentFolder.h"
63#import "SOGoAppointmentOccurence.h"
64#import "SOGoFreeBusyObject.h"
65
66#import "SOGoAppointmentObject.h"
67
68@implementation SOGoAppointmentObject
69
70- (NSString *) componentTag
71{
72  return @"vevent";
73}
74
75- (SOGoComponentOccurence *) occurence: (iCalRepeatableEntityObject *) occ
76{
77  NSArray *allEvents;
78
79  allEvents = [[occ parent] events];
80
81  return [SOGoAppointmentOccurence
82	   occurenceWithComponent: occ
83	   withMasterComponent: [allEvents objectAtIndex: 0]
84	   inContainer: self];
85}
86
87/**
88 * Return a new exception in the recurrent event.
89 * @param theRecurrenceID the ID of the occurence.
90 * @return a new occurence.
91 */
92- (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) theRecurrenceID
93{
94  iCalEvent *newOccurence, *master;
95  NSCalendarDate *date, *firstDate;
96  unsigned int interval, nbrDays;
97
98  newOccurence = (iCalEvent *) [super newOccurenceWithID: theRecurrenceID];
99  date = [newOccurence recurrenceId];
100
101  master = [self component: NO secure: NO];
102  firstDate = [master startDate];
103
104  interval = [[master endDate]
105               timeIntervalSinceDate: firstDate];
106  if ([newOccurence isAllDay])
107    {
108      nbrDays = ((float) abs (interval) / 86400);
109      [newOccurence setAllDayWithStartDate: date
110                                  duration: nbrDays];
111    }
112  else
113    {
114      [newOccurence setStartDate: date];
115      [newOccurence setEndDate: [date addYear: 0
116                                        month: 0
117                                          day: 0
118                                         hour: 0
119                                       minute: 0
120                                       second: interval]];
121    }
122
123  return newOccurence;
124}
125
126- (iCalRepeatableEntityObject *) lookupOccurrence: (NSString *) recID
127{
128  return [[self calendar: NO secure: NO] eventWithRecurrenceID: recID];
129}
130
131- (SOGoAppointmentObject *) _lookupEvent: (NSString *) eventUID
132				  forUID: (NSString *) uid
133{
134  SOGoAppointmentFolder *folder;
135  SOGoAppointmentObject *object;
136  NSArray *folders;
137  NSEnumerator *e;
138  NSString *possibleName;
139
140  object = nil;
141  folders = [container lookupCalendarFoldersForUID: uid];
142  e = [folders objectEnumerator];
143  while ( object == nil && (folder = [e nextObject]) )
144    {
145      object = [folder lookupName: nameInContainer
146                        inContext: context
147                          acquire: NO];
148      if ([object isKindOfClass: [NSException class]] || [object isNew])
149        {
150          possibleName = [folder resourceNameForEventUID: eventUID];
151          if (possibleName)
152            {
153              object = [folder lookupName: possibleName
154                                inContext: context acquire: NO];
155              if ([object isKindOfClass: [NSException class]] || [object isNew])
156                object = nil;
157            }
158          else
159            object = nil;
160        }
161    }
162
163  if (!object)
164    {
165      // Create the event in the user's personal calendar.
166      folder = [[SOGoUser userWithLogin: uid]
167                 personalCalendarFolderInContext: context];
168      object = [SOGoAppointmentObject objectWithName: nameInContainer
169				                                 inContainer: folder];
170      [object setIsNew: YES];
171    }
172
173  return object;
174}
175
176//
177// This method will *ONLY* add or update event information in attendees' calendars.
178// It will NOT touch to the organizer calendar in anyway. This method is meant
179// to reflect changes in attendees' calendars when the organizer makes changes
180// to the event.
181//
182- (void) _addOrUpdateEvent: (iCalEvent *) newEvent
183                  oldEvent: (iCalEvent *) oldEvent
184                    forUID: (NSString *) theUID
185                     owner: (NSString *) theOwner
186{
187  if (![theUID isEqualToString: theOwner])
188    {
189      SOGoAppointmentObject *attendeeObject;
190      iCalCalendar *iCalendarToSave;
191      iCalPerson *attendee;
192      SOGoUser *user;
193
194      iCalendarToSave = nil;
195      user = [SOGoUser userWithLogin: theUID];
196      attendeeObject = [self _lookupEvent: [newEvent uid] forUID: theUID];
197      attendee = [newEvent userAsAttendee: user];
198
199      // If the atttende's role is NON-PARTICIPANT, we write nothing to its calendar
200      if ([[attendee role] caseInsensitiveCompare: @"NON-PARTICIPANT"] == NSOrderedSame)
201        {
202          // If the attendee's previous role was not NON-PARTICIPANT we must also delete
203          // the event from its calendar
204          attendee = [oldEvent userAsAttendee: user];
205          if ([[attendee role] caseInsensitiveCompare: @"NON-PARTICIPANT"] != NSOrderedSame)
206            {
207              NSString *currentUID;
208
209              currentUID = [attendee uidInContext: context];
210              if (currentUID)
211                [self _removeEventFromUID: currentUID
212                                    owner: owner
213                         withRecurrenceId: [oldEvent recurrenceId]];
214
215            }
216
217          return;
218        }
219
220      if ([newEvent recurrenceId])
221        {
222          // We must add an occurence to a non-existing event.
223          if ([attendeeObject isNew])
224            {
225              iCalEvent *ownerEvent;
226
227              // We check if the attendee that was added to a single occurence is
228              // present in the master component. If not, we create a calendar with
229              // a single event for the occurence.
230              ownerEvent = [[[newEvent parent] events] objectAtIndex: 0];
231
232              if (![ownerEvent userAsAttendee: user])
233                {
234                  iCalendarToSave = [[[newEvent parent] mutableCopy] autorelease];
235                  [iCalendarToSave removeChildren: [iCalendarToSave childrenWithTag: @"vevent"]];
236                  [iCalendarToSave addChild: [[newEvent copy] autorelease]];
237                }
238            }
239          else
240            {
241              // Only update this occurrence in attendee's calendar
242              // TODO : when updating the master event, handle exception dates
243              // in attendee's calendar (add exception dates and remove matching
244              // occurrences) -- see _updateRecurrenceIDsWithEvent:
245              NSCalendarDate *currentId;
246              NSArray *occurences;
247              iCalEvent *occurence;
248              int max, count;
249
250              iCalendarToSave = [attendeeObject calendar: NO  secure: NO];
251
252              // If recurrenceId is defined, remove the occurence from
253              // the repeating event. If a recurrenceId is defined in the
254              // new event, let's make sure we don't already have one in
255              // the calendar already. If so, also remove it.
256              if ([oldEvent recurrenceId] || [newEvent recurrenceId])
257                {
258                  // FIXME: use  _eventFromRecurrenceId:...
259                  occurences = [iCalendarToSave events];
260                  currentId = ([oldEvent recurrenceId] ? [oldEvent recurrenceId]: [newEvent recurrenceId]);
261                  if (currentId)
262                    {
263                      max = [occurences count];
264                      count = 0;
265                      while (count < max)
266                        {
267                          occurence = [occurences objectAtIndex: count];
268                          if ([occurence recurrenceId] &&
269                              [[occurence recurrenceId] compare: currentId] == NSOrderedSame)
270                            {
271                              [iCalendarToSave removeChild: occurence];
272                              break;
273                            }
274                          count++;
275                        }
276                    }
277                }
278
279              [iCalendarToSave addChild: [[newEvent copy] autorelease]];
280            }
281        }
282      else
283        {
284          iCalendarToSave = [newEvent parent];
285        }
286
287      // Save the event in the attendee's calendar
288      if (iCalendarToSave)
289        [attendeeObject saveCalendar: iCalendarToSave];
290    }
291}
292
293
294//
295// This method will *ONLY* delete event information in attendees' calendars.
296// It will NOT touch to the organizer calendar in anyway. This method is meant
297// to reflect changes in attendees' calendars when the organizer makes changes
298// to the event.
299//
300- (void) _removeEventFromUID: (NSString *) theUID
301                       owner: (NSString *) theOwner
302            withRecurrenceId: (NSCalendarDate *) recurrenceId
303{
304  if (![theUID isEqualToString: theOwner])
305    {
306      SOGoAppointmentFolder *folder;
307      SOGoAppointmentObject *object;
308      iCalEntityObject *currentOccurence;
309      iCalRepeatableEntityObject *event;
310      iCalCalendar *calendar;
311      NSCalendarDate *currentId;
312      NSArray *occurences;
313      int max, count;
314
315      // Invitations are always written to the personal folder; it's not necessay
316      // to look into all folders of the user
317      // FIXME: why look only in the personal calendar here?
318      folder = [[SOGoUser userWithLogin: theUID]
319                personalCalendarFolderInContext: context];
320      object = [folder lookupName: nameInContainer
321                        inContext: context
322                          acquire: NO];
323      if (![object isKindOfClass: [NSException class]])
324        {
325          if (recurrenceId == nil)
326            [object delete];
327          else
328            {
329              calendar = [object calendar: NO secure: NO];
330
331              // If recurrenceId is defined, remove the occurence from
332              // the repeating event.
333              occurences = [calendar events];
334              max = [occurences count];
335              count = 0;
336              while (count < max)
337                {
338                  currentOccurence = [occurences objectAtIndex: count];
339                  currentId = [currentOccurence recurrenceId];
340                  if (currentId && [currentId compare: recurrenceId] == NSOrderedSame)
341                    {
342                      [[calendar children] removeObject: currentOccurence];
343                      break;
344                    }
345                  count++;
346                }
347
348              // Add an date exception.
349              event = (iCalRepeatableEntityObject*)[calendar firstChildWithTag: [object componentTag]];
350              if (event)
351                {
352                  [event addToExceptionDates: recurrenceId];
353                  [event increaseSequence];
354                  [event setLastModified: [NSCalendarDate calendarDate]];
355
356                  // We save the updated iCalendar in the database.
357                  [object saveCalendar: calendar];
358                }
359              else
360                {
361                  // No more child; kill the parent
362                  [object delete];
363                }
364            }
365        }
366      else
367        [self errorWithFormat: @"Unable to find event with UID %@ in %@'s calendar - skipping delete operation. This can be normal for NON-PARTICIPANT attendees.", nameInContainer, theUID];
368    }
369}
370
371//
372//
373//
374- (void) _handleRemovedUsers: (NSArray *) attendees
375            withRecurrenceId: (NSCalendarDate *) recurrenceId
376{
377  NSEnumerator *enumerator;
378  iCalPerson *currentAttendee;
379  NSString *currentUID;
380
381  enumerator = [attendees objectEnumerator];
382  while ((currentAttendee = [enumerator nextObject]))
383    {
384      currentUID = [currentAttendee uidInContext: context];
385      if (currentUID)
386        [self _removeEventFromUID: currentUID
387                            owner: owner
388                 withRecurrenceId: recurrenceId];
389    }
390}
391
392//
393//
394//
395- (void) _removeDelegationChain: (iCalPerson *) delegate
396                        inEvent: (iCalEvent *) event
397{
398  NSString *delegatedTo, *mailTo;
399
400  delegatedTo = [delegate delegatedTo];
401  if ([delegatedTo length] > 0)
402    {
403      mailTo = [delegatedTo rfc822Email];
404      delegate = [event findAttendeeWithEmail: mailTo];
405      if (delegate)
406        {
407          [self _removeDelegationChain: delegate
408                               inEvent: event];
409          [event removeFromAttendees: delegate];
410        }
411      else
412        [self errorWithFormat:@"broken chain: delegate with email '%@' was not found", mailTo];
413    }
414}
415
416//
417// This method returns YES when any attendee has been removed
418// and NO otherwise.
419//
420- (BOOL) _requireResponseFromAttendees: (iCalEvent *) event
421{
422  NSArray *attendees;
423  iCalPerson *currentAttendee;
424  BOOL listHasChanged = NO;
425  int count, max;
426
427  attendees = [event attendees];
428  max = [attendees count];
429
430  for (count = 0; count < max; count++)
431    {
432      currentAttendee = [attendees objectAtIndex: count];
433      if ([[currentAttendee delegatedTo] length] > 0)
434        {
435          [self _removeDelegationChain: currentAttendee
436                               inEvent: event];
437          [currentAttendee setDelegatedTo: nil];
438          listHasChanged = YES;
439        }
440      [currentAttendee setRsvp: @"TRUE"];
441      [currentAttendee setParticipationStatus: iCalPersonPartStatNeedsAction];
442    }
443
444  return listHasChanged;
445}
446
447//
448//
449//
450- (BOOL) _shouldScheduleEvent: (iCalPerson *) thePerson
451{
452  //NSArray *userAgents;
453  NSString *v;
454  BOOL b;
455  //int i;
456
457  b = YES;
458
459  if (thePerson && (v = [thePerson value: 0  ofAttribute: @"SCHEDULE-AGENT"]))
460    {
461      if ([v caseInsensitiveCompare: @"NONE"] == NSOrderedSame ||
462          [v caseInsensitiveCompare: @"CLIENT"] == NSOrderedSame)
463        b = NO;
464    }
465
466  //
467  // If we have to deal with Thunderbird/Lightning, we always send invitation
468  // reponses, as Lightning v2.6 (at least this version) sets SCHEDULE-AGENT
469  // to NONE/CLIENT when responding to an external invitation received by
470  // SOGo - so no invitation responses are ever sent by Lightning. See
471  // https://bugzilla.mozilla.org/show_bug.cgi?id=865726 and
472  // https://bugzilla.mozilla.org/show_bug.cgi?id=997784
473  //
474  // This code has been disabled - see 0003274.
475  //
476#if 0
477  userAgents = [[context request] headersForKey: @"User-Agent"];
478
479  for (i = 0; i < [userAgents count]; i++)
480    {
481      if ([[userAgents objectAtIndex: i] rangeOfString: @"Thunderbird"].location != NSNotFound &&
482          [[userAgents objectAtIndex: i] rangeOfString: @"Lightning"].location != NSNotFound)
483        {
484          b = YES;
485          break;
486        }
487    }
488#endif
489
490  return b;
491}
492
493//
494//
495//
496- (void) _handleSequenceUpdateInEvent: (iCalEvent *) newEvent
497		    ignoringAttendees: (NSArray *) attendees
498		         fromOldEvent: (iCalEvent *) oldEvent
499{
500  NSMutableArray *updateAttendees;
501  NSEnumerator *enumerator;
502  iCalPerson *currentAttendee;
503  NSString *currentUID;
504
505  updateAttendees = [NSMutableArray arrayWithArray: [newEvent attendees]];
506  [updateAttendees removeObjectsInArray: attendees];
507
508  enumerator = [updateAttendees objectEnumerator];
509  while ((currentAttendee = [enumerator nextObject]))
510    {
511      currentUID = [currentAttendee uidInContext: context];
512      if (currentUID)
513        [self _addOrUpdateEvent: newEvent
514                       oldEvent: oldEvent
515                         forUID: currentUID
516                          owner: owner];
517    }
518
519  if ([self _shouldScheduleEvent: [newEvent organizer]])
520    [self sendEMailUsingTemplateNamed: @"Update"
521			    forObject: [newEvent itipEntryWithMethod: @"request"]
522		       previousObject: oldEvent
523			  toAttendees: updateAttendees
524			     withType: @"calendar:invitation-update"];
525}
526
527// This method scans the list of attendees.
528- (NSException *) _handleAttendeesAvailability: (NSArray *) theAttendees
529                                      forEvent: (iCalEvent *) theEvent
530{
531  iCalPerson *currentAttendee;
532  SOGoUser *user;
533  SOGoUserSettings *us;
534  NSMutableArray *unavailableAttendees;
535  NSEnumerator *enumerator;
536  NSString *currentUID, *ownerUID;
537  NSMutableString *reason;
538  NSDictionary *values;
539  NSMutableDictionary *value, *moduleSettings;
540  id whiteList;
541
542  int i, count;
543
544  i = count = 0;
545
546  // Build list of the attendees uids
547  unavailableAttendees = [[NSMutableArray alloc] init];
548  enumerator = [theAttendees objectEnumerator];
549  ownerUID = [[[self context] activeUser] login];
550
551  while ((currentAttendee = [enumerator nextObject]))
552    {
553      currentUID = [currentAttendee uidInContext: context];
554
555      if (currentUID)
556        {
557          user = [SOGoUser userWithLogin: currentUID];
558          us = [user userSettings];
559          moduleSettings = [us objectForKey:@"Calendar"];
560
561          // Check if the user prevented their account from beeing invited to events
562          if ([[moduleSettings objectForKey:@"PreventInvitations"] boolValue])
563            {
564              // Check if the user have a whiteList
565              whiteList = [moduleSettings objectForKey:@"PreventInvitationsWhitelist"];
566
567              // For backward <= 2.2.17 compatibility
568              if ([whiteList isKindOfClass: [NSString class]])
569                whiteList = [whiteList objectFromJSONString];
570
571              // If the filter have a hit, do not add the currentUID to the unavailableAttendees array
572              if (![whiteList objectForKey:ownerUID])
573                {
574                  values = [NSDictionary dictionaryWithObject:[user cn] forKey:@"Cn"];
575                  [unavailableAttendees addObject:values];
576                }
577            }
578        }
579    }
580
581  count = [unavailableAttendees count];
582
583  if (count > 0)
584    {
585      reason = [NSMutableString stringWithString:[self labelForKey: @"Inviting the following persons is prohibited:"]];
586
587      // Add all the unavailable users in the warning message
588      for (i = 0; i < count; i++)
589        {
590          value = [unavailableAttendees objectAtIndex:i];
591          [reason appendString:[value keysWithFormat: @"\n %{Cn}"]];
592          if (i < count-2)
593            [reason appendString:@", "];
594        }
595
596      [unavailableAttendees release];
597
598      return [NSException exceptionWithHTTPStatus:409 reason: reason];
599    }
600
601  [unavailableAttendees release];
602
603  return nil;
604}
605
606//
607// This methods scans the list of attendees. If they are
608// considered as resource, it checks for conflicting
609// dates for the event and potentially auto-accept/decline
610// the invitation.
611//
612// For normal attendees, it'll return an exception with
613// conflicting dates, unless we force the save.//
614// We check for between startDate + 1 second and
615// endDate - 1 second
616//
617// Note that it doesn't matter if it changes the participation
618// status since in case of an error, nothing will get saved.
619//
620- (NSException *) _handleAttendeesConflicts: (NSArray *) theAttendees
621                                   forEvent: (iCalEvent *) theEvent
622                                      force: (BOOL) forceSave
623{
624  iCalPerson *currentAttendee;
625  NSMutableArray *attendees;
626  NSEnumerator *enumerator;
627  NSString *currentUID;
628  SOGoUser *user, *currentUser;
629
630  _resourceHasAutoAccepted = NO;
631
632  // Build a list of the attendees uids
633  attendees = [NSMutableArray arrayWithCapacity: [theAttendees count]];
634  enumerator = [theAttendees objectEnumerator];
635  while ((currentAttendee = [enumerator nextObject]))
636    {
637      currentUID = [currentAttendee uidInContext: context];
638      if (currentUID)
639        {
640          [attendees addObject: currentUID];
641        }
642    }
643
644  // If the active user is not the owner of the calendar, check possible conflict when
645  // the owner is a resource
646  currentUser = [context activeUser];
647  if (!activeUserIsOwner && ![currentUser isSuperUser])
648    {
649      [attendees addObject: owner];
650    }
651
652  enumerator = [attendees objectEnumerator];
653  while ((currentUID = [enumerator nextObject]))
654    {
655      NSCalendarDate *start, *end, *rangeStartDate, *rangeEndDate;
656      SOGoAppointmentFolder *folder;
657      SOGoFreeBusyObject *fb;
658      NGCalendarDateRange *range;
659      NSMutableArray *fbInfo;
660      NSArray *allOccurences;
661
662      BOOL must_delete;
663      int i, j, delta;
664
665      user = [SOGoUser userWithLogin: currentUID];
666
667      // We get the start/end date for our conflict range. If the event to be added is recurring, we
668      // check for at least a year to start with.
669      start = [[theEvent startDate] dateByAddingYears: 0  months: 0  days: 0  hours: 0  minutes: 0  seconds: 1];
670      end = [[theEvent endDate] dateByAddingYears: ([theEvent isRecurrent] ? 1 : 0)  months: 0  days: 0  hours: 0  minutes: 0  seconds: -1];
671
672      folder = [user personalCalendarFolderInContext: context];
673
674      // Deny access to the resource if the ACLs don't allow the user
675      if ([user isResource] && ![folder aclSQLListingFilter])
676        {
677          NSDictionary *values;
678          NSString *reason;
679
680          values = [NSDictionary dictionaryWithObjectsAndKeys:
681                                   [user cn], @"Cn",
682                                 [user systemEmail], @"SystemEmail", nil];
683          reason = [values keysWithFormat: [self labelForKey: @"Cannot access resource: \"%{Cn} %{SystemEmail}\""]];
684          return [NSException exceptionWithHTTPStatus:409 reason: reason];
685        }
686
687      fb = [SOGoFreeBusyObject objectWithName: @"freebusy.ifb" inContainer: [user homeFolderInContext: context]];
688      fbInfo = (NSMutableArray *)[fb fetchFreeBusyInfosFrom: start to: end];
689
690      //
691      // We must also check here for repetitive events that don't overlap our event.
692      // We remove all events that don't overlap. The events here are already
693      // decomposed.
694      //
695      if ([theEvent isRecurrent])
696        allOccurences = [theEvent recurrenceRangesWithinCalendarDateRange: [NGCalendarDateRange calendarDateRangeWithStartDate: start
697                                                                                                                       endDate: end]
698                                           firstInstanceCalendarDateRange: [NGCalendarDateRange calendarDateRangeWithStartDate: [theEvent startDate]
699                                                                                                                       endDate: [theEvent endDate]]];
700      else
701        allOccurences = nil;
702
703      for (i = [fbInfo count]-1; i >= 0; i--)
704        {
705	  // We first remove any occurences in the freebusy that corresponds to the
706	  // current event. We do this to avoid raising a conflict if we move a 1 hour
707	  // meeting from 12:00-13:00 to 12:15-13:15. We would overlap on ourself otherwise.
708          if ([[[fbInfo objectAtIndex: i] objectForKey: @"c_uid"] compare: [theEvent uid]] == NSOrderedSame)
709            {
710              [fbInfo removeObjectAtIndex: i];
711              continue;
712            }
713
714          // Ignore transparent events
715          if (![[[fbInfo objectAtIndex: i] objectForKey: @"c_isopaque"] boolValue])
716            {
717              [fbInfo removeObjectAtIndex: i];
718              continue;
719            }
720
721          // No need to check if the event isn't recurrent here as it's handled correctly
722          // when we compute the "end" date.
723          if ([allOccurences count])
724            {
725              must_delete = YES;
726
727              // We MUST use the -uniqueChildWithTag method here because the event has been flattened, so its timezone has been
728              // modified in SOGoAppointmentFolder: -fixupCycleRecord: ....
729              rangeStartDate = [[fbInfo objectAtIndex: i] objectForKey: @"startDate"];
730              delta = [[rangeStartDate timeZoneDetail] timeZoneSecondsFromGMT] - [[[(iCalDateTime *)[theEvent uniqueChildWithTag: @"dtstart"] timeZone] periodForDate: [theEvent startDate]] secondsOffsetFromGMT];
731              rangeStartDate = [rangeStartDate dateByAddingYears: 0  months: 0  days: 0  hours: 0  minutes: 0  seconds: delta];
732
733              rangeEndDate = [[fbInfo objectAtIndex: i] objectForKey: @"endDate"];
734              delta = [[rangeEndDate timeZoneDetail] timeZoneSecondsFromGMT] - [[[(iCalDateTime *)[theEvent uniqueChildWithTag: @"dtend"] timeZone] periodForDate: [theEvent endDate]] secondsOffsetFromGMT];
735              rangeEndDate = [rangeEndDate dateByAddingYears: 0  months: 0  days: 0  hours: 0  minutes: 0  seconds: delta];
736
737              range = [NGCalendarDateRange calendarDateRangeWithStartDate: rangeStartDate
738                                                                  endDate: rangeEndDate];
739
740              for (j = 0; j < [allOccurences count]; j++)
741                {
742                  if ([range doesIntersectWithDateRange: [allOccurences objectAtIndex: j]])
743                    {
744                      must_delete = NO;
745                      break;
746                    }
747                }
748              if (must_delete)
749                [fbInfo removeObjectAtIndex: i];
750            }
751        }
752
753      // Find the attendee associated to the current UID
754      currentAttendee = nil;
755      for (i = 0; i < [theAttendees count]; i++)
756        {
757          currentAttendee = [theAttendees objectAtIndex: i];
758          if ([[currentAttendee uidInContext: context] isEqualToString: currentUID])
759            break;
760          else
761            currentAttendee = nil;
762        }
763
764      if ([fbInfo count])
765        {
766          SOGoDateFormatter *formatter;
767
768          formatter = [[context activeUser] dateFormatterInContext: context];
769
770          if ([user isResource])
771            {
772              // We always force the auto-accept if numberOfSimultaneousBookings <= 0 (ie., no limit
773              // is imposed) or if numberOfSimultaneousBookings is greater than the number of
774              // overlapping events.
775              // When numberOfSimultaneousBookings is set to -1, only force the auto-accept
776              // once the conflict has been raised and the action is forced by the user.
777              if ([user numberOfSimultaneousBookings] <= 0 ||
778                  [user numberOfSimultaneousBookings] > [fbInfo count])
779                {
780                  if (currentAttendee && ([user numberOfSimultaneousBookings] >= 0 || forceSave))
781                    {
782                      [[currentAttendee attributes] removeObjectForKey: @"RSVP"];
783                      [currentAttendee setParticipationStatus: iCalPersonPartStatAccepted];
784		      _resourceHasAutoAccepted = YES;
785                    }
786                }
787              else
788                {
789                  iCalCalendar *calendar;
790                  NSDictionary *values, *info;
791                  NSString *reason;
792                  iCalEvent *event;
793
794                  calendar =  [iCalCalendar parseSingleFromSource: [[fbInfo objectAtIndex: 0] objectForKey: @"c_content"]];
795                  event = [[calendar events] lastObject];
796
797                  values = [NSDictionary dictionaryWithObjectsAndKeys:
798                                           [NSString stringWithFormat: @"%d", [user numberOfSimultaneousBookings]], @"NumberOfSimultaneousBookings",
799                                         [user cn], @"Cn",
800                                         [user systemEmail], @"SystemEmail",
801                                  ([event summary] ? [event summary] : @""), @"EventTitle",
802                                      [formatter formattedDateAndTime: [[fbInfo objectAtIndex: 0] objectForKey: @"startDate"]], @"StartDate",
803                                         nil];
804
805                  reason = [values keysWithFormat: [self labelForKey: @"Maximum number of simultaneous bookings (%{NumberOfSimultaneousBookings}) reached for resource \"%{Cn} %{SystemEmail}\". The conflicting event is \"%{EventTitle}\", and starts on %{StartDate}."]];
806
807                  info = [NSDictionary dictionaryWithObject: reason forKey: @"reject"];
808
809                  return [NSException exceptionWithHTTPStatus: 409
810                                                       reason: [info jsonRepresentation]];
811                }
812            }
813          //
814          // We are dealing with a normal attendee. Lets check if we have conflicts, unless
815          // we are being asked to force the save anyway
816          //
817          if (!forceSave && !_resourceHasAutoAccepted)
818            {
819              NSMutableDictionary *info;
820              NSMutableArray *conflicts;
821              NSString *formattedEnd;
822              SOGoUser *ownerUser;
823              id o;
824
825              info = [NSMutableDictionary dictionary];
826              conflicts = [NSMutableArray array];
827
828              if (currentAttendee)
829                {
830                  if ([currentAttendee cn])
831                    [info setObject: [currentAttendee cn]  forKey: @"attendee_name"];
832                  if ([currentAttendee rfc822Email])
833                    [info setObject: [currentAttendee rfc822Email]  forKey: @"attendee_email"];
834                }
835              else if ([owner isEqualToString: currentUID])
836                {
837                  ownerUser = [SOGoUser userWithLogin: owner];
838                  if ([ownerUser cn])
839                    [info setObject: [ownerUser cn]  forKey: @"attendee_name"];
840                  if ([ownerUser systemEmail])
841                    [info setObject: [ownerUser systemEmail]  forKey: @"attendee_email"];
842                }
843
844              for (i = 0; i < [fbInfo count]; i++)
845                {
846                  o = [fbInfo objectAtIndex: i];
847                  end = [o objectForKey: @"endDate"];
848                  if ([[o objectForKey: @"startDate"] isDateOnSameDay: end])
849                    formattedEnd = [formatter formattedTime: end];
850                  else
851                    formattedEnd = [formatter formattedDateAndTime: end];
852
853                  [conflicts addObject: [NSDictionary dictionaryWithObjectsAndKeys: [formatter formattedDateAndTime: [o objectForKey: @"startDate"]], @"startDate",
854                                                      formattedEnd, @"endDate", nil]];
855                }
856
857              [info setObject: conflicts  forKey: @"conflicts"];
858
859              // We immediately raise an exception, without processing the possible other attendees.
860              return [NSException exceptionWithHTTPStatus: 409
861                                                   reason: [info jsonRepresentation]];
862            }
863        } // if ([fbInfo count]) ...
864      else if (currentAttendee && [user isResource])
865        {
866          // No conflict, we auto-accept. We do this for resources automatically if no
867          // double-booking is observed. If it's not the desired behavior, just don't
868          // set the resource as one!
869          [[currentAttendee attributes] removeObjectForKey: @"RSVP"];
870          [currentAttendee setParticipationStatus: iCalPersonPartStatAccepted];
871          _resourceHasAutoAccepted = YES;
872        }
873    }
874
875  return nil;
876}
877
878//
879//
880//
881- (NSException *) _handleAddedUsers: (NSArray *) attendees
882                          fromEvent: (iCalEvent *) newEvent
883                              force: (BOOL) forceSave
884{
885  iCalPerson *currentAttendee;
886  NSEnumerator *enumerator;
887  NSString *currentUID;
888  NSException *e;
889
890  // We check for conflicts
891  if ((e = [self _handleAttendeesConflicts: attendees  forEvent: newEvent  force: forceSave]))
892    return e;
893  if ((e = [self _handleAttendeesAvailability: attendees  forEvent: newEvent]))
894    return e;
895
896  enumerator = [attendees objectEnumerator];
897  while ((currentAttendee = [enumerator nextObject]))
898    {
899      currentUID = [currentAttendee uidInContext: context];
900      if (currentUID)
901      [self _addOrUpdateEvent: newEvent
902                     oldEvent: nil
903                       forUID: currentUID
904                        owner: owner];
905    }
906
907  return nil;
908}
909
910
911//
912//
913//
914- (void) _addOrDeleteAttendees: (NSArray *) theAttendees
915inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent
916                           add: (BOOL) shouldAdd
917{
918
919  NSArray *events;
920  iCalEvent *e;
921  int i,j;
922
923  // We don't add/delete attendees to all recurrence exceptions if
924  // the modification was actually NOT made on the master event
925  if ([theEvent recurrenceId])
926    return;
927
928  events = [[theEvent parent] events];
929
930  for (i = 0; i < [events count]; i++)
931    {
932      e = [events objectAtIndex: i];
933      if ([e recurrenceId])
934        for (j = 0; j < [theAttendees count]; j++) {
935          if (shouldAdd)
936            [e addToAttendees: [theAttendees objectAtIndex: j]];
937          else
938            [e removeFromAttendees: [theAttendees objectAtIndex: j]];
939        }
940    }
941}
942
943//
944//
945//
946- (NSException *) _handleUpdatedEvent: (iCalEvent *) newEvent
947		         fromOldEvent: (iCalEvent *) oldEvent
948                                force: (BOOL) forceSave
949{
950  NSArray *addedAttendees, *deletedAttendees, *updatedAttendees;
951  iCalEventChanges *changes;
952  NSException *ex;
953
954  addedAttendees = nil;
955  deletedAttendees = nil;
956  updatedAttendees = nil;
957
958  changes = [newEvent getChangesRelativeToEvent: oldEvent];
959  if ([changes sequenceShouldBeIncreased])
960    {
961      // Set new attendees status to "needs action" and recompute changes when
962      // the list of attendees has changed. The list might have changed since
963      // by changing a major property of the event, we remove all the delegation
964      // chains to "other" attendees
965      if ([self _requireResponseFromAttendees: newEvent])
966        changes = [newEvent getChangesRelativeToEvent: oldEvent];
967    }
968
969  deletedAttendees = [changes deletedAttendees];
970
971  if ([deletedAttendees count])
972    {
973      // We delete the attendees in all exception occurences, if
974      // the attendees were removed from the master event.
975      [self _addOrDeleteAttendees: deletedAttendees
976            inRecurrenceExceptionsForEvent: newEvent
977                              add: NO];
978
979      [self _handleRemovedUsers: deletedAttendees
980               withRecurrenceId: [newEvent recurrenceId]];
981      if ([self _shouldScheduleEvent: [newEvent organizer]])
982	[self sendEMailUsingTemplateNamed: @"Deletion"
983				forObject: [newEvent itipEntryWithMethod: @"cancel"]
984			   previousObject: oldEvent
985			      toAttendees: deletedAttendees
986				 withType: @"calendar:cancellation"];
987    }
988
989  if ((ex = [self _handleAttendeesConflicts: [newEvent attendees] forEvent: newEvent  force: forceSave]))
990    return ex;
991  if ((ex = [self _handleAttendeesAvailability: [newEvent attendees] forEvent: newEvent]))
992    return ex;
993
994  addedAttendees = [changes insertedAttendees];
995
996  // We insert the attendees in all exception occurences, if
997  // the attendees were added to the master event.
998  [self _addOrDeleteAttendees: addedAttendees
999        inRecurrenceExceptionsForEvent: newEvent
1000                          add: YES];
1001
1002  if ([changes sequenceShouldBeIncreased])
1003    {
1004      [newEvent increaseSequence];
1005
1006      // Update attendees calendars and send them an update
1007      // notification by email. We ignore the newly added
1008      // attendees as we don't want to send them invitation
1009      // update emails
1010      [self _handleSequenceUpdateInEvent: newEvent
1011                       ignoringAttendees: addedAttendees
1012                            fromOldEvent: oldEvent];
1013    }
1014  else
1015    {
1016      // If other attributes have changed, update the event
1017      // in each attendee's calendar
1018      if ([[changes updatedProperties] count])
1019        {
1020          NSEnumerator *enumerator;
1021          iCalPerson *currentAttendee;
1022          NSString *currentUID;
1023
1024          updatedAttendees = [newEvent attendees];
1025          enumerator = [updatedAttendees objectEnumerator];
1026          while ((currentAttendee = [enumerator nextObject]))
1027            {
1028              currentUID = [currentAttendee uidInContext: context];
1029              if (currentUID)
1030                [self _addOrUpdateEvent: newEvent
1031                               oldEvent: oldEvent
1032                                 forUID: currentUID
1033                                  owner: owner];
1034            }
1035        }
1036    }
1037
1038  if ([addedAttendees count])
1039    {
1040      // Send an invitation to new attendees
1041      if ((ex = [self _handleAddedUsers: addedAttendees fromEvent: newEvent  force: forceSave]))
1042        return ex;
1043
1044      if ([self _shouldScheduleEvent: [newEvent organizer]])
1045	[self sendEMailUsingTemplateNamed: @"Invitation"
1046				forObject: [newEvent itipEntryWithMethod: @"request"]
1047			   previousObject: oldEvent
1048			      toAttendees: addedAttendees
1049				 withType: @"calendar:invitation"];
1050    }
1051
1052  if ([changes hasMajorChanges])
1053    [self sendReceiptEmailForObject: newEvent
1054                     addedAttendees: addedAttendees
1055                   deletedAttendees: deletedAttendees
1056                   updatedAttendees: updatedAttendees
1057                          operation: EventUpdated];
1058
1059  return nil;
1060}
1061
1062//
1063// Workflow :                             +----------------------+
1064//                                        |                      |
1065// [saveComponent:]---> _handleAddedUsers:fromEvent: <-+         |
1066//       |                                             |         v
1067//       +------------> _handleUpdatedEvent:fromOldEvent: ---> _addOrUpdateEvent:oldEvent:forUID:owner:  <-----------+
1068//                               |           |                   ^                                          |
1069//                               v           v                   |                                          |
1070//  _handleRemovedUsers:withRecurrenceId:  _handleSequenceUpdateInEvent:ignoringAttendees:fromOldEvent:     |
1071//                     |                                                                                    |
1072//                     |             [DELETEAction:]                                                        |
1073//                     |                    |              {_handleAdded/Updated...}<--+                    |
1074//                     |                    v                                          |                    |
1075//                     |         [prepareDeleteOccurence:]                    [PUTAction:]                  |
1076//                     |               |              |                            |                        |
1077//                     v               v              v                            v                        |
1078// _removeEventFromUID:owner:withRecurrenceId:  [changeParticipationStatus:withDelegate:forRecurrenceId:]   |
1079//                     |                                          |                                         |
1080//                     |                                          v                                         |
1081//                     +------------------------> _handleAttendee:withDelegate:ownerUser:statusChange:inEvent: ---> [sendResponseToOrganizer:from:]
1082//                                                  |
1083//                                                  v
1084//  _updateAttendee:withDelegate:ownerUser:forEventUID:withRecurrenceId:withSequence:forUID:shouldAddSentBy:
1085//
1086//
1087- (NSException *) saveComponent: (iCalEvent *) newEvent
1088{
1089  return [self saveComponent: newEvent  force: NO];
1090}
1091
1092- (NSException *) saveComponent: (iCalEvent *) newEvent
1093                          force: (BOOL) forceSave
1094{
1095  iCalEvent *oldEvent, *oldMasterEvent;
1096  NSCalendarDate *recurrenceId;
1097  NSString *recurrenceTime;
1098  SOGoUser *ownerUser;
1099  NSArray *attendees;
1100  NSException *ex;
1101
1102  [[newEvent parent] setMethod: @""];
1103  ownerUser = [SOGoUser userWithLogin: owner];
1104
1105  [self expandGroupsInEvent: newEvent];
1106
1107  // We first update the event. It is important to do this initially
1108  // as the event's UID might get modified.
1109  [super updateComponent: newEvent];
1110
1111  if ([self isNew])
1112    {
1113      // New event -- send invitation to all attendees
1114      attendees = [newEvent attendeesWithoutUser: ownerUser];
1115
1116      // We catch conflicts and abort the save process immediately
1117      // in case of one with resources
1118      if ((ex = [self _handleAddedUsers: attendees fromEvent: newEvent  force: forceSave]))
1119        return ex;
1120
1121      if ([attendees count])
1122        {
1123	  if ([self _shouldScheduleEvent: [newEvent organizer]])
1124	    [self sendEMailUsingTemplateNamed: @"Invitation"
1125				    forObject: [newEvent itipEntryWithMethod: @"request"]
1126			       previousObject: nil
1127				  toAttendees: attendees
1128				     withType: @"calendar:invitation"];
1129        }
1130
1131      [self sendReceiptEmailForObject: newEvent
1132                       addedAttendees: attendees
1133                     deletedAttendees: nil
1134                     updatedAttendees: nil
1135                            operation: EventCreated];
1136    }
1137  else
1138    {
1139      BOOL hasOrganizer;
1140
1141      // Event is modified -- sent update status to all attendees
1142      // and modify their calendars.
1143      recurrenceId = [newEvent recurrenceId];
1144      if (recurrenceId == nil)
1145        oldEvent = [self component: NO secure: NO];
1146      else
1147        {
1148          // If recurrenceId is defined, find the specified occurence
1149          // within the repeating vEvent.
1150          recurrenceTime = [NSString stringWithFormat: @"%f", [recurrenceId timeIntervalSince1970]];
1151          oldEvent = (iCalEvent*)[self lookupOccurrence: recurrenceTime];
1152          if (oldEvent == nil) // If no occurence found, create one
1153            oldEvent = (iCalEvent *)[self newOccurenceWithID: recurrenceTime];
1154        }
1155
1156      oldMasterEvent = (iCalEvent *)[[oldEvent parent] firstChildWithTag: [self componentTag]];
1157      hasOrganizer = [[[oldMasterEvent organizer] email] length];
1158
1159      if (!hasOrganizer || [oldMasterEvent userIsOrganizer: ownerUser])
1160      // The owner is the organizer of the event; handle the modifications. We aslo
1161      // catch conflicts just like when the events are created
1162        if ((ex = [self _handleUpdatedEvent: newEvent fromOldEvent: oldEvent  force: forceSave]))
1163          return ex;
1164    }
1165
1166  [super saveComponent: newEvent];
1167  [self flush];
1168
1169  return nil;
1170}
1171
1172//
1173// This method is used to update the status of an attendee.
1174//
1175// - theOwnerUser is owner of the calendar where the attendee
1176//   participation state has changed.
1177// - uid is the actual UID of the user for whom we must
1178//   update the calendar event (with the participation change)
1179// - delegate is the delegate attendee if any
1180//
1181// This method is called multiple times, in order to update the
1182// status of the attendee in calendars for the particular event UID.
1183//
1184- (NSException *) _updateAttendee: (iCalPerson *) attendee
1185                     withDelegate: (iCalPerson *) delegate
1186                        ownerUser: (SOGoUser *) theOwnerUser
1187                      forEventUID: (NSString *) eventUID
1188                 withRecurrenceId: (NSCalendarDate *) recurrenceId
1189                     withSequence: (NSNumber *) sequence
1190                           forUID: (NSString *) uid
1191                  shouldAddSentBy: (BOOL) b
1192{
1193  SOGoAppointmentObject *eventObject;
1194  iCalCalendar *calendar;
1195  iCalEntityObject *event;
1196  iCalPerson *otherAttendee, *otherDelegate;
1197  NSString *recurrenceTime, *delegateEmail;
1198  NSException *error;
1199  BOOL addDelegate, removeDelegate;
1200
1201  // If the atttende's role is NON-PARTICIPANT, we write nothing to its calendar
1202  if ([[attendee role] caseInsensitiveCompare: @"NON-PARTICIPANT"] == NSOrderedSame)
1203    return nil;
1204
1205  error = nil;
1206
1207  eventObject = [self _lookupEvent: eventUID forUID: uid];
1208  if (![eventObject isNew])
1209    {
1210      if (recurrenceId == nil)
1211        {
1212          // We must update main event and all its occurences (if any).
1213          calendar = [eventObject calendar: NO secure: NO];
1214          event = (iCalEntityObject*)[calendar firstChildWithTag: [self componentTag]];
1215        }
1216      else
1217        {
1218          // If recurrenceId is defined, find the specified occurence
1219          // within the repeating vEvent.
1220          recurrenceTime = [NSString stringWithFormat: @"%f", [recurrenceId timeIntervalSince1970]];
1221          event = [eventObject lookupOccurrence: recurrenceTime];
1222
1223          if (event == nil)
1224          // If no occurence found, create one
1225          event = [eventObject newOccurenceWithID: recurrenceTime];
1226        }
1227
1228      if ([[event sequence] intValue] <= [sequence intValue])
1229        {
1230          SOGoUser *currentUser;
1231
1232          currentUser = [context activeUser];
1233          otherAttendee = [event userAsAttendee: theOwnerUser];
1234
1235          delegateEmail = [otherAttendee delegatedTo];
1236          if ([delegateEmail length])
1237            delegateEmail = [delegateEmail rfc822Email];
1238          if ([delegateEmail length])
1239            otherDelegate = [event findAttendeeWithEmail: delegateEmail];
1240          else
1241            otherDelegate = nil;
1242
1243          /* we handle the addition/deletion of delegate users */
1244          addDelegate = NO;
1245          removeDelegate = NO;
1246          if (delegate)
1247            {
1248              if (otherDelegate)
1249                {
1250                  if (![delegate hasSameEmailAddress: otherDelegate])
1251                    {
1252                      removeDelegate = YES;
1253                      addDelegate = YES;
1254                    }
1255                }
1256              else
1257                addDelegate = YES;
1258            }
1259          else
1260            {
1261              if (otherDelegate)
1262                removeDelegate = YES;
1263            }
1264
1265          if (removeDelegate)
1266            {
1267              while (otherDelegate)
1268                {
1269                  [event removeFromAttendees: otherDelegate];
1270
1271                  // Verify if the delegate was already delegate
1272                  delegateEmail = [otherDelegate delegatedTo];
1273                  if ([delegateEmail length])
1274                    delegateEmail = [delegateEmail rfc822Email];
1275
1276                  if ([delegateEmail length])
1277                    otherDelegate = [event findAttendeeWithEmail: delegateEmail];
1278                  else
1279                    otherDelegate = nil;
1280                }
1281            }
1282          if (addDelegate)
1283            [event addToAttendees: delegate];
1284
1285          [otherAttendee setPartStat: [attendee partStat]];
1286          [otherAttendee setDelegatedTo: [attendee delegatedTo]];
1287          [otherAttendee setDelegatedFrom: [attendee delegatedFrom]];
1288
1289          // Remove the RSVP attribute, as an action from the attendee
1290          // was actually performed, and this confuses iCal (bug #1850)
1291          [[otherAttendee attributes] removeObjectForKey: @"RSVP"];
1292
1293          // If one has accepted / declined an invitation on behalf of
1294          // the attendee, we add the user to the SENT-BY attribute.
1295          if (b && ![[currentUser login] isEqualToString: [theOwnerUser login]])
1296            {
1297              NSString *currentEmail, *quotedEmail;
1298              currentEmail = [[currentUser allEmails] objectAtIndex: 0];
1299              quotedEmail = [NSString stringWithFormat: @"\"MAILTO:%@\"", currentEmail];
1300              [otherAttendee setValue: 0 ofAttribute: @"SENT-BY"
1301                                   to: quotedEmail];
1302            }
1303          else
1304            {
1305              // We must REMOVE any SENT-BY here. This is important since if A accepted
1306              // the event for B and then, B changes by theirself their participation status,
1307              // we don't want to keep the previous SENT-BY attribute there.
1308              [(NSMutableDictionary *)[otherAttendee attributes] removeObjectForKey: @"SENT-BY"];
1309            }
1310        }
1311
1312      // We save the updated iCalendar in the database.
1313      [event setLastModified: [NSCalendarDate calendarDate]];
1314      error = [eventObject saveCalendar: [event parent]];
1315    }
1316
1317  return error;
1318}
1319
1320
1321//
1322// This method is invoked from the SOGo Web interface or from the DAV interface.
1323//
1324// - theOwnerUser is owner of the calendar where the attendee
1325//   participation state has changed.
1326//
1327- (NSException *) _handleAttendee: (iCalPerson *) attendee
1328                     withDelegate: (iCalPerson *) delegate
1329                        ownerUser: (SOGoUser *) theOwnerUser
1330                     statusChange: (NSString *) newStatus
1331                          inEvent: (iCalEvent *) event
1332{
1333  iCalPerson *otherAttendee, *otherDelegate;
1334  NSString *currentStatus, *organizerUID;
1335  SOGoUser *ownerUser, *currentUser;
1336  NSString *delegateEmail;
1337  NSException *ex;
1338
1339  BOOL addDelegate, removeDelegate;
1340
1341  currentStatus = [attendee partStat];
1342  otherAttendee = attendee;
1343  ex = nil;
1344
1345  delegateEmail = [otherAttendee delegatedTo];
1346  if ([delegateEmail length])
1347    delegateEmail = [delegateEmail rfc822Email];
1348
1349  if ([delegateEmail length])
1350    otherDelegate = [event findAttendeeWithEmail: delegateEmail];
1351  else
1352    otherDelegate = nil;
1353
1354  // We handle the addition/deletion of delegate users
1355  addDelegate = NO;
1356  removeDelegate = NO;
1357  if (delegate)
1358    {
1359      if (otherDelegate)
1360        {
1361          // There was already a delegate
1362          if (![delegate hasSameEmailAddress: otherDelegate])
1363            {
1364              // The delegate has changed
1365              removeDelegate = YES;
1366              addDelegate = YES;
1367            }
1368        }
1369      else
1370        // There was no previous delegate
1371        addDelegate = YES;
1372    }
1373  else
1374    {
1375      if (otherDelegate)
1376        // The user has removed the delegate
1377        removeDelegate = YES;
1378    }
1379
1380  if (addDelegate || removeDelegate
1381                  || [currentStatus caseInsensitiveCompare: newStatus] != NSOrderedSame)
1382    {
1383      NSMutableArray *delegates;
1384      NSString *delegatedUID;
1385
1386      delegatedUID = nil;
1387      [attendee setPartStat: newStatus];
1388
1389      // If one has accepted / declined an invitation on behalf of
1390      // the attendee, we add the user to the SENT-BY attribute.
1391      currentUser = [context activeUser];
1392      if (![[currentUser login] isEqualToString: [theOwnerUser login]])
1393        {
1394          NSString *currentEmail, *quotedEmail;
1395          currentEmail = [[currentUser allEmails] objectAtIndex: 0];
1396          quotedEmail = [NSString stringWithFormat: @"\"MAILTO:%@\"", currentEmail];
1397          [attendee setValue: 0 ofAttribute: @"SENT-BY"
1398                          to: quotedEmail];
1399        }
1400      else
1401        {
1402          // We must REMOVE any SENT-BY here. This is important since if A accepted
1403          // the event for B and then, B changes by theirself their participation status,
1404          // we don't want to keep the previous SENT-BY attribute there.
1405          [(NSMutableDictionary *)[attendee attributes] removeObjectForKey: @"SENT-BY"];
1406        }
1407
1408      [attendee setDelegatedTo: [delegate email]];
1409
1410      if (removeDelegate)
1411        {
1412          delegates = [NSMutableArray array];
1413
1414          while (otherDelegate)
1415            {
1416              [delegates addObject: otherDelegate];
1417
1418              delegatedUID = [otherDelegate uidInContext: context];
1419              if (delegatedUID)
1420                // Delegate attendee is a local user; remove event from their calendar
1421                [self _removeEventFromUID: delegatedUID
1422                                    owner: [theOwnerUser login]
1423                         withRecurrenceId: [event recurrenceId]];
1424
1425              [event removeFromAttendees: otherDelegate];
1426
1427              // Verify if the delegate was already delegated
1428              delegateEmail = [otherDelegate delegatedTo];
1429              if ([delegateEmail length])
1430                delegateEmail = [delegateEmail rfc822Email];
1431
1432              if ([delegateEmail length])
1433                otherDelegate = [event findAttendeeWithEmail: delegateEmail];
1434              else
1435                otherDelegate = nil;
1436            }
1437
1438	  if ([self _shouldScheduleEvent: [event organizer]])
1439	    [self sendEMailUsingTemplateNamed: @"Deletion"
1440				    forObject: [event itipEntryWithMethod: @"cancel"]
1441			       previousObject: nil
1442				  toAttendees: delegates
1443				     withType: @"calendar:cancellation"];
1444        } // if (removeDelegate)
1445
1446      if (addDelegate)
1447        {
1448          delegatedUID = [delegate uidInContext: context];
1449          delegates = [NSArray arrayWithObject: delegate];
1450          [event addToAttendees: delegate];
1451
1452          if (delegatedUID)
1453            // Delegate attendee is a local user; add event to their calendar
1454            [self _addOrUpdateEvent: event
1455                           oldEvent: nil
1456                             forUID: delegatedUID
1457                              owner: [theOwnerUser login]];
1458
1459	  if ([self _shouldScheduleEvent: [event organizer]])
1460	    [self sendEMailUsingTemplateNamed: @"Invitation"
1461				    forObject: [event itipEntryWithMethod: @"request"]
1462			       previousObject: nil
1463				  toAttendees: delegates
1464				     withType: @"calendar:invitation"];
1465        } // if (addDelegate)
1466
1467      // If the current user isn't the organizer of the event
1468      // that has just been updated, we update the event and
1469      // send a notification
1470      ownerUser = [SOGoUser userWithLogin: owner];
1471      if (!(ex || [event userIsOrganizer: ownerUser]))
1472        {
1473          if ([event isStillRelevant])
1474            [self sendResponseToOrganizer: event
1475                                     from: ownerUser];
1476
1477          organizerUID = [[event organizer] uidInContext: context];
1478
1479          // Event is an exception to a recurring event; retrieve organizer from master event
1480          if (!organizerUID)
1481            organizerUID = [[(iCalEntityObject*)[[event parent] firstChildWithTag: [self componentTag]] organizer] uidInContext: context];
1482
1483          if (organizerUID)
1484            // Update the attendee in organizer's calendar.
1485            ex = [self _updateAttendee: attendee
1486                          withDelegate: delegate
1487                             ownerUser: theOwnerUser
1488                           forEventUID: [event uid]
1489                      withRecurrenceId: [event recurrenceId]
1490                          withSequence: [event sequence]
1491                                forUID: organizerUID
1492                       shouldAddSentBy: YES];
1493        }
1494
1495      // We update the calendar of all attendees that are
1496      // local to the system. This is useful in case user A accepts
1497      // invitation from organizer B and users C, D, E who are also
1498      // attendees need to verify if A has accepted.
1499      NSArray *attendees;
1500      iCalPerson *att;
1501      NSString *uid;
1502      int i;
1503
1504      attendees = [event attendees];
1505      for (i = 0; i < [attendees count]; i++)
1506        {
1507          att = [attendees objectAtIndex: i];
1508          uid = [att uidInContext: context];
1509          if (uid && att != attendee && ![uid isEqualToString: delegatedUID])
1510            [self _updateAttendee: attendee
1511                     withDelegate: delegate
1512                        ownerUser: theOwnerUser
1513                      forEventUID: [event uid]
1514                 withRecurrenceId: [event recurrenceId]
1515                     withSequence: [event sequence]
1516                           forUID: uid
1517                  shouldAddSentBy: YES];
1518        }
1519    }
1520
1521  return ex;
1522}
1523
1524//
1525//
1526//
1527- (NSDictionary *) _caldavSuccessCodeWithRecipient: (NSString *) recipient
1528{
1529  NSMutableArray *element;
1530  NSDictionary *code;
1531
1532  element = [NSMutableArray array];
1533  [element addObject: davElementWithContent (@"recipient", XMLNS_CALDAV, recipient)];
1534  [element addObject: davElementWithContent (@"request-status", XMLNS_CALDAV, @"2.0;Success")];
1535  code = davElementWithContent (@"response", XMLNS_CALDAV, element);
1536
1537  return code;
1538}
1539
1540//
1541// Old CalDAV scheduling (draft 4 and below) methods. We keep them since we still
1542// advertise for its support but we do everything within the calendar-auto-scheduling code
1543//
1544- (NSArray *) postCalDAVEventRequestTo: (NSArray *) recipients
1545                                  from: (NSString *) originator
1546{
1547  NSEnumerator *recipientsEnum;
1548  NSMutableArray *elements;
1549  NSString *recipient;
1550
1551  elements = [NSMutableArray array];
1552
1553  recipientsEnum = [recipients objectEnumerator];
1554
1555  while ((recipient = [recipientsEnum nextObject]))
1556    if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
1557      {
1558        [elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
1559      }
1560
1561  return elements;
1562}
1563
1564- (NSArray *) postCalDAVEventCancelTo: (NSArray *) recipients
1565                                 from: (NSString *) originator
1566{
1567  NSEnumerator *recipientsEnum;
1568  NSMutableArray *elements;
1569
1570  NSString *recipient;
1571
1572  elements = [NSMutableArray array];
1573
1574  recipientsEnum = [recipients objectEnumerator];
1575
1576  while ((recipient = [recipientsEnum nextObject]))
1577    if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
1578      {
1579        [elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
1580      }
1581
1582  return elements;
1583}
1584
1585- (NSArray *) postCalDAVEventReplyTo: (NSArray *) recipients
1586                                from: (NSString *) originator
1587{
1588  NSEnumerator *recipientsEnum;
1589  NSMutableArray *elements;
1590  NSString *recipient;
1591
1592  elements = [NSMutableArray array];
1593  recipientsEnum = [recipients objectEnumerator];
1594
1595  while ((recipient = [recipientsEnum nextObject]))
1596    if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
1597      {
1598        [elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
1599      }
1600
1601  return elements;
1602}
1603
1604//
1605//
1606//
1607- (NSException *) changeParticipationStatus: (NSString *) status
1608                               withDelegate: (iCalPerson *) delegate
1609                                      alarm: (iCalAlarm *) alarm
1610{
1611  return [self changeParticipationStatus: status
1612                            withDelegate: delegate
1613                                   alarm: alarm
1614                         forRecurrenceId: nil];
1615}
1616
1617//
1618//
1619//
1620- (NSException *) changeParticipationStatus: (NSString *) _status
1621                               withDelegate: (iCalPerson *) delegate
1622                                      alarm: (iCalAlarm *) alarm
1623                            forRecurrenceId: (NSCalendarDate *) _recurrenceId
1624{
1625  iCalCalendar *calendar;
1626  iCalEvent *event;
1627  iCalPerson *attendee;
1628  NSException *ex;
1629  SOGoUser *ownerUser, *delegatedUser;
1630  NSString *recurrenceTime, *delegatedUid, *domain;
1631
1632  event = nil;
1633  ex = nil;
1634  delegatedUser = nil;
1635
1636  calendar = [[self calendar: NO secure: NO] mutableCopy];
1637  [calendar autorelease];
1638
1639  if (_recurrenceId)
1640    {
1641      // If _recurrenceId is defined, find the specified occurence
1642      // within the repeating vEvent.
1643      recurrenceTime = [NSString stringWithFormat: @"%f", [_recurrenceId timeIntervalSince1970]];
1644      event = (iCalEvent*)[self lookupOccurrence: recurrenceTime];
1645
1646      // If no occurence found, create one
1647      if (event == nil)
1648        event = (iCalEvent*)[self newOccurenceWithID: recurrenceTime];
1649    }
1650  else
1651    // No specific occurence specified; return the first vEvent of
1652    // the vCalendar.
1653    event = (iCalEvent*)[calendar firstChildWithTag: [self componentTag]];
1654
1655  if (event)
1656    {
1657      // ownerUser will actually be the owner of the calendar
1658      // where the participation change on the event occurs. The particpation
1659      // change will be on the attendee corresponding to the ownerUser.
1660      ownerUser = [SOGoUser userWithLogin: owner];
1661
1662      attendee = [event userAsAttendee: ownerUser];
1663      if (attendee)
1664        {
1665          if (delegate && ![[delegate email] isEqualToString: [attendee delegatedTo]])
1666            {
1667              delegatedUid = [delegate uidInContext: context];
1668              if (delegatedUid)
1669                delegatedUser = [SOGoUser userWithLogin: delegatedUid];
1670              if (delegatedUser != nil && [event userIsOrganizer: delegatedUser])
1671                ex = [NSException exceptionWithHTTPStatus: 409
1672                                                   reason: @"delegate is organizer"];
1673              if ([event isAttendee: [[delegate email] rfc822Email]])
1674                ex = [NSException exceptionWithHTTPStatus: 409
1675                                                   reason: @"delegate is a participant"];
1676              else {
1677                NSDictionary *dict;
1678                domain = [[context activeUser] domain];
1679                dict = [[SOGoUserManager sharedUserManager] contactInfosForUserWithUIDorEmail: [[delegate email] rfc822Email]
1680                                                                                     inDomain: domain];
1681                if (dict && [[dict objectForKey: @"isGroup"] boolValue])
1682                  ex = [NSException exceptionWithHTTPStatus: 409
1683                                                     reason: @"delegate is a group"];
1684              }
1685            }
1686          if (ex == nil)
1687            {
1688              // Remove the RSVP attribute, as an action from the attendee
1689              // was actually performed, and this confuses iCal (bug #1850)
1690              [[attendee attributes] removeObjectForKey: @"RSVP"];
1691              ex = [self _handleAttendee: attendee
1692                            withDelegate: delegate
1693                               ownerUser: ownerUser
1694                            statusChange: _status
1695                                 inEvent: event];
1696            }
1697          if (ex == nil)
1698            {
1699              // We generate the updated iCalendar file and we save it in
1700              // the database. We do this ONLY when using SOGo from the
1701              // Web interface or over ActiveSync.
1702              // Over DAV, it'll be handled directly in PUTAction:
1703              if (![context request] ||
1704                  [[context request] handledByDefaultHandler] ||
1705                  [[[context request] requestHandlerKey] isEqualToString: @"Microsoft-Server-ActiveSync"])
1706                {
1707                  // If an alarm was specified, let's use it. This would happen if an attendee accepts/declines/etc. an
1708                  // event invitation and also sets an alarm along the way. This would happen ONLY from the web interface.
1709                  [event removeAllAlarms];
1710
1711                  if (alarm)
1712                    {
1713                      [event addToAlarms: alarm];
1714                    }
1715
1716                  [event setLastModified: [NSCalendarDate calendarDate]];
1717                  ex = [self saveCalendar: [event parent]];
1718                }
1719            }
1720        }
1721      else
1722        ex = [NSException exceptionWithHTTPStatus: 404 // Not Found
1723                                           reason: @"user does not participate in this calendar event"];
1724    }
1725      else
1726        ex = [NSException exceptionWithHTTPStatus: 500 // Server Error
1727                                           reason: @"unable to parse event record"];
1728
1729  return ex;
1730}
1731
1732//
1733//
1734//
1735- (void) prepareDeleteOccurence: (iCalEvent *) occurence
1736{
1737  SOGoUser *ownerUser, *currentUser;
1738  NSCalendarDate *recurrenceId;
1739  NSArray *attendees;
1740  iCalEvent *event;
1741  BOOL send_receipt;
1742
1743  ownerUser = [SOGoUser userWithLogin: owner];
1744  event = [self component: NO secure: NO];
1745  send_receipt = YES;
1746
1747  if (occurence == nil)
1748    {
1749      // No occurence specified; use the master event.
1750      occurence = event;
1751      recurrenceId = nil;
1752    }
1753  else
1754    // Retrieve this occurence ID.
1755    recurrenceId = [occurence recurrenceId];
1756
1757  if ([occurence userIsAttendee: ownerUser])
1758    {
1759      // The current user deletes the occurence; let the organizer know that
1760      // the user has declined this occurence.
1761      [self changeParticipationStatus: @"DECLINED"
1762                         withDelegate: nil
1763                                alarm: nil
1764                      forRecurrenceId: recurrenceId];
1765      send_receipt = NO;
1766    }
1767  else
1768    {
1769      // The organizer deletes an occurence.
1770      currentUser = [context activeUser];
1771
1772      if (recurrenceId)
1773        attendees = [occurence attendeesWithoutUser: currentUser];
1774      else
1775        attendees = [[event parent] attendeesWithoutUser: currentUser];
1776
1777      //if (![attendees count] && event != occurence)
1778      //attendees = [event attendeesWithoutUser: currentUser];
1779
1780      if ([attendees count])
1781        {
1782          // Remove the event from all attendees calendars
1783          // and send them an email.
1784          [self _handleRemovedUsers: attendees
1785                   withRecurrenceId: recurrenceId];
1786
1787	  if ([self _shouldScheduleEvent: [event organizer]])
1788	    [self sendEMailUsingTemplateNamed: @"Deletion"
1789				    forObject: [occurence itipEntryWithMethod: @"cancel"]
1790			       previousObject: nil
1791				  toAttendees: attendees
1792				     withType: @"calendar:cancellation"];
1793        }
1794    }
1795
1796  if (send_receipt)
1797    [self sendReceiptEmailForObject: event
1798		     addedAttendees: nil
1799		   deletedAttendees: nil
1800		   updatedAttendees: nil
1801			  operation: EventDeleted];
1802}
1803
1804- (NSException *) prepareDelete
1805{
1806  [self prepareDeleteOccurence: nil];
1807
1808  return [super prepareDelete];
1809}
1810
1811- (NSDictionary *) _partStatsFromCalendar: (iCalCalendar *) calendar
1812{
1813  NSMutableDictionary *partStats;
1814  NSArray *allEvents;
1815  int count, max;
1816  iCalEvent *currentEvent;
1817  iCalPerson *ownerAttendee;
1818  NSString *key;
1819  SOGoUser *ownerUser;
1820
1821  ownerUser = [SOGoUser userWithLogin: owner];
1822
1823  allEvents = [calendar events];
1824  max = [allEvents count];
1825  partStats = [NSMutableDictionary dictionaryWithCapacity: max];
1826
1827  for (count = 0; count < max; count++)
1828    {
1829      currentEvent = [allEvents objectAtIndex: count];
1830      ownerAttendee = [currentEvent userAsAttendee: ownerUser];
1831      if (ownerAttendee)
1832        {
1833          if (count == 0)
1834            key = @"master";
1835          else
1836            key = [[currentEvent recurrenceId] iCalFormattedDateTimeString];
1837          [partStats setObject: ownerAttendee forKey: key];
1838        }
1839    }
1840
1841  return partStats;
1842}
1843
1844- (iCalCalendar *) _setupResponseInRequestCalendar: (iCalCalendar *) rqCalendar
1845{
1846  iCalCalendar *calendar;
1847  NSArray *keys;
1848  NSDictionary *partStats, *newPartStats;
1849  NSString *partStat, *key;
1850  int count, max;
1851
1852  calendar = [self calendar: NO secure: NO];
1853  partStats = [self _partStatsFromCalendar: calendar];
1854  keys = [partStats allKeys];
1855  max = [keys count];
1856  if (max > 0)
1857    {
1858      newPartStats = [self _partStatsFromCalendar: rqCalendar];
1859      if ([keys isEqualToArray: [newPartStats allKeys]])
1860        {
1861          for (count = 0; count < max; count++)
1862            {
1863              key = [keys objectAtIndex: count];
1864              partStat = [[newPartStats objectForKey: key] partStat];
1865              [[partStats objectForKey: key] setPartStat: partStat];
1866            }
1867        }
1868    }
1869
1870  return calendar;
1871}
1872
1873- (void) _adjustTransparencyInRequestCalendar: (iCalCalendar *) rqCalendar
1874{
1875  NSArray *allEvents;
1876  iCalEvent *event;
1877  int i;
1878
1879  allEvents = [rqCalendar events];
1880  for (i = 0; i < [allEvents count]; i++)
1881    {
1882      event = [allEvents objectAtIndex: i];
1883      if ([event isAllDay] && [event isOpaque])
1884          [event setTransparency: @"TRANSPARENT"];
1885    }
1886}
1887
1888//
1889// iOS devices (and potentially others) send event invitations with no PARTSTAT defined.
1890// This confuses DAV clients like Thunderbird, or event SOGo web. The RFC says:
1891//
1892//    Description: This parameter can be specified on properties with a
1893//    CAL-ADDRESS value type. The parameter identifies the participation
1894//    status for the calendar user specified by the property value. The
1895//    parameter values differ depending on whether they are associated with
1896//    a group scheduled "VEVENT", "VTODO" or "VJOURNAL". The values MUST
1897//    match one of the values allowed for the given calendar component. If
1898//    not specified on a property that allows this parameter, the default
1899//    value is NEEDS-ACTION.
1900//
1901- (void) _adjustPartStatInRequestCalendar: (iCalCalendar *) rqCalendar
1902{
1903  NSArray *allObjects, *allAttendees;
1904  iCalPerson *attendee;
1905  id entity;
1906
1907  int i, j;
1908
1909  allObjects = [rqCalendar allObjects];
1910
1911  for (i = 0; i < [allObjects count]; i++)
1912    {
1913      entity = [allObjects objectAtIndex: i];
1914
1915      if ([entity isKindOfClass: [iCalEvent class]])
1916        {
1917          allAttendees = [entity attendees];
1918
1919          for (j = 0; j < [allAttendees count]; j++)
1920            {
1921              attendee = [allAttendees objectAtIndex: j];
1922
1923              if (![[attendee partStat] length])
1924                [attendee setPartStat: @"NEEDS-ACTION"];
1925            }
1926        }
1927    }
1928}
1929
1930/**
1931 * Verify vCalendar for any inconsistency or missing attributes.
1932 * Currently only check if the events have an end date or a duration.
1933 * We also check for the default transparency parameters.
1934 * We also check for broken ORGANIZER such as "ORGANIZER;:mailto:sogo3@example.com"
1935 * @param rq the HTTP PUT request
1936 */
1937- (void) _adjustEventsInRequestCalendar: (iCalCalendar *) rqCalendar
1938{
1939  NSArray *allEvents;
1940  iCalEvent *event;
1941  iCalTimeZone *tz;
1942  NSUInteger i;
1943  int j;
1944
1945  allEvents = [rqCalendar events];
1946
1947  for (i = 0; i < [allEvents count]; i++)
1948    {
1949      event = [allEvents objectAtIndex: i];
1950
1951      tz = [event adjustInContext: context withTimezones: nil];
1952      if (tz)
1953        [rqCalendar addTimeZone: tz];
1954
1955      if ([event organizer])
1956        {
1957          NSString *uid;
1958
1959          if (![[[event organizer] cn] length])
1960            {
1961              [[event organizer] setCn: [[event organizer] rfc822Email]];
1962            }
1963
1964          // We now make sure that the organizer, if managed by SOGo, is using
1965          // its default email when creating events and inviting attendees.
1966          uid = [[event organizer] uidInContext: context];
1967          if (uid)
1968            {
1969	      iCalPerson *attendee, *organizer;
1970              NSDictionary *defaultIdentity;
1971	      SOGoUser *organizerUser;
1972	      NSArray *allAttendees;
1973
1974	      organizerUser = [SOGoUser userWithLogin: uid];
1975              defaultIdentity = [organizerUser primaryIdentity];
1976	      organizer = [[event organizer] copy];
1977              [organizer setCn: [defaultIdentity objectForKey: @"fullName"]];
1978              [organizer setEmail: [defaultIdentity objectForKey: @"email"]];
1979
1980	      // We now check if one of the attendee is also the organizer. If so,
1981	      // we remove it. See bug #3905 (https://sogo.nu/bugs/view.php?id=3905)
1982	      // for more details. This is a Calendar app bug on Apple Yosemite.
1983	      allAttendees = [event attendees];
1984
1985	      for (j = [allAttendees count]-1; j >= 0; j--)
1986		{
1987		  attendee = [allAttendees objectAtIndex: j];
1988		  if ([organizerUser hasEmail: [attendee rfc822Email]])
1989		    [event removeFromAttendees: attendee];
1990		}
1991
1992	      // We reset the organizer
1993	      [event setOrganizer: organizer];
1994	      RELEASE(organizer);
1995            }
1996        }
1997    }
1998}
1999
2000
2001- (void) _decomposeGroupsInRequestCalendar: (iCalCalendar *) rqCalendar
2002{
2003  NSArray *allEvents;
2004  iCalEvent *event;
2005  int i;
2006
2007  // The algorithm is pretty straightforward:
2008  //
2009  // We get all events
2010  //   We get all attendees
2011  //     If some are groups, we decompose them
2012  // We regenerate the iCalendar string
2013  //
2014  allEvents = [rqCalendar events];
2015  for (i = 0; i < [allEvents count]; i++)
2016    {
2017      event = [allEvents objectAtIndex: i];
2018      [self expandGroupsInEvent: event];
2019    }
2020}
2021
2022
2023//
2024// If theRecurrenceId is nil, it returns immediately the
2025// first event that has a RECURRENCE-ID.
2026//
2027// Otherwise, it return values that matches.
2028//
2029- (iCalEvent *) _eventFromRecurrenceId: (NSCalendarDate *) theRecurrenceId
2030                                events: (NSArray *) allEvents
2031{
2032  iCalEvent *event;
2033  int i;
2034
2035  for (i = 0; i < [allEvents count]; i++)
2036    {
2037      event = [allEvents objectAtIndex: i];
2038
2039      if ([event recurrenceId] && !theRecurrenceId)
2040        return event;
2041
2042      if ([event recurrenceId] && [[event recurrenceId] compare: theRecurrenceId] == NSOrderedSame)
2043        return event;
2044    }
2045
2046  return nil;
2047}
2048
2049//
2050//
2051//
2052- (NSCalendarDate *) _addedExDate: (iCalEvent *) oldEvent
2053                         newEvent: (iCalEvent *) newEvent
2054{
2055  NSArray *oldExDates, *newExDates;
2056  NSMutableArray *dates;
2057  int i;
2058
2059  dates = [NSMutableArray array];
2060
2061  newExDates = [newEvent childrenWithTag: @"exdate"];
2062  for (i = 0; i < [newExDates count]; i++)
2063    [dates addObject: [[newExDates objectAtIndex: i] dateTime]];
2064
2065  oldExDates = [oldEvent childrenWithTag: @"exdate"];
2066  for (i = 0; i < [oldExDates count]; i++)
2067    [dates removeObject: [[oldExDates objectAtIndex: i] dateTime]];
2068
2069  return [dates lastObject];
2070}
2071
2072
2073//
2074//
2075//
2076- (id) DELETEAction: (WOContext *) _ctx
2077{
2078  [self prepareDelete];
2079  return [super DELETEAction: _ctx];
2080}
2081
2082//
2083// This method is meant to be the common point of any save operation from web
2084// and DAV requests, as well as from code making use of SOGo as a library
2085// (OpenChange)
2086//
2087- (NSException *) updateContentWithCalendar: (iCalCalendar *) calendar
2088                                fromRequest: (WORequest *) rq
2089{
2090  SOGoUser *ownerUser;
2091  NSException *ex;
2092  NSArray *roles;
2093
2094  BOOL ownerIsOrganizer;
2095
2096  if (calendar == fullCalendar || calendar == safeCalendar
2097                               || calendar == originalCalendar)
2098    [NSException raise: NSInvalidArgumentException format: @"the 'calendar' argument must be a distinct instance" @" from the original object"];
2099
2100  ownerUser = [SOGoUser userWithLogin: owner];
2101
2102  roles = [[context activeUser] rolesForObject: self
2103                                     inContext: context];
2104  //
2105  // We check if we gave only the "Respond To" right and someone is actually
2106  // responding to one of our invitation. In this case, _setupResponseCalendarInRequest
2107  // will only take the new attendee status and actually discard any other modifications.
2108  //
2109  if ([roles containsObject: @"ComponentResponder"] && ![roles containsObject: @"ComponentModifier"])
2110    calendar = [self _setupResponseInRequestCalendar: calendar];
2111  else
2112    {
2113      if (![[rq headersForKey: @"X-SOGo"] containsObject: @"NoGroupsDecomposition"])
2114        [self _decomposeGroupsInRequestCalendar: calendar];
2115
2116      if ([[ownerUser domainDefaults] iPhoneForceAllDayTransparency] && [rq isIPhone])
2117	[self _adjustTransparencyInRequestCalendar: calendar];
2118
2119      [self _adjustEventsInRequestCalendar: calendar];
2120      [self adjustClassificationInRequestCalendar: calendar];
2121      [self _adjustPartStatInRequestCalendar: calendar];
2122    }
2123
2124  //
2125  // We first check if it's a new event
2126  //
2127  if ([self isNew])
2128    {
2129      iCalEvent *event;
2130      NSArray *attendees;
2131      NSString *eventUID;
2132
2133      event = [[calendar events] objectAtIndex: 0];
2134      eventUID = [event uid];
2135      attendees = nil;
2136
2137      // make sure eventUID doesn't conflict with an existing event -  see bug #1853
2138      // TODO: send out a no-uid-conflict (DAV:href) xml element (rfc4791 section 5.3.2.1)
2139      if ([container resourceNameForEventUID: eventUID])
2140        {
2141          return [NSException exceptionWithHTTPStatus: 409
2142                                               reason: [NSString stringWithFormat: @"Event UID already in use. (%@)", eventUID]];
2143        }
2144
2145      //
2146      // New event and we're the organizer -- send invitation to all attendees
2147      //
2148      ownerIsOrganizer = [event userIsOrganizer: ownerUser];
2149
2150      // We handle the situation where the SOGo Integrator extension isn't installed or
2151      // if the SENT-BY isn't set. That can happen if Bob invites Alice by creating the event
2152      // in Annie's calendar. Annie should be the organizer, and Bob the SENT-BY. But most
2153      // broken CalDAV client that aren't identity-aware will create the event in Annie's calendar
2154      // and set Bob as the organizer. We fix this for them. See #3368 for details.
2155      if (!ownerIsOrganizer &&
2156	  [[context activeUser] hasEmail: [[event organizer] rfc822Email]])
2157	{
2158	  [[event organizer] setCn: [ownerUser cn]];
2159	  [[event organizer] setEmail: [[ownerUser allEmails] objectAtIndex: 0]];
2160	  [[event organizer] setSentBy: [NSString stringWithFormat: @"\"MAILTO:%@\"", [[[context activeUser] allEmails] objectAtIndex: 0]]];
2161	  ownerIsOrganizer = YES;
2162	}
2163
2164      if (ownerIsOrganizer)
2165	{
2166	  attendees = [event attendeesWithoutUser: ownerUser];
2167	  if ([attendees count])
2168	    {
2169	      if ((ex = [self _handleAddedUsers: attendees fromEvent: event  force: YES]))
2170		return ex;
2171	      else
2172		{
2173		  // We might have auto-accepted resources here. If that's the
2174		  // case, let's regenerate the versitstring and replace the
2175		  // one from the request.
2176		  [rq setContent: [[[event parent] versitString] dataUsingEncoding: [rq contentEncoding]]];
2177		}
2178
2179	      if ([self _shouldScheduleEvent: [event organizer]])
2180		[self sendEMailUsingTemplateNamed: @"Invitation"
2181					forObject: [event itipEntryWithMethod: @"request"]
2182				   previousObject: nil
2183				      toAttendees: attendees
2184					 withType: @"calendar:invitation"];
2185	    }
2186	}
2187      //
2188      // We aren't the organizer but we're an attendee. That can happen when
2189      // we receive an external invitation (IMIP/ITIP) and we accept it
2190      // from a CUA - it gets added to a specific CalDAV calendar using a PUT
2191      //
2192      else if ([event userIsAttendee: ownerUser] && [self _shouldScheduleEvent: [event userAsAttendee: ownerUser]])
2193        {
2194          [self sendResponseToOrganizer: event
2195                                   from: ownerUser];
2196        }
2197
2198      [self sendReceiptEmailForObject: event
2199		       addedAttendees: attendees
2200		     deletedAttendees: nil
2201		     updatedAttendees: nil
2202			    operation: EventCreated];
2203    }  // if ([self isNew])
2204  else
2205    {
2206      iCalCalendar *oldCalendar;
2207      iCalEvent *oldEvent, *newEvent;
2208      iCalEventChanges *changes;
2209      NSMutableArray *oldEvents, *newEvents;
2210      NSCalendarDate *recurrenceId;
2211      int i;
2212
2213      //
2214      // We check what has changed in the event and react accordingly.
2215      //
2216      newEvents = [NSMutableArray arrayWithArray: [calendar events]];
2217
2218      oldCalendar = [self calendar: NO secure: NO];
2219      oldEvents = [NSMutableArray arrayWithArray: [oldCalendar events]];
2220      recurrenceId = nil;
2221
2222      for (i = [newEvents count]-1; i >= 0; i--)
2223        {
2224          newEvent = [newEvents objectAtIndex: i];
2225
2226          if ([newEvent recurrenceId])
2227            {
2228              // Find the corresponding RECURRENCE-ID in the old calendar
2229              // If not present, we assume it was created before the PUT
2230              oldEvent = [self _eventFromRecurrenceId: [newEvent recurrenceId]
2231                                               events: oldEvents];
2232
2233              if (oldEvent == nil)
2234                {
2235                  NSString *recurrenceTime;
2236                  recurrenceTime = [NSString stringWithFormat: @"%f", [[newEvent recurrenceId] timeIntervalSince1970]];
2237                  oldEvent = (iCalEvent *)[self newOccurenceWithID: recurrenceTime];
2238                }
2239
2240              // If present, we look for changes
2241              changes = [iCalEventChanges changesFromEvent: oldEvent  toEvent: newEvent];
2242
2243              if ([changes sequenceShouldBeIncreased] | [changes hasAttendeeChanges])
2244                {
2245                  // We found a RECURRENCE-ID with changes, we consider it
2246                  recurrenceId = [newEvent recurrenceId];
2247                  break;
2248                }
2249              else
2250                {
2251                  [newEvents removeObject: newEvent];
2252                  [oldEvents removeObject: oldEvent];
2253                }
2254            }
2255
2256          oldEvent = nil;
2257          newEvent = nil;
2258        }
2259
2260      // If no changes were observed, let's see if we have any left overs
2261      // in the oldEvents or in the newEvents array
2262      if (!oldEvent && !newEvent)
2263        {
2264          // We check if we only have to deal with the MASTER event
2265          if ([oldEvents count] && [newEvents count] == [oldEvents count])
2266            {
2267              oldEvent = [oldEvents objectAtIndex: 0];
2268              newEvent = [newEvents objectAtIndex: 0];
2269            }
2270          // A RECURRENCE-ID was added
2271          else if ([newEvents count] > [oldEvents count])
2272            {
2273              oldEvent = nil;
2274              newEvent = [self _eventFromRecurrenceId: nil  events: newEvents];
2275              recurrenceId = [newEvent recurrenceId];
2276            }
2277          // A RECURRENCE-ID was removed
2278          else
2279            {
2280              oldEvent = [self _eventFromRecurrenceId: nil  events: oldEvents];
2281              newEvent = nil;
2282              recurrenceId = [oldEvent recurrenceId];
2283            }
2284        }
2285
2286      // We check if the PUT call is actually an PART-STATE change
2287      // from one of the attendees - here's the logic :
2288      //
2289      // if owner == organizer
2290      //
2291      //    if [context activeUser] == organizer
2292      //      [send the invitation update]
2293      //    else
2294      //      [react on SENT-BY as someone else is acting for the organizer]
2295      //
2296      //
2297      int newCount = [[newEvent attendees] count], oldCount = [[oldEvent attendees] count];
2298      if (newCount > 0 || oldCount > 0)
2299        {
2300          BOOL userIsOrganizer;
2301
2302          // newEvent might be nil here, if we're deleting a RECURRENCE-ID with attendees
2303          // If that's the case, we use the oldEvent to obtain the organizer
2304          if (newEvent)
2305            {
2306              ownerIsOrganizer = [newEvent userIsOrganizer: ownerUser];
2307              userIsOrganizer = [newEvent userIsOrganizer: [context activeUser]];
2308            }
2309          else
2310            {
2311              ownerIsOrganizer = [oldEvent userIsOrganizer: ownerUser];
2312              userIsOrganizer = [oldEvent userIsOrganizer: [context activeUser]];
2313            }
2314
2315	  // We handle the situation where the SOGo Integrator extension isn't installed or
2316	  // if the SENT-BY isn't set. That can happen if Bob invites Alice by creating the event
2317	  // in Annie's calendar. Annie should be the organizer, and Bob the SENT-BY. But most
2318	  // broken CalDAV client that aren't identity-aware will create the event in Annie's calendar
2319	  // and set Bob as the organizer. We fix this for them.  See #3368 for details.
2320          //
2321          // We also handle the case where Bob invites Alice and Bob has full access to Alice's calendar
2322          // After inviting ALice, Bob opens the event in Alice's calendar and accept/declines the event.
2323          //
2324	  if (!userIsOrganizer &&
2325              !ownerIsOrganizer &&
2326	      [[context activeUser] hasEmail: [[newEvent organizer] rfc822Email]])
2327	    {
2328	      [[newEvent organizer] setCn: [ownerUser cn]];
2329	      [[newEvent organizer] setEmail: [[ownerUser allEmails] objectAtIndex: 0]];
2330	      [[newEvent organizer] setSentBy: [NSString stringWithFormat: @"\"MAILTO:%@\"", [[[context activeUser] allEmails] objectAtIndex: 0]]];
2331	      ownerIsOrganizer = YES;
2332	    }
2333
2334          // With Thunderbird 10, if you create a recurring event with an exception
2335          // occurence, and invite someone, the PUT will have the organizer in the
2336          // recurrence-id and not in the master event. We must fix this, otherwise
2337          // SOGo will break.
2338          if (!recurrenceId && ![[[[[newEvent parent] events] objectAtIndex: 0] organizer] uidInContext: context])
2339            [[[[newEvent parent] events] objectAtIndex: 0] setOrganizer: [newEvent organizer]];
2340
2341          if (ownerIsOrganizer)
2342            {
2343	      // We check ACLs of the 'organizer' - in case someone forges the SENT-BY
2344	      NSString *uid;
2345
2346	      uid = [[oldEvent organizer] uidInContext: context];
2347
2348	      if (uid && [[[context activeUser] login] caseInsensitiveCompare: uid] != NSOrderedSame)
2349		{
2350		  SOGoAppointmentObject *organizerObject;
2351
2352		  organizerObject = [self _lookupEvent: [oldEvent uid] forUID: uid];
2353		  roles = [[context activeUser] rolesForObject: organizerObject
2354						     inContext: context];
2355
2356		  if (![roles containsObject: @"ComponentModifier"] && ![[context activeUser] isSuperUser])
2357		    {
2358		      return [NSException exceptionWithHTTPStatus: 409
2359							   reason: @"Not allowed to perform this action. Wrong SENT-BY being used regarding access rights on organizer's calendar."];
2360		    }
2361		}
2362
2363              // A RECCURENCE-ID was removed
2364              if (!newEvent && oldEvent)
2365                [self prepareDeleteOccurence: oldEvent];
2366              // The master event was changed, A RECCURENCE-ID was added or modified
2367              else if ((ex = [self _handleUpdatedEvent: newEvent  fromOldEvent: oldEvent  force: YES]))
2368                return ex;
2369            } // if (ownerIsOrganizer) ..
2370          //
2371          // else => attendee is responding
2372          //
2373          //   if [context activeUser] == attendee
2374          //       [we change the PART-STATE]
2375          //   else
2376          //      [react on SENT-BY as someone else is acting for the attendee]
2377          else
2378            {
2379              iCalPerson *attendee, *delegate;
2380              NSString *delegateEmail;
2381
2382              attendee = [oldEvent userAsAttendee: [SOGoUser userWithLogin: owner]];
2383
2384              if (!attendee)
2385                attendee = [newEvent userAsAttendee: [SOGoUser userWithLogin: owner]];
2386              else
2387                {
2388                  // We must do an extra check here since Bob could have invited Alice
2389                  // using alice@example.com but she would have accepted with ATTENDEE set
2390                  // to sexy@example.com. That would duplicate the ATTENDEE and set the
2391                  // participation status to ACCEPTED for sexy@example.com but leave it
2392                  // to NEEDS-ACTION to alice@example. This can happen in Mozilla Thunderbird/Lightning
2393                  // when a user with multiple identities accepts an event invitation to one
2394                  // of its identity (which is different than the email address associated with
2395                  // the mail account) prior doing a calendar refresh.
2396                  NSMutableArray *attendees;
2397                  iCalPerson *participant;
2398
2399                  attendees = [NSMutableArray arrayWithArray: [newEvent attendeesWithoutUser: [SOGoUser userWithLogin: owner]]];
2400
2401                  participant = [newEvent participantForUser: [SOGoUser userWithLogin: owner]
2402                                                    attendee: attendee];
2403                  [attendee setPartStat: [participant partStat]];
2404                  [attendee setDelegatedFrom: [participant delegatedFrom]];
2405                  [attendee setDelegatedTo: [participant delegatedTo]];
2406                  [attendees addObject: attendee];
2407                  [newEvent setAttendees: attendees];
2408                }
2409
2410              // We first check of the sequences are alright. We don't accept attendees
2411              // accepting "old" invitations. If that's the case, we return a 409
2412              if ([[newEvent sequence] intValue] < [[oldEvent sequence] intValue])
2413                return [NSException exceptionWithHTTPStatus: 409
2414                                                     reason: @"sequences don't match"];
2415
2416              // Remove the RSVP attribute, as an action from the attendee
2417              // was actually performed, and this confuses iCal (bug #1850)
2418              [[attendee attributes] removeObjectForKey: @"RSVP"];
2419
2420              delegate = nil;
2421              delegateEmail = [attendee delegatedTo];
2422
2423              if ([delegateEmail length])
2424                {
2425                  if ([[delegateEmail lowercaseString] hasPrefix: @"mailto:"])
2426                    delegateEmail = [delegateEmail substringFromIndex: 7];
2427                  if ([delegateEmail length])
2428                    delegate = [newEvent findAttendeeWithEmail: delegateEmail];
2429                }
2430
2431              changes = [iCalEventChanges changesFromEvent: oldEvent  toEvent: newEvent];
2432
2433              // The current user deletes the occurence; let the organizer know that
2434              // the user has declined this occurence.
2435              if ([[changes updatedProperties] containsObject: @"exdate"])
2436                {
2437                  [self changeParticipationStatus: @"DECLINED"
2438                                     withDelegate: nil // FIXME (specify delegate?)
2439                                            alarm: nil
2440                                  forRecurrenceId: [self _addedExDate: oldEvent  newEvent: newEvent]];
2441                }
2442              else if (attendee)
2443                {
2444                  [self changeParticipationStatus: [attendee partStat]
2445                                     withDelegate: delegate
2446                                            alarm: nil
2447                                  forRecurrenceId: recurrenceId];
2448                }
2449              // All attendees and the organizer field were removed. Apple iCal does
2450              // that when we remove the last attendee of an event.
2451              //
2452              // We must update previous's attendees' calendars to actually
2453              // remove the event in each of them.
2454              else
2455                {
2456                  [self _handleRemovedUsers: [changes deletedAttendees]
2457                           withRecurrenceId: recurrenceId];
2458                }
2459            }
2460        } // if ([[newEvent attendees] count] || [[oldEvent attendees] count])
2461      else
2462        {
2463          changes = [iCalEventChanges changesFromEvent: oldEvent  toEvent: newEvent];
2464          if ([changes hasMajorChanges])
2465            [self sendReceiptEmailForObject: newEvent
2466                             addedAttendees: nil
2467                           deletedAttendees: nil
2468                           updatedAttendees: nil
2469                                  operation: EventUpdated];
2470        }
2471    }  // else of if (isNew) ...
2472
2473  unsigned int baseVersion;
2474  // We must NOT invoke [super PUTAction:] here as it'll resave
2475  // the content string and we could have etag mismatches.
2476  baseVersion = (isNew ? 0 : version);
2477
2478  ex = [self saveComponent: calendar
2479               baseVersion: baseVersion];
2480
2481  return ex;
2482}
2483
2484//
2485// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we
2486// simply invoke super's PUTAction.
2487//
2488// We also check if we must force transparency on all day events
2489// from iPhone clients.
2490//
2491- (id) PUTAction: (WOContext *) _ctx
2492{
2493  NSException *ex;
2494  NSString *etag;
2495  WORequest *rq;
2496  WOResponse *response;
2497  iCalCalendar *rqCalendar;
2498
2499  rq = [_ctx request];
2500  rqCalendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
2501
2502  // We are unable to parse the received calendar, we return right away
2503  // with a 400 error code.
2504  if (!rqCalendar)
2505    {
2506      return [NSException exceptionWithHTTPStatus: 400
2507                                           reason: @"Unable to parse event."];
2508    }
2509
2510  if (![self isNew])
2511    {
2512      //
2513      // We must check for etag changes prior doing anything since an attendee could
2514      // have changed its participation status and the organizer didn't get the
2515      // copy and is trying to do a modification to the event.
2516      //
2517      ex = [self matchesRequestConditionInContext: context];
2518      if (ex)
2519        return ex;
2520    }
2521
2522  ex = [self updateContentWithCalendar: rqCalendar fromRequest: rq];
2523  if (ex)
2524    response = (WOResponse *) ex;
2525  else
2526    {
2527      response = [_ctx response];
2528      if (isNew)
2529        [response setStatus: 201 /* Created */];
2530      else
2531        [response setStatus: 204 /* No Content */];
2532      etag = [self davEntityTag];
2533      if (etag)
2534        [response setHeader: etag forKey: @"etag"];
2535    }
2536
2537  return response;
2538}
2539
2540- (BOOL) resourceHasAutoAccepted
2541{
2542  return _resourceHasAutoAccepted;
2543}
2544
2545@end /* SOGoAppointmentObject */
2546