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