2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
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
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.
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
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>
34 #if APPLE_Foundation_LIBRARY || NeXT_Foundation_LIBRARY
35 @interface NSDate(UsedPrivates)
36 - (id)initWithTimeIntervalSince1970:(NSTimeInterval)_interval;
40 @implementation SOGoAppointmentFolder
42 static BOOL debugOn = NO;
43 static NSTimeZone *MET = nil;
46 static BOOL didInit = NO;
51 MET = [[NSTimeZone timeZoneWithAbbreviation:@"MET"] retain];
54 + (NSString *)globallyUniqueObjectId {
56 4C08AE1A-A808-11D8-AC5A-000393BBAFF6
57 SOGo-Web-28273-18283-288182
58 printf( "%x", *(int *) &f);
61 static int sequence = 0;
62 static float rndm = 0;
65 if (pid == 0) { /* break if we fork ;-) */
70 f = [[NSDate date] timeIntervalSince1970];
71 return [NSString stringWithFormat:@"%0X-%0X-%0X-%0X",
72 pid, *(int *)&f, sequence++, random];
81 - (NSArray *)calendarUIDs {
82 /* this is used for group calendars (this folder just returns itself) */
85 s = [[self container] nameInContainer];
86 return [s isNotNull] ? [NSArray arrayWithObjects:&s count:1] : nil;
91 - (BOOL)isValidAppointmentName:(NSString *)_key {
92 if ([_key length] == 0)
98 - (id)appointmentWithName:(NSString *)_key inContext:(id)_ctx {
99 static Class aptClass = Nil;
103 aptClass = NSClassFromString(@"SOGoAppointmentObject");
104 if (aptClass == Nil) {
105 [self errorWithFormat:@"missing SOGoAppointmentObject class!"];
109 apt = [[aptClass alloc] initWithName:_key inContainer:self];
110 return [apt autorelease];
113 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
116 /* first check attributes directly bound to the application */
117 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]))
120 if ([self isValidAppointmentName:_key])
121 return [self appointmentWithName:_key inContext:_ctx];
123 /* return 404 to stop acquisition */
124 return [NSException exceptionWithHTTPStatus:404 /* Not Found */];
129 - (NSTimeZone *)viewTimeZone {
130 // TODO: should use a cookie for configuration? we default to MET
136 - (NSMutableDictionary *)fixupRecord:(NSDictionary *)_record
137 fetchRange:(NGCalendarDateRange *)_r
139 NSMutableDictionary *md;
142 md = [[_record mutableCopy] autorelease];
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"];
152 [self logWithFormat:@"missing 'startdate' in record?"];
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"];
162 [self logWithFormat:@"missing 'enddate' in record?"];
167 - (NSMutableDictionary *)fixupCycleRecord:(NSDictionary *)_record
168 cycleRange:(NGCalendarDateRange *)_r
170 NSMutableDictionary *md;
173 md = [[_record mutableCopy] autorelease];
176 tmp = [_r startDate];
177 [tmp setTimeZone:[self viewTimeZone]];
178 [md setObject:tmp forKey:@"startDate"];
180 [tmp setTimeZone:[self viewTimeZone]];
181 [md setObject:tmp forKey:@"endDate"];
186 - (void)_flattenCycleRecord:(NSDictionary *)_row
187 forRange:(NGCalendarDateRange *)_r
188 intoArray:(NSMutableArray *)_ma
190 NSMutableDictionary *row;
191 NSDictionary *cycleinfo;
192 NSCalendarDate *startDate, *endDate;
193 NGCalendarDateRange *fir;
194 NSArray *rules, *exRules, *exDates, *ranges;
197 cycleinfo = [[_row objectForKey:@"cycleinfo"] propertyList];
198 if (cycleinfo == nil) {
199 [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row];
203 row = [self fixupRecord:_row fetchRange:_r];
204 [row removeObjectForKey:@"cycleinfo"];
206 startDate = [row objectForKey:@"startDate"];
207 endDate = [row objectForKey:@"endDate"];
208 fir = [NGCalendarDateRange calendarDateRangeWithStartDate:startDate
210 rules = [cycleinfo objectForKey:@"rules"];
211 exRules = [cycleinfo objectForKey:@"exRules"];
212 exDates = [cycleinfo objectForKey:@"exDates"];
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;
224 rRange = [ranges objectAtIndex:i];
225 fixedRow = [self fixupCycleRecord:row cycleRange:rRange];
226 if (fixedRow != nil) [_ma addObject:fixedRow];
230 - (NSArray *)fixupRecords:(NSArray *)_records
231 fetchRange:(NGCalendarDateRange *)_r
233 // TODO: is the result supposed to be sorted by date?
237 if (_records == nil) return nil;
238 if ((count = [_records count]) == 0)
241 ma = [NSMutableArray arrayWithCapacity:count];
242 for (i = 0; i < count; i++) {
243 id row; // TODO: what is the type of the record?
245 row = [_records objectAtIndex:i];
246 row = [self fixupRecord:row fetchRange:_r];
247 if (row != nil) [ma addObject:row];
252 - (NSArray *)fixupCyclicRecords:(NSArray *)_records
253 fetchRange:(NGCalendarDateRange *)_r
255 // TODO: is the result supposed to be sorted by date?
259 if (_records == nil) return nil;
260 if ((count = [_records count]) == 0)
263 ma = [NSMutableArray arrayWithCapacity:count];
264 for (i = 0; i < count; i++) {
265 id row; // TODO: what is the type of the record?
267 row = [_records objectAtIndex:i];
268 [self _flattenCycleRecord:row forRange:_r intoArray:ma];
273 - (NSArray *)fetchFields:(NSArray *)_fields
274 fromFolder:(OCSFolder *)_folder
275 from:(NSCalendarDate *)_startDate
276 to:(NSCalendarDate *)_endDate
278 EOQualifier *qualifier;
279 NSMutableArray *fields, *ma = nil;
282 NGCalendarDateRange *r;
284 if (_folder == nil) {
285 [self errorWithFormat:@"(%s): missing folder for fetch!",
286 __PRETTY_FUNCTION__];
290 r = [NGCalendarDateRange calendarDateRangeWithStartDate:_startDate
293 /* prepare mandatory fields */
295 fields = [NSMutableArray arrayWithArray:_fields];
296 [fields addObject:@"uid"];
297 [fields addObject:@"startdate"];
298 [fields addObject:@"enddate"];
301 [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate];
303 sql = [NSString stringWithFormat:@"(startdate < %d) AND (enddate > %d)"
304 @" AND (iscycle = 0)",
305 (unsigned int)[_endDate timeIntervalSince1970],
306 (unsigned int)[_startDate timeIntervalSince1970]];
308 /* fetch non-recurrent apts first */
309 qualifier = [EOQualifier qualifierWithQualifierFormat:sql];
311 records = [_folder fetchFields:fields matchingQualifier:qualifier];
312 if (records != nil) {
313 records = [self fixupRecords:records fetchRange:r];
315 [self logWithFormat:@"fetched %i records: %@", [records count], records];
316 ma = [NSMutableArray arrayWithArray:records];
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];
326 [fields addObject:@"cycleinfo"];
328 records = [_folder fetchFields:fields matchingQualifier:qualifier];
329 if (records != nil) {
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];
337 else if (ma == nil) {
338 [self errorWithFormat:@"(%s): fetch failed!", __PRETTY_FUNCTION__];
345 /* override this in subclasses */
346 - (NSArray *)fetchFields:(NSArray *)_fields
347 from:(NSCalendarDate *)_startDate
348 to:(NSCalendarDate *)_endDate
352 if ((folder = [self ocsFolder]) == nil) {
353 [self errorWithFormat:@"(%s): missing folder for fetch!",
354 __PRETTY_FUNCTION__];
357 return [self fetchFields:_fields fromFolder:folder
358 from:_startDate to:_endDate];
362 - (NSArray *)fetchFreebusyInfosFrom:(NSCalendarDate *)_startDate
363 to:(NSCalendarDate *)_endDate
365 static NSArray *infos = nil; // TODO: move to a plist file
367 infos = [[NSArray alloc] init];
369 return [self fetchFields:infos from:_startDate to:_endDate];
373 - (NSArray *)fetchOverviewInfosFrom:(NSCalendarDate *)_startDate
374 to:(NSCalendarDate *)_endDate
376 static NSArray *infos = nil; // TODO: move to a plist file
378 infos = [[NSArray alloc] initWithObjects:
380 @"location", @"orgmail", @"status", @"ispublic",
383 return [self fetchFields:infos
388 - (NSArray *)fetchCoreInfosFrom:(NSCalendarDate *)_startDate
389 to:(NSCalendarDate *)_endDate
391 static NSArray *infos = nil; // TODO: move to a plist file
393 infos = [[NSArray alloc] initWithObjects:
394 @"title", @"location", @"orgmail",
395 @"status", @"ispublic",
396 @"isallday", @"isopaque",
397 @"participants", @"partmails",
398 @"partstates", @"sequence", nil];
400 return [self fetchFields:infos
407 - (NSString *)baseURLForAptWithUID:(NSString *)_uid inContext:(id)_ctx {
410 if ([_uid length] == 0)
413 url = [self baseURLInContext:_ctx];
414 if (![url hasSuffix:@"/"])
415 url = [url stringByAppendingString:@"/"];
416 return [url stringByAppendingString:_uid];
419 /* folder management */
421 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
422 // TODO: DUP to SOGoGroupFolder
423 NSException *error = nil;
427 if (![_uid isNotNull])
430 if (_ctx == nil) _ctx = [[WOApplication application] context];
432 /* create subcontext, so that we don't destroy our environment */
434 if ((ctx = [_ctx createSubContext]) == nil) {
435 [self errorWithFormat:@"could not create SOPE subcontext!"];
441 path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil;
445 result = [[ctx application] traversePathArray:path inContext:ctx
446 error:&error acquire:NO];
448 [self errorWithFormat:@"folder lookup failed (uid=%@): %@",
453 [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@",
454 _uid, [path componentsJoinedByString:@"=>"], result];
458 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
459 /* Note: can return NSNull objects in the array! */
460 NSMutableArray *folders;
464 if ([_uids count] == 0) return nil;
465 folders = [NSMutableArray arrayWithCapacity:16];
466 e = [_uids objectEnumerator];
467 while ((uid = [e nextObject])) {
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]])
476 if (![folder isNotNull])
477 [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid];
479 /* Note: intentionally add 'null' folders to allow a mapping */
480 [folders addObject:folder ? folder : [NSNull null]];
485 - (NSArray *)uidsFromICalPersons:(NSArray *)_persons {
486 /* Note: can return NSNull objects in the array! */
487 NSMutableArray *uids;
488 AgenorUserManager *um;
494 count = [_persons count];
495 uids = [NSMutableArray arrayWithCapacity:count + 1];
496 um = [AgenorUserManager sharedUserManager];
498 for (i = 0; i < count; i++) {
503 person = [_persons objectAtIndex:i];
504 email = [person rfc822Email];
505 if ([email isNotNull]) {
506 uid = [um getUIDForEmail:email];
511 [uids addObject:(uid != nil ? uid : (id)[NSNull null])];
516 - (NSArray *)lookupCalendarFoldersForICalPerson:(NSArray *)_persons
519 /* Note: can return NSNull objects in the array! */
522 if ((uids = [self uidsFromICalPersons:_persons]) == nil)
525 return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
528 - (id)lookupGroupFolderForUIDs:(NSArray *)_uids inContext:(id)_ctx {
529 SOGoCustomGroupFolder *folder;
534 folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self];
535 return [folder autorelease];
537 - (id)lookupGroupCalendarFolderForUIDs:(NSArray *)_uids inContext:(id)_ctx {
538 SOGoCustomGroupFolder *folder;
540 if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil)
543 folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO];
544 if (![folder isNotNull])
546 if ([folder isKindOfClass:[NSException class]]) {
547 [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@",
557 - (NSArray *)fetchAllSOGoAppointments {
559 Note: very expensive method, do not use unless absolutely required.
560 returns an array of SOGoAppointment objects.
562 Note that we can leave out the filenames, supposed to be stored
563 in the 'uid' field of the iCalendar object!
565 NSMutableArray *events;
567 NSEnumerator *contents;
570 /* fetch all raw contents */
572 files = [self fetchContentStringsAndNamesOfAllObjects];
573 if (![files isNotNull]) return nil;
574 if ([files isKindOfClass:[NSException class]]) return (id)files;
576 /* transform to SOGo appointments */
578 events = [NSMutableArray arrayWithCapacity:[files count]];
579 contents = [files objectEnumerator];
580 while ((content = [contents nextObject]) != nil) {
581 SOGoAppointment *event;
583 event = [[SOGoAppointment alloc] initWithICalString:content];
584 if (![event isNotNull]) {
585 [self errorWithFormat:@"(%s): could not parse an iCal file!",
586 __PRETTY_FUNCTION__];
590 [events addObject:event];
599 - (id)GETAction:(WOContext *)_ctx {
600 // TODO: I guess this should really be done by SOPE (redirect to
605 uri = [[_ctx request] uri];
606 if (![uri hasSuffix:@"/"]) uri = [uri stringByAppendingString:@"/"];
607 uri = [uri stringByAppendingString:@"schedule"];
610 [r setStatus:302 /* moved */];
611 [r setHeader:uri forKey:@"location"];
617 - (NSString *)outlookFolderClass {
618 return @"IPF.Appointment";
621 @end /* SOGoAppointmentFolder */