]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentObject.m
4fbebbdb036eb91768c95413860982d2d09d18d2
[scalable-opengroupware.org] / SoObjects / Appointments / SOGoAppointmentObject.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo 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   OGo 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 "SOGoAppointmentObject.h"
23
24 #import <NGCards/iCalCalendar.h>
25 #import <NGCards/iCalEvent.h>
26 #import <NGCards/iCalEventChanges.h>
27 #import <NGCards/iCalPerson.h>
28
29 #import <SOGo/AgenorUserManager.h>
30 #import <SOGo/SOGoObject.h>
31 #import <SOGo/SOGoPermissions.h>
32
33 #import "iCalEntityObject+Agenor.h"
34
35 #import "common.h"
36
37 #import "NSArray+Appointments.h"
38
39 @implementation SOGoAppointmentObject
40
41 - (NSString *) componentTag
42 {
43   return @"vevent";
44 }
45
46 /* iCal handling */
47 - (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt
48 {
49   AgenorUserManager *um;
50   NSMutableArray *uids;
51   NSArray *attendees;
52   unsigned i, count;
53   NSString *email, *uid;
54   
55   if (![_apt isNotNull])
56     return nil;
57   
58   if ((attendees = [_apt attendees]) == nil)
59     return nil;
60   count = [attendees count];
61   uids = [NSMutableArray arrayWithCapacity:count + 1];
62   
63   um = [AgenorUserManager sharedUserManager];
64   
65   /* add organizer */
66   
67   email = [[_apt organizer] rfc822Email];
68   if ([email isNotNull]) {
69     uid = [um getUIDForEmail:email];
70     if ([uid isNotNull]) {
71       [uids addObject:uid];
72     }
73     else
74       [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
75   }
76
77   /* add attendees */
78   
79   for (i = 0; i < count; i++)
80     {
81       iCalPerson *person;
82     
83       person = [attendees objectAtIndex:i];
84       email  = [person rfc822Email];
85       if (![email isNotNull]) continue;
86     
87       uid = [um getUIDForEmail:email];
88       if (![uid isNotNull]) {
89         [self logWithFormat:@"Note: got no uid for email: '%@'", email];
90         continue;
91       }
92       if (![uids containsObject:uid])
93         [uids addObject:uid];
94     }
95
96   return uids;
97 }
98
99 /* folder management */
100
101 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
102   // TODO: what does this do? lookup the home of the organizer?
103   return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
104 }
105 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
106   return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
107 }
108
109 /* store in all the other folders */
110
111 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
112   NSEnumerator *e;
113   id folder;
114   NSException *allErrors = nil;
115   id ctx;
116
117   ctx = [[WOApplication application] context];
118   
119   e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
120              objectEnumerator];
121   while ((folder = [e nextObject]) != nil) {
122     NSException           *error;
123     SOGoAppointmentObject *apt;
124     
125     if (![folder isNotNull]) /* no folder was found for given UID */
126       continue;
127
128     apt = [folder lookupName: [self nameInContainer] inContext:ctx
129                   acquire: NO];
130     if ([apt isKindOfClass: [NSException class]])
131       {
132         [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
133               [self nameInContainer], folder];
134         [self logWithFormat:@"the exception reason was: %@",
135               [(NSException *) apt reason]];
136         continue;
137       }
138
139     if (![apt isNotNull]) {
140       [self logWithFormat:@"Note: did not find '%@' in folder: %@",
141               [self nameInContainer], folder];
142       continue;
143     }
144     if ([apt isKindOfClass: [NSException class]]) {
145       [self logWithFormat:@"Exception: %@", [(NSException *) apt reason]];
146       continue;
147     }
148     
149     if ((error = [apt primarySaveContentString:_iCal]) != nil) {
150       [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
151       // TODO: make compound
152       allErrors = error;
153     }
154   }
155
156   return allErrors;
157 }
158
159 - (NSException *)deleteInUIDs:(NSArray *)_uids {
160   NSEnumerator *e;
161   id folder;
162   NSException *allErrors = nil;
163   id ctx;
164   
165   ctx = [[WOApplication application] context];
166   
167   e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
168              objectEnumerator];
169   while ((folder = [e nextObject])) {
170     NSException           *error;
171     SOGoAppointmentObject *apt;
172     
173     apt = [folder lookupName:[self nameInContainer] inContext:ctx
174                   acquire:NO];
175     if ([apt isKindOfClass: [NSException class]]) {
176       [self logWithFormat: @"%@", [(NSException *) apt reason]];
177       continue;
178     }
179     
180     if ((error = [apt primaryDelete]) != nil) {
181       [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
182       // TODO: make compound
183       allErrors = error;
184     }
185   }
186   return allErrors;
187 }
188
189 /* "iCal multifolder saves" */
190
191 - (NSException *) saveContentString: (NSString *) _iCal
192                        baseSequence: (int) _v
193 {
194   /* 
195      Note: we need to delete in all participants folders and send iMIP messages
196            for all external accounts.
197      
198      Steps:
199      - fetch stored content
200      - parse old content
201      - check if sequence matches (or if 0=ignore)
202      - extract old attendee list + organizer (make unique)
203      - parse new content (ensure that sequence is increased!)
204      - extract new attendee list + organizer (make unique)
205      - make a diff => new, same, removed
206      - write to new, same
207      - delete in removed folders
208      - send iMIP mail for all folders not found
209   */
210   AgenorUserManager *um;
211   iCalEvent *oldApt, *newApt;
212   iCalEventChanges *changes;
213   iCalPerson *organizer;
214   NSString *oldContent, *uid;
215   NSArray *uids, *props;
216   NSMutableArray *attendees, *storeUIDs, *removedUIDs;
217   NSException *storeError, *delError;
218   BOOL updateForcesReconsider;
219   
220   updateForcesReconsider = NO;
221
222   if ([_iCal length] == 0)
223     return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
224                         reason: @"got no iCalendar content to store!"];
225
226   um = [AgenorUserManager sharedUserManager];
227
228   /* handle old content */
229   
230   oldContent = [self contentAsString]; /* if nil, this is a new appointment */
231   if ([oldContent length] == 0)
232     {
233     /* new appointment */
234       [self debugWithFormat:@"saving new appointment: %@", _iCal];
235       oldApt = nil;
236     }
237   else
238     oldApt = (iCalEvent *) [self component: NO];
239   
240   /* compare sequence if requested */
241
242   if (_v != 0) {
243     // TODO
244   }
245   
246   /* handle new content */
247   
248   newApt = (iCalEvent *) [self component: NO];
249   if (!newApt)
250     return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
251                         reason: @"could not parse iCalendar content!"];
252
253   /* diff */
254   
255   changes = [iCalEventChanges changesFromEvent: oldApt toEvent: newApt];
256   uids = [um getUIDsForICalPersons: [changes deletedAttendees]
257              applyStrictMapping: NO];
258   removedUIDs = [NSMutableArray arrayWithArray: uids];
259
260   uids = [um getUIDsForICalPersons: [newApt attendees]
261              applyStrictMapping: NO];
262   storeUIDs = [NSMutableArray arrayWithArray: uids];
263   props = [changes updatedProperties];
264
265   /* detect whether sequence has to be increased */
266   if ([changes hasChanges])
267     [newApt increaseSequence];
268
269   /* preserve organizer */
270
271   organizer = [newApt organizer];
272   uid = [um getUIDForICalPerson: organizer];
273   if (!uid)
274     uid = [self ownerInContext: nil];
275   if (uid) {
276     if (![storeUIDs containsObject:uid])
277       [storeUIDs addObject:uid];
278     [removedUIDs removeObject:uid];
279   }
280
281   /* organizer might have changed completely */
282
283   if (oldApt && ([props containsObject: @"organizer"])) {
284     uid = [um getUIDForICalPerson:[oldApt organizer]];
285     if (uid) {
286       if (![storeUIDs containsObject:uid]) {
287         if (![removedUIDs containsObject:uid]) {
288           [removedUIDs addObject:uid];
289         }
290       }
291     }
292   }
293
294   [self debugWithFormat:@"UID ops:\n  store: %@\n  remove: %@",
295                         storeUIDs, removedUIDs];
296
297   /* if time did change, all participants have to re-decide ...
298    * ... exception from that rule: the organizer
299    */
300
301   if (oldApt != nil &&
302       ([props containsObject: @"startDate"] ||
303        [props containsObject: @"endDate"]   ||
304        [props containsObject: @"duration"]))
305   {
306     NSArray  *ps;
307     unsigned i, count;
308     
309     ps    = [newApt attendees];
310     count = [ps count];
311     for (i = 0; i < count; i++) {
312       iCalPerson *p;
313       
314       p = [ps objectAtIndex:i];
315       if (![p hasSameEmailAddress:organizer])
316         [p setParticipationStatus:iCalPersonPartStatNeedsAction];
317     }
318     _iCal = [[newApt parent] versitString];
319     updateForcesReconsider = YES;
320   }
321
322   /* perform storing */
323
324   storeError = [self saveContentString: _iCal inUIDs: storeUIDs];
325   delError = [self deleteInUIDs: removedUIDs];
326
327   // TODO: make compound
328   if (storeError != nil) return storeError;
329   if (delError   != nil) return delError;
330
331   /* email notifications */
332   if ([self sendEMailNotifications])
333     {
334       attendees = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
335       [attendees removePerson: organizer];
336       [self sendEMailUsingTemplateNamed: @"Invitation"
337             forOldObject: nil
338             andNewObject: newApt
339             toAttendees: attendees];
340
341       if (updateForcesReconsider) {
342         attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
343         [attendees removeObjectsInArray:[changes insertedAttendees]];
344         [attendees removePerson:organizer];
345         [self sendEMailUsingTemplateNamed: @"Update"
346               forOldObject: oldApt
347               andNewObject: newApt
348               toAttendees: attendees];
349       }
350
351       attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
352       [attendees removePerson: organizer];
353       if ([attendees count])
354         {
355           iCalEvent *canceledApt;
356     
357           canceledApt = [newApt copy];
358           [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
359           [self sendEMailUsingTemplateNamed: @"Removal"
360                 forOldObject: nil
361                 andNewObject: canceledApt
362                 toAttendees: attendees];
363           [canceledApt release];
364         }
365     }
366
367   return nil;
368 }
369
370 - (NSException *)deleteWithBaseSequence:(int)_v {
371   /* 
372      Note: We need to delete in all participants folders and send iMIP messages
373            for all external accounts.
374            Delete is basically identical to save with all attendees and the
375            organizer being deleted.
376
377      Steps:
378      - fetch stored content
379      - parse old content
380      - check if sequence matches (or if 0=ignore)
381      - extract old attendee list + organizer (make unique)
382      - delete in removed folders
383      - send iMIP mail for all folders not found
384   */
385   iCalEvent *apt;
386   NSArray *removedUIDs;
387   NSMutableArray *attendees;
388
389   /* load existing content */
390
391   apt = (iCalEvent *) [self component: NO];
392   
393   /* compare sequence if requested */
394
395 //   if (_v != 0) {
396 //     // TODO
397 //   }
398   
399   removedUIDs = [self attendeeUIDsFromAppointment:apt];
400
401   if ([self sendEMailNotifications])
402     {
403       /* send notification email to attendees excluding organizer */
404       attendees = [NSMutableArray arrayWithArray:[apt attendees]];
405       [attendees removePerson:[apt organizer]];
406   
407       /* flag appointment as being canceled */
408       [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
409       [apt increaseSequence];
410
411       /* remove all attendees to signal complete removal */
412       [apt removeAllAttendees];
413
414       /* send notification email */
415       [self sendEMailUsingTemplateNamed: @"Deletion"
416             forOldObject: nil
417             andNewObject: apt
418             toAttendees: attendees];
419     }
420
421   /* perform */
422
423   return [self deleteInUIDs:removedUIDs];
424 }
425
426 - (NSException *) saveContentString: (NSString *) _iCalString
427 {
428   return [self saveContentString: _iCalString baseSequence: 0];
429 }
430
431 - (NSException *) changeParticipationStatus: (NSString *) _status
432                                   inContext: (id) _ctx
433 {
434   iCalEvent *apt;
435   iCalPerson *p;
436   NSString *newContent;
437   NSException *ex;
438   NSString *myEMail;
439   
440   ex = nil;
441
442   // TODO: do we need to use SOGoAppointment? (prefer iCalEvent?)
443   apt = (iCalEvent *) [self component: NO];
444
445   if (apt)
446     {
447       myEMail = [[_ctx activeUser] email];
448       p = [apt findParticipantWithEmail: myEMail];
449       if (p)
450         {
451   // TODO: send iMIP reply mails?
452   
453           [p setPartStat:_status];
454           newContent = [[apt parent] versitString];
455           if (newContent)
456             {
457               ex = [self saveContentString:newContent];
458               if (ex)
459                 // TODO: why is the exception wrapped?
460                 /* Server Error */
461                 ex = [NSException exceptionWithHTTPStatus: 500
462                                   reason: [ex reason]];
463             }
464           else
465             ex
466               = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
467                              reason: @"Could not generate iCalendar data ..."];
468         }
469       else
470         ex = [NSException exceptionWithHTTPStatus: 404 /* Not Found */
471                           reason: @"user does not participate in this "
472                           @"appointment"];
473     }
474   else
475     ex = [NSException exceptionWithHTTPStatus:500 /* Server Error */
476                       reason:@"unable to parse appointment record"];
477
478   return ex;
479 }
480
481
482 /* message type */
483
484 - (NSString *) outlookMessageClass
485 {
486   return @"IPM.Appointment";
487 }
488
489 - (NSException *) saveContentString: (NSString *) contentString
490                         baseVersion: (unsigned int) baseVersion
491 {
492   NSString *newContentString, *oldContentString;
493   iCalCalendar *eventCalendar;
494   iCalEvent *event;
495   NSArray *organizers;
496
497   oldContentString = [self contentAsString];
498   if (oldContentString)
499     newContentString = contentString;
500   else
501     {
502       eventCalendar = [iCalCalendar parseSingleFromSource: contentString];
503       event = (iCalEvent *) [eventCalendar firstChildWithTag: [self componentTag]];
504       organizers = [event childrenWithTag: @"organizer"];
505       if ([organizers count])
506         newContentString = contentString;
507       else
508         {
509           [event setOrganizerWithUid: [[self container] ownerInContext: nil]];
510           newContentString = [eventCalendar versitString];
511         }
512     }
513
514   return [super saveContentString: newContentString
515                 baseVersion: baseVersion];
516 }
517
518 @end /* SOGoAppointmentObject */