The UID was being used to check if the changes in an appointment had been made by its organiser. In this case, the UID is the user name, without taking the domain into account. The `owner` variable, however, is a full email address, so the comparison was never successful. This caused the update notification mail not to be sent.
2268 lines
79 KiB
Objective-C
2268 lines
79 KiB
Objective-C
/*
|
|
Copyright (C) 2007-2014 Inverse inc.
|
|
Copyright (C) 2004-2005 SKYRIX Software AG
|
|
|
|
This file is part of SOGo
|
|
|
|
SOGo is free software; you can redistribute it and/or modify it under
|
|
the terms of the GNU Lesser General Public License as published by the
|
|
Free Software Foundation; either version 2, or (at your option) any
|
|
later version.
|
|
|
|
SOGo is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
|
License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with OGo; see the file COPYING. If not, write to the
|
|
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
|
|
02111-1307, USA.
|
|
*/
|
|
|
|
#import <Foundation/NSCalendarDate.h>
|
|
#import <Foundation/NSDictionary.h>
|
|
#import <Foundation/NSEnumerator.h>
|
|
#import <Foundation/NSTimeZone.h>
|
|
#import <Foundation/NSValue.h>
|
|
#import <Foundation/NSPredicate.h>
|
|
|
|
#import <NGObjWeb/NSException+HTTP.h>
|
|
#import <NGObjWeb/WOContext+SoObjects.h>
|
|
#import <NGObjWeb/WOResponse.h>
|
|
#import <NGExtensions/NGCalendarDateRange.h>
|
|
#import <NGExtensions/NSNull+misc.h>
|
|
#import <NGExtensions/NSObject+Logs.h>
|
|
#import <NGCards/iCalCalendar.h>
|
|
#import <NGCards/iCalDateTime.h>
|
|
#import <NGCards/iCalEvent.h>
|
|
#import <NGCards/iCalEventChanges.h>
|
|
#import <NGCards/iCalPerson.h>
|
|
#import <NGCards/iCalRecurrenceCalculator.h>
|
|
#import <NGCards/NSCalendarDate+NGCards.h>
|
|
#import <SaxObjC/XMLNamespaces.h>
|
|
|
|
#import <NGCards/iCalDateTime.h>
|
|
#import <NGCards/iCalTimeZone.h>
|
|
#import <NGCards/iCalTimeZonePeriod.h>
|
|
#import <NGCards/NSString+NGCards.h>
|
|
|
|
#import <SOGo/SOGoConstants.h>
|
|
#import <SOGo/SOGoUserManager.h>
|
|
#import <SOGo/NSArray+Utilities.h>
|
|
#import <SOGo/NSDictionary+Utilities.h>
|
|
#import <SOGo/NSObject+DAV.h>
|
|
#import <SOGo/NSString+Utilities.h>
|
|
#import <SOGo/SOGoObject.h>
|
|
#import <SOGo/SOGoPermissions.h>
|
|
#import <SOGo/SOGoGroup.h>
|
|
#import <SOGo/SOGoUser.h>
|
|
#import <SOGo/SOGoUserSettings.h>
|
|
#import <SOGo/SOGoDomainDefaults.h>
|
|
#import <SOGo/SOGoWebDAVValue.h>
|
|
#import <SOGo/WORequest+SOGo.h>
|
|
|
|
#import "iCalCalendar+SOGo.h"
|
|
#import "iCalEventChanges+SOGo.h"
|
|
#import "iCalEntityObject+SOGo.h"
|
|
#import "iCalPerson+SOGo.h"
|
|
#import "NSArray+Appointments.h"
|
|
#import "SOGoAppointmentFolder.h"
|
|
#import "SOGoAppointmentOccurence.h"
|
|
#import "SOGoCalendarComponent.h"
|
|
|
|
#import "SOGoAppointmentObject.h"
|
|
|
|
@implementation SOGoAppointmentObject
|
|
|
|
- (NSString *) componentTag
|
|
{
|
|
return @"vevent";
|
|
}
|
|
|
|
- (SOGoComponentOccurence *) occurence: (iCalRepeatableEntityObject *) occ
|
|
{
|
|
NSArray *allEvents;
|
|
|
|
allEvents = [[occ parent] events];
|
|
|
|
return [SOGoAppointmentOccurence
|
|
occurenceWithComponent: occ
|
|
withMasterComponent: [allEvents objectAtIndex: 0]
|
|
inContainer: self];
|
|
}
|
|
|
|
/**
|
|
* Return a new exception in the recurrent event.
|
|
* @param theRecurrenceID the ID of the occurence.
|
|
* @return a new occurence.
|
|
*/
|
|
- (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) theRecurrenceID
|
|
{
|
|
iCalEvent *newOccurence, *master;
|
|
NSCalendarDate *date, *firstDate;
|
|
unsigned int interval, nbrDays;
|
|
|
|
newOccurence = (iCalEvent *) [super newOccurenceWithID: theRecurrenceID];
|
|
date = [newOccurence recurrenceId];
|
|
|
|
master = [self component: NO secure: NO];
|
|
firstDate = [master startDate];
|
|
|
|
interval = [[master endDate]
|
|
timeIntervalSinceDate: firstDate];
|
|
if ([newOccurence isAllDay])
|
|
{
|
|
nbrDays = ((float) abs (interval) / 86400);
|
|
[newOccurence setAllDayWithStartDate: date
|
|
duration: nbrDays];
|
|
}
|
|
else
|
|
{
|
|
[newOccurence setStartDate: date];
|
|
[newOccurence setEndDate: [date addYear: 0
|
|
month: 0
|
|
day: 0
|
|
hour: 0
|
|
minute: 0
|
|
second: interval]];
|
|
}
|
|
|
|
return newOccurence;
|
|
}
|
|
|
|
- (iCalRepeatableEntityObject *) lookupOccurrence: (NSString *) recID
|
|
{
|
|
return [[self calendar: NO secure: NO] eventWithRecurrenceID: recID];
|
|
}
|
|
|
|
- (SOGoAppointmentObject *) _lookupEvent: (NSString *) eventUID
|
|
forUID: (NSString *) uid
|
|
{
|
|
SOGoAppointmentFolder *folder;
|
|
SOGoAppointmentObject *object;
|
|
NSArray *folders;
|
|
NSEnumerator *e;
|
|
NSString *possibleName;
|
|
|
|
object = nil;
|
|
folders = [container lookupCalendarFoldersForUID: uid];
|
|
e = [folders objectEnumerator];
|
|
while ( object == nil && (folder = [e nextObject]) )
|
|
{
|
|
object = [folder lookupName: nameInContainer
|
|
inContext: context
|
|
acquire: NO];
|
|
if ([object isKindOfClass: [NSException class]] || [object isNew])
|
|
{
|
|
possibleName = [folder resourceNameForEventUID: eventUID];
|
|
if (possibleName)
|
|
{
|
|
object = [folder lookupName: possibleName
|
|
inContext: context acquire: NO];
|
|
if ([object isKindOfClass: [NSException class]] || [object isNew])
|
|
object = nil;
|
|
}
|
|
else
|
|
object = nil;
|
|
}
|
|
}
|
|
|
|
if (!object)
|
|
{
|
|
// Create the event in the user's personal calendar.
|
|
folder = [[SOGoUser userWithLogin: uid]
|
|
personalCalendarFolderInContext: context];
|
|
object = [SOGoAppointmentObject objectWithName: nameInContainer
|
|
inContainer: folder];
|
|
[object setIsNew: YES];
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _addOrUpdateEvent: (iCalEvent *) theEvent
|
|
forUID: (NSString *) theUID
|
|
owner: (NSString *) theOwner
|
|
{
|
|
if (![theUID isEqualToString: theOwner])
|
|
{
|
|
SOGoAppointmentObject *attendeeObject;
|
|
iCalCalendar *iCalendarToSave;
|
|
|
|
iCalendarToSave = nil;
|
|
attendeeObject = [self _lookupEvent: [theEvent uid] forUID: theUID];
|
|
|
|
// We must add an occurence to a non-existing event. We have
|
|
// to handle this with care, as in the postCalDAVEventRequestTo:from:
|
|
if ([attendeeObject isNew] && [theEvent recurrenceId])
|
|
{
|
|
iCalEvent *ownerEvent;
|
|
iCalPerson *person;
|
|
SOGoUser *user;
|
|
|
|
// We check if the attendee that was added to a single occurence is
|
|
// present in the master component. If not, we add it with a participation
|
|
// status set to "DECLINED".
|
|
ownerEvent = [[[theEvent parent] events] objectAtIndex: 0];
|
|
user = [SOGoUser userWithLogin: theUID];
|
|
if (![ownerEvent userAsAttendee: user])
|
|
{
|
|
// Update the master event in the owner's calendar with the
|
|
// status of the new attendee set as "DECLINED".
|
|
person = [iCalPerson elementWithTag: @"attendee"];
|
|
[person setCn: [user cn]];
|
|
[person setEmail: [[user allEmails] objectAtIndex: 0]];
|
|
[person setParticipationStatus: iCalPersonPartStatDeclined];
|
|
[person setRsvp: @"TRUE"];
|
|
[person setRole: @"REQ-PARTICIPANT"];
|
|
[ownerEvent addToAttendees: person];
|
|
|
|
iCalendarToSave = [ownerEvent parent];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO : if [theEvent recurrenceId], only update this occurrence
|
|
// in attendee's calendar
|
|
|
|
// TODO : when updating the master event, handle exception dates
|
|
// in attendee's calendar (add exception dates and remove matching
|
|
// occurrences) -- see _updateRecurrenceIDsWithEvent:
|
|
|
|
iCalendarToSave = [theEvent parent];
|
|
}
|
|
|
|
// Save the event in the attendee's calendar
|
|
if (iCalendarToSave)
|
|
[attendeeObject saveCalendar: iCalendarToSave];
|
|
}
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _removeEventFromUID: (NSString *) theUID
|
|
owner: (NSString *) theOwner
|
|
withRecurrenceId: (NSCalendarDate *) recurrenceId
|
|
{
|
|
if (![theUID isEqualToString: theOwner])
|
|
{
|
|
SOGoAppointmentFolder *folder;
|
|
SOGoAppointmentObject *object;
|
|
iCalEntityObject *currentOccurence;
|
|
iCalRepeatableEntityObject *event;
|
|
iCalCalendar *calendar;
|
|
NSCalendarDate *currentId;
|
|
NSArray *occurences;
|
|
int max, count;
|
|
|
|
// Invitations are always written to the personal folder; it's not necessay
|
|
// to look into all folders of the user
|
|
folder = [[SOGoUser userWithLogin: theUID]
|
|
personalCalendarFolderInContext: context];
|
|
object = [folder lookupName: nameInContainer
|
|
inContext: context acquire: NO];
|
|
if (![object isKindOfClass: [NSException class]])
|
|
{
|
|
if (recurrenceId == nil)
|
|
[object delete];
|
|
else
|
|
{
|
|
calendar = [object calendar: NO secure: NO];
|
|
|
|
// If recurrenceId is defined, remove the occurence from
|
|
// the repeating event.
|
|
occurences = [calendar events];
|
|
max = [occurences count];
|
|
count = 1;
|
|
while (count < max)
|
|
{
|
|
currentOccurence = [occurences objectAtIndex: count];
|
|
currentId = [currentOccurence recurrenceId];
|
|
if ([currentId compare: recurrenceId] == NSOrderedSame)
|
|
{
|
|
[[calendar children] removeObject: currentOccurence];
|
|
break;
|
|
}
|
|
count++;
|
|
}
|
|
|
|
// Add an date exception.
|
|
event = (iCalRepeatableEntityObject*)[calendar firstChildWithTag: [object componentTag]];
|
|
[event addToExceptionDates: recurrenceId];
|
|
|
|
[event increaseSequence];
|
|
|
|
// We save the updated iCalendar in the database.
|
|
[object saveCalendar: calendar];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _handleRemovedUsers: (NSArray *) attendees
|
|
withRecurrenceId: (NSCalendarDate *) recurrenceId
|
|
{
|
|
NSEnumerator *enumerator;
|
|
iCalPerson *currentAttendee;
|
|
NSString *currentUID;
|
|
|
|
enumerator = [attendees objectEnumerator];
|
|
while ((currentAttendee = [enumerator nextObject]))
|
|
{
|
|
currentUID = [currentAttendee uid];
|
|
if (currentUID)
|
|
[self _removeEventFromUID: currentUID
|
|
owner: owner
|
|
withRecurrenceId: recurrenceId];
|
|
}
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _removeDelegationChain: (iCalPerson *) delegate
|
|
inEvent: (iCalEvent *) event
|
|
{
|
|
NSString *delegatedTo, *mailTo;
|
|
|
|
delegatedTo = [delegate delegatedTo];
|
|
if ([delegatedTo length] > 0)
|
|
{
|
|
mailTo = [delegatedTo rfc822Email];
|
|
delegate = [event findAttendeeWithEmail: mailTo];
|
|
if (delegate)
|
|
{
|
|
[self _removeDelegationChain: delegate
|
|
inEvent: event];
|
|
[event removeFromAttendees: delegate];
|
|
}
|
|
else
|
|
[self errorWithFormat:@"broken chain: delegate with email '%@' was not found", mailTo];
|
|
}
|
|
}
|
|
|
|
//
|
|
// This method returns YES when any attendee has been removed
|
|
// and NO otherwise.
|
|
//
|
|
- (BOOL) _requireResponseFromAttendees: (iCalEvent *) event
|
|
{
|
|
NSArray *attendees;
|
|
iCalPerson *currentAttendee;
|
|
BOOL listHasChanged = NO;
|
|
int count, max;
|
|
|
|
attendees = [event attendees];
|
|
max = [attendees count];
|
|
|
|
for (count = 0; count < max; count++)
|
|
{
|
|
currentAttendee = [attendees objectAtIndex: count];
|
|
if ([[currentAttendee delegatedTo] length] > 0)
|
|
{
|
|
[self _removeDelegationChain: currentAttendee
|
|
inEvent: event];
|
|
[currentAttendee setDelegatedTo: nil];
|
|
listHasChanged = YES;
|
|
}
|
|
[currentAttendee setRsvp: @"TRUE"];
|
|
[currentAttendee setParticipationStatus: iCalPersonPartStatNeedsAction];
|
|
}
|
|
|
|
return listHasChanged;
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _handleSequenceUpdateInEvent: (iCalEvent *) newEvent
|
|
ignoringAttendees: (NSArray *) attendees
|
|
fromOldEvent: (iCalEvent *) oldEvent
|
|
{
|
|
NSMutableArray *updateAttendees;
|
|
NSEnumerator *enumerator;
|
|
iCalPerson *currentAttendee;
|
|
NSString *currentUID;
|
|
|
|
updateAttendees = [NSMutableArray arrayWithArray: [newEvent attendees]];
|
|
[updateAttendees removeObjectsInArray: attendees];
|
|
|
|
enumerator = [updateAttendees objectEnumerator];
|
|
while ((currentAttendee = [enumerator nextObject]))
|
|
{
|
|
currentUID = [currentAttendee uid];
|
|
if (currentUID)
|
|
[self _addOrUpdateEvent: newEvent
|
|
forUID: currentUID
|
|
owner: owner];
|
|
}
|
|
|
|
[self sendEMailUsingTemplateNamed: @"Update"
|
|
forObject: [newEvent itipEntryWithMethod: @"request"]
|
|
previousObject: oldEvent
|
|
toAttendees: updateAttendees
|
|
withType: @"calendar:invitation-update"];
|
|
}
|
|
|
|
// This method scans the list of attendees.
|
|
- (NSException *) _handleAttendeeAvailability: (NSArray *) theAttendees
|
|
forEvent: (iCalEvent *) theEvent
|
|
{
|
|
iCalPerson *currentAttendee;
|
|
SOGoUser *user;
|
|
SOGoUserSettings *us;
|
|
NSMutableArray *unavailableAttendees;
|
|
NSEnumerator *enumerator;
|
|
NSString *currentUID, *ownerUID, *whiteListString;
|
|
NSMutableString *reason;
|
|
NSDictionary *values;
|
|
NSMutableDictionary *value, *moduleSettings, *whiteList;
|
|
int i, count;
|
|
i = count = 0;
|
|
|
|
// Build list of the attendees uids without ressources
|
|
unavailableAttendees = [[NSMutableArray alloc] init];
|
|
enumerator = [theAttendees objectEnumerator];
|
|
ownerUID = [[[self context] activeUser] login];
|
|
|
|
while ((currentAttendee = [enumerator nextObject]))
|
|
{
|
|
currentUID = [currentAttendee uid];
|
|
|
|
if (currentUID)
|
|
{
|
|
user = [SOGoUser userWithLogin: currentUID];
|
|
us = [user userSettings];
|
|
moduleSettings = [us objectForKey:@"Calendar"];
|
|
|
|
// Check if the user prevented their account from beeing invited to events
|
|
if (![user isResource] && [[moduleSettings objectForKey:@"PreventInvitations"] boolValue])
|
|
{
|
|
// Check if the user have a whiteList
|
|
whiteListString = [moduleSettings objectForKey:@"PreventInvitationsWhitelist"];
|
|
whiteList = [whiteListString objectFromJSONString];
|
|
|
|
// If the filter have a hit, do not add the currentUID to the unavailableAttendees array
|
|
if (![whiteList objectForKey:ownerUID])
|
|
{
|
|
values = [NSDictionary dictionaryWithObject:[user cn] forKey:@"Cn"];
|
|
[unavailableAttendees addObject:values];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
count = [unavailableAttendees count];
|
|
|
|
if (count > 0)
|
|
{
|
|
reason = [NSMutableString stringWithString:[self labelForKey: @"Inviting the following persons is prohibited:"]];
|
|
|
|
// Add all the unavailable users in the warning message
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
value = [unavailableAttendees objectAtIndex:i];
|
|
[reason appendString:[value keysWithFormat: @"\n %{Cn}"]];
|
|
if (i < count-2)
|
|
[reason appendString:@", "];
|
|
}
|
|
|
|
[unavailableAttendees release];
|
|
|
|
return [NSException exceptionWithHTTPStatus:409 reason: reason];
|
|
}
|
|
|
|
[unavailableAttendees release];
|
|
|
|
return nil;
|
|
}
|
|
|
|
//
|
|
// This methods scans the list of attendees. If they are
|
|
// considered as resource, it checks for conflicting
|
|
// dates for the event.
|
|
//
|
|
// We check for between startDate + 1 second and
|
|
// endDate - 1 second
|
|
//
|
|
//
|
|
// It also CHANGES the participation status of resources
|
|
// depending on constraints defined on them.
|
|
//
|
|
// Note that it doesn't matter if it changes the participation
|
|
// status since in case of an error, nothing will get saved.
|
|
//
|
|
- (NSException *) _handleResourcesConflicts: (NSArray *) theAttendees
|
|
forEvent: (iCalEvent *) theEvent
|
|
{
|
|
iCalPerson *currentAttendee;
|
|
NSMutableArray *attendees;
|
|
NSEnumerator *enumerator;
|
|
NSString *currentUID;
|
|
SOGoUser *user, *currentUser;
|
|
|
|
// Build a list of the attendees uids
|
|
attendees = [NSMutableArray arrayWithCapacity: [theAttendees count]];
|
|
enumerator = [theAttendees objectEnumerator];
|
|
while ((currentAttendee = [enumerator nextObject]))
|
|
{
|
|
currentUID = [currentAttendee uid];
|
|
if (currentUID)
|
|
{
|
|
[attendees addObject: currentUID];
|
|
}
|
|
}
|
|
|
|
// If the active user is not the owner of the calendar, check possible conflict when
|
|
// the owner is a resource
|
|
currentUser = [context activeUser];
|
|
if (!activeUserIsOwner && ![currentUser isSuperUser])
|
|
{
|
|
[attendees addObject: owner];
|
|
}
|
|
|
|
enumerator = [attendees objectEnumerator];
|
|
while ((currentUID = [enumerator nextObject]))
|
|
{
|
|
user = [SOGoUser userWithLogin: currentUID];
|
|
|
|
if ([user isResource])
|
|
{
|
|
NSCalendarDate *start, *end, *rangeStartDate, *rangeEndDate;
|
|
SOGoAppointmentFolder *folder;
|
|
NGCalendarDateRange *range;
|
|
NSMutableArray *fbInfo;
|
|
NSArray *allOccurences;
|
|
|
|
BOOL must_delete;
|
|
int i, j, delta;
|
|
|
|
// We get the start/end date for our conflict range. If the event to be added is recurring, we
|
|
// check for at least a year to start with.
|
|
start = [[theEvent startDate] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: 1];
|
|
end = [[theEvent endDate] dateByAddingYears: ([theEvent isRecurrent] ? 1 : 0) months: 0 days: 0 hours: 0 minutes: 0 seconds: -1];
|
|
|
|
folder = [user personalCalendarFolderInContext: context];
|
|
|
|
// Deny access to the resource if the ACLs don't allow the user
|
|
if (![folder aclSQLListingFilter])
|
|
{
|
|
NSDictionary *values;
|
|
NSString *reason;
|
|
|
|
values = [NSDictionary dictionaryWithObjectsAndKeys:
|
|
[user cn], @"Cn",
|
|
[user systemEmail], @"SystemEmail"];
|
|
reason = [values keysWithFormat: [self labelForKey: @"Cannot access resource: \"%{Cn} %{SystemEmail}\""]];
|
|
return [NSException exceptionWithHTTPStatus:403 reason: reason];
|
|
}
|
|
|
|
fbInfo = [NSMutableArray arrayWithArray: [folder fetchFreeBusyInfosFrom: start
|
|
to: end]];
|
|
|
|
// We first remove any occurences in the freebusy that corresponds to the
|
|
// current event. We do this to avoid raising a conflict if we move a 1 hour
|
|
// meeting from 12:00-13:00 to 12:15-13:15. We would overlap on ourself otherwise.
|
|
//
|
|
// We must also check here for repetitive events that don't overlap our event.
|
|
// We remove all events that don't overlap. The events here are already
|
|
// decomposed.
|
|
//
|
|
if ([theEvent isRecurrent])
|
|
allOccurences = [theEvent recurrenceRangesWithinCalendarDateRange: [NGCalendarDateRange calendarDateRangeWithStartDate: start
|
|
endDate: end]
|
|
firstInstanceCalendarDateRange: [NGCalendarDateRange calendarDateRangeWithStartDate: [theEvent startDate]
|
|
endDate: [theEvent endDate]]];
|
|
else
|
|
allOccurences = nil;
|
|
|
|
for (i = [fbInfo count]-1; i >= 0; i--)
|
|
{
|
|
// We MUST use the -uniqueChildWithTag method here because the event has been flattened, so its timezone has been
|
|
// modified in SOGoAppointmentFolder: -fixupCycleRecord: ....
|
|
rangeStartDate = [[fbInfo objectAtIndex: i] objectForKey: @"startDate"];
|
|
delta = [[rangeStartDate timeZoneDetail] timeZoneSecondsFromGMT] - [[[(iCalDateTime *)[theEvent uniqueChildWithTag: @"dtstart"] timeZone] periodForDate: [theEvent startDate]] secondsOffsetFromGMT];
|
|
rangeStartDate = [rangeStartDate dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: delta];
|
|
|
|
rangeEndDate = [[fbInfo objectAtIndex: i] objectForKey: @"endDate"];
|
|
delta = [[rangeEndDate timeZoneDetail] timeZoneSecondsFromGMT] - [[[(iCalDateTime *)[theEvent uniqueChildWithTag: @"dtend"] timeZone] periodForDate: [theEvent endDate]] secondsOffsetFromGMT];
|
|
rangeEndDate = [rangeEndDate dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: delta];
|
|
|
|
range = [NGCalendarDateRange calendarDateRangeWithStartDate: rangeStartDate
|
|
endDate: rangeEndDate];
|
|
|
|
if ([[[fbInfo objectAtIndex: i] objectForKey: @"c_uid"] compare: [theEvent uid]] == NSOrderedSame)
|
|
{
|
|
[fbInfo removeObjectAtIndex: i];
|
|
continue;
|
|
}
|
|
|
|
// No need to check if the event isn't recurrent here as it's handled correctly
|
|
// when we compute the "end" date.
|
|
if ([allOccurences count])
|
|
{
|
|
must_delete = YES;
|
|
|
|
for (j = 0; j < [allOccurences count]; j++)
|
|
{
|
|
if ([range doesIntersectWithDateRange: [allOccurences objectAtIndex: j]])
|
|
{
|
|
must_delete = NO;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (must_delete)
|
|
[fbInfo removeObjectAtIndex: i];
|
|
}
|
|
}
|
|
|
|
// Find the attendee associated to the current UID
|
|
for (i = 0; i < [theAttendees count]; i++)
|
|
{
|
|
currentAttendee = [theAttendees objectAtIndex: i];
|
|
if ([[currentAttendee uid] isEqualToString: currentUID])
|
|
break;
|
|
else
|
|
currentAttendee = nil;
|
|
}
|
|
|
|
if ([fbInfo count])
|
|
{
|
|
// If we always force the auto-accept if numberOfSimultaneousBookings <= 0 (ie., no limit
|
|
// is imposed) or if numberOfSimultaneousBookings is greater than the number of
|
|
// overlapping events
|
|
if ([user numberOfSimultaneousBookings] <= 0 ||
|
|
[user numberOfSimultaneousBookings] > [fbInfo count])
|
|
{
|
|
if (currentAttendee)
|
|
{
|
|
[[currentAttendee attributes] removeObjectForKey: @"RSVP"];
|
|
[currentAttendee setParticipationStatus: iCalPersonPartStatAccepted];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
iCalCalendar *calendar;
|
|
NSDictionary *values;
|
|
NSString *reason;
|
|
iCalEvent *event;
|
|
|
|
calendar = [iCalCalendar parseSingleFromSource: [[fbInfo objectAtIndex: 0] objectForKey: @"c_content"]];
|
|
event = [[calendar events] lastObject];
|
|
|
|
values = [NSDictionary dictionaryWithObjectsAndKeys:
|
|
[NSString stringWithFormat: @"%d", [user numberOfSimultaneousBookings]], @"NumberOfSimultaneousBookings",
|
|
[user cn], @"Cn",
|
|
[user systemEmail], @"SystemEmail",
|
|
([event summary] ? [event summary] : @""), @"EventTitle",
|
|
[[fbInfo objectAtIndex: 0] objectForKey: @"startDate"], @"StartDate",
|
|
nil];
|
|
|
|
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}."]];
|
|
|
|
return [NSException exceptionWithHTTPStatus: 403
|
|
reason: reason];
|
|
}
|
|
}
|
|
else if (currentAttendee)
|
|
{
|
|
// No conflict, we auto-accept. We do this for resources automatically if no
|
|
// double-booking is observed. If it's not the desired behavior, just don't
|
|
// set the resource as one!
|
|
[[currentAttendee attributes] removeObjectForKey: @"RSVP"];
|
|
[currentAttendee setParticipationStatus: iCalPersonPartStatAccepted];
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (NSException *) _handleAddedUsers: (NSArray *) attendees
|
|
fromEvent: (iCalEvent *) newEvent
|
|
{
|
|
iCalPerson *currentAttendee;
|
|
NSEnumerator *enumerator;
|
|
NSString *currentUID;
|
|
NSException *e;
|
|
|
|
// We check for conflicts
|
|
if ((e = [self _handleResourcesConflicts: attendees forEvent: newEvent]))
|
|
return e;
|
|
if ((e = [self _handleAttendeeAvailability: attendees forEvent: newEvent]))
|
|
return e;
|
|
|
|
enumerator = [attendees objectEnumerator];
|
|
while ((currentAttendee = [enumerator nextObject]))
|
|
{
|
|
currentUID = [currentAttendee uid];
|
|
if (currentUID)
|
|
[self _addOrUpdateEvent: newEvent
|
|
forUID: currentUID
|
|
owner: owner];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _addOrDeleteAttendees: (NSArray *) theAttendees
|
|
inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent
|
|
add: (BOOL) shouldAdd
|
|
{
|
|
|
|
NSArray *events;
|
|
iCalEvent *e;
|
|
int i,j;
|
|
|
|
// We don't add/delete attendees to all recurrence exceptions if
|
|
// the modification was actually NOT made on the master event
|
|
if ([theEvent recurrenceId])
|
|
return;
|
|
|
|
events = [[theEvent parent] events];
|
|
|
|
for (i = 0; i < [events count]; i++)
|
|
{
|
|
e = [events objectAtIndex: i];
|
|
if ([e recurrenceId])
|
|
for (j = 0; j < [theAttendees count]; j++)
|
|
if (shouldAdd)
|
|
[e addToAttendees: [theAttendees objectAtIndex: j]];
|
|
else
|
|
[e removeFromAttendees: [theAttendees objectAtIndex: j]];
|
|
}
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (NSException *) _handleUpdatedEvent: (iCalEvent *) newEvent
|
|
fromOldEvent: (iCalEvent *) oldEvent
|
|
{
|
|
NSArray *addedAttendees, *deletedAttendees, *updatedAttendees;
|
|
iCalEventChanges *changes;
|
|
NSException *ex;
|
|
|
|
addedAttendees = nil;
|
|
deletedAttendees = nil;
|
|
updatedAttendees = nil;
|
|
|
|
changes = [newEvent getChangesRelativeToEvent: oldEvent];
|
|
if ([changes sequenceShouldBeIncreased])
|
|
{
|
|
// Set new attendees status to "needs action" and recompute changes when
|
|
// the list of attendees has changed. The list might have changed since
|
|
// by changing a major property of the event, we remove all the delegation
|
|
// chains to "other" attendees
|
|
if ([self _requireResponseFromAttendees: newEvent])
|
|
changes = [newEvent getChangesRelativeToEvent: oldEvent];
|
|
}
|
|
|
|
deletedAttendees = [changes deletedAttendees];
|
|
|
|
// We delete the attendees in all exception occurences, if
|
|
// the attendees were removed from the master event.
|
|
[self _addOrDeleteAttendees: deletedAttendees
|
|
inRecurrenceExceptionsForEvent: newEvent
|
|
add: NO];
|
|
|
|
if ([deletedAttendees count])
|
|
{
|
|
[self _handleRemovedUsers: deletedAttendees
|
|
withRecurrenceId: [newEvent recurrenceId]];
|
|
[self sendEMailUsingTemplateNamed: @"Deletion"
|
|
forObject: [newEvent itipEntryWithMethod: @"cancel"]
|
|
previousObject: oldEvent
|
|
toAttendees: deletedAttendees
|
|
withType: @"calendar:cancellation"];
|
|
}
|
|
|
|
if ((ex = [self _handleResourcesConflicts: [newEvent attendees] forEvent: newEvent]))
|
|
return ex;
|
|
if ((ex = [self _handleAttendeeAvailability: [newEvent attendees] forEvent: newEvent]))
|
|
return ex;
|
|
|
|
addedAttendees = [changes insertedAttendees];
|
|
|
|
// We insert the attendees in all exception occurences, if
|
|
// the attendees were added to the master event.
|
|
[self _addOrDeleteAttendees: addedAttendees
|
|
inRecurrenceExceptionsForEvent: newEvent
|
|
add: YES];
|
|
|
|
if ([changes sequenceShouldBeIncreased])
|
|
{
|
|
[newEvent increaseSequence];
|
|
|
|
// Update attendees calendars and send them an update
|
|
// notification by email. We ignore the newly added
|
|
// attendees as we don't want to send them invitation
|
|
// update emails
|
|
[self _handleSequenceUpdateInEvent: newEvent
|
|
ignoringAttendees: addedAttendees
|
|
fromOldEvent: oldEvent];
|
|
}
|
|
else
|
|
{
|
|
// If other attributes have changed, update the event
|
|
// in each attendee's calendar
|
|
if ([[changes updatedProperties] count])
|
|
{
|
|
NSEnumerator *enumerator;
|
|
iCalPerson *currentAttendee;
|
|
NSString *currentUID;
|
|
|
|
updatedAttendees = [newEvent attendees];
|
|
enumerator = [updatedAttendees objectEnumerator];
|
|
while ((currentAttendee = [enumerator nextObject]))
|
|
{
|
|
currentUID = [currentAttendee uid];
|
|
if (currentUID)
|
|
[self _addOrUpdateEvent: newEvent
|
|
forUID: currentUID
|
|
owner: owner];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ([addedAttendees count])
|
|
{
|
|
// Send an invitation to new attendees
|
|
if ((ex = [self _handleAddedUsers: addedAttendees fromEvent: newEvent]))
|
|
return ex;
|
|
|
|
[self sendEMailUsingTemplateNamed: @"Invitation"
|
|
forObject: [newEvent itipEntryWithMethod: @"request"]
|
|
previousObject: oldEvent
|
|
toAttendees: addedAttendees
|
|
withType: @"calendar:invitation"];
|
|
}
|
|
|
|
[self sendReceiptEmailForObject: newEvent
|
|
addedAttendees: addedAttendees
|
|
deletedAttendees: deletedAttendees
|
|
updatedAttendees: updatedAttendees
|
|
operation: EventUpdated];
|
|
|
|
return nil;
|
|
}
|
|
|
|
//
|
|
// Workflow : +----------------------+
|
|
// | |
|
|
// [saveComponent:]---> _handleAddedUsers:fromEvent: <-+ |
|
|
// | | v
|
|
// +------------> _handleUpdatedEvent:fromOldEvent: ---> _addOrUpdateEvent:forUID:owner: <-----------+
|
|
// | | ^ |
|
|
// v v | |
|
|
// _handleRemovedUsers:withRecurrenceId: _handleSequenceUpdateInEvent:ignoringAttendees:fromOldEvent: |
|
|
// | |
|
|
// | [DELETEAction:] |
|
|
// | | {_handleAdded/Updated...}<--+ |
|
|
// | v | |
|
|
// | [prepareDeleteOccurence:] [PUTAction:] |
|
|
// | | | | |
|
|
// v v v v |
|
|
// _removeEventFromUID:owner:withRecurrenceId: [changeParticipationStatus:withDelegate:forRecurrenceId:] |
|
|
// | | |
|
|
// | v |
|
|
// +------------------------> _handleAttendee:withDelegate:ownerUser:statusChange:inEvent: ---> [sendResponseToOrganizer:from:]
|
|
// |
|
|
// v
|
|
// _updateAttendee:withDelegate:ownerUser:forEventUID:withRecurrenceId:withSequence:forUID:shouldAddSentBy:
|
|
//
|
|
//
|
|
- (NSException *) saveComponent: (iCalEvent *) newEvent
|
|
{
|
|
iCalEvent *oldEvent, *oldMasterEvent;
|
|
NSCalendarDate *recurrenceId;
|
|
NSString *recurrenceTime;
|
|
SOGoUser *ownerUser;
|
|
NSArray *attendees;
|
|
NSException *ex;
|
|
|
|
[[newEvent parent] setMethod: @""];
|
|
ownerUser = [SOGoUser userWithLogin: owner];
|
|
|
|
[self expandGroupsInEvent: newEvent];
|
|
|
|
// We first update the event. It is important to do this initially
|
|
// as the event's UID might get modified.
|
|
[super updateComponent: newEvent];
|
|
|
|
if ([self isNew])
|
|
{
|
|
// New event -- send invitation to all attendees
|
|
attendees = [newEvent attendeesWithoutUser: ownerUser];
|
|
|
|
// We catch conflicts and abort the save process immediately
|
|
// in case of one with resources
|
|
if ((ex = [self _handleAddedUsers: attendees fromEvent: newEvent]))
|
|
return ex;
|
|
|
|
if ([attendees count])
|
|
{
|
|
[self sendEMailUsingTemplateNamed: @"Invitation"
|
|
forObject: [newEvent itipEntryWithMethod: @"request"]
|
|
previousObject: nil
|
|
toAttendees: attendees
|
|
withType: @"calendar:invitation"];
|
|
}
|
|
|
|
[self sendReceiptEmailForObject: newEvent
|
|
addedAttendees: attendees
|
|
deletedAttendees: nil
|
|
updatedAttendees: nil
|
|
operation: EventCreated];
|
|
}
|
|
else
|
|
{
|
|
BOOL hasOrganizer;
|
|
|
|
// Event is modified -- sent update status to all attendees
|
|
// and modify their calendars.
|
|
recurrenceId = [newEvent recurrenceId];
|
|
if (recurrenceId == nil)
|
|
oldEvent = [self component: NO secure: NO];
|
|
else
|
|
{
|
|
// If recurrenceId is defined, find the specified occurence
|
|
// within the repeating vEvent.
|
|
recurrenceTime = [NSString stringWithFormat: @"%f", [recurrenceId timeIntervalSince1970]];
|
|
oldEvent = (iCalEvent*)[self lookupOccurrence: recurrenceTime];
|
|
if (oldEvent == nil) // If no occurence found, create one
|
|
oldEvent = (iCalEvent *)[self newOccurenceWithID: recurrenceTime];
|
|
}
|
|
|
|
oldMasterEvent = (iCalEvent *)[[oldEvent parent] firstChildWithTag: [self componentTag]];
|
|
hasOrganizer = [[[oldMasterEvent organizer] email] length];
|
|
|
|
if (!hasOrganizer || [oldMasterEvent userIsOrganizer: ownerUser])
|
|
// The owner is the organizer of the event; handle the modifications. We aslo
|
|
// catch conflicts just like when the events are created
|
|
if ((ex = [self _handleUpdatedEvent: newEvent fromOldEvent: oldEvent]))
|
|
return ex;
|
|
}
|
|
|
|
[super saveComponent: newEvent];
|
|
[self flush];
|
|
|
|
return nil;
|
|
}
|
|
|
|
//
|
|
// This method is used to update the status of an attendee.
|
|
//
|
|
// - theOwnerUser is owner of the calendar where the attendee
|
|
// participation state has changed.
|
|
// - uid is the actual UID of the user for whom we must
|
|
// update the calendar event (with the participation change)
|
|
// - delegate is the delegate attendee if any
|
|
//
|
|
// This method is called multiple times, in order to update the
|
|
// status of the attendee in calendars for the particular event UID.
|
|
//
|
|
- (NSException *) _updateAttendee: (iCalPerson *) attendee
|
|
withDelegate: (iCalPerson *) delegate
|
|
ownerUser: (SOGoUser *) theOwnerUser
|
|
forEventUID: (NSString *) eventUID
|
|
withRecurrenceId: (NSCalendarDate *) recurrenceId
|
|
withSequence: (NSNumber *) sequence
|
|
forUID: (NSString *) uid
|
|
shouldAddSentBy: (BOOL) b
|
|
{
|
|
SOGoAppointmentObject *eventObject;
|
|
iCalCalendar *calendar;
|
|
iCalEntityObject *event;
|
|
iCalPerson *otherAttendee, *otherDelegate;
|
|
NSString *recurrenceTime, *delegateEmail;
|
|
NSException *error;
|
|
BOOL addDelegate, removeDelegate;
|
|
|
|
error = nil;
|
|
|
|
eventObject = [self _lookupEvent: eventUID forUID: uid];
|
|
if (![eventObject isNew])
|
|
{
|
|
if (recurrenceId == nil)
|
|
{
|
|
// We must update main event and all its occurences (if any).
|
|
calendar = [eventObject calendar: NO secure: NO];
|
|
event = (iCalEntityObject*)[calendar firstChildWithTag: [self componentTag]];
|
|
}
|
|
else
|
|
{
|
|
// If recurrenceId is defined, find the specified occurence
|
|
// within the repeating vEvent.
|
|
recurrenceTime = [NSString stringWithFormat: @"%f", [recurrenceId timeIntervalSince1970]];
|
|
event = [eventObject lookupOccurrence: recurrenceTime];
|
|
|
|
if (event == nil)
|
|
// If no occurence found, create one
|
|
event = [eventObject newOccurenceWithID: recurrenceTime];
|
|
}
|
|
|
|
if ([[event sequence] intValue] <= [sequence intValue])
|
|
{
|
|
SOGoUser *currentUser;
|
|
|
|
currentUser = [context activeUser];
|
|
otherAttendee = [event userAsAttendee: theOwnerUser];
|
|
|
|
delegateEmail = [otherAttendee delegatedTo];
|
|
if ([delegateEmail length])
|
|
delegateEmail = [delegateEmail rfc822Email];
|
|
if ([delegateEmail length])
|
|
otherDelegate = [event findAttendeeWithEmail: delegateEmail];
|
|
else
|
|
otherDelegate = NO;
|
|
|
|
/* we handle the addition/deletion of delegate users */
|
|
addDelegate = NO;
|
|
removeDelegate = NO;
|
|
if (delegate)
|
|
{
|
|
if (otherDelegate)
|
|
{
|
|
if (![delegate hasSameEmailAddress: otherDelegate])
|
|
{
|
|
removeDelegate = YES;
|
|
addDelegate = YES;
|
|
}
|
|
}
|
|
else
|
|
addDelegate = YES;
|
|
}
|
|
else
|
|
{
|
|
if (otherDelegate)
|
|
removeDelegate = YES;
|
|
}
|
|
|
|
if (removeDelegate)
|
|
{
|
|
while (otherDelegate)
|
|
{
|
|
[event removeFromAttendees: otherDelegate];
|
|
|
|
// Verify if the delegate was already delegate
|
|
delegateEmail = [otherDelegate delegatedTo];
|
|
if ([delegateEmail length])
|
|
delegateEmail = [delegateEmail rfc822Email];
|
|
|
|
if ([delegateEmail length])
|
|
otherDelegate = [event findAttendeeWithEmail: delegateEmail];
|
|
else
|
|
otherDelegate = NO;
|
|
}
|
|
}
|
|
if (addDelegate)
|
|
[event addToAttendees: delegate];
|
|
|
|
[otherAttendee setPartStat: [attendee partStat]];
|
|
[otherAttendee setDelegatedTo: [attendee delegatedTo]];
|
|
[otherAttendee setDelegatedFrom: [attendee delegatedFrom]];
|
|
|
|
// Remove the RSVP attribute, as an action from the attendee
|
|
// was actually performed, and this confuses iCal (bug #1850)
|
|
[[otherAttendee attributes] removeObjectForKey: @"RSVP"];
|
|
|
|
// If one has accepted / declined an invitation on behalf of
|
|
// the attendee, we add the user to the SENT-BY attribute.
|
|
if (b && ![[currentUser login] isEqualToString: [theOwnerUser login]])
|
|
{
|
|
NSString *currentEmail, *quotedEmail;
|
|
currentEmail = [[currentUser allEmails] objectAtIndex: 0];
|
|
quotedEmail = [NSString stringWithFormat: @"\"MAILTO:%@\"", currentEmail];
|
|
[otherAttendee setValue: 0 ofAttribute: @"SENT-BY"
|
|
to: quotedEmail];
|
|
}
|
|
else
|
|
{
|
|
// We must REMOVE any SENT-BY here. This is important since if A accepted
|
|
// the event for B and then, B changes by theirself their participation status,
|
|
// we don't want to keep the previous SENT-BY attribute there.
|
|
[(NSMutableDictionary *)[otherAttendee attributes] removeObjectForKey: @"SENT-BY"];
|
|
}
|
|
}
|
|
|
|
// We save the updated iCalendar in the database.
|
|
error = [eventObject saveCalendar: [event parent]];
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
|
|
//
|
|
// This method is invoked from the SOGo Web interface or from the DAV interface.
|
|
//
|
|
// - theOwnerUser is owner of the calendar where the attendee
|
|
// participation state has changed.
|
|
//
|
|
- (NSException *) _handleAttendee: (iCalPerson *) attendee
|
|
withDelegate: (iCalPerson *) delegate
|
|
ownerUser: (SOGoUser *) theOwnerUser
|
|
statusChange: (NSString *) newStatus
|
|
inEvent: (iCalEvent *) event
|
|
{
|
|
NSString *currentStatus, *organizerUID;
|
|
SOGoUser *ownerUser, *currentUser;
|
|
NSException *ex;
|
|
|
|
ex = nil;
|
|
|
|
currentStatus = [attendee partStat];
|
|
|
|
iCalPerson *otherAttendee, *otherDelegate;
|
|
NSString *delegateEmail;
|
|
BOOL addDelegate, removeDelegate;
|
|
|
|
otherAttendee = attendee;
|
|
|
|
delegateEmail = [otherAttendee delegatedTo];
|
|
if ([delegateEmail length])
|
|
delegateEmail = [delegateEmail rfc822Email];
|
|
|
|
if ([delegateEmail length])
|
|
otherDelegate = [event findAttendeeWithEmail: delegateEmail];
|
|
else
|
|
otherDelegate = nil;
|
|
|
|
/* We handle the addition/deletion of delegate users */
|
|
addDelegate = NO;
|
|
removeDelegate = NO;
|
|
if (delegate)
|
|
{
|
|
if (otherDelegate)
|
|
{
|
|
// There was already a delegate
|
|
if (![delegate hasSameEmailAddress: otherDelegate])
|
|
{
|
|
// The delegate has changed
|
|
removeDelegate = YES;
|
|
addDelegate = YES;
|
|
}
|
|
}
|
|
else
|
|
// There was no previous delegate
|
|
addDelegate = YES;
|
|
}
|
|
else
|
|
{
|
|
if (otherDelegate)
|
|
// The user has removed the delegate
|
|
removeDelegate = YES;
|
|
}
|
|
|
|
if (addDelegate || removeDelegate
|
|
|| [currentStatus caseInsensitiveCompare: newStatus] != NSOrderedSame)
|
|
{
|
|
NSMutableArray *delegates;
|
|
NSString *delegatedUID;
|
|
|
|
delegatedUID = nil;
|
|
[attendee setPartStat: newStatus];
|
|
|
|
// If one has accepted / declined an invitation on behalf of
|
|
// the attendee, we add the user to the SENT-BY attribute.
|
|
currentUser = [context activeUser];
|
|
if (![[currentUser login] isEqualToString: [theOwnerUser login]])
|
|
{
|
|
NSString *currentEmail, *quotedEmail;
|
|
currentEmail = [[currentUser allEmails] objectAtIndex: 0];
|
|
quotedEmail = [NSString stringWithFormat: @"\"MAILTO:%@\"", currentEmail];
|
|
[attendee setValue: 0 ofAttribute: @"SENT-BY"
|
|
to: quotedEmail];
|
|
}
|
|
else
|
|
{
|
|
// We must REMOVE any SENT-BY here. This is important since if A accepted
|
|
// the event for B and then, B changes by theirself their participation status,
|
|
// we don't want to keep the previous SENT-BY attribute there.
|
|
[(NSMutableDictionary *)[attendee attributes] removeObjectForKey: @"SENT-BY"];
|
|
}
|
|
|
|
[attendee setDelegatedTo: [delegate email]];
|
|
|
|
if (removeDelegate)
|
|
{
|
|
delegates = [NSMutableArray array];
|
|
|
|
while (otherDelegate)
|
|
{
|
|
[delegates addObject: otherDelegate];
|
|
|
|
delegatedUID = [otherDelegate uid];
|
|
if (delegatedUID)
|
|
// Delegate attendee is a local user; remove event from their calendar
|
|
[self _removeEventFromUID: delegatedUID
|
|
owner: [theOwnerUser login]
|
|
withRecurrenceId: [event recurrenceId]];
|
|
|
|
[event removeFromAttendees: otherDelegate];
|
|
|
|
// Verify if the delegate was already delegated
|
|
delegateEmail = [otherDelegate delegatedTo];
|
|
if ([delegateEmail length])
|
|
delegateEmail = [delegateEmail rfc822Email];
|
|
|
|
if ([delegateEmail length])
|
|
otherDelegate = [event findAttendeeWithEmail: delegateEmail];
|
|
else
|
|
otherDelegate = NO;
|
|
}
|
|
|
|
[self sendEMailUsingTemplateNamed: @"Deletion"
|
|
forObject: [event itipEntryWithMethod: @"cancel"]
|
|
previousObject: nil
|
|
toAttendees: delegates
|
|
withType: @"calendar:cancellation"];
|
|
} // if (removeDelegate)
|
|
|
|
if (addDelegate)
|
|
{
|
|
delegatedUID = [delegate uid];
|
|
delegates = [NSArray arrayWithObject: delegate];
|
|
[event addToAttendees: delegate];
|
|
|
|
if (delegatedUID)
|
|
// Delegate attendee is a local user; add event to their calendar
|
|
[self _addOrUpdateEvent: event
|
|
forUID: delegatedUID
|
|
owner: [theOwnerUser login]];
|
|
|
|
[self sendEMailUsingTemplateNamed: @"Invitation"
|
|
forObject: [event itipEntryWithMethod: @"request"]
|
|
previousObject: nil
|
|
toAttendees: delegates
|
|
withType: @"calendar:invitation"];
|
|
} // if (addDelegate)
|
|
|
|
// If the current user isn't the organizer of the event
|
|
// that has just been updated, we update the event and
|
|
// send a notification
|
|
ownerUser = [SOGoUser userWithLogin: owner];
|
|
if (!(ex || [event userIsOrganizer: ownerUser]))
|
|
{
|
|
if ([event isStillRelevant])
|
|
[self sendResponseToOrganizer: event
|
|
from: ownerUser];
|
|
|
|
organizerUID = [[event organizer] uid];
|
|
|
|
// Event is an exception to a recurring event; retrieve organizer from master event
|
|
if (!organizerUID)
|
|
organizerUID = [[(iCalEntityObject*)[[event parent] firstChildWithTag: [self componentTag]] organizer] uid];
|
|
|
|
if (organizerUID)
|
|
// Update the attendee in organizer's calendar.
|
|
ex = [self _updateAttendee: attendee
|
|
withDelegate: delegate
|
|
ownerUser: theOwnerUser
|
|
forEventUID: [event uid]
|
|
withRecurrenceId: [event recurrenceId]
|
|
withSequence: [event sequence]
|
|
forUID: organizerUID
|
|
shouldAddSentBy: YES];
|
|
}
|
|
|
|
// We update the calendar of all attendees that are
|
|
// local to the system. This is useful in case user A accepts
|
|
// invitation from organizer B and users C, D, E who are also
|
|
// attendees need to verify if A has accepted.
|
|
NSArray *attendees;
|
|
iCalPerson *att;
|
|
NSString *uid;
|
|
int i;
|
|
|
|
attendees = [event attendees];
|
|
for (i = 0; i < [attendees count]; i++)
|
|
{
|
|
att = [attendees objectAtIndex: i];
|
|
uid = [att uid];
|
|
if (uid && att != attendee && ![uid isEqualToString: delegatedUID])
|
|
[self _updateAttendee: attendee
|
|
withDelegate: delegate
|
|
ownerUser: theOwnerUser
|
|
forEventUID: [event uid]
|
|
withRecurrenceId: [event recurrenceId]
|
|
withSequence: [event sequence]
|
|
forUID: uid
|
|
shouldAddSentBy: YES];
|
|
}
|
|
}
|
|
|
|
return ex;
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (NSDictionary *) _caldavSuccessCodeWithRecipient: (NSString *) recipient
|
|
{
|
|
NSMutableArray *element;
|
|
NSDictionary *code;
|
|
|
|
element = [NSMutableArray array];
|
|
[element addObject: davElementWithContent (@"recipient", XMLNS_CALDAV, recipient)];
|
|
[element addObject: davElementWithContent (@"request-status", XMLNS_CALDAV, @"2.0;Success")];
|
|
code = davElementWithContent (@"response", XMLNS_CALDAV, element);
|
|
|
|
return code;
|
|
}
|
|
|
|
//
|
|
// Old CalDAV scheduling (draft 4 and below) methods. We keep them since we still
|
|
// advertise for its support but we do everything within the calendar-auto-scheduling code
|
|
//
|
|
- (NSArray *) postCalDAVEventRequestTo: (NSArray *) recipients
|
|
from: (NSString *) originator
|
|
{
|
|
NSEnumerator *recipientsEnum;
|
|
NSMutableArray *elements;
|
|
NSString *recipient;
|
|
|
|
elements = [NSMutableArray array];
|
|
|
|
recipientsEnum = [recipients objectEnumerator];
|
|
|
|
while ((recipient = [recipientsEnum nextObject]))
|
|
if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
|
|
{
|
|
[elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
- (NSArray *) postCalDAVEventCancelTo: (NSArray *) recipients
|
|
from: (NSString *) originator
|
|
{
|
|
NSEnumerator *recipientsEnum;
|
|
NSMutableArray *elements;
|
|
|
|
NSString *recipient;
|
|
|
|
elements = [NSMutableArray array];
|
|
|
|
recipientsEnum = [recipients objectEnumerator];
|
|
|
|
while ((recipient = [recipientsEnum nextObject]))
|
|
if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
|
|
{
|
|
[elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
- (NSArray *) postCalDAVEventReplyTo: (NSArray *) recipients
|
|
from: (NSString *) originator
|
|
{
|
|
NSEnumerator *recipientsEnum;
|
|
NSMutableArray *elements;
|
|
NSString *recipient;
|
|
|
|
elements = [NSMutableArray array];
|
|
recipientsEnum = [recipients objectEnumerator];
|
|
|
|
while ((recipient = [recipientsEnum nextObject]))
|
|
if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
|
|
{
|
|
[elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (NSException *) changeParticipationStatus: (NSString *) status
|
|
withDelegate: (iCalPerson *) delegate
|
|
alarm: (iCalAlarm *) alarm
|
|
{
|
|
return [self changeParticipationStatus: status
|
|
withDelegate: delegate
|
|
alarm: alarm
|
|
forRecurrenceId: nil];
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (NSException *) changeParticipationStatus: (NSString *) _status
|
|
withDelegate: (iCalPerson *) delegate
|
|
alarm: (iCalAlarm *) alarm
|
|
forRecurrenceId: (NSCalendarDate *) _recurrenceId
|
|
{
|
|
iCalCalendar *calendar;
|
|
iCalEvent *event;
|
|
iCalPerson *attendee;
|
|
NSException *ex;
|
|
SOGoUser *ownerUser, *delegatedUser;
|
|
NSString *recurrenceTime, *delegatedUid;
|
|
|
|
event = nil;
|
|
ex = nil;
|
|
delegatedUser = nil;
|
|
|
|
calendar = [[self calendar: NO secure: NO] mutableCopy];
|
|
[calendar autorelease];
|
|
|
|
if (_recurrenceId)
|
|
{
|
|
// If _recurrenceId is defined, find the specified occurence
|
|
// within the repeating vEvent.
|
|
recurrenceTime = [NSString stringWithFormat: @"%f", [_recurrenceId timeIntervalSince1970]];
|
|
event = (iCalEvent*)[self lookupOccurrence: recurrenceTime];
|
|
|
|
if (event == nil)
|
|
// If no occurence found, create one
|
|
event = (iCalEvent*)[self newOccurenceWithID: recurrenceTime];
|
|
}
|
|
else
|
|
// No specific occurence specified; return the first vEvent of
|
|
// the vCalendar.
|
|
event = (iCalEvent*)[calendar firstChildWithTag: [self componentTag]];
|
|
|
|
if (event)
|
|
{
|
|
// ownerUser will actually be the owner of the calendar
|
|
// where the participation change on the event occurs. The particpation
|
|
// change will be on the attendee corresponding to the ownerUser.
|
|
ownerUser = [SOGoUser userWithLogin: owner];
|
|
|
|
attendee = [event userAsAttendee: ownerUser];
|
|
if (attendee)
|
|
{
|
|
if (delegate && ![[delegate email] isEqualToString: [attendee delegatedTo]])
|
|
{
|
|
delegatedUid = [delegate uid];
|
|
if (delegatedUid)
|
|
delegatedUser = [SOGoUser userWithLogin: delegatedUid];
|
|
if (delegatedUser != nil && [event userIsOrganizer: delegatedUser])
|
|
ex = [NSException exceptionWithHTTPStatus: 403
|
|
reason: @"delegate is organizer"];
|
|
if ([event isAttendee: [[delegate email] rfc822Email]])
|
|
ex = [NSException exceptionWithHTTPStatus: 403
|
|
reason: @"delegate is a participant"];
|
|
else if ([SOGoGroup groupWithEmail: [[delegate email] rfc822Email]
|
|
inDomain: [ownerUser domain]])
|
|
ex = [NSException exceptionWithHTTPStatus: 403
|
|
reason: @"delegate is a group"];
|
|
}
|
|
if (ex == nil)
|
|
{
|
|
// Remove the RSVP attribute, as an action from the attendee
|
|
// was actually performed, and this confuses iCal (bug #1850)
|
|
[[attendee attributes] removeObjectForKey: @"RSVP"];
|
|
ex = [self _handleAttendee: attendee
|
|
withDelegate: delegate
|
|
ownerUser: ownerUser
|
|
statusChange: _status
|
|
inEvent: event];
|
|
}
|
|
if (ex == nil)
|
|
{
|
|
// We generate the updated iCalendar file and we save it in
|
|
// the database. We do this ONLY when using SOGo from the
|
|
// Web interface or over ActiveSync.
|
|
// Over DAV, it'll be handled directly in PUTAction:
|
|
if (![context request] || [[context request] handledByDefaultHandler]
|
|
|| [[[context request] requestHandlerKey] isEqualToString: @"Microsoft-Server-ActiveSync"])
|
|
{
|
|
// If an alarm was specified, let's use it. This would happen if an attendee accepts/declines/etc. an
|
|
// event invitation and also sets an alarm along the way. This would happen ONLY from the web interface.
|
|
[event removeAllAlarms];
|
|
|
|
if (alarm)
|
|
{
|
|
[event addToAlarms: alarm];
|
|
}
|
|
|
|
ex = [self saveCalendar: [event parent]];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
ex = [NSException exceptionWithHTTPStatus: 404 // Not Found
|
|
reason: @"user does not participate in this calendar event"];
|
|
}
|
|
else
|
|
ex = [NSException exceptionWithHTTPStatus: 500 // Server Error
|
|
reason: @"unable to parse event record"];
|
|
|
|
return ex;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (BOOL) _shouldScheduleEvent: (iCalPerson *) theOrganizer
|
|
{
|
|
NSArray *userAgents;
|
|
NSString *v;
|
|
BOOL b;
|
|
int i;
|
|
|
|
b = YES;
|
|
|
|
if (theOrganizer && (v = [theOrganizer value: 0 ofAttribute: @"SCHEDULE-AGENT"]))
|
|
{
|
|
if ([v caseInsensitiveCompare: @"NONE"] == NSOrderedSame ||
|
|
[v caseInsensitiveCompare: @"CLIENT"] == NSOrderedSame)
|
|
b = NO;
|
|
}
|
|
|
|
//
|
|
// If we have to deal with Thunderbird/Lightning, we always send invitation
|
|
// reponses, as Lightning v2.6 (at least this version) sets SCHEDULE-AGENT
|
|
// to NONE/CLIENT when responding to an external invitation received by
|
|
// SOGo - so no invitation responses are ever sent by Lightning. See
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=865726 and
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=997784
|
|
//
|
|
userAgents = [[context request] headersForKey: @"User-Agent"];
|
|
|
|
for (i = 0; i < [userAgents count]; i++)
|
|
{
|
|
if ([[userAgents objectAtIndex: i] rangeOfString: @"Thunderbird"].location != NSNotFound &&
|
|
[[userAgents objectAtIndex: i] rangeOfString: @"Lightning"].location != NSNotFound)
|
|
{
|
|
b = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return b;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) prepareDeleteOccurence: (iCalEvent *) occurence
|
|
{
|
|
SOGoUser *ownerUser, *currentUser;
|
|
NSCalendarDate *recurrenceId;
|
|
NSArray *attendees;
|
|
iCalEvent *event;
|
|
BOOL send_receipt;
|
|
|
|
|
|
ownerUser = [SOGoUser userWithLogin: owner];
|
|
event = [self component: NO secure: NO];
|
|
send_receipt = YES;
|
|
|
|
if (![self _shouldScheduleEvent: [event organizer]])
|
|
return;
|
|
|
|
if (occurence == nil)
|
|
{
|
|
// No occurence specified; use the master event.
|
|
occurence = event;
|
|
recurrenceId = nil;
|
|
}
|
|
else
|
|
// Retrieve this occurence ID.
|
|
recurrenceId = [occurence recurrenceId];
|
|
|
|
if ([event userIsOrganizer: ownerUser])
|
|
{
|
|
// The organizer deletes an occurence.
|
|
currentUser = [context activeUser];
|
|
attendees = [occurence attendeesWithoutUser: currentUser];
|
|
|
|
#warning Make sure this is correct ..
|
|
if (![attendees count] && event != occurence)
|
|
attendees = [event attendeesWithoutUser: currentUser];
|
|
|
|
if ([attendees count])
|
|
{
|
|
// Remove the event from all attendees calendars
|
|
// and send them an email.
|
|
[self _handleRemovedUsers: attendees
|
|
withRecurrenceId: recurrenceId];
|
|
[self sendEMailUsingTemplateNamed: @"Deletion"
|
|
forObject: [occurence itipEntryWithMethod: @"cancel"]
|
|
previousObject: nil
|
|
toAttendees: attendees
|
|
withType: @"calendar:cancellation"];
|
|
}
|
|
}
|
|
else if ([occurence userIsAttendee: ownerUser])
|
|
{
|
|
// The current user deletes the occurence; let the organizer know that
|
|
// the user has declined this occurence.
|
|
[self changeParticipationStatus: @"DECLINED"
|
|
withDelegate: nil
|
|
alarm: nil
|
|
forRecurrenceId: recurrenceId];
|
|
send_receipt = NO;
|
|
}
|
|
|
|
if (send_receipt)
|
|
[self sendReceiptEmailForObject: event
|
|
addedAttendees: nil
|
|
deletedAttendees: nil
|
|
updatedAttendees: nil
|
|
operation: EventDeleted];
|
|
}
|
|
|
|
- (NSException *) prepareDelete
|
|
{
|
|
[self prepareDeleteOccurence: nil];
|
|
|
|
return [super prepareDelete];
|
|
}
|
|
|
|
- (NSDictionary *) _partStatsFromCalendar: (iCalCalendar *) calendar
|
|
{
|
|
NSMutableDictionary *partStats;
|
|
NSArray *allEvents;
|
|
int count, max;
|
|
iCalEvent *currentEvent;
|
|
iCalPerson *ownerAttendee;
|
|
NSString *key;
|
|
SOGoUser *ownerUser;
|
|
|
|
ownerUser = [SOGoUser userWithLogin: owner];
|
|
|
|
allEvents = [calendar events];
|
|
max = [allEvents count];
|
|
partStats = [NSMutableDictionary dictionaryWithCapacity: max];
|
|
|
|
for (count = 0; count < max; count++)
|
|
{
|
|
currentEvent = [allEvents objectAtIndex: count];
|
|
ownerAttendee = [currentEvent userAsAttendee: ownerUser];
|
|
if (ownerAttendee)
|
|
{
|
|
if (count == 0)
|
|
key = @"master";
|
|
else
|
|
key = [[currentEvent recurrenceId] iCalFormattedDateTimeString];
|
|
[partStats setObject: ownerAttendee forKey: key];
|
|
}
|
|
}
|
|
|
|
return partStats;
|
|
}
|
|
|
|
- (iCalCalendar *) _setupResponseInRequestCalendar: (iCalCalendar *) rqCalendar
|
|
{
|
|
iCalCalendar *calendar;
|
|
NSArray *keys;
|
|
NSDictionary *partStats, *newPartStats;
|
|
NSString *partStat, *key;
|
|
int count, max;
|
|
|
|
calendar = [self calendar: NO secure: NO];
|
|
partStats = [self _partStatsFromCalendar: calendar];
|
|
keys = [partStats allKeys];
|
|
max = [keys count];
|
|
if (max > 0)
|
|
{
|
|
newPartStats = [self _partStatsFromCalendar: rqCalendar];
|
|
if ([keys isEqualToArray: [newPartStats allKeys]])
|
|
{
|
|
for (count = 0; count < max; count++)
|
|
{
|
|
key = [keys objectAtIndex: count];
|
|
partStat = [[newPartStats objectForKey: key] partStat];
|
|
[[partStats objectForKey: key] setPartStat: partStat];
|
|
}
|
|
}
|
|
}
|
|
|
|
return calendar;
|
|
}
|
|
|
|
- (void) _adjustTransparencyInRequestCalendar: (iCalCalendar *) rqCalendar
|
|
{
|
|
NSArray *allEvents;
|
|
iCalEvent *event;
|
|
int i;
|
|
|
|
allEvents = [rqCalendar events];
|
|
for (i = 0; i < [allEvents count]; i++)
|
|
{
|
|
event = [allEvents objectAtIndex: i];
|
|
if ([event isAllDay] && [event isOpaque])
|
|
[event setTransparency: @"TRANSPARENT"];
|
|
}
|
|
}
|
|
|
|
- (void) _adjustClassificationInRequestCalendar: (iCalCalendar *) rqCalendar
|
|
{
|
|
SOGoUserDefaults *userDefaults;
|
|
NSString *accessClass;
|
|
NSArray *allObjects;
|
|
id entity;
|
|
|
|
int i;
|
|
|
|
userDefaults = [[context activeUser] userDefaults];
|
|
allObjects = [rqCalendar allObjects];
|
|
|
|
for (i = 0; i < [allObjects count]; i++)
|
|
{
|
|
entity = [allObjects objectAtIndex: i];
|
|
|
|
if ([entity respondsToSelector: @selector(accessClass)])
|
|
{
|
|
accessClass = [entity accessClass];
|
|
|
|
if (!accessClass || [accessClass length] == 0)
|
|
{
|
|
if ([entity isKindOfClass: [iCalEvent class]])
|
|
[entity setAccessClass: [userDefaults calendarEventsDefaultClassification]];
|
|
else if ([entity isKindOfClass: [iCalToDo class]])
|
|
[entity setAccessClass: [userDefaults calendarTasksDefaultClassification]];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// iOS devices (and potentially others) send event invitations with no PARTSTAT defined.
|
|
// This confuses DAV clients like Thunderbird, or event SOGo web. The RFC says:
|
|
//
|
|
// Description: This parameter can be specified on properties with a
|
|
// CAL-ADDRESS value type. The parameter identifies the participation
|
|
// status for the calendar user specified by the property value. The
|
|
// parameter values differ depending on whether they are associated with
|
|
// a group scheduled "VEVENT", "VTODO" or "VJOURNAL". The values MUST
|
|
// match one of the values allowed for the given calendar component. If
|
|
// not specified on a property that allows this parameter, the default
|
|
// value is NEEDS-ACTION.
|
|
//
|
|
- (void) _adjustPartStatInRequestCalendar: (iCalCalendar *) rqCalendar
|
|
{
|
|
NSArray *allObjects, *allAttendees;
|
|
iCalPerson *attendee;
|
|
id entity;
|
|
|
|
int i, j;
|
|
|
|
allObjects = [rqCalendar allObjects];
|
|
|
|
for (i = 0; i < [allObjects count]; i++)
|
|
{
|
|
entity = [allObjects objectAtIndex: i];
|
|
|
|
if ([entity isKindOfClass: [iCalEvent class]])
|
|
{
|
|
allAttendees = [entity attendees];
|
|
|
|
for (j = 0; j < [allAttendees count]; j++)
|
|
{
|
|
attendee = [allAttendees objectAtIndex: j];
|
|
|
|
if (![[attendee partStat] length])
|
|
[attendee setPartStat: @"NEEDS-ACTION"];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify vCalendar for any inconsistency or missing attributes.
|
|
* Currently only check if the events have an end date or a duration.
|
|
* We also check for the default transparency parameters.
|
|
* We also check for broken ORGANIZER such as "ORGANIZER;:mailto:sogo3@example.com"
|
|
* @param rq the HTTP PUT request
|
|
*/
|
|
- (void) _adjustEventsInRequestCalendar: (iCalCalendar *) rqCalendar
|
|
{
|
|
NSArray *allEvents;
|
|
iCalEvent *event;
|
|
NSUInteger i;
|
|
|
|
allEvents = [rqCalendar events];
|
|
|
|
for (i = 0; i < [allEvents count]; i++)
|
|
{
|
|
event = [allEvents objectAtIndex: i];
|
|
|
|
if (![event hasEndDate] && ![event hasDuration])
|
|
{
|
|
// No end date, no duration
|
|
if ([event isAllDay])
|
|
[event setDuration: @"P1D"];
|
|
else
|
|
[event setDuration: @"PT1H"];
|
|
[self warnWithFormat: @"Invalid event: no end date; setting duration to %@", [event duration]];
|
|
}
|
|
|
|
if ([event organizer] && ![[[event organizer] cn] length])
|
|
{
|
|
[[event organizer] setCn: [[event organizer] rfc822Email]];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void) _decomposeGroupsInRequestCalendar: (iCalCalendar *) rqCalendar
|
|
{
|
|
NSArray *allEvents;
|
|
iCalEvent *event;
|
|
int i;
|
|
|
|
// The algorithm is pretty straightforward:
|
|
//
|
|
// We get all events
|
|
// We get all attendees
|
|
// If some are groups, we decompose them
|
|
// We regenerate the iCalendar string
|
|
//
|
|
allEvents = [rqCalendar events];
|
|
for (i = 0; i < [allEvents count]; i++)
|
|
{
|
|
event = [allEvents objectAtIndex: i];
|
|
[self expandGroupsInEvent: event];
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// If theRecurrenceId is nil, it returns immediately the
|
|
// first event that has a RECURRENCE-ID.
|
|
//
|
|
// Otherwise, it return values that matches.
|
|
//
|
|
- (iCalEvent *) _eventFromRecurrenceId: (NSCalendarDate *) theRecurrenceId
|
|
events: (NSArray *) allEvents
|
|
{
|
|
iCalEvent *event;
|
|
int i;
|
|
|
|
for (i = 0; i < [allEvents count]; i++)
|
|
{
|
|
event = [allEvents objectAtIndex: i];
|
|
|
|
if ([event recurrenceId] && !theRecurrenceId)
|
|
return event;
|
|
|
|
if ([event recurrenceId] && [[event recurrenceId] compare: theRecurrenceId] == NSOrderedSame)
|
|
return event;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (NSCalendarDate *) _addedExDate: (iCalEvent *) oldEvent
|
|
newEvent: (iCalEvent *) newEvent
|
|
{
|
|
NSArray *oldExDates, *newExDates;
|
|
NSMutableArray *dates;
|
|
int i;
|
|
|
|
dates = [NSMutableArray array];
|
|
|
|
newExDates = [newEvent childrenWithTag: @"exdate"];
|
|
for (i = 0; i < [newExDates count]; i++)
|
|
[dates addObject: [[newExDates objectAtIndex: i] dateTime]];
|
|
|
|
oldExDates = [oldEvent childrenWithTag: @"exdate"];
|
|
for (i = 0; i < [oldExDates count]; i++)
|
|
[dates removeObject: [[oldExDates objectAtIndex: i] dateTime]];
|
|
|
|
return [dates lastObject];
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (id) DELETEAction: (WOContext *) _ctx
|
|
{
|
|
[self prepareDelete];
|
|
return [super DELETEAction: _ctx];
|
|
}
|
|
|
|
//
|
|
// This method is meant to be the common point of any save operation from web
|
|
// and DAV requests, as well as from code making use of SOGo as a library
|
|
// (OpenChange)
|
|
//
|
|
- (NSException *) updateContentWithCalendar: (iCalCalendar *) calendar
|
|
fromRequest: (WORequest *) rq
|
|
{
|
|
NSException *ex;
|
|
NSArray *roles;
|
|
SOGoUser *ownerUser;
|
|
|
|
if (calendar == fullCalendar || calendar == safeCalendar
|
|
|| calendar == originalCalendar)
|
|
[NSException raise: NSInvalidArgumentException format: @"the 'calendar' argument must be a distinct instance" @" from the original object"];
|
|
|
|
ownerUser = [SOGoUser userWithLogin: owner];
|
|
|
|
roles = [[context activeUser] rolesForObject: self
|
|
inContext: context];
|
|
//
|
|
// We check if we gave only the "Respond To" right and someone is actually
|
|
// responding to one of our invitation. In this case, _setupResponseCalendarInRequest
|
|
// will only take the new attendee status and actually discard any other modifications.
|
|
//
|
|
if ([roles containsObject: @"ComponentResponder"] && ![roles containsObject: @"ComponentModifier"])
|
|
calendar = [self _setupResponseInRequestCalendar: calendar];
|
|
else
|
|
{
|
|
if (![[rq headersForKey: @"X-SOGo"] containsObject: @"NoGroupsDecomposition"])
|
|
[self _decomposeGroupsInRequestCalendar: calendar];
|
|
|
|
if ([[ownerUser domainDefaults] iPhoneForceAllDayTransparency] && [rq isIPhone])
|
|
{
|
|
[self _adjustTransparencyInRequestCalendar: calendar];
|
|
}
|
|
|
|
[self _adjustEventsInRequestCalendar: calendar];
|
|
[self _adjustClassificationInRequestCalendar: calendar];
|
|
[self _adjustPartStatInRequestCalendar: calendar];
|
|
}
|
|
|
|
//
|
|
// We first check if it's a new event
|
|
//
|
|
if ([self isNew])
|
|
{
|
|
iCalEvent *event;
|
|
NSArray *attendees;
|
|
NSString *eventUID;
|
|
BOOL scheduling;
|
|
|
|
attendees = nil;
|
|
|
|
event = [[calendar events] objectAtIndex: 0];
|
|
eventUID = [event uid];
|
|
scheduling = [self _shouldScheduleEvent: [event organizer]];
|
|
|
|
// make sure eventUID doesn't conflict with an existing event - see bug #1853
|
|
// TODO: send out a no-uid-conflict (DAV:href) xml element (rfc4791 section 5.3.2.1)
|
|
if ([container resourceNameForEventUID: eventUID])
|
|
{
|
|
return [NSException exceptionWithHTTPStatus: 403
|
|
reason: [NSString stringWithFormat: @"Event UID already in use. (%s)", eventUID]];
|
|
}
|
|
|
|
//
|
|
// New event and we're the organizer -- send invitation to all attendees
|
|
//
|
|
if (scheduling && [event userIsOrganizer: ownerUser])
|
|
{
|
|
attendees = [event attendeesWithoutUser: ownerUser];
|
|
if ([attendees count])
|
|
{
|
|
if ((ex = [self _handleAddedUsers: attendees fromEvent: event]))
|
|
return ex;
|
|
else
|
|
{
|
|
// We might have auto-accepted resources here. If that's the
|
|
// case, let's regenerate the versitstring and replace the
|
|
// one from the request.
|
|
[rq setContent: [[[event parent] versitString] dataUsingEncoding: [rq contentEncoding]]];
|
|
}
|
|
|
|
[self sendEMailUsingTemplateNamed: @"Invitation"
|
|
forObject: [event itipEntryWithMethod: @"request"]
|
|
previousObject: nil
|
|
toAttendees: attendees
|
|
withType: @"calendar:invitation"];
|
|
}
|
|
}
|
|
//
|
|
// We aren't the organizer but we're an attendee. That can happen when
|
|
// we receive an external invitation (IMIP/ITIP) and we accept it
|
|
// from a CUA - it gets added to a specific CalDAV calendar using a PUT
|
|
//
|
|
else if (scheduling && [event userIsAttendee: ownerUser])
|
|
{
|
|
[self sendIMIPReplyForEvent: event
|
|
from: ownerUser
|
|
to: [event organizer]];
|
|
}
|
|
|
|
[self sendReceiptEmailForObject: event
|
|
addedAttendees: attendees
|
|
deletedAttendees: nil
|
|
updatedAttendees: nil
|
|
operation: EventCreated];
|
|
} // if ([self isNew])
|
|
else
|
|
{
|
|
iCalCalendar *oldCalendar;
|
|
iCalEvent *oldEvent, *newEvent;
|
|
iCalEventChanges *changes;
|
|
NSMutableArray *oldEvents, *newEvents;
|
|
NSCalendarDate *recurrenceId;
|
|
int i;
|
|
|
|
//
|
|
// We check what has changed in the event and react accordingly.
|
|
//
|
|
newEvents = [NSMutableArray arrayWithArray: [calendar events]];
|
|
|
|
oldCalendar = [self calendar: NO secure: NO];
|
|
oldEvents = [NSMutableArray arrayWithArray: [oldCalendar events]];
|
|
recurrenceId = nil;
|
|
|
|
for (i = [newEvents count]-1; i >= 0; i--)
|
|
{
|
|
newEvent = [newEvents objectAtIndex: i];
|
|
|
|
if ([newEvent recurrenceId])
|
|
{
|
|
// Find the corresponding RECURRENCE-ID in the old calendar
|
|
// If not present, we assume it was created before the PUT
|
|
oldEvent = [self _eventFromRecurrenceId: [newEvent recurrenceId]
|
|
events: oldEvents];
|
|
|
|
if (oldEvent == nil)
|
|
{
|
|
NSString *recurrenceTime;
|
|
recurrenceTime = [NSString stringWithFormat: @"%f", [[newEvent recurrenceId] timeIntervalSince1970]];
|
|
oldEvent = (iCalEvent *)[self newOccurenceWithID: recurrenceTime];
|
|
}
|
|
|
|
// If present, we look for changes
|
|
changes = [iCalEventChanges changesFromEvent: oldEvent toEvent: newEvent];
|
|
|
|
if ([changes sequenceShouldBeIncreased] | [changes hasAttendeeChanges])
|
|
{
|
|
// We found a RECURRENCE-ID with changes, we consider it
|
|
recurrenceId = [newEvent recurrenceId];
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
[newEvents removeObject: newEvent];
|
|
[oldEvents removeObject: oldEvent];
|
|
}
|
|
}
|
|
|
|
oldEvent = nil;
|
|
newEvent = nil;
|
|
}
|
|
|
|
// If no changes were observed, let's see if we have any left overs
|
|
// in the oldEvents or in the newEvents array
|
|
if (!oldEvent && !newEvent)
|
|
{
|
|
// We check if we only have to deal with the MASTER event
|
|
if ([newEvents count] == [oldEvents count])
|
|
{
|
|
oldEvent = [oldEvents objectAtIndex: 0];
|
|
newEvent = [newEvents objectAtIndex: 0];
|
|
}
|
|
// A RECURRENCE-ID was added
|
|
else if ([newEvents count] > [oldEvents count])
|
|
{
|
|
oldEvent = nil;
|
|
newEvent = [self _eventFromRecurrenceId: nil events: newEvents];
|
|
recurrenceId = [newEvent recurrenceId];
|
|
}
|
|
// A RECURRENCE-ID was removed
|
|
else
|
|
{
|
|
oldEvent = [self _eventFromRecurrenceId: nil events: oldEvents];
|
|
newEvent = nil;
|
|
recurrenceId = [oldEvent recurrenceId];
|
|
}
|
|
}
|
|
|
|
// We check if the PUT call is actually an PART-STATE change
|
|
// from one of the attendees - here's the logic :
|
|
//
|
|
// if owner == organizer
|
|
//
|
|
// if [context activeUser] == organizer
|
|
// [send the invitation update]
|
|
// else
|
|
// [react on SENT-BY as someone else is acting for the organizer]
|
|
//
|
|
//
|
|
if ([[newEvent attendees] count] || [[oldEvent attendees] count])
|
|
{
|
|
BOOL userIsOrganizer;
|
|
|
|
// newEvent might be nil here, if we're deleting a RECURRENCE-ID with attendees
|
|
// If that's the case, we use the oldEvent to obtain the organizer
|
|
if (newEvent)
|
|
userIsOrganizer = [newEvent userIsOrganizer: ownerUser];
|
|
else
|
|
userIsOrganizer = [oldEvent userIsOrganizer: ownerUser];
|
|
|
|
// With Thunderbird 10, if you create a recurring event with an exception
|
|
// occurence, and invite someone, the PUT will have the organizer in the
|
|
// recurrence-id and not in the master event. We must fix this, otherwise
|
|
// SOGo will break.
|
|
if (!recurrenceId && ![[[[[newEvent parent] events] objectAtIndex: 0] organizer] uid])
|
|
[[[[newEvent parent] events] objectAtIndex: 0] setOrganizer: [newEvent organizer]];
|
|
|
|
if (userIsOrganizer)
|
|
{
|
|
// A RECCURENCE-ID was removed
|
|
if (!newEvent && oldEvent)
|
|
[self prepareDeleteOccurence: oldEvent];
|
|
// The master event was changed, A RECCURENCE-ID was added or modified
|
|
else if ((ex = [self _handleUpdatedEvent: newEvent fromOldEvent: oldEvent]))
|
|
return ex;
|
|
}
|
|
//
|
|
// else => attendee is responding
|
|
//
|
|
// if [context activeUser] == attendee
|
|
// [we change the PART-STATE]
|
|
// else
|
|
// [react on SENT-BY as someone else is acting for the attendee]
|
|
else
|
|
{
|
|
iCalPerson *attendee, *delegate;
|
|
NSString *delegateEmail;
|
|
|
|
attendee = [newEvent userAsAttendee: [SOGoUser userWithLogin: owner]];
|
|
|
|
// We first check of the sequences are alright. We don't accept attendees
|
|
// accepting "old" invitations. If that's the case, we return a 403
|
|
if ([[newEvent sequence] intValue] < [[oldEvent sequence] intValue])
|
|
return [NSException exceptionWithHTTPStatus:403
|
|
reason: @"sequences don't match"];
|
|
|
|
// Remove the RSVP attribute, as an action from the attendee
|
|
// was actually performed, and this confuses iCal (bug #1850)
|
|
[[attendee attributes] removeObjectForKey: @"RSVP"];
|
|
|
|
delegate = nil;
|
|
delegateEmail = [attendee delegatedTo];
|
|
|
|
if ([delegateEmail length])
|
|
{
|
|
delegateEmail = [delegateEmail substringFromIndex: 7];
|
|
if ([delegateEmail length])
|
|
delegate = [newEvent findAttendeeWithEmail: delegateEmail];
|
|
}
|
|
|
|
changes = [iCalEventChanges changesFromEvent: oldEvent toEvent: newEvent];
|
|
|
|
// The current user deletes the occurence; let the organizer know that
|
|
// the user has declined this occurence.
|
|
if ([[changes updatedProperties] containsObject: @"exdate"])
|
|
{
|
|
[self changeParticipationStatus: @"DECLINED"
|
|
withDelegate: nil // FIXME (specify delegate?)
|
|
alarm: nil
|
|
forRecurrenceId: [self _addedExDate: oldEvent newEvent: newEvent]];
|
|
}
|
|
else if (attendee)
|
|
{
|
|
[self changeParticipationStatus: [attendee partStat]
|
|
withDelegate: delegate
|
|
alarm: nil
|
|
forRecurrenceId: recurrenceId];
|
|
}
|
|
// All attendees and the organizer field were removed. Apple iCal does
|
|
// that when we remove the last attendee of an event.
|
|
//
|
|
// We must update previous's attendees' calendars to actually
|
|
// remove the event in each of them.
|
|
else
|
|
{
|
|
[self _handleRemovedUsers: [changes deletedAttendees]
|
|
withRecurrenceId: recurrenceId];
|
|
}
|
|
}
|
|
} // if ([[newEvent attendees] count] || [[oldEvent attendees] count])
|
|
else
|
|
{
|
|
[self sendReceiptEmailForObject: newEvent
|
|
addedAttendees: nil
|
|
deletedAttendees: nil
|
|
updatedAttendees: nil
|
|
operation: EventUpdated];
|
|
}
|
|
} // else of if (isNew) ...
|
|
|
|
unsigned int baseVersion;
|
|
// We must NOT invoke [super PUTAction:] here as it'll resave
|
|
// the content string and we could have etag mismatches.
|
|
baseVersion = (isNew ? 0 : version);
|
|
|
|
ex = [self saveComponent: calendar
|
|
baseVersion: baseVersion];
|
|
|
|
return ex;
|
|
}
|
|
|
|
//
|
|
// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we
|
|
// simply invoke super's PUTAction.
|
|
//
|
|
// We also check if we must force transparency on all day events
|
|
// from iPhone clients.
|
|
//
|
|
- (id) PUTAction: (WOContext *) _ctx
|
|
{
|
|
NSException *ex;
|
|
NSString *etag;
|
|
WORequest *rq;
|
|
WOResponse *response;
|
|
iCalCalendar *rqCalendar;
|
|
|
|
rq = [_ctx request];
|
|
rqCalendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
|
|
|
|
if (![self isNew])
|
|
{
|
|
//
|
|
// We must check for etag changes prior doing anything since an attendee could
|
|
// have changed its participation status and the organizer didn't get the
|
|
// copy and is trying to do a modification to the event.
|
|
//
|
|
ex = [self matchesRequestConditionInContext: context];
|
|
if (ex)
|
|
return ex;
|
|
}
|
|
|
|
ex = [self updateContentWithCalendar: rqCalendar fromRequest: rq];
|
|
if (ex)
|
|
response = (WOResponse *) ex;
|
|
else
|
|
{
|
|
response = [_ctx response];
|
|
if (isNew)
|
|
[response setStatus: 201 /* Created */];
|
|
else
|
|
[response setStatus: 204 /* No Content */];
|
|
etag = [self davEntityTag];
|
|
if (etag)
|
|
[response setHeader: etag forKey: @"etag"];
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
@end /* SOGoAppointmentObject */
|