]> err.no Git - scalable-opengroupware.org/blob - SOGo/SoObjects/Appointments/SOGoAppointmentFolder.m
7b62a57806f1fabae67cf75976d778fbf4084781
[scalable-opengroupware.org] / SOGo / SoObjects / Appointments / SOGoAppointmentFolder.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 #include "SOGoAppointmentFolder.h"
23 #include <SOGo/SOGoCustomGroupFolder.h>
24 #include <SOGo/SOGoAppointment.h>
25 #include <SOGo/AgenorUserManager.h>
26 #include <OGoContentStore/OCSFolder.h>
27 #include <SaxObjC/SaxObjC.h>
28 #include <NGiCal/NGiCal.h>
29 #include <NGExtensions/NGCalendarDateRange.h>
30 #include "common.h"
31 #include <unistd.h>
32 #include <stdlib.h>
33
34 #if APPLE_Foundation_LIBRARY || NeXT_Foundation_LIBRARY
35 @interface NSDate(UsedPrivates)
36 - (id)initWithTimeIntervalSince1970:(NSTimeInterval)_interval;
37 @end
38 #endif
39
40 @implementation SOGoAppointmentFolder
41
42 static BOOL       debugOn = NO;
43 static NSTimeZone *MET    = nil;
44
45 + (void)initialize {
46   static BOOL didInit = NO;
47
48   if (didInit) return;
49   didInit = YES;
50
51   MET = [[NSTimeZone timeZoneWithAbbreviation:@"MET"] retain];
52 }
53
54 + (NSString *)globallyUniqueObjectId {
55   /*
56     4C08AE1A-A808-11D8-AC5A-000393BBAFF6
57     SOGo-Web-28273-18283-288182
58     printf( "%x", *(int *) &f);
59   */
60   static int   pid = 0;
61   static int   sequence = 0;
62   static float rndm = 0;
63   float f;
64
65   if (pid == 0) { /* break if we fork ;-) */
66       pid = getpid();
67       rndm = random();
68   }
69   sequence++;
70   f = [[NSDate date] timeIntervalSince1970];
71   return [NSString stringWithFormat:@"%0X-%0X-%0X-%0X",
72                    pid, *(int *)&f, sequence++, random];
73 }
74
75 - (void)dealloc {
76   [super dealloc];
77 }
78
79 /* selection */
80
81 - (NSArray *)calendarUIDs {
82   /* this is used for group calendars (this folder just returns itself) */
83   NSString *s;
84   
85   s = [[self container] nameInContainer];
86   return [s isNotNull] ? [NSArray arrayWithObjects:&s count:1] : nil;
87 }
88
89 /* name lookup */
90
91 - (BOOL)isValidAppointmentName:(NSString *)_key {
92   if ([_key length] == 0)
93     return NO;
94   
95   return YES;
96 }
97
98 - (id)appointmentWithName:(NSString *)_key inContext:(id)_ctx {
99   static Class aptClass = Nil;
100   id apt;
101   
102   if (aptClass == Nil)
103     aptClass = NSClassFromString(@"SOGoAppointmentObject");
104   if (aptClass == Nil) {
105     [self errorWithFormat:@"missing SOGoAppointmentObject class!"];
106     return nil;
107   }
108   
109   apt = [[aptClass alloc] initWithName:_key inContainer:self];
110   return [apt autorelease];
111 }
112
113 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
114   id obj;
115   
116   /* first check attributes directly bound to the application */
117   if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]))
118     return obj;
119   
120   if ([self isValidAppointmentName:_key])
121     return [self appointmentWithName:_key inContext:_ctx];
122   
123   /* return 404 to stop acquisition */
124   return [NSException exceptionWithHTTPStatus:404 /* Not Found */];
125 }
126
127 /* timezone */
128
129 - (NSTimeZone *)viewTimeZone {
130   // TODO: should use a cookie for configuration? we default to MET
131   return MET;
132 }
133
134 /* fetching */
135
136 - (NSMutableDictionary *)fixupRecord:(NSDictionary *)_record
137   fetchRange:(NGCalendarDateRange *)_r
138 {
139   NSMutableDictionary *md;
140   id tmp;
141   
142   md = [[_record mutableCopy] autorelease];
143  
144   if ((tmp = [_record objectForKey:@"startdate"])) {
145     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
146           (NSTimeInterval)[tmp unsignedIntValue]];
147     [tmp setTimeZone:[self viewTimeZone]];
148     if (tmp) [md setObject:tmp forKey:@"startDate"];
149     [tmp release];
150   }
151   else
152     [self logWithFormat:@"missing 'startdate' in record?"];
153
154   if ((tmp = [_record objectForKey:@"enddate"])) {
155     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
156           (NSTimeInterval)[tmp unsignedIntValue]];
157     [tmp setTimeZone:[self viewTimeZone]];
158     if (tmp) [md setObject:tmp forKey:@"endDate"];
159     [tmp release];
160   }
161   else
162     [self logWithFormat:@"missing 'enddate' in record?"];
163
164   return md;
165 }
166
167 - (NSMutableDictionary *)fixupCycleRecord:(NSDictionary *)_record
168   cycleRange:(NGCalendarDateRange *)_r
169 {
170   NSMutableDictionary *md;
171   id tmp;
172   
173   md = [[_record mutableCopy] autorelease];
174   
175   /* cycle is in _r */
176   tmp = [_r startDate];
177   [tmp setTimeZone:[self viewTimeZone]];
178   [md setObject:tmp forKey:@"startDate"];
179   tmp = [_r endDate];
180   [tmp setTimeZone:[self viewTimeZone]];
181   [md setObject:tmp forKey:@"endDate"];
182   
183   return md;
184 }
185
186 - (void)_flattenCycleRecord:(NSDictionary *)_row
187   forRange:(NGCalendarDateRange *)_r
188   intoArray:(NSMutableArray *)_ma
189 {
190   NSMutableDictionary *row;
191   NSDictionary        *cycleinfo;
192   NSCalendarDate      *startDate, *endDate;
193   NGCalendarDateRange *fir;
194   NSArray             *rules, *exRules, *exDates, *ranges;
195   unsigned            i, count;
196
197   cycleinfo  = [[_row objectForKey:@"cycleinfo"] propertyList];
198   if (cycleinfo == nil) {
199     [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row];
200     return;
201   }
202
203   row = [self fixupRecord:_row fetchRange:_r];
204   [row removeObjectForKey:@"cycleinfo"];
205
206   startDate = [row objectForKey:@"startDate"];
207   endDate   = [row objectForKey:@"endDate"];
208   fir       = [NGCalendarDateRange calendarDateRangeWithStartDate:startDate
209                                    endDate:endDate];
210   rules     = [cycleinfo objectForKey:@"rules"];
211   exRules   = [cycleinfo objectForKey:@"exRules"];
212   exDates   = [cycleinfo objectForKey:@"exDates"];
213
214   ranges = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange:_r
215                                      firstInstanceCalendarDateRange:fir
216                                      recurrenceRules:rules
217                                      exceptionRules:exRules
218                                      exceptionDates:exDates];
219   count = [ranges count];
220   for (i = 0; i < count; i++) {
221     NGCalendarDateRange *rRange;
222     id fixedRow;
223     
224     rRange   = [ranges objectAtIndex:i];
225     fixedRow = [self fixupCycleRecord:row cycleRange:rRange];
226     if (fixedRow != nil) [_ma addObject:fixedRow];
227   }
228 }
229
230 - (NSArray *)fixupRecords:(NSArray *)_records
231   fetchRange:(NGCalendarDateRange *)_r
232 {
233   // TODO: is the result supposed to be sorted by date?
234   NSMutableArray *ma;
235   unsigned i, count;
236
237   if (_records == nil) return nil;
238   if ((count = [_records count]) == 0)
239     return _records;
240   
241   ma = [NSMutableArray arrayWithCapacity:count];
242   for (i = 0; i < count; i++) {
243     id row; // TODO: what is the type of the record?
244     
245     row = [_records objectAtIndex:i];
246     row = [self fixupRecord:row fetchRange:_r];
247     if (row != nil) [ma addObject:row];
248   }
249   return ma;
250 }
251
252 - (NSArray *)fixupCyclicRecords:(NSArray *)_records
253   fetchRange:(NGCalendarDateRange *)_r
254 {
255   // TODO: is the result supposed to be sorted by date?
256   NSMutableArray *ma;
257   unsigned i, count;
258   
259   if (_records == nil) return nil;
260   if ((count = [_records count]) == 0)
261     return _records;
262   
263   ma = [NSMutableArray arrayWithCapacity:count];
264   for (i = 0; i < count; i++) {
265     id row; // TODO: what is the type of the record?
266     
267     row = [_records objectAtIndex:i];
268     [self _flattenCycleRecord:row forRange:_r intoArray:ma];
269   }
270   return ma;
271 }
272
273 - (NSArray *)fetchFields:(NSArray *)_fields
274   fromFolder:(OCSFolder *)_folder
275   from:(NSCalendarDate *)_startDate
276   to:(NSCalendarDate *)_endDate 
277 {
278   EOQualifier         *qualifier;
279   NSMutableArray      *fields, *ma = nil;
280   NSArray             *records;
281   NSString            *sql;
282   NGCalendarDateRange *r;
283
284   if (_folder == nil) {
285     [self errorWithFormat:@"(%s): missing folder for fetch!",
286             __PRETTY_FUNCTION__];
287     return nil;
288   }
289   
290   r = [NGCalendarDateRange calendarDateRangeWithStartDate:_startDate
291                            endDate:_endDate];
292
293   /* prepare mandatory fields */
294
295   fields    = [NSMutableArray arrayWithArray:_fields];
296   [fields addObject:@"uid"];
297   [fields addObject:@"startdate"];
298   [fields addObject:@"enddate"];
299   
300   if (debugOn)
301     [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate];
302   
303   sql = [NSString stringWithFormat:@"(startdate < %d) AND (enddate > %d)"
304                                    @" AND (iscycle = 0)",
305                   (unsigned int)[_endDate   timeIntervalSince1970],
306                   (unsigned int)[_startDate timeIntervalSince1970]];
307
308   /* fetch non-recurrent apts first */
309   qualifier = [EOQualifier qualifierWithQualifierFormat:sql];
310
311   records   = [_folder fetchFields:fields matchingQualifier:qualifier];
312   if (records != nil) {
313     records = [self fixupRecords:records fetchRange:r];
314     if (debugOn)
315       [self logWithFormat:@"fetched %i records: %@", [records count], records];
316     ma = [NSMutableArray arrayWithArray:records];
317   }
318
319   /* fetch recurrent apts now */
320   sql = [NSString stringWithFormat:@"(startdate < %d) AND (cycleenddate > %d)"
321                                    @" AND (iscycle = 1)",
322                   (unsigned int)[_endDate   timeIntervalSince1970],
323                   (unsigned int)[_startDate timeIntervalSince1970]];
324   qualifier = [EOQualifier qualifierWithQualifierFormat:sql];
325
326   [fields addObject:@"cycleinfo"];
327
328   records = [_folder fetchFields:fields matchingQualifier:qualifier];
329   if (records != nil) {
330     if (debugOn)
331       [self logWithFormat:@"fetched %i cyclic records: %@",
332         [records count], records];
333     records = [self fixupCyclicRecords:records fetchRange:r];
334     if (!ma) ma = [NSMutableArray arrayWithCapacity:[records count]];
335     [ma addObjectsFromArray:records];
336   }
337   else if (ma == nil) {
338     [self errorWithFormat:@"(%s): fetch failed!", __PRETTY_FUNCTION__];
339     return nil;
340   }
341
342   return ma;
343 }
344
345 /* override this in subclasses */
346 - (NSArray *)fetchFields:(NSArray *)_fields
347   from:(NSCalendarDate *)_startDate
348   to:(NSCalendarDate *)_endDate 
349 {
350   OCSFolder *folder;
351   
352   if ((folder = [self ocsFolder]) == nil) {
353     [self errorWithFormat:@"(%s): missing folder for fetch!",
354       __PRETTY_FUNCTION__];
355     return nil;
356   }
357   return [self fetchFields:_fields fromFolder:folder
358                from:_startDate to:_endDate];
359 }
360
361
362 - (NSArray *)fetchFreebusyInfosFrom:(NSCalendarDate *)_startDate
363   to:(NSCalendarDate *)_endDate
364 {
365   static NSArray *infos = nil; // TODO: move to a plist file
366   if (infos == nil) {
367     infos = [[NSArray alloc] init];
368   }
369   return [self fetchFields:infos from:_startDate to:_endDate];
370 }
371
372
373 - (NSArray *)fetchOverviewInfosFrom:(NSCalendarDate *)_startDate
374   to:(NSCalendarDate *)_endDate
375 {
376   static NSArray *infos = nil; // TODO: move to a plist file
377   if (infos == nil) {
378     infos = [[NSArray alloc] initWithObjects:
379                                @"title", 
380                                @"location", @"orgmail", @"status", @"ispublic", 
381                                @"isallday", nil];
382   }
383   return [self fetchFields:infos
384                from:_startDate
385                to:_endDate];
386 }
387
388 - (NSArray *)fetchCoreInfosFrom:(NSCalendarDate *)_startDate
389   to:(NSCalendarDate *)_endDate 
390 {
391   static NSArray *infos = nil; // TODO: move to a plist file
392   if (infos == nil) {
393     infos = [[NSArray alloc] initWithObjects:
394                                @"title", @"location", @"orgmail",
395                                @"status", @"ispublic",
396                                @"isallday", @"isopaque",
397                                @"participants", @"partmails",
398                                @"partstates", @"sequence", nil];
399   }
400   return [self fetchFields:infos
401                from:_startDate
402                to:_endDate];
403 }
404
405 /* URL generation */
406
407 - (NSString *)baseURLForAptWithUID:(NSString *)_uid inContext:(id)_ctx {
408   NSString *url;
409   
410   if ([_uid length] == 0)
411     return nil;
412   
413   url = [self baseURLInContext:_ctx];
414   if (![url hasSuffix:@"/"])
415     url = [url stringByAppendingString:@"/"];
416   return [url stringByAppendingString:_uid];
417 }
418
419 /* folder management */
420
421 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
422   // TODO: DUP to SOGoGroupFolder
423   NSException *error = nil;
424   NSArray     *path;
425   id          ctx, result;
426
427   if (![_uid isNotNull])
428     return nil;
429   
430   if (_ctx == nil) _ctx = [[WOApplication application] context];
431   
432   /* create subcontext, so that we don't destroy our environment */
433   
434   if ((ctx = [_ctx createSubContext]) == nil) {
435     [self errorWithFormat:@"could not create SOPE subcontext!"];
436     return nil;
437   }
438   
439   /* build path */
440   
441   path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil;
442   
443   /* traverse path */
444   
445   result = [[ctx application] traversePathArray:path inContext:ctx
446                               error:&error acquire:NO];
447   if (error != nil) {
448     [self errorWithFormat:@"folder lookup failed (uid=%@): %@",
449             _uid, error];
450     return nil;
451   }
452   
453   [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@",
454           _uid, [path componentsJoinedByString:@"=>"], result];
455   return result;
456 }
457
458 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
459   /* Note: can return NSNull objects in the array! */
460   NSMutableArray *folders;
461   NSEnumerator *e;
462   NSString     *uid;
463   
464   if ([_uids count] == 0) return nil;
465   folders = [NSMutableArray arrayWithCapacity:16];
466   e = [_uids objectEnumerator];
467   while ((uid = [e nextObject])) {
468     id folder;
469     
470     folder = [self lookupHomeFolderForUID:uid inContext:nil];
471     if ([folder isNotNull]) {
472       folder = [folder lookupName:@"Calendar" inContext:nil acquire:NO];
473       if ([folder isKindOfClass:[NSException class]])
474         folder = nil;
475     }
476     if (![folder isNotNull])
477       [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid];
478     
479     /* Note: intentionally add 'null' folders to allow a mapping */
480     [folders addObject:folder ? folder : [NSNull null]];
481   }
482   return folders;
483 }
484
485 - (NSArray *)uidsFromICalPersons:(NSArray *)_persons {
486   /* Note: can return NSNull objects in the array! */
487   NSMutableArray    *uids;
488   AgenorUserManager *um;
489   unsigned          i, count;
490   
491   if (_persons == nil)
492     return nil;
493
494   count = [_persons count];
495   uids  = [NSMutableArray arrayWithCapacity:count + 1];
496   um    = [AgenorUserManager sharedUserManager];
497   
498   for (i = 0; i < count; i++) {
499     iCalPerson *person;
500     NSString   *email;
501     NSString   *uid;
502     
503     person = [_persons objectAtIndex:i];
504     email  = [person rfc822Email];
505     if ([email isNotNull]) {
506       uid = [um getUIDForEmail:email];
507     }
508     else
509       uid = nil;
510     
511     [uids addObject:(uid != nil ? uid : (id)[NSNull null])];
512   }
513   return uids;
514 }
515
516 - (NSArray *)lookupCalendarFoldersForICalPerson:(NSArray *)_persons
517   inContext:(id)_ctx
518 {
519   /* Note: can return NSNull objects in the array! */
520   NSArray *uids;
521
522   if ((uids = [self uidsFromICalPersons:_persons]) == nil)
523     return nil;
524   
525   return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
526 }
527
528 - (id)lookupGroupFolderForUIDs:(NSArray *)_uids inContext:(id)_ctx {
529   SOGoCustomGroupFolder *folder;
530   
531   if (_uids == nil)
532     return nil;
533
534   folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self];
535   return [folder autorelease];
536 }
537 - (id)lookupGroupCalendarFolderForUIDs:(NSArray *)_uids inContext:(id)_ctx {
538   SOGoCustomGroupFolder *folder;
539   
540   if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil)
541     return nil;
542   
543   folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO];
544   if (![folder isNotNull])
545     return nil;
546   if ([folder isKindOfClass:[NSException class]]) {
547     [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@",
548             folder];
549     return nil;
550   }
551   
552   return folder;
553 }
554
555 /* bulk fetches */
556
557 - (NSArray *)fetchAllSOGoAppointments {
558   /* 
559      Note: very expensive method, do not use unless absolutely required.
560            returns an array of SOGoAppointment objects.
561            
562      Note that we can leave out the filenames, supposed to be stored
563      in the 'uid' field of the iCalendar object!
564   */
565   NSMutableArray *events;
566   NSDictionary *files;
567   NSEnumerator *contents;
568   NSString     *content;
569   
570   /* fetch all raw contents */
571   
572   files = [self fetchContentStringsAndNamesOfAllObjects];
573   if (![files isNotNull]) return nil;
574   if ([files isKindOfClass:[NSException class]]) return (id)files;
575   
576   /* transform to SOGo appointments */
577   
578   events   = [NSMutableArray arrayWithCapacity:[files count]];
579   contents = [files objectEnumerator];
580   while ((content = [contents nextObject]) != nil) {
581     SOGoAppointment *event;
582     
583     event = [[SOGoAppointment alloc] initWithICalString:content];
584     if (![event isNotNull]) {
585       [self errorWithFormat:@"(%s): could not parse an iCal file!",
586               __PRETTY_FUNCTION__];
587       continue;
588     }
589
590     [events addObject:event];
591     [event release];
592   }
593   
594   return events;
595 }
596
597 /* GET */
598
599 - (id)GETAction:(WOContext *)_ctx {
600   // TODO: I guess this should really be done by SOPE (redirect to
601   //       default method)
602   WOResponse *r;
603   NSString *uri;
604
605   uri = [[_ctx request] uri];
606   if (![uri hasSuffix:@"/"]) uri = [uri stringByAppendingString:@"/"];
607   uri = [uri stringByAppendingString:@"schedule"];
608
609   r = [_ctx response];
610   [r setStatus:302 /* moved */];
611   [r setHeader:uri forKey:@"location"];
612   return r;
613 }
614
615 /* folder type */
616
617 - (NSString *)outlookFolderClass {
618   return @"IPF.Appointment";
619 }
620
621 @end /* SOGoAppointmentFolder */