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