]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentFolder.m
9d4ceb742e5cbd9a5d27dec15e6a7b5be7f2331c
[scalable-opengroupware.org] / 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 #import <GDLContentStore/GCSFolder.h>
23 #import <SaxObjC/SaxObjC.h>
24 #import <NGCards/NGCards.h>
25 #import <NGObjWeb/SoObject+SoDAV.h>
26 #import <NGObjWeb/WOContext.h>
27 #import <NGObjWeb/WOMessage.h>
28 #import <NGExtensions/NGCalendarDateRange.h>
29
30 // #import <NGObjWeb/SoClassSecurityInfo.h>
31 #import <SOGo/SOGoCustomGroupFolder.h>
32 #import <SOGo/AgenorUserManager.h>
33 #import <SOGo/SOGoPermissions.h>
34 #import <SOGo/NSString+Utilities.h>
35
36 #import "common.h"
37
38 #import "SOGoAppointmentObject.h"
39 #import "SOGoTaskObject.h"
40
41 #import "SOGoAppointmentFolder.h"
42
43 #if APPLE_Foundation_LIBRARY || NeXT_Foundation_LIBRARY
44 @interface NSDate(UsedPrivates)
45 - (id)initWithTimeIntervalSince1970:(NSTimeInterval)_interval;
46 @end
47 #endif
48
49 @implementation SOGoAppointmentFolder
50
51 static NGLogger   *logger    = nil;
52 static NSNumber   *sharedYes = nil;
53
54 + (int) version
55 {
56   return [super version] + 1 /* v1 */;
57 }
58
59 + (void) initialize
60 {
61   NGLoggerManager *lm;
62   static BOOL     didInit = NO;
63 //   SoClassSecurityInfo *securityInfo;
64
65   if (didInit) return;
66   didInit = YES;
67   
68   NSAssert2([super version] == 0,
69             @"invalid superclass (%@) version %i !",
70             NSStringFromClass([self superclass]), [super version]);
71
72   lm      = [NGLoggerManager defaultLoggerManager];
73   logger  = [lm loggerForDefaultKey:@"SOGoAppointmentFolderDebugEnabled"];
74
75 //   securityInfo = [self soClassSecurityInfo];
76 //   [securityInfo declareRole: SOGoRole_Delegate
77 //                 asDefaultForPermission: SoPerm_AddDocumentsImagesAndFiles];
78 //   [securityInfo declareRole: SOGoRole_Delegate
79 //                 asDefaultForPermission: SoPerm_ChangeImagesAndFiles];
80 //   [securityInfo declareRoles: [NSArray arrayWithObjects:
81 //                                          SOGoRole_Delegate,
82 //                                        SOGoRole_Assistant, nil]
83 //                 asDefaultForPermission: SoPerm_View];
84
85   sharedYes = [[NSNumber numberWithBool:YES] retain];
86 }
87
88 - (void) dealloc
89 {
90   [self->uidToFilename release];
91   [super dealloc];
92 }
93
94 /* logging */
95
96 - (id) debugLogger
97 {
98   return logger;
99 }
100
101 /* selection */
102
103 - (NSArray *) calendarUIDs 
104 {
105   /* this is used for group calendars (this folder just returns itself) */
106   NSString *s;
107   
108   s = [[self container] nameInContainer];
109   return [s isNotNull] ? [NSArray arrayWithObjects:&s count:1] : nil;
110 }
111
112 /* name lookup */
113
114 - (BOOL) isValidAppointmentName: (NSString *)_key
115 {
116   return ([_key length] != 0);
117 }
118
119 - (id) lookupActionForCalDAVMethod: (NSString *)_key
120 {
121   SoSelectorInvocation *invocation;
122   NSString *name;
123
124   name = [NSString stringWithFormat: @"%@:", [_key davMethodToObjC]];
125
126   invocation = [[SoSelectorInvocation alloc]
127                  initWithSelectorNamed: name
128                  addContextParameter: YES];
129   [invocation autorelease];
130
131   return invocation;
132 }
133
134 - (void) appendObject: (NSDictionary *) object
135           withBaseURL: (NSString *) baseURL
136      toREPORTResponse: (WOResponse *) r
137 {
138   SOGoContentObject *ocsObject;
139   NSString *c_name, *etagLine, *calString;
140
141   c_name = [object objectForKey: @"c_name"];
142
143   ocsObject = [SOGoContentObject objectWithName: c_name
144                                  inContainer: self];
145
146   [r appendContentString: @"  <D:response>\r\n"];
147   [r appendContentString: @"    <D:href>"];
148   [r appendContentString: baseURL];
149   if (![baseURL hasSuffix: @"/"])
150     [r appendContentString: @"/"];
151   [r appendContentString: c_name];
152   [r appendContentString: @"</D:href>\r\n"];
153
154   [r appendContentString: @"    <D:propstat>\r\n"];
155   [r appendContentString: @"      <D:prop>\r\n"];
156   etagLine = [NSString stringWithFormat: @"        <D:getetag>%@</D:getetag>\r\n",
157                        [ocsObject davEntityTag]];
158   [r appendContentString: etagLine];
159   [r appendContentString: @"      </D:prop>\r\n"];
160   [r appendContentString: @"      <D:status>HTTP/1.1 200 OK</D:status>\r\n"];
161   [r appendContentString: @"    </D:propstat>\r\n"];
162   [r appendContentString: @"    <C:calendar-data>"];
163   calString = [[ocsObject contentAsString] stringByEscapingXMLString];
164   [r appendContentString: calString];
165   [r appendContentString: @"</C:calendar-data>\r\n"];
166   [r appendContentString: @"  </D:response>\r\n"];
167 }
168
169 - (void) _appendTimeRange: (id <DOMElement>) timeRangeElement
170                  toFilter: (NSMutableDictionary *) filter
171 {
172   NSCalendarDate *parsedDate;
173
174   parsedDate = [[timeRangeElement attribute: @"start"] asCalendarDate];
175   [filter setObject: parsedDate forKey: @"start"];
176   parsedDate = [[timeRangeElement attribute: @"end"] asCalendarDate];
177   [filter setObject: parsedDate forKey: @"end"];
178 }
179
180 - (NSDictionary *) _parseCalendarFilter: (id <DOMElement>) filterElement
181 {
182   NSMutableDictionary *filterData;
183   id <DOMNode> parentNode;
184   id <DOMNodeList> ranges;
185   NSString *componentName;
186
187   parentNode = [filterElement parentNode];
188   if ([[parentNode tagName] isEqualToString: @"comp-filter"]
189       && [[parentNode attribute: @"name"] isEqualToString: @"VCALENDAR"])
190     {
191       componentName = [[filterElement attribute: @"name"] lowercaseString];
192       filterData = [NSMutableDictionary new];
193       [filterData autorelease];
194       [filterData setObject: componentName forKey: @"name"];
195       ranges = [filterElement getElementsByTagName: @"time-range"];
196       if ([ranges count])
197         [self _appendTimeRange: [ranges objectAtIndex: 0]
198               toFilter: filterData];
199     }
200   else
201     filterData = nil;
202
203   return filterData;
204 }
205
206 - (NSArray *) _parseCalendarFilters: (id <DOMElement>) parentNode
207 {
208   NSEnumerator *children;
209   id<DOMElement> node;
210   NSMutableArray *filters;
211   NSDictionary *filter;
212
213   filters = [NSMutableArray new];
214
215   children = [[parentNode getElementsByTagName: @"comp-filter"] objectEnumerator];
216   node = [children nextObject];
217   while (node)
218     {
219       filter = [self _parseCalendarFilter: node];
220       if (filter)
221         [filters addObject: filter];
222       node = [children nextObject];
223     }
224
225   return filters;
226 }
227
228 - (void) _appendComponentsMatchingFilters: (NSArray *) filters
229                                toResponse: (WOResponse *) response
230                                 inContext: (WOContext *) context
231 {
232   NSArray *apts;
233   unsigned int count, max;
234   NSDictionary *currentFilter, *appointment;
235   NSEnumerator *appointments;
236   NSString *baseURL;
237
238   baseURL = [self baseURLInContext: context];
239
240   max = [filters count];
241   for (count = 0; count < max; count++)
242     {
243       currentFilter = [filters objectAtIndex: 0];
244       apts = [self fetchCoreInfosFrom: [currentFilter objectForKey: @"start"]
245                    to: [currentFilter objectForKey: @"end"]
246                    component: [currentFilter objectForKey: @"name"]];
247       appointments = [apts objectEnumerator];
248       appointment = [appointments nextObject];
249       while (appointment)
250         {
251           [self appendObject: appointment
252                 withBaseURL: baseURL
253                 toREPORTResponse: response];
254           appointment = [appointments nextObject];
255         }
256     }
257 }
258
259 - (id) davCalendarQuery: (id) context
260 {
261   WOResponse *r;
262   NSArray *filters;
263   id <DOMDocument> document;
264
265   r = [context response];
266   [r setStatus: 207];
267   [r setContentEncoding: NSUTF8StringEncoding];
268   [r setHeader: @"text/xml; charset=\"utf-8\"" forKey: @"content-type"];
269   [r setHeader: @"no-cache" forKey: @"pragma"];
270   [r setHeader: @"no-cache" forKey: @"cache-control"];
271   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"];
272   [r appendContentString: @"<D:multistatus xmlns:D=\"DAV:\""
273      @" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\r\n"];
274
275   document = [[context request] contentAsDOMDocument];
276   filters = [self _parseCalendarFilters: [document documentElement]];
277   [self _appendComponentsMatchingFilters: filters
278         toResponse: r
279         inContext: context];
280   [r appendContentString:@"</D:multistatus>\r\n"];
281
282   return r;
283 }
284
285 - (Class) objectClassForContent: (NSString *) content
286 {
287   iCalCalendar *calendar;
288   NSArray *elements;
289   NSString *firstTag;
290   Class objectClass;
291
292   objectClass = Nil;
293
294   calendar = [iCalCalendar parseSingleFromSource: content];
295   if (calendar)
296     {
297       elements = [calendar allObjects];
298       if ([elements count])
299         {
300           firstTag = [[[elements objectAtIndex: 0] tag] uppercaseString];
301           if ([firstTag isEqualToString: @"VEVENT"])
302             objectClass = [SOGoAppointmentObject class];
303           else if ([firstTag isEqualToString: @"VTODO"])
304             objectClass = [SOGoTaskObject class];
305         }
306     }
307
308   return objectClass;
309 }
310
311 - (id) deduceObjectForName: (NSString *)_key
312                  inContext: (id)_ctx
313 {
314   WORequest *request;
315   NSString *method;
316   Class objectClass;
317   id obj;
318
319   request = [_ctx request];
320   method = [request method];
321   if ([method isEqualToString: @"PUT"])
322     objectClass = [self objectClassForContent: [request contentAsString]];
323   else
324     objectClass = [self objectClassForResourceNamed: _key];
325
326   if (objectClass)
327     obj = [objectClass objectWithName: _key inContainer: self];
328   else
329     obj = nil;
330
331   return obj;
332 }
333
334 - (BOOL) requestNamedIsHandledLater: (NSString *) name
335                           inContext: (WOContext *) context
336 {
337   return [name isEqualToString: @"OPTIONS"];
338 }
339
340 - (id) lookupName: (NSString *)_key
341         inContext: (id)_ctx
342           acquire: (BOOL)_flag
343 {
344   id obj;
345   NSString *url;
346   BOOL handledLater;
347
348   /* first check attributes directly bound to the application */
349   handledLater = [self requestNamedIsHandledLater: _key inContext: _ctx];
350   if (handledLater)
351     obj = nil;
352   else
353     {
354       obj = [super lookupName:_key inContext:_ctx acquire:NO];
355       if (!obj)
356         {
357           if ([_key hasPrefix: @"{urn:ietf:params:xml:ns:caldav}"])
358             obj
359               = [self lookupActionForCalDAVMethod: [_key substringFromIndex: 31]];
360           else if ([self isValidAppointmentName:_key])
361             {
362               url = [[[_ctx request] uri] urlWithoutParameters];
363               if ([url hasSuffix: @"AsTask"])
364                 obj = [SOGoTaskObject objectWithName: _key
365                                       inContainer: self];
366               else if ([url hasSuffix: @"AsAppointment"])
367                 obj = [SOGoAppointmentObject objectWithName: _key
368                                              inContainer: self];
369               else
370                 obj = [self deduceObjectForName: _key
371                             inContext: _ctx];
372             }
373         }
374       if (!obj)
375         obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
376     }
377
378   return obj;
379 }
380
381 - (NSArray *) davComplianceClassesInContext: (id)_ctx
382 {
383   NSMutableArray *classes;
384   NSArray *primaryClasses;
385
386   classes = [NSMutableArray new];
387   [classes autorelease];
388
389   primaryClasses = [super davComplianceClassesInContext: _ctx];
390   if (primaryClasses)
391     [classes addObjectsFromArray: primaryClasses];
392   [classes addObject: @"access-control"];
393   [classes addObject: @"calendar-access"];
394
395   return classes;
396 }
397
398 - (NSString *) groupDavResourceType
399 {
400   return @"vevent-collection";
401 }
402
403 /* vevent UID handling */
404
405 - (NSString *) resourceNameForEventUID: (NSString *)_u
406                               inFolder: (GCSFolder *)_f
407 {
408   static NSArray *nameFields = nil;
409   EOQualifier *qualifier;
410   NSArray     *records;
411   
412   if (![_u isNotNull]) return nil;
413   if (_f == nil) {
414     [self errorWithFormat:@"(%s): missing folder for fetch!",
415             __PRETTY_FUNCTION__];
416     return nil;
417   }
418   
419   if (nameFields == nil)
420     nameFields = [[NSArray alloc] initWithObjects:@"c_name", nil];
421   
422   qualifier = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _u];
423   records   = [_f fetchFields: nameFields matchingQualifier: qualifier];
424   
425   if ([records count] == 1)
426     return [[records objectAtIndex:0] valueForKey:@"c_name"];
427   if ([records count] == 0)
428     return nil;
429   
430   [self errorWithFormat:
431           @"The storage contains more than file with the same UID!"];
432   return [[records objectAtIndex:0] valueForKey:@"c_name"];
433 }
434
435 - (NSString *) resourceNameForEventUID: (NSString *) _uid
436 {
437   /* caches UIDs */
438   GCSFolder *folder;
439   NSString  *rname;
440   
441   if (![_uid isNotNull])
442     return nil;
443   if ((rname = [self->uidToFilename objectForKey:_uid]) != nil)
444     return [rname isNotNull] ? rname : nil;
445   
446   if ((folder = [self ocsFolder]) == nil) {
447     [self errorWithFormat:@"(%s): missing folder for fetch!",
448       __PRETTY_FUNCTION__];
449     return nil;
450   }
451
452   if (self->uidToFilename == nil)
453     self->uidToFilename = [[NSMutableDictionary alloc] initWithCapacity:16];
454   
455   if ((rname = [self resourceNameForEventUID:_uid inFolder:folder]) == nil)
456     [self->uidToFilename setObject:[NSNull null] forKey:_uid];
457   else
458     [self->uidToFilename setObject:rname forKey:_uid];
459   
460   return rname;
461 }
462
463 - (Class) objectClassForResourceNamed: (NSString *) c_name
464 {
465   EOQualifier *qualifier;
466   NSArray *records;
467   NSString *component;
468   Class objectClass;
469
470   qualifier = [EOQualifier qualifierWithQualifierFormat:@"c_name = %@", c_name];
471   records = [[self ocsFolder] fetchFields: [NSArray arrayWithObject: @"component"]
472                               matchingQualifier: qualifier];
473
474   if ([records count])
475     {
476       component = [[records objectAtIndex:0] valueForKey: @"component"];
477       if ([component isEqualToString: @"vevent"])
478         objectClass = [SOGoAppointmentObject class];
479       else if ([component isEqualToString: @"vtodo"])
480         objectClass = [SOGoTaskObject class];
481       else
482         objectClass = Nil;
483     }
484   else
485     objectClass = Nil;
486   
487   return objectClass;
488 }
489
490 /* fetching */
491
492 - (NSMutableDictionary *) fixupRecord: (NSDictionary *) _record
493                            fetchRange: (NGCalendarDateRange *) _r
494 {
495   NSMutableDictionary *md;
496   id tmp;
497   
498   md = [[_record mutableCopy] autorelease];
499  
500   if ((tmp = [_record objectForKey:@"startdate"])) {
501     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
502           (NSTimeInterval)[tmp unsignedIntValue]];
503     [tmp setTimeZone: [self userTimeZone]];
504     if (tmp) [md setObject:tmp forKey:@"startDate"];
505     [tmp release];
506   }
507   else
508     [self logWithFormat:@"missing 'startdate' in record?"];
509
510   if ((tmp = [_record objectForKey:@"enddate"])) {
511     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
512           (NSTimeInterval)[tmp unsignedIntValue]];
513     [tmp setTimeZone: [self userTimeZone]];
514     if (tmp) [md setObject:tmp forKey:@"endDate"];
515     [tmp release];
516   }
517   else
518     [self logWithFormat:@"missing 'enddate' in record?"];
519
520   return md;
521 }
522
523 - (NSMutableDictionary *) fixupCycleRecord: (NSDictionary *) _record
524                                 cycleRange: (NGCalendarDateRange *) _r
525 {
526   NSMutableDictionary *md;
527   id tmp;
528   
529   md = [[_record mutableCopy] autorelease];
530   
531   /* cycle is in _r */
532   tmp = [_r startDate];
533   [tmp setTimeZone:[self userTimeZone]];
534   [md setObject:tmp forKey:@"startDate"];
535   tmp = [_r endDate];
536   [tmp setTimeZone:[self userTimeZone]];
537   [md setObject:tmp forKey:@"endDate"];
538   
539   return md;
540 }
541
542 - (void) _flattenCycleRecord: (NSDictionary *) _row
543                     forRange: (NGCalendarDateRange *) _r
544                    intoArray: (NSMutableArray *) _ma
545 {
546   NSMutableDictionary *row;
547   NSDictionary        *cycleinfo;
548   NSCalendarDate      *startDate, *endDate;
549   NGCalendarDateRange *fir;
550   NSArray             *rules, *exRules, *exDates, *ranges;
551   unsigned            i, count;
552
553   cycleinfo  = [[_row objectForKey:@"cycleinfo"] propertyList];
554   if (cycleinfo == nil) {
555     [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row];
556     return;
557   }
558
559   row = [self fixupRecord:_row fetchRange: _r];
560   [row removeObjectForKey:@"cycleinfo"];
561   [row setObject:sharedYes forKey:@"isRecurrentEvent"];
562
563   startDate = [row objectForKey:@"startDate"];
564   endDate   = [row objectForKey:@"endDate"];
565   fir       = [NGCalendarDateRange calendarDateRangeWithStartDate:startDate
566                                    endDate:endDate];
567   rules     = [cycleinfo objectForKey:@"rules"];
568   exRules   = [cycleinfo objectForKey:@"exRules"];
569   exDates   = [cycleinfo objectForKey:@"exDates"];
570
571   ranges = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange:_r
572                                      firstInstanceCalendarDateRange:fir
573                                      recurrenceRules:rules
574                                      exceptionRules:exRules
575                                      exceptionDates:exDates];
576   count = [ranges count];
577   for (i = 0; i < count; i++) {
578     NGCalendarDateRange *rRange;
579     id fixedRow;
580     
581     rRange   = [ranges objectAtIndex:i];
582     fixedRow = [self fixupCycleRecord:row cycleRange:rRange];
583     if (fixedRow != nil) [_ma addObject:fixedRow];
584   }
585 }
586
587 - (NSArray *) fixupRecords: (NSArray *) _records
588                 fetchRange: (NGCalendarDateRange *) _r
589 {
590   // TODO: is the result supposed to be sorted by date?
591   NSMutableArray *ma;
592   unsigned i, count;
593
594   if (_records == nil) return nil;
595   if ((count = [_records count]) == 0)
596     return _records;
597   
598   ma = [NSMutableArray arrayWithCapacity:count];
599   for (i = 0; i < count; i++) {
600     id row; // TODO: what is the type of the record?
601     
602     row = [_records objectAtIndex:i];
603     row = [self fixupRecord:row fetchRange:_r];
604     if (row != nil) [ma addObject:row];
605   }
606   return ma;
607 }
608
609 - (NSArray *) fixupCyclicRecords: (NSArray *) _records
610                       fetchRange: (NGCalendarDateRange *) _r
611 {
612   // TODO: is the result supposed to be sorted by date?
613   NSMutableArray *ma;
614   unsigned i, count;
615   
616   if (_records == nil) return nil;
617   if ((count = [_records count]) == 0)
618     return _records;
619   
620   ma = [NSMutableArray arrayWithCapacity:count];
621   for (i = 0; i < count; i++) {
622     id row; // TODO: what is the type of the record?
623     
624     row = [_records objectAtIndex:i];
625     [self _flattenCycleRecord:row forRange:_r intoArray:ma];
626   }
627   return ma;
628 }
629
630 - (NSString *) _sqlStringForComponent: (id) _component
631 {
632   NSString *sqlString;
633   NSArray *components;
634
635   if (_component)
636     {
637       if ([_component isKindOfClass: [NSArray class]])
638         components = _component;
639       else
640         components = [NSArray arrayWithObject: _component];
641
642       sqlString
643         = [NSString stringWithFormat: @" AND (component = '%@')",
644                     [components componentsJoinedByString: @"' OR component = '"]];
645     }
646   else
647     sqlString = @"";
648
649   return sqlString;
650 }
651
652 - (NSString *) _sqlStringRangeFrom: (NSCalendarDate *) _startDate
653                                 to: (NSCalendarDate *) _endDate
654 {
655   unsigned int start, end;
656
657   start = (unsigned int) [_startDate timeIntervalSince1970];
658   end = (unsigned int) [_endDate timeIntervalSince1970];
659
660   return [NSString stringWithFormat:
661                      @" AND (startdate <= %d) AND (enddate >= %d)",
662                    end, start];
663 }
664
665 - (NSArray *) fetchFields: (NSArray *) _fields
666                fromFolder: (GCSFolder *) _folder
667                      from: (NSCalendarDate *) _startDate
668                        to: (NSCalendarDate *) _endDate 
669                 component: (id) _component
670 {
671   EOQualifier *qualifier;
672   NSMutableArray *fields, *ma = nil;
673   NSArray *records;
674   NSString *sql, *dateSqlString, *componentSqlString; /* , *owner; */
675   NGCalendarDateRange *r;
676
677   if (_folder == nil) {
678     [self errorWithFormat:@"(%s): missing folder for fetch!",
679             __PRETTY_FUNCTION__];
680     return nil;
681   }
682   
683   if (_startDate && _endDate)
684     {
685       r = [NGCalendarDateRange calendarDateRangeWithStartDate: _startDate
686                                endDate: _endDate];
687       dateSqlString = [self _sqlStringRangeFrom: _startDate to: _endDate];
688     }
689   else
690     {
691       r = nil;
692       dateSqlString = @"";
693     }
694
695   componentSqlString = [self _sqlStringForComponent: _component];
696
697   /* prepare mandatory fields */
698
699   fields = [NSMutableArray arrayWithArray: _fields];
700   [fields addObject: @"uid"];
701   [fields addObject: @"startdate"];
702   [fields addObject: @"enddate"];
703
704   if (logger)
705     [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate];
706
707   sql = [NSString stringWithFormat: @"(iscycle = 0)%@%@",
708                   dateSqlString, componentSqlString];
709
710   /* fetch non-recurrent apts first */
711   qualifier = [EOQualifier qualifierWithQualifierFormat: sql];
712
713   records = [_folder fetchFields: fields matchingQualifier: qualifier];
714   if (records)
715     {
716       if (r)
717         records = [self fixupRecords: records fetchRange: r];
718       if (logger)
719         [self debugWithFormat: @"fetched %i records: %@",
720               [records count], records];
721       ma = [NSMutableArray arrayWithArray: records];
722     }
723
724   /* fetch recurrent apts now */
725   sql = [NSString stringWithFormat: @"(iscycle = 1)%@%@",
726                   dateSqlString, componentSqlString];
727   qualifier = [EOQualifier qualifierWithQualifierFormat: sql];
728
729   [fields addObject: @"cycleinfo"];
730
731   records = [_folder fetchFields: fields matchingQualifier: qualifier];
732   if (records)
733     {
734       if (logger)
735         [self debugWithFormat: @"fetched %i cyclic records: %@",
736               [records count], records];
737       if (r)
738         records = [self fixupCyclicRecords: records fetchRange: r];
739       if (!ma)
740         ma = [NSMutableArray arrayWithCapacity: [records count]];
741
742 //       owner = [self ownerInContext: nil];
743       [ma addObjectsFromArray: records];
744     }
745   else if (!ma)
746     {
747       [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__];
748       return nil;
749     }
750
751   /* NOTE: why do we sort here?
752      This probably belongs to UI but cannot be achieved as fast there as
753      we can do it here because we're operating on a mutable array -
754      having the apts sorted is never a bad idea, though
755   */
756   [ma sortUsingSelector: @selector (compareAptsAscending:)];
757   if (logger)
758     [self debugWithFormat:@"returning %i records", [ma count]];
759
760 //   [ma makeObjectsPerform: @selector (setObject:forKey:)
761 //       withObject: owner
762 //       withObject: @"owner"];
763
764   return ma;
765 }
766
767 /* override this in subclasses */
768 - (NSArray *) fetchFields: (NSArray *) _fields
769                      from: (NSCalendarDate *) _startDate
770                        to: (NSCalendarDate *) _endDate 
771                 component: (id) _component
772 {
773   GCSFolder *folder;
774   
775   if ((folder = [self ocsFolder]) == nil) {
776     [self errorWithFormat:@"(%s): missing folder for fetch!",
777       __PRETTY_FUNCTION__];
778     return nil;
779   }
780
781   return [self fetchFields: _fields fromFolder: folder
782                from: _startDate to: _endDate
783                component: _component];
784 }
785
786
787 - (NSArray *) fetchFreeBusyInfosFrom: (NSCalendarDate *) _startDate
788                                   to: (NSCalendarDate *) _endDate
789 {
790   static NSArray *infos = nil; // TODO: move to a plist file
791   
792   if (!infos)
793     infos = [[NSArray alloc] initWithObjects: @"partmails", @"partstates",
794                              @"isopaque", @"status", nil];
795
796   return [self fetchFields: infos from: _startDate to: _endDate
797                component: @"vevent"];
798 }
799
800 - (NSArray *) fetchCoreInfosFrom: (NSCalendarDate *) _startDate
801                               to: (NSCalendarDate *) _endDate
802                        component: (id) _component
803 {
804   static NSArray *infos = nil; // TODO: move to a plist file
805
806   if (!infos)
807     infos = [[NSArray alloc] initWithObjects:
808                                @"c_name", @"component",
809                              @"title", @"location", @"orgmail",
810                              @"status", @"ispublic",
811                              @"isallday", @"isopaque",
812                              @"participants", @"partmails",
813                              @"partstates", @"sequence", @"priority", nil];
814
815   return [self fetchFields: infos from: _startDate to: _endDate component: _component];
816 }
817
818 - (void) deleteEntriesWithIds: (NSArray *) ids
819 {
820   Class objectClass;
821   unsigned int count, max;
822   NSString *currentId, *currentUser;
823   WOContext *context;
824   id deleteObject;
825
826   context = [[WOApplication application] context];
827   currentUser = [[context activeUser] login];
828
829   max = [ids count];
830   for (count = 0; count < max; count++)
831     {
832       currentId = [ids objectAtIndex: count];
833       objectClass
834         = [self objectClassForResourceNamed: currentId];
835       deleteObject = [objectClass objectWithName: currentId
836                                   inContainer: self];
837       if ([currentUser isEqualToString: [deleteObject ownerInContext: nil]])
838         {
839           [deleteObject delete];
840           [deleteObject primaryDelete];
841         }
842     }
843 }
844
845 /* URL generation */
846
847 - (NSString *) baseURLForAptWithUID: (NSString *)_uid
848                           inContext: (id)_ctx
849 {
850   // TODO: who calls this?
851   NSString *url;
852   
853   if ([_uid length] == 0)
854     return nil;
855   
856   url = [self baseURLInContext:_ctx];
857   if (![url hasSuffix:@"/"])
858     url = [url stringByAppendingString:@"/"];
859   
860   // TODO: this should run a query to determine the uid!
861   return [url stringByAppendingString:_uid];
862 }
863
864 /* folder management */
865
866 - (id) lookupHomeFolderForUID: (NSString *) _uid
867                     inContext: (id)_ctx
868 {
869   // TODO: DUP to SOGoGroupFolder
870   NSException *error = nil;
871   NSArray     *path;
872   id          ctx, result;
873
874   if (![_uid isNotNull])
875     return nil;
876   
877   if (_ctx == nil) _ctx = [[WOApplication application] context];
878   
879   /* create subcontext, so that we don't destroy our environment */
880   
881   if ((ctx = [_ctx createSubContext]) == nil) {
882     [self errorWithFormat:@"could not create SOPE subcontext!"];
883     return nil;
884   }
885   
886   /* build path */
887   
888   path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil;
889   
890   /* traverse path */
891   
892   result = [[ctx application] traversePathArray:path inContext:ctx
893                               error:&error acquire:NO];
894   if (error != nil) {
895     [self errorWithFormat:@"folder lookup failed (uid=%@): %@",
896             _uid, error];
897     return nil;
898   }
899   
900   [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@",
901           _uid, [path componentsJoinedByString:@"=>"], result];
902   return result;
903 }
904
905 - (SOGoAppointmentFolder *) lookupCalendarFolderForUID: (NSString *) uid
906 {
907   SOGoFolder *upperContainer;
908   SOGoUserFolder *userFolder;
909   SOGoAppointmentFolder *calendarFolder;
910
911   upperContainer = [[self container] container];
912   userFolder = [SOGoUserFolder objectWithName: uid
913                                inContainer: upperContainer];
914   calendarFolder = [SOGoAppointmentFolder objectWithName: @"Calendar"
915                                           inContainer: userFolder];
916   [calendarFolder
917     setOCSPath: [NSString stringWithFormat: @"/Users/%@/Calendar", uid]];
918   [calendarFolder setOwner: uid];
919
920   return calendarFolder;
921 }
922
923 - (NSArray *) lookupCalendarFoldersForUIDs: (NSArray *) _uids
924                                  inContext: (id)_ctx
925 {
926   /* Note: can return NSNull objects in the array! */
927   NSMutableArray *folders;
928   NSEnumerator *e;
929   NSString     *uid;
930   
931   if ([_uids count] == 0) return nil;
932   folders = [NSMutableArray arrayWithCapacity:16];
933   e = [_uids objectEnumerator];
934   while ((uid = [e nextObject])) {
935     id folder;
936     
937     folder = [self lookupCalendarFolderForUID: uid];
938     if (![folder isNotNull])
939       [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid];
940     
941     /* Note: intentionally add 'null' folders to allow a mapping */
942     [folders addObject:folder ? folder : [NSNull null]];
943   }
944   return folders;
945 }
946
947 - (NSArray *) lookupFreeBusyObjectsForUIDs: (NSArray *) _uids
948                                  inContext: (id) _ctx
949 {
950   /* Note: can return NSNull objects in the array! */
951   NSMutableArray *objs;
952   NSEnumerator   *e;
953   NSString       *uid;
954   
955   if ([_uids count] == 0) return nil;
956   objs = [NSMutableArray arrayWithCapacity:16];
957   e    = [_uids objectEnumerator];
958   while ((uid = [e nextObject])) {
959     id obj;
960     
961     obj = [self lookupHomeFolderForUID:uid inContext:nil];
962     if ([obj isNotNull]) {
963       obj = [obj lookupName:@"freebusy.ifb" inContext:nil acquire:NO];
964       if ([obj isKindOfClass:[NSException class]])
965         obj = nil;
966     }
967     if (![obj isNotNull])
968       [self logWithFormat:@"Note: did not find freebusy.ifb for uid: '%@'", uid];
969     
970     /* Note: intentionally add 'null' folders to allow a mapping */
971     [objs addObject:obj ? obj : [NSNull null]];
972   }
973   return objs;
974 }
975
976 - (NSArray *) uidsFromICalPersons: (NSArray *) _persons
977 {
978   /* Note: can return NSNull objects in the array! */
979   NSMutableArray    *uids;
980   AgenorUserManager *um;
981   unsigned          i, count;
982   
983   if (_persons == nil)
984     return nil;
985
986   count = [_persons count];
987   uids  = [NSMutableArray arrayWithCapacity:count + 1];
988   um    = [AgenorUserManager sharedUserManager];
989   
990   for (i = 0; i < count; i++) {
991     iCalPerson *person;
992     NSString   *email;
993     NSString   *uid;
994     
995     person = [_persons objectAtIndex:i];
996     email  = [person rfc822Email];
997     if ([email isNotNull]) {
998       uid = [um getUIDForEmail:email];
999     }
1000     else
1001       uid = nil;
1002     
1003     [uids addObject:(uid != nil ? uid : (id)[NSNull null])];
1004   }
1005   return uids;
1006 }
1007
1008 - (NSArray *)lookupCalendarFoldersForICalPerson: (NSArray *) _persons
1009                                       inContext: (id) _ctx
1010 {
1011   /* Note: can return NSNull objects in the array! */
1012   NSArray *uids;
1013
1014   if ((uids = [self uidsFromICalPersons:_persons]) == nil)
1015     return nil;
1016   
1017   return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
1018 }
1019
1020 - (id) lookupGroupFolderForUIDs: (NSArray *) _uids
1021                       inContext: (id)_ctx
1022 {
1023   SOGoCustomGroupFolder *folder;
1024   
1025   if (_uids == nil)
1026     return nil;
1027
1028   folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self];
1029   return [folder autorelease];
1030 }
1031
1032 - (id) lookupGroupCalendarFolderForUIDs: (NSArray *) _uids
1033                               inContext: (id) _ctx
1034 {
1035   SOGoCustomGroupFolder *folder;
1036   
1037   if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil)
1038     return nil;
1039   
1040   folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO];
1041   if (![folder isNotNull])
1042     return nil;
1043   if ([folder isKindOfClass:[NSException class]]) {
1044     [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@",
1045             folder];
1046     return nil;
1047   }
1048   
1049   return folder;
1050 }
1051
1052 /* bulk fetches */
1053
1054 - (NSArray *) fetchAllSOGoAppointments
1055 {
1056   /* 
1057      Note: very expensive method, do not use unless absolutely required.
1058            returns an array of SOGoAppointment objects.
1059            
1060      Note that we can leave out the filenames, supposed to be stored
1061      in the 'uid' field of the iCalendar object!
1062   */
1063   NSMutableArray *events;
1064   NSDictionary *files;
1065   NSEnumerator *contents;
1066   NSString     *content;
1067   
1068   /* fetch all raw contents */
1069   
1070   files = [self fetchContentStringsAndNamesOfAllObjects];
1071   if (![files isNotNull]) return nil;
1072   if ([files isKindOfClass:[NSException class]]) return (id)files;
1073   
1074   /* transform to SOGo appointments */
1075   
1076   events   = [NSMutableArray arrayWithCapacity:[files count]];
1077   contents = [files objectEnumerator];
1078   while ((content = [contents nextObject]) != nil)
1079     [events addObject: [iCalCalendar parseSingleFromSource: content]];
1080   
1081   return events;
1082 }
1083
1084 /* folder type */
1085
1086 - (NSString *) folderType
1087 {
1088   return @"Appointment";
1089 }
1090
1091 - (NSString *) outlookFolderClass
1092 {
1093   return @"IPF.Appointment";
1094 }
1095
1096 @end /* SOGoAppointmentFolder */