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