2379 lines
84 KiB
Objective-C
2379 lines
84 KiB
Objective-C
/*
|
|
Copyright (C) 2007-2015 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/iCalToDo.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 uidInContext: context];
|
|
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 uidInContext: context];
|
|
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;
|
|
NSMutableString *reason;
|
|
NSDictionary *values;
|
|
NSMutableDictionary *value, *moduleSettings;
|
|
id whiteList;
|
|
|
|
int i, count;
|
|
|
|
i = count = 0;
|
|
|
|
// Build list of the attendees uids
|
|
unavailableAttendees = [[NSMutableArray alloc] init];
|
|
enumerator = [theAttendees objectEnumerator];
|
|
ownerUID = [[[self context] activeUser] login];
|
|
|
|
while ((currentAttendee = [enumerator nextObject]))
|
|
{
|
|
currentUID = [currentAttendee uidInContext: context];
|
|
|
|
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 ([[moduleSettings objectForKey:@"PreventInvitations"] boolValue])
|
|
{
|
|
// Check if the user have a whiteList
|
|
whiteList = [moduleSettings objectForKey:@"PreventInvitationsWhitelist"];
|
|
|
|
// For backward <= 2.2.17 compatibility
|
|
if ([whiteList isKindOfClass: [NSString class]])
|
|
whiteList = [whiteList 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 uidInContext: context];
|
|
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"], nil;
|
|
reason = [values keysWithFormat: [self labelForKey: @"Cannot access resource: \"%{Cn} %{SystemEmail}\""]];
|
|
return [NSException exceptionWithHTTPStatus:409 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 uidInContext: context] 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: 409
|
|
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 uidInContext: context];
|
|
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 uidInContext: context];
|
|
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 = nil;
|
|
|
|
/* 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 = nil;
|
|
}
|
|
}
|
|
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 uidInContext: context];
|
|
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 = nil;
|
|
}
|
|
|
|
[self sendEMailUsingTemplateNamed: @"Deletion"
|
|
forObject: [event itipEntryWithMethod: @"cancel"]
|
|
previousObject: nil
|
|
toAttendees: delegates
|
|
withType: @"calendar:cancellation"];
|
|
} // if (removeDelegate)
|
|
|
|
if (addDelegate)
|
|
{
|
|
delegatedUID = [delegate uidInContext: context];
|
|
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] uidInContext: context];
|
|
|
|
// Event is an exception to a recurring event; retrieve organizer from master event
|
|
if (!organizerUID)
|
|
organizerUID = [[(iCalEntityObject*)[[event parent] firstChildWithTag: [self componentTag]] organizer] uidInContext: context];
|
|
|
|
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 uidInContext: context];
|
|
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 uidInContext: context];
|
|
if (delegatedUid)
|
|
delegatedUser = [SOGoUser userWithLogin: delegatedUid];
|
|
if (delegatedUser != nil && [event userIsOrganizer: delegatedUser])
|
|
ex = [NSException exceptionWithHTTPStatus: 409
|
|
reason: @"delegate is organizer"];
|
|
if ([event isAttendee: [[delegate email] rfc822Email]])
|
|
ex = [NSException exceptionWithHTTPStatus: 409
|
|
reason: @"delegate is a participant"];
|
|
else if ([SOGoGroup groupWithEmail: [[delegate email] rfc822Email]
|
|
inDomain: [ownerUser domain]])
|
|
ex = [NSException exceptionWithHTTPStatus: 409
|
|
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"];
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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;
|
|
int j;
|
|
|
|
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])
|
|
{
|
|
NSString *uid;
|
|
|
|
if (![[[event organizer] cn] length])
|
|
{
|
|
[[event organizer] setCn: [[event organizer] rfc822Email]];
|
|
}
|
|
|
|
// We now make sure that the organizer, if managed by SOGo, is using
|
|
// its default email when creating events and inviting attendees.
|
|
uid = [[event organizer] uidInContext: context];
|
|
if (uid)
|
|
{
|
|
iCalPerson *attendee, *organizer;
|
|
NSDictionary *defaultIdentity;
|
|
SOGoUser *organizerUser;
|
|
NSArray *allAttendees;
|
|
|
|
organizerUser = [SOGoUser userWithLogin: uid];
|
|
defaultIdentity = [organizerUser defaultIdentity];
|
|
organizer = [[event organizer] copy];
|
|
[organizer setCn: [defaultIdentity objectForKey: @"fullName"]];
|
|
[organizer setEmail: [defaultIdentity objectForKey: @"email"]];
|
|
|
|
// We now check if one of the attendee is also the organizer. If so,
|
|
// we remove it. See bug #3905 (https://sogo.nu/bugs/view.php?id=3905)
|
|
// for more details. This is a Calendar app bug on Apple Yosemite.
|
|
allAttendees = [event attendees];
|
|
|
|
for (j = [allAttendees count]-1; j >= 0; j--)
|
|
{
|
|
attendee = [allAttendees objectAtIndex: j];
|
|
if ([organizerUser hasEmail: [attendee rfc822Email]])
|
|
[event removeFromAttendees: attendee];
|
|
}
|
|
|
|
// We reset the organizer
|
|
[event setOrganizer: organizer];
|
|
RELEASE(organizer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// This is similar to what we do in SOGoAppointmentFolder: -importCalendar:
|
|
//
|
|
- (void) _adjustFloatingTimeInRequestCalendar: (iCalCalendar *) rqCalendar
|
|
{
|
|
iCalDateTime *startDate, *endDate;
|
|
NSString *startDateAsString;
|
|
SOGoUserDefaults *ud;
|
|
NSArray *allEvents;
|
|
iCalTimeZone *tz;
|
|
iCalEvent *event;
|
|
int i, delta;
|
|
|
|
allEvents = [rqCalendar events];
|
|
for (i = 0; i < [allEvents count]; i++)
|
|
{
|
|
event = [allEvents objectAtIndex: i];
|
|
|
|
if ([event isAllDay])
|
|
continue;
|
|
|
|
startDate = (iCalDateTime *)[event uniqueChildWithTag: @"dtstart"];
|
|
startDateAsString = [[startDate valuesAtIndex: 0 forKey: @""] objectAtIndex: 0];
|
|
|
|
if (![startDate timeZone] &&
|
|
![startDateAsString hasSuffix: @"Z"] &&
|
|
![startDateAsString hasSuffix: @"z"])
|
|
{
|
|
ud = [[context activeUser] userDefaults];
|
|
tz = [iCalTimeZone timeZoneForName: [ud timeZoneName]];
|
|
if ([rqCalendar addTimeZone: tz])
|
|
{
|
|
delta = [[tz periodForDate: [startDate dateTime]] secondsOffsetFromGMT];
|
|
|
|
[event setStartDate: [[event startDate] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: -delta]];
|
|
[startDate setTimeZone: tz];
|
|
|
|
endDate = (iCalDateTime *) [event uniqueChildWithTag: @"dtend"];
|
|
if (endDate)
|
|
{
|
|
[event setEndDate: [[event endDate] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: -delta]];
|
|
[endDate setTimeZone: tz];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (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];
|
|
[self _adjustFloatingTimeInRequestCalendar: 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: 409
|
|
reason: [NSString stringWithFormat: @"Event UID already in use. (%@)", 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] uidInContext: context])
|
|
[[[[newEvent parent] events] objectAtIndex: 0] setOrganizer: [newEvent organizer]];
|
|
|
|
if (userIsOrganizer)
|
|
{
|
|
// We check ACLs of the 'organizer' - in case someone forges the SENT-BY
|
|
NSString *uid;
|
|
|
|
uid = [[oldEvent organizer] uidInContext: context];
|
|
|
|
if (uid && [[[context activeUser] login] caseInsensitiveCompare: uid] != NSOrderedSame)
|
|
{
|
|
SOGoAppointmentObject *organizerObject;
|
|
|
|
organizerObject = [self _lookupEvent: [oldEvent uid] forUID: uid];
|
|
roles = [[context activeUser] rolesForObject: organizerObject
|
|
inContext: context];
|
|
|
|
if (![roles containsObject: @"ComponentModifier"])
|
|
{
|
|
return [NSException exceptionWithHTTPStatus: 409
|
|
reason: @"Not allowed to perform this action. Wrong SENT-BY being used regarding access rights on organizer's calendar."];
|
|
}
|
|
}
|
|
|
|
// 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 = [oldEvent userAsAttendee: [SOGoUser userWithLogin: owner]];
|
|
|
|
if (!attendee)
|
|
attendee = [newEvent userAsAttendee: [SOGoUser userWithLogin: owner]];
|
|
else
|
|
{
|
|
// We must do an extra check here since Bob could have invited Alice
|
|
// using alice@example.com but she would have accepted with ATTENDEE set
|
|
// to sexy@example.com. That would duplicate the ATTENDEE and set the
|
|
// participation status to ACCEPTED for sexy@example.com but leave it
|
|
// to NEEDS-ACTION to alice@example. This can happen in Mozilla Thunderbird/Lightning
|
|
// when a user with multiple identities accepts an event invitation to one
|
|
// of its identity (which is different than the email address associated with
|
|
// the mail account) prior doing a calendar refresh.
|
|
NSMutableArray *attendees;
|
|
iCalPerson *participant;
|
|
|
|
attendees = [NSMutableArray arrayWithArray: [newEvent attendeesWithoutUser: [SOGoUser userWithLogin: owner]]];
|
|
|
|
participant = [newEvent participantForUser: [SOGoUser userWithLogin: owner]
|
|
attendee: attendee];
|
|
[attendee setPartStat: [participant partStat]];
|
|
[attendee setDelegatedFrom: [participant delegatedFrom]];
|
|
[attendee setDelegatedTo: [participant delegatedTo]];
|
|
[attendees addObject: attendee];
|
|
[newEvent setAttendees: attendees];
|
|
}
|
|
|
|
// We first check of the sequences are alright. We don't accept attendees
|
|
// accepting "old" invitations. If that's the case, we return a 409
|
|
if ([[newEvent sequence] intValue] < [[oldEvent sequence] intValue])
|
|
return [NSException exceptionWithHTTPStatus: 409
|
|
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 */
|