Dependency Graph
View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
0005789 | SOGo | Apple iPhone OS | public | 2023-06-14 14:20 | 2024-09-27 07:28 |
Reporter | alex87 | Assigned To | sebastien | ||
Priority | normal | Severity | minor | Reproducibility | have not tried |
Status | feedback | Resolution | reopened | ||
Product Version | 5.8.3 | ||||
Summary | 0005789: Wrong time Events | ||||
Description | Apple Iphone IOS 15.X Create Event Time/Zone Europe/Moscow. Web client sogo 30 minute wrong. | ||||
Tags | No tags attached. | ||||
has duplicate | 0006042 | new | Incorrect display of event time when created from Mac calendar client |
|
|
BEGIN:VCALENDAR |
|
В SoObjects/Appointments/SOGoAppointmentObject.m
If you collect from the source, I added this hack. The problem is on the part of makosi and their crooked timezones. Couldn't find a more concise solution. Если собираете из исходников, я дописал вот такой хак. Проблема со стороны макоси и их кривых таймзон. Более лаконичного решения не нашёл. Ребята отвечают на баги неохотно, отдавая предпочтение платной поддержке |
|
file SOGoAppointmentObject.m edited from source version 5.8.3 published download. А не поделитесь правленым SOGoAppointmentObject.m |
|
Вот, поправил для 5.8.3, для предыдущих вплоть до где-то 5.7.0 подходит SOGoAppointmentObject.m (97,181 bytes)
/* Copyright (C) 2007-2022 Inverse inc. 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/NSTimeZone.h> #import <Foundation/NSValue.h> #import <NGObjWeb/NSException+HTTP.h> #import <NGObjWeb/WOContext+SoObjects.h> #import <NGObjWeb/WOResponse.h> #import <NGExtensions/NGCalendarDateRange.h> #import <NGExtensions/NSCalendarDate+misc.h> #import <NGExtensions/NSNull+misc.h> #import <NGExtensions/NSObject+Logs.h> #import <NGCards/iCalDateTime.h> #import <NGCards/iCalEvent.h> #import <NGCards/iCalToDo.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/NSArray+Utilities.h> #import <SOGo/NSDictionary+Utilities.h> #import <SOGo/NSObject+DAV.h> #import <SOGo/NSString+Utilities.h> #import <SOGo/SOGoDateFormatter.h> #import <SOGo/SOGoUserManager.h> #import <SOGo/SOGoUser.h> #import <SOGo/SOGoUserSettings.h> #import <SOGo/SOGoDomainDefaults.h> #import <SOGo/WORequest+SOGo.h> #import <SOGo/SOGoSystemDefaults.h> #import "iCalCalendar+SOGo.h" #import "iCalEvent+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 "SOGoFreeBusyObject.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; } // // 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 *) newEvent oldEvent: (iCalEvent *) oldEvent forUID: (NSString *) theUID owner: (NSString *) theOwner { if (![theUID isEqualToString: theOwner]) { SOGoAppointmentObject *attendeeObject; iCalCalendar *iCalendarToSave; iCalPerson *attendee; SOGoUser *user; iCalendarToSave = nil; user = [SOGoUser userWithLogin: theUID]; attendeeObject = [self _lookupEvent: [newEvent uid] forUID: theUID]; attendee = [newEvent userAsAttendee: user]; // If the atttende's role is NON-PARTICIPANT, we write nothing to its calendar if ([[attendee role] caseInsensitiveCompare: @"NON-PARTICIPANT"] == NSOrderedSame) { // If the attendee's previous role was not NON-PARTICIPANT we must also delete // the event from its calendar attendee = [oldEvent userAsAttendee: user]; if ([[attendee role] caseInsensitiveCompare: @"NON-PARTICIPANT"] != NSOrderedSame) { NSString *currentUID; currentUID = [attendee uidInContext: context]; if (currentUID) [self _removeEventFromUID: currentUID owner: owner withRecurrenceId: [oldEvent recurrenceId]]; } return; } if ([newEvent recurrenceId]) { // We must add an occurence to a non-existing event. if ([attendeeObject isNew]) { iCalEvent *ownerEvent; // We check if the attendee that was added to a single occurence is // present in the master component. If not, we create a calendar with // a single event for the occurence. ownerEvent = [[[newEvent parent] events] objectAtIndex: 0]; 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 already. If so, also remove it. if ([oldEvent recurrenceId] || [newEvent recurrenceId]) { // FIXME: use _eventFromRecurrenceId:... occurences = [iCalendarToSave events]; currentId = ([oldEvent recurrenceId] ? [oldEvent recurrenceId]: [newEvent recurrenceId]); if (currentId) { max = [occurences count]; count = 0; while (count < max) { occurence = [occurences objectAtIndex: count]; if ([occurence recurrenceId] && [[occurence recurrenceId] compare: currentId] == NSOrderedSame) { [iCalendarToSave removeChild: occurence]; break; } count++; } } } [iCalendarToSave addChild: [[newEvent copy] autorelease]]; } } else { 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 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 // FIXME: why look only in the personal calendar here? 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 = 0; while (count < max) { currentOccurence = [occurences objectAtIndex: count]; currentId = [currentOccurence recurrenceId]; if (currentId && [currentId compare: recurrenceId] == NSOrderedSame) { [[calendar children] removeObject: currentOccurence]; break; } count++; } // Add an date exception. event = (iCalRepeatableEntityObject*)[calendar firstChildWithTag: [object componentTag]]; if (event) { [event addToExceptionDates: recurrenceId]; [event increaseSequence]; [event setLastModified: [NSCalendarDate calendarDate]]; // We save the updated iCalendar in the database. [object saveCalendar: calendar]; } else { // No more child; kill the parent [object delete]; } } } else [self errorWithFormat: @"Unable to find event with UID %@ in %@'s calendar - skipping delete operation. This can be normal for NON-PARTICIPANT attendees.", nameInContainer, theUID]; } } // // // - (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; } // // // - (BOOL) _shouldScheduleEvent: (iCalPerson *) thePerson { //NSArray *userAgents; NSString *v; BOOL b; //int i; b = YES; if (thePerson && (v = [thePerson 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 // // This code has been disabled - see 0003274. // #if 0 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; } } #endif return b; } // // // - (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 oldEvent: oldEvent forUID: currentUID owner: owner]; } if ([self _shouldScheduleEvent: [newEvent organizer]]) [self sendEMailUsingTemplateNamed: @"Update" forObject: [newEvent itipEntryWithMethod: @"request"] previousObject: oldEvent toAttendees: updateAttendees withType: @"calendar:invitation-update"]; } // This method scans the list of attendees. - (NSException *) _handleAttendeesAvailability: (NSArray *) theAttendees forEvent: (iCalEvent *) theEvent { iCalPerson *currentAttendee; SOGoUser *user; SOGoUserSettings *us; NSMutableArray *unavailableAttendees, *unavailableEmails; NSEnumerator *enumerator; NSString *currentUID, *ownerUID; NSMutableString *reason; NSDictionary *values, *info; NSMutableDictionary *value, *moduleSettings; id whiteList; int i, count; i = count = 0; // Build list of the attendees uids unavailableAttendees = [[NSMutableArray alloc] init]; unavailableEmails = [NSMutableArray array]; 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]; [unavailableEmails addObject: [currentAttendee rfc822Email]]; } } } } 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:@", "]; } if (count < [theAttendees count]) { info = [NSDictionary dictionaryWithObjectsAndKeys: reason, @"reject", unavailableEmails, @"unavailableAttendees", nil]; reason = [NSMutableString stringWithString: [info jsonRepresentation]]; } [unavailableAttendees release]; return [self 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 and potentially auto-accept/decline // the invitation. // // For normal attendees, it'll return an exception with // conflicting dates, unless we force the save.// // We check for between startDate + 1 second and // endDate - 1 second // // Note that it doesn't matter if it changes the participation // status since in case of an error, nothing will get saved. // - (NSException *) _handleAttendeesConflicts: (NSArray *) theAttendees forEvent: (iCalEvent *) theEvent force: (BOOL) forceSave { iCalPerson *currentAttendee; NSMutableArray *attendees; NSEnumerator *enumerator; NSString *currentUID; SOGoUser *user, *currentUser; _resourceHasAutoAccepted = NO; // 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])) { NSCalendarDate *start, *end, *rangeStartDate, *rangeEndDate; SOGoAppointmentFolder *folder; SOGoFreeBusyObject *fb; NGCalendarDateRange *range; NSMutableArray *fbInfo; NSArray *allOccurences; BOOL must_delete; int i, j, delta; user = [SOGoUser userWithLogin: currentUID]; // 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 ([user isResource] && ![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 [self exceptionWithHTTPStatus:409 reason: reason]; } fb = [SOGoFreeBusyObject objectWithName: @"freebusy.ifb" inContainer: [user homeFolderInContext: context]]; fbInfo = (NSMutableArray *)[fb fetchFreeBusyInfosFrom: start to: end]; // // 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 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. if ([[[fbInfo objectAtIndex: i] objectForKey: @"c_uid"] compare: [theEvent uid]] == NSOrderedSame) { [fbInfo removeObjectAtIndex: i]; continue; } // Ignore transparent events if (![[[fbInfo objectAtIndex: i] objectForKey: @"c_isopaque"] boolValue]) { [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; // 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]; 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 currentAttendee = nil; for (i = 0; i < [theAttendees count]; i++) { currentAttendee = [theAttendees objectAtIndex: i]; if ([[currentAttendee uidInContext: context] isEqualToString: currentUID]) break; else currentAttendee = nil; } if ([fbInfo count]) { SOGoDateFormatter *formatter; formatter = [[context activeUser] dateFormatterInContext: context]; if ([user isResource]) { // 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. // When numberOfSimultaneousBookings is set to -1, only force the auto-accept // once the conflict has been raised and the action is forced by the user. if ([user numberOfSimultaneousBookings] <= 0 || [user numberOfSimultaneousBookings] > [fbInfo count]) { if (currentAttendee && ([user numberOfSimultaneousBookings] >= 0 || forceSave)) { [[currentAttendee attributes] removeObjectForKey: @"RSVP"]; [currentAttendee setParticipationStatus: iCalPersonPartStatAccepted]; _resourceHasAutoAccepted = YES; } } else { iCalCalendar *calendar; NSDictionary *values, *info; 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", [formatter formattedDateAndTime: [[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}."]]; info = [NSDictionary dictionaryWithObject: reason forKey: @"reject"]; return [self exceptionWithHTTPStatus: 409 reason: [info jsonRepresentation]]; } } // // We are dealing with a normal attendee. Lets check if we have conflicts, unless // we are being asked to force the save anyway // if (!forceSave && !_resourceHasAutoAccepted) { NSMutableDictionary *info; NSMutableArray *conflicts; NSString *formattedEnd; SOGoUser *ownerUser; id o; info = [NSMutableDictionary dictionary]; conflicts = [NSMutableArray array]; if (currentAttendee) { if ([currentAttendee cn]) [info setObject: [currentAttendee cn] forKey: @"attendee_name"]; if ([currentAttendee rfc822Email]) [info setObject: [currentAttendee rfc822Email] forKey: @"attendee_email"]; } else if ([owner isEqualToString: currentUID]) { ownerUser = [SOGoUser userWithLogin: owner]; if ([ownerUser cn]) [info setObject: [ownerUser cn] forKey: @"attendee_name"]; if ([ownerUser systemEmail]) [info setObject: [ownerUser systemEmail] forKey: @"attendee_email"]; } for (i = 0; i < [fbInfo count]; i++) { o = [fbInfo objectAtIndex: i]; end = [o objectForKey: @"endDate"]; if ([[o objectForKey: @"startDate"] isDateOnSameDay: end]) formattedEnd = [formatter formattedTime: end]; else formattedEnd = [formatter formattedDateAndTime: end]; [conflicts addObject: [NSDictionary dictionaryWithObjectsAndKeys: [formatter formattedDateAndTime: [o objectForKey: @"startDate"]], @"startDate", formattedEnd, @"endDate", nil]]; } [info setObject: conflicts forKey: @"conflicts"]; // We immediately raise an exception, without processing the possible other attendees. return [self exceptionWithHTTPStatus: 409 reason: [info jsonRepresentation]]; } } // if ([fbInfo count]) ... else if (currentAttendee && [user isResource]) { // 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]; _resourceHasAutoAccepted = YES; } } return nil; } // // // - (NSException *) _handleAddedUsers: (NSArray *) attendees fromEvent: (iCalEvent *) newEvent force: (BOOL) forceSave { iCalPerson *currentAttendee; NSEnumerator *enumerator; NSString *currentUID; NSException *e; // We check for conflicts if ((e = [self _handleAttendeesConflicts: attendees forEvent: newEvent force: forceSave])) return e; if ((e = [self _handleAttendeesAvailability: attendees forEvent: newEvent])) return e; enumerator = [attendees objectEnumerator]; while ((currentAttendee = [enumerator nextObject])) { currentUID = [currentAttendee uidInContext: context]; if (currentUID) [self _addOrUpdateEvent: newEvent oldEvent: nil 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 force: (BOOL) forceSave { 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]; if ([deletedAttendees count]) { // We delete the attendees in all exception occurences, if // the attendees were removed from the master event. [self _addOrDeleteAttendees: deletedAttendees inRecurrenceExceptionsForEvent: newEvent add: NO]; [self _handleRemovedUsers: deletedAttendees withRecurrenceId: [newEvent recurrenceId]]; if ([self _shouldScheduleEvent: [newEvent organizer]]) [self sendEMailUsingTemplateNamed: @"Deletion" forObject: [newEvent itipEntryWithMethod: @"cancel"] previousObject: oldEvent toAttendees: deletedAttendees withType: @"calendar:cancellation"]; } if ((ex = [self _handleAttendeesConflicts: [newEvent attendees] forEvent: newEvent force: forceSave])) return ex; if ((ex = [self _handleAttendeesAvailability: [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 oldEvent: oldEvent forUID: currentUID owner: owner]; } } } if ([addedAttendees count]) { // Send an invitation to new attendees if ((ex = [self _handleAddedUsers: addedAttendees fromEvent: newEvent force: forceSave])) return ex; if ([self _shouldScheduleEvent: [newEvent organizer]]) [self sendEMailUsingTemplateNamed: @"Invitation" forObject: [newEvent itipEntryWithMethod: @"request"] previousObject: oldEvent toAttendees: addedAttendees withType: @"calendar:invitation"]; } if ([changes hasMajorChanges]) [self sendReceiptEmailForObject: newEvent addedAttendees: addedAttendees deletedAttendees: deletedAttendees updatedAttendees: updatedAttendees operation: EventUpdated]; return nil; } // // Workflow : +----------------------+ // | | // [saveComponent:]---> _handleAddedUsers:fromEvent: <-+ | // | | v // +------------> _handleUpdatedEvent:fromOldEvent: ---> _addOrUpdateEvent:oldEvent: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 { return [self saveComponent: newEvent force: NO]; } - (NSException *) saveComponent: (iCalEvent *) newEvent force: (BOOL) forceSave { 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 force: forceSave])) return ex; if ([attendees count]) { if ([self _shouldScheduleEvent: [newEvent organizer]]) [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 force: forceSave])) 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; // If the atttende's role is NON-PARTICIPANT, we write nothing to its calendar if ([[attendee role] caseInsensitiveCompare: @"NON-PARTICIPANT"] == NSOrderedSame) return nil; 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. [event setLastModified: [NSCalendarDate calendarDate]]; 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 { iCalPerson *otherAttendee, *otherDelegate; NSString *currentStatus, *organizerUID; SOGoUser *ownerUser, *currentUser; NSString *delegateEmail; NSException *ex; BOOL addDelegate, removeDelegate; currentStatus = [attendee partStat]; otherAttendee = attendee; ex = nil; 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; } if ([self _shouldScheduleEvent: [event organizer]]) [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 oldEvent: nil forUID: delegatedUID owner: [theOwnerUser login]]; if ([self _shouldScheduleEvent: [event organizer]]) [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, *domain; 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 no occurence found, create one if (event == nil) 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 = [self exceptionWithHTTPStatus: 409 reason: @"delegate is organizer"]; if ([event isAttendee: [[delegate email] rfc822Email]]) ex = [self exceptionWithHTTPStatus: 409 reason: @"delegate is a participant"]; else { NSDictionary *dict; domain = [[context activeUser] domain]; dict = [[SOGoUserManager sharedUserManager] contactInfosForUserWithUIDorEmail: [[delegate email] rfc822Email] inDomain: domain]; if (dict && [[dict objectForKey: @"isGroup"] boolValue]) ex = [self 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]; } [event setLastModified: [NSCalendarDate calendarDate]]; ex = [self saveCalendar: [event parent]]; } } } else ex = [self exceptionWithHTTPStatus: 404 // Not Found reason: @"user does not participate in this calendar event"]; } else ex = [self exceptionWithHTTPStatus: 500 // Server Error reason: @"unable to parse event record"]; return ex; } // // // - (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 (occurence == nil) { // No occurence specified; use the master event. occurence = event; recurrenceId = nil; } else // Retrieve this occurence ID. recurrenceId = [occurence recurrenceId]; 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 (or a user with sufficient rights) deletes an occurence currentUser = [context activeUser]; if (recurrenceId) { if (activeUserIsOwner) attendees = [occurence attendeesWithoutUser: currentUser]; else attendees = [occurence attendeesWithoutUser: ownerUser]; } else attendees = [[event parent] attendeesWithoutUser: currentUser]; //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]; if ([self _shouldScheduleEvent: [event organizer]]) [self sendEMailUsingTemplateNamed: @"Deletion" forObject: [occurence itipEntryWithMethod: @"cancel"] previousObject: nil toAttendees: attendees withType: @"calendar:cancellation"]; } } 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"]; } } // // [1] iOS devices (and potentially others) send event invitations with no PARTSTAT defined. // This confuses DAV clients like Thunderbird, or even SOGo web. RFC 5545 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. // // [2] Thunderbird (and potentially others) send event invitations with no RSVP defined. // Without any RSVP, the Web interface won't allow the user to respond to the invitation. // For this reason, we set it to TRUE when missing, even though RFC 5545 says the default // value is FALSE. // - (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"]; if (![[attendee rsvp] length]) [attendee setRsvp: @"TRUE"]; } } } } /** * 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; iCalTimeZone *tz; NSUInteger i; int j; allEvents = [rqCalendar events]; for (i = 0; i < [allEvents count]; i++) { event = [allEvents objectAtIndex: i]; tz = [event adjustInContext: context withTimezones: nil]; if (tz) [rqCalendar addTimeZone: tz]; 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 primaryIdentity]; 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); } } } } - (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 { SOGoUser *ownerUser; NSException *ex; NSArray *roles; BOOL ownerIsOrganizer; 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; event = [[calendar events] objectAtIndex: 0]; eventUID = [event uid]; attendees = nil; // 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 [self exceptionWithHTTPStatus: 409 reason: [NSString stringWithFormat: @"Event UID already in use. (%@)", eventUID]]; } // // New event and we're the organizer -- send invitation to all attendees // ownerIsOrganizer = [event userIsOrganizer: ownerUser]; // We handle the situation where the SOGo Integrator extension isn't installed or // if the SENT-BY isn't set. That can happen if Bob invites Alice by creating the event // in Annie's calendar. Annie should be the organizer, and Bob the SENT-BY. But most // broken CalDAV client that aren't identity-aware will create the event in Annie's calendar // and set Bob as the organizer. We fix this for them. See #3368 for details. if (!ownerIsOrganizer && [[context activeUser] hasEmail: [[event organizer] rfc822Email]]) { [[event organizer] setCn: [ownerUser cn]]; [[event organizer] setEmail: [[ownerUser allEmails] objectAtIndex: 0]]; [[event organizer] setSentBy: [NSString stringWithFormat: @"\"MAILTO:%@\"", [[[context activeUser] allEmails] objectAtIndex: 0]]]; ownerIsOrganizer = YES; } if (ownerIsOrganizer) { attendees = [event attendeesWithoutUser: ownerUser]; if ([attendees count]) { if ((ex = [self _handleAddedUsers: attendees fromEvent: event force: YES])) 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]]]; } if ([self _shouldScheduleEvent: [event organizer]]) [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 ([event userIsAttendee: ownerUser] && [self _shouldScheduleEvent: [event userAsAttendee: ownerUser]]) { [self sendResponseToOrganizer: event from: ownerUser]; } [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 ([oldEvents count] && [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] // // int newCount = [[newEvent attendees] count], oldCount = [[oldEvent attendees] count]; if (newCount > 0 || oldCount > 0) { 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) { ownerIsOrganizer = [newEvent userIsOrganizer: ownerUser]; userIsOrganizer = [newEvent userIsOrganizer: [context activeUser]]; } else { ownerIsOrganizer = [oldEvent userIsOrganizer: ownerUser]; userIsOrganizer = [oldEvent userIsOrganizer: [context activeUser]]; } // We handle the situation where the SOGo Integrator extension isn't installed or // if the SENT-BY isn't set. That can happen if Bob invites Alice by creating the event // in Annie's calendar. Annie should be the organizer, and Bob the SENT-BY. But most // broken CalDAV client that aren't identity-aware will create the event in Annie's calendar // and set Bob as the organizer. We fix this for them. See #3368 for details. // // We also handle the case where Bob invites Alice and Bob has full access to Alice's calendar // After inviting ALice, Bob opens the event in Alice's calendar and accept/declines the event. // if (!userIsOrganizer && !ownerIsOrganizer && [[context activeUser] hasEmail: [[newEvent organizer] rfc822Email]]) { [[newEvent organizer] setCn: [ownerUser cn]]; [[newEvent organizer] setEmail: [[ownerUser allEmails] objectAtIndex: 0]]; [[newEvent organizer] setSentBy: [NSString stringWithFormat: @"\"MAILTO:%@\"", [[[context activeUser] allEmails] objectAtIndex: 0]]]; ownerIsOrganizer = YES; } // 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 (ownerIsOrganizer) { // 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"] && ![[context activeUser] isSuperUser]) { return [self 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 force: YES])) return ex; } // if (ownerIsOrganizer) .. // // 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 [self 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]) { if ([[delegateEmail lowercaseString] hasPrefix: @"mailto:"]) 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 { changes = [iCalEventChanges changesFromEvent: oldEvent toEvent: newEvent]; if ([changes hasMajorChanges]) [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; BOOL mustUpdate; rq = [_ctx request]; NSString *myString = [rq contentAsString]; NSString *myStringEdited = [myString stringByReplacingOccurrencesOfString:@"TZOFFSETTO:+023017" withString:@"TZOFFSETTO:+0300"]; NSString *myStringEdited2 = [myStringEdited stringByReplacingOccurrencesOfString:@"TZOFFSETFROM:+023017" withString:@"TZOFFSETFROM:+0300"]; rqCalendar = [iCalCalendar parseSingleFromSource: myStringEdited2]; mustUpdate = YES; ex = nil; // We are unable to parse the received calendar, we return right away // with a 400 error code. if (!rqCalendar) { return [NSException exceptionWithDAVStatus: 400 reason: @"Unable to parse event."]; } if ([self isNew]) { iCalEvent *masterEvent; SOGoUser *ownerUser; ownerUser = [SOGoUser userWithLogin: owner]; masterEvent = [[rqCalendar events] objectAtIndex: 0]; if (![masterEvent userIsOrganizer: ownerUser] && [masterEvent userIsAttendee: ownerUser]) { /// // This is a new event, but the user is an attendee; check if the event has // already been saved to another calendar. // iCalCalendar *currentCalendar; iCalEvent *currentMasterEvent; NSArray *folders; NSEnumerator *e; NSString *organizerUID; SOGoAppointmentFolder *folder; SOGoAppointmentObject *object; object = nil; folders = [container lookupCalendarFoldersForUID: owner]; e = [folders objectEnumerator]; while ( object == nil && (folder = [e nextObject]) ) { if (folder != [self container]) { object = [folder lookupName: nameInContainer inContext: context acquire: NO]; if (![object isKindOfClass: [NSException class]] && ![object isNew]) { currentCalendar = [object calendar: NO secure: NO]; currentMasterEvent = [[currentCalendar events] objectAtIndex: 0]; if ([[masterEvent sequence] compare: [currentMasterEvent sequence]] == NSOrderedAscending || [[masterEvent sequence] isEqualToNumber: [currentMasterEvent sequence]]) // Found older copy in another calendar, delete it [object delete]; else // Found a higher sequence, ignore PUT mustUpdate = NO; } } } // Verify if the event is still present in the organizer calendar organizerUID = [[masterEvent organizer] uidInContext: context]; if (organizerUID && ![[SOGoSystemDefaults sharedSystemDefaults] disableOrganizerEventCheck]) { object = [self _lookupEvent: [masterEvent uid] forUID: organizerUID]; if ([object isNew]) { // The event has vanished return [NSException exceptionWithDAVStatus: 412 reason: @"Precondition Failed"]; } } } } else { // // 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; } if (mustUpdate) ex = [self updateContentWithCalendar: rqCalendar fromRequest: rq]; if (ex) response = (WOResponse *) ex; else { response = [_ctx response]; if (isNew && mustUpdate) [response setStatus: 201 /* Created */]; else [response setStatus: 204 /* No Content */]; etag = [self davEntityTag]; if (etag) [response setHeader: etag forKey: @"etag"]; } return response; } - (BOOL) resourceHasAutoAccepted { return _resourceHasAutoAccepted; } @end /* SOGoAppointmentObject */ |
|
Respect. Complie succes. Спасибо. Скомпилировал, установил баг. Ушел. |
|
The issue is on Apple side. You may need to open an issue on their BTS. Sebastien |
|
Helo please fix version 5.9.1 - SOGoAppointmentObject.m |
|
Ну вы уж совсем не ленитесь диффнуть-то) rqCalendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]]; меняйте на NSString myString = [rq contentAsString]; |
|
I can see, that this problem also happens on Android platform. |
|
Date Modified | Username | Field | Change |
---|---|---|---|
2023-06-14 14:20 | alex87 | New Issue | |
2023-06-14 14:20 | alex87 | File Added: Screenshot_4.jpg | |
2023-06-14 14:58 | alex87 | Note Added: 0017027 | |
2023-06-19 11:57 | S1ash | Note Added: 0017055 | |
2023-06-20 06:07 | alex87 | Note Added: 0017057 | |
2023-06-20 09:27 | S1ash | Note Added: 0017060 | |
2023-06-20 09:27 | S1ash | File Added: SOGoAppointmentObject.m | |
2023-06-21 08:14 | alex87 | Note Added: 0017068 | |
2023-06-27 13:04 | sebastien | Note Added: 0017092 | |
2023-06-27 13:04 | sebastien | Assigned To | => sebastien |
2023-06-27 13:04 | sebastien | Status | new => feedback |
2023-09-11 14:24 | qhivert | Status | feedback => closed |
2023-09-11 14:24 | qhivert | Resolution | open => won't fix |
2024-03-21 10:54 | alex87 | Status | closed => feedback |
2024-03-21 10:54 | alex87 | Resolution | won't fix => reopened |
2024-03-21 10:54 | alex87 | Note Added: 0017663 | |
2024-03-21 11:21 | S1ash | Note Added: 0017664 | |
2024-09-27 07:10 | stanito | Note Added: 0017903 | |
2024-09-27 07:28 | Christian Mack | Relationship added | has duplicate 0006042 |