diff --git a/NEWS b/NEWS index c15919e90..9260515f9 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,15 @@ +3.3.0 (2017-XX-XX) +------------------ + +New features + - [core] can now invite attendees to exceptions only (#2561) + +Enhancements + - + +Bug fixes + - + 3.2.10 (2017-07-05) ------------------- diff --git a/SoObjects/Appointments/NSArray+Appointments.h b/SoObjects/Appointments/NSArray+Appointments.h index aa1c2856d..5df062b58 100644 --- a/SoObjects/Appointments/NSArray+Appointments.h +++ b/SoObjects/Appointments/NSArray+Appointments.h @@ -1,8 +1,6 @@ /* NSArray+Appointments.h - this file is part of SOGo * - * Copyright (C) 2006 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2006-2017 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/NSArray+Appointments.m b/SoObjects/Appointments/NSArray+Appointments.m index e4d243e4f..ec29cb094 100644 --- a/SoObjects/Appointments/NSArray+Appointments.m +++ b/SoObjects/Appointments/NSArray+Appointments.m @@ -1,8 +1,6 @@ /* NSArray+Appointments.m - this file is part of SOGo * - * Copyright (C) 2006 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2006-2017 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.m b/SoObjects/Appointments/SOGoAppointmentFolder.m index 54697cb1d..95d8b80fe 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.m +++ b/SoObjects/Appointments/SOGoAppointmentFolder.m @@ -1233,8 +1233,15 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir content = [theRecord objectForKey: @"c_cycleinfo"]; if (![content isNotNull]) { - [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", - theRecord]; + // If c_iscycle is set but c_cycleinfo is null, that means we're dealing with a vcalendar that + // contains ONLY one or more vevent with recurrence-id set for each of them. This can happen if + // an organizer invites an attendee only to one or many occurences of a repetitive event. + iCalCalendar *c; + + c = [iCalCalendar parseSingleFromSource: [theRecord objectForKey: @"c_content"]]; + [theRecords addObjectsFromArray: [self _fixupRecords: [c quickRecordsFromContent: [theRecord objectForKey: @"c_content"] + container: nil + nameInContainer: [theRecord objectForKey: @"c_name"]]]]; return; } diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index 0fb3e8c34..ef55f1465 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -171,9 +171,13 @@ } // +// This method will *ONLY* add or update event information in attendees' calendars. +// It will NOT touch to the organizer calendar in anyway. This method is meant +// to reflect changes in attendees' calendars when the organizer makes changes +// to the event. // -// -- (void) _addOrUpdateEvent: (iCalEvent *) theEvent +- (void) _addOrUpdateEvent: (iCalEvent *) newEvent + oldEvent: (iCalEvent *) oldEvent forUID: (NSString *) theUID owner: (NSString *) theOwner { @@ -183,56 +187,84 @@ 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]) + attendeeObject = [self _lookupEvent: [newEvent uid] forUID: theUID]; + + if ([newEvent 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]) + // We must add an occurence to a non-existing event. + if ([attendeeObject isNew]) { - // 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]; + iCalEvent *ownerEvent; + 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 = [[[newEvent parent] events] objectAtIndex: 0]; + user = [SOGoUser userWithLogin: theUID]; + + if (![ownerEvent userAsAttendee: user]) + { + iCalendarToSave = [[[newEvent parent] mutableCopy] autorelease]; + [iCalendarToSave removeChildren: [iCalendarToSave childrenWithTag: @"vevent"]]; + [iCalendarToSave addChild: [[newEvent copy] autorelease]]; + } + } + else + { + // 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: + NSCalendarDate *currentId; + NSArray *occurences; + iCalEvent *occurence; + int max, count; + + iCalendarToSave = [attendeeObject calendar: NO secure: NO]; + + // If recurrenceId is defined, remove the occurence from + // the repeating event. If a recurrenceId is defined in the + // new event, let's make sure we don't already have one in + // the calendar alright. If so, also remove it. + if ([oldEvent recurrenceId] || [newEvent recurrenceId]) + { + // FIXME: use _eventFromRecurrenceId:... + occurences = [iCalendarToSave events]; + max = [occurences count]; + count = 0; + while (count < max) + { + occurence = [occurences objectAtIndex: count]; + currentId = ([oldEvent recurrenceId] ? [oldEvent recurrenceId]: [newEvent recurrenceId]); + if (currentId && [[occurence recurrenceId] compare: currentId] == NSOrderedSame) + { + [[iCalendarToSave children] removeObject: occurence]; + break; + } + count++; + } + } - iCalendarToSave = [ownerEvent parent]; + [iCalendarToSave addChild: [[newEvent copy] autorelease]]; } } 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]; + iCalendarToSave = [newEvent parent]; } - + // Save the event in the attendee's calendar if (iCalendarToSave) [attendeeObject saveCalendar: iCalendarToSave]; } } + // -// +// This method will *ONLY* delete event information in attendees' calendars. +// It will NOT touch to the organizer calendar in anyway. This method is meant +// to reflect changes in attendees' calendars when the organizer makes changes +// to the event. // - (void) _removeEventFromUID: (NSString *) theUID owner: (NSString *) theOwner @@ -251,14 +283,16 @@ // Invitations are always written to the personal folder; it's not necessay // to look into all folders of the user + // FIXME: why look only in the personal calendar here? folder = [[SOGoUser userWithLogin: theUID] personalCalendarFolderInContext: context]; object = [folder lookupName: nameInContainer - inContext: context acquire: NO]; + inContext: context + acquire: NO]; if (![object isKindOfClass: [NSException class]]) { if (recurrenceId == nil) - [object delete]; + [object delete]; else { calendar = [object calendar: NO secure: NO]; @@ -267,7 +301,7 @@ // the repeating event. occurences = [calendar events]; max = [occurences count]; - count = 1; + count = 0; while (count < max) { currentOccurence = [occurences objectAtIndex: count]; @@ -290,6 +324,8 @@ [object saveCalendar: calendar]; } } + else + [self errorWithFormat: @"Unable to find event with UID %@ in %@'s calendar - skipping delete operation", nameInContainer, theUID]; } } @@ -310,7 +346,7 @@ if (currentUID) [self _removeEventFromUID: currentUID owner: owner - withRecurrenceId: recurrenceId]; + withRecurrenceId: recurrenceId]; } } @@ -436,6 +472,7 @@ currentUID = [currentAttendee uidInContext: context]; if (currentUID) [self _addOrUpdateEvent: newEvent + oldEvent: oldEvent forUID: currentUID owner: owner]; } @@ -794,6 +831,7 @@ currentUID = [currentAttendee uidInContext: context]; if (currentUID) [self _addOrUpdateEvent: newEvent + oldEvent: nil forUID: currentUID owner: owner]; } @@ -826,12 +864,10 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent e = [events objectAtIndex: i]; if ([e recurrenceId]) for (j = 0; j < [theAttendees count]; j++) { - if (shouldAdd) { + if (shouldAdd) [e addToAttendees: [theAttendees objectAtIndex: j]]; - } - else { + else [e removeFromAttendees: [theAttendees objectAtIndex: j]]; - } } } } @@ -924,6 +960,7 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent currentUID = [currentAttendee uidInContext: context]; if (currentUID) [self _addOrUpdateEvent: newEvent + oldEvent: oldEvent forUID: currentUID owner: owner]; } @@ -959,7 +996,7 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent // | | // [saveComponent:]---> _handleAddedUsers:fromEvent: <-+ | // | | v -// +------------> _handleUpdatedEvent:fromOldEvent: ---> _addOrUpdateEvent:forUID:owner: <-----------+ +// +------------> _handleUpdatedEvent:fromOldEvent: ---> _addOrUpdateEvent:oldEvent:forUID:owner: <-----------+ // | | ^ | // v v | | // _handleRemovedUsers:withRecurrenceId: _handleSequenceUpdateInEvent:ignoringAttendees:fromOldEvent: | @@ -1342,6 +1379,7 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent if (delegatedUID) // Delegate attendee is a local user; add event to their calendar [self _addOrUpdateEvent: event + oldEvent: nil forUID: delegatedUID owner: [theOwnerUser login]]; @@ -1636,15 +1674,29 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent else // Retrieve this occurence ID. recurrenceId = [occurence recurrenceId]; - - if ([event userIsOrganizer: ownerUser]) + + 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; + } + else { // The organizer deletes an occurence. currentUser = [context activeUser]; - attendees = [occurence attendeesWithoutUser: currentUser]; + + if (recurrenceId) + attendees = [occurence attendeesWithoutUser: currentUser]; + else + attendees = [[event parent] attendeesWithoutUser: currentUser]; - if (![attendees count] && event != occurence) - attendees = [event attendeesWithoutUser: currentUser]; + //if (![attendees count] && event != occurence) + //attendees = [event attendeesWithoutUser: currentUser]; if ([attendees count]) { @@ -1661,17 +1713,7 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent 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 diff --git a/SoObjects/Appointments/SOGoCalendarComponent.m b/SoObjects/Appointments/SOGoCalendarComponent.m index 2a6fb5592..06b0928d2 100644 --- a/SoObjects/Appointments/SOGoCalendarComponent.m +++ b/SoObjects/Appointments/SOGoCalendarComponent.m @@ -804,15 +804,17 @@ /* sender */ shortSenderEmail = [[object organizer] rfc822Email]; if ([shortSenderEmail length]) - { - senderEmail = [[object organizer] mailAddress]; - } + senderEmail = [[object organizer] mailAddress]; else { shortSenderEmail = [[previousObject organizer] rfc822Email]; senderEmail = [[previousObject organizer] mailAddress]; } + // No organizer, grab the event's owner + if (![senderEmail length]) + senderEmail = shortSenderEmail = [[ownerUser defaultIdentity] objectForKey: @"email"]; + /* calendar part */ eventBodyPart = [self _bodyPartForICalObject: object]; diff --git a/SoObjects/Appointments/iCalCalendar+SOGo.h b/SoObjects/Appointments/iCalCalendar+SOGo.h index eae3f8175..2768ea892 100644 --- a/SoObjects/Appointments/iCalCalendar+SOGo.h +++ b/SoObjects/Appointments/iCalCalendar+SOGo.h @@ -1,8 +1,6 @@ /* iCalCalendar+SOGo.h - this file is part of SOGo * - * Copyright (C) 2012 Inverse inc - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2012-2017 Inverse inc * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/iCalCalendar+SOGo.m b/SoObjects/Appointments/iCalCalendar+SOGo.m index ab3c78d9e..9ac4342b8 100644 --- a/SoObjects/Appointments/iCalCalendar+SOGo.m +++ b/SoObjects/Appointments/iCalCalendar+SOGo.m @@ -1,6 +1,6 @@ /* iCalCalendar+SOGo.m - this file is part of SOGo * - * Copyright (C) 2012-2014 Inverse inc + * Copyright (C) 2012-2017 Inverse inc * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,12 +20,16 @@ #import #import +#import +#import #import +#import #import #import "iCalCalendar+SOGo.h" #import "iCalEntityObject+SOGo.h" +#import "NSArray+Appointments.h" @implementation iCalCalendar (SOGoExtensions) @@ -45,9 +49,10 @@ /* master occurrence */ component = [components objectAtIndex: 0]; - if ([component hasRecurrenceRules]) + if ([component hasRecurrenceRules] || [component recurrenceId]) { - count = 1; // skip master event + // Skip the master event if required + count = ([component recurrenceId] ? 0 : 1); while (!occurrence && count < max) { component = [components objectAtIndex: count]; @@ -101,4 +106,63 @@ return [(id)element quickRecordFromContent: theContent container: theContainer nameInContainer: nameInContainer]; } +- (NSArray *) quickRecordsFromContent: (NSString *) theContent + container: (id) theContainer + nameInContainer: (NSString *) nameInContainer +{ + NSCalendarDate *recurrenceId; + NSMutableDictionary *record; + NSMutableArray *allRecords; + NSNumber *dateSecs; + NSArray *elements; + + int i; + + allRecords = [NSMutableArray array]; + + // FIXME: what about tasks? + elements = [self events]; + + for (i = 0; i < [elements count]; i++) + { + record = [(id)[elements objectAtIndex: i] quickRecordFromContent: theContent container: theContainer nameInContainer: nameInContainer]; + recurrenceId = [[elements objectAtIndex: i] recurrenceId]; + dateSecs = [NSNumber numberWithDouble: [recurrenceId timeIntervalSince1970]]; + + [record setObject: nameInContainer forKey: @"c_name"]; + [record setObject: dateSecs forKey: @"c_recurrence_id"]; + + [allRecords addObject: record]; + } + + return allRecords; +} + +- (NSArray *) attendeesWithoutUser: (SOGoUser *) user +{ + NSMutableArray *allAttendees; + NSArray *events, *attendees; + iCalPerson *attendee; + iCalEvent *event; + + int i, j; + + allAttendees = [NSMutableArray array]; + events = [self events]; + + for (i = 0; i < [events count]; i++) + { + event = [events objectAtIndex: i]; + attendees = [event attendees]; + for (j = 0; j < [attendees count]; j++) + { + attendee = [attendees objectAtIndex: j]; + [allAttendees removePerson: attendee]; + [allAttendees addObject: attendee]; + } + } + + return allAttendees; +} + @end diff --git a/SoObjects/Appointments/iCalEvent+SOGo.m b/SoObjects/Appointments/iCalEvent+SOGo.m index 4dd3b7948..b2f2deeb5 100644 --- a/SoObjects/Appointments/iCalEvent+SOGo.m +++ b/SoObjects/Appointments/iCalEvent+SOGo.m @@ -126,7 +126,7 @@ boolTmp = ((isAllDay) ? 1 : 0); [row setObject: [NSNumber numberWithInt: boolTmp] forKey: @"c_isallday"]; - boolTmp = (([self isRecurrent]) ? 1 : 0); + boolTmp = ((([self isRecurrent] || [self recurrenceId])) ? 1 : 0); [row setObject: [NSNumber numberWithInt: boolTmp] forKey: @"c_iscycle"]; boolTmp = (([self isOpaque]) ? 1 : 0); diff --git a/UI/Scheduler/UIxCalListingActions.m b/UI/Scheduler/UIxCalListingActions.m index 51f6ebe15..4cf0782fc 100644 --- a/UI/Scheduler/UIxCalListingActions.m +++ b/UI/Scheduler/UIxCalListingActions.m @@ -265,8 +265,8 @@ static NSArray *tasksFields = nil; NSString *aDateField; int daylightOffset; unsigned int count; - static NSString *fields[] = { @"startDate", @"c_startdate", - @"endDate", @"c_enddate" }; + + static NSString *fields[] = { @"startDate", @"c_startdate", @"endDate", @"c_enddate" }; /* WARNING: This condition has been put and removed many times, please leave it. Here is the story...