]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentFolder.m
Install libs to /usr/lib
[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 <Foundation/NSCalendarDate.h>
23 #import <Foundation/NSEnumerator.h>
24 #import <Foundation/NSUserDefaults.h>
25 #import <Foundation/NSValue.h>
26
27 #import <NGObjWeb/NSException+HTTP.h>
28 #import <NGObjWeb/SoObject+SoDAV.h>
29 #import <NGObjWeb/SoSecurityManager.h>
30 #import <NGObjWeb/WOContext+SoObjects.h>
31 #import <NGObjWeb/WOMessage.h>
32 #import <NGObjWeb/WORequest.h>
33 #import <NGObjWeb/WOResponse.h>
34 #import <NGExtensions/NGLoggerManager.h>
35 #import <NGExtensions/NSString+misc.h>
36 #import <GDLContentStore/GCSFolder.h>
37 #import <DOM/DOMProtocols.h>
38 #import <EOControl/EOQualifier.h>
39 #import <NGCards/iCalCalendar.h>
40 #import <NGCards/iCalDateTime.h>
41 #import <NGCards/iCalPerson.h>
42 #import <NGCards/iCalRecurrenceCalculator.h>
43 #import <NGCards/NSString+NGCards.h>
44 #import <NGExtensions/NGCalendarDateRange.h>
45 #import <NGExtensions/NSNull+misc.h>
46 #import <NGExtensions/NSObject+Logs.h>
47 #import <SaxObjC/SaxObjC.h>
48 #import <SaxObjC/XMLNamespaces.h>
49
50 // #import <NGObjWeb/SoClassSecurityInfo.h>
51 #import <SOGo/SOGoCache.h>
52 #import <SOGo/SOGoCustomGroupFolder.h>
53 #import <SOGo/LDAPUserManager.h>
54 #import <SOGo/SOGoPermissions.h>
55 #import <SOGo/NSArray+Utilities.h>
56 #import <SOGo/NSString+Utilities.h>
57 #import <SOGo/SOGoUser.h>
58
59 #import "SOGoAppointmentObject.h"
60 #import "SOGoAppointmentFolders.h"
61 #import "SOGoTaskObject.h"
62
63 #import "SOGoAppointmentFolder.h"
64
65 #if APPLE_Foundation_LIBRARY || NeXT_Foundation_LIBRARY
66 @interface NSDate(UsedPrivates)
67 - (id)initWithTimeIntervalSince1970:(NSTimeInterval)_interval;
68 @end
69 #endif
70
71 @implementation SOGoAppointmentFolder
72
73 static NGLogger   *logger    = nil;
74 static NSNumber   *sharedYes = nil;
75
76 + (int) version
77 {
78   return [super version] + 1 /* v1 */;
79 }
80
81 + (void) initialize
82 {
83   NGLoggerManager *lm;
84   static BOOL     didInit = NO;
85 //   SoClassSecurityInfo *securityInfo;
86
87   if (didInit) return;
88   didInit = YES;
89   
90   NSAssert2([super version] == 0,
91             @"invalid superclass (%@) version %i !",
92             NSStringFromClass([self superclass]), [super version]);
93
94   lm      = [NGLoggerManager defaultLoggerManager];
95   logger  = [lm loggerForDefaultKey: @"SOGoAppointmentFolderDebugEnabled"];
96
97 //   securityInfo = [self soClassSecurityInfo];
98 //   [securityInfo declareRole: SOGoRole_Delegate
99 //                 asDefaultForPermission: SoPerm_AddDocumentsImagesAndFiles];
100 //   [securityInfo declareRole: SOGoRole_Delegate
101 //                 asDefaultForPermission: SoPerm_ChangeImagesAndFiles];
102 //   [securityInfo declareRoles: [NSArray arrayWithObjects:
103 //                                          SOGoRole_Delegate,
104 //                                        SOGoRole_Assistant, nil]
105 //                 asDefaultForPermission: SoPerm_View];
106
107   sharedYes = [[NSNumber numberWithBool: YES] retain];
108 }
109
110 - (id) initWithName: (NSString *) name
111         inContainer: (id) newContainer
112 {
113   if ((self = [super initWithName: name inContainer: newContainer]))
114     {
115       timeZone = [[context activeUser] timeZone];
116       aclMatrix = [NSMutableDictionary new];
117       stripFields = nil;
118       uidToFilename = nil;
119     }
120
121   return self;
122 }
123
124 - (void) dealloc
125 {
126   [aclMatrix release];
127   [stripFields release];
128   [uidToFilename release];
129   [super dealloc];
130 }
131
132 /* logging */
133
134 - (id) debugLogger
135 {
136   return logger;
137 }
138
139 /* selection */
140
141 - (NSArray *) calendarUIDs 
142 {
143   /* this is used for group calendars (this folder just returns itself) */
144   NSString *s;
145   
146   s = [[self container] nameInContainer];
147 //   [self logWithFormat:@"CAL UID: %@", s];
148   return [s isNotNull] ? [NSArray arrayWithObjects:&s count:1] : nil;
149 }
150
151 /* name lookup */
152
153 - (BOOL) isValidAppointmentName: (NSString *)_key
154 {
155   return ([_key length] != 0);
156 }
157
158 - (void) appendObject: (NSDictionary *) object
159           withBaseURL: (NSString *) baseURL
160      toREPORTResponse: (WOResponse *) r
161 {
162   SOGoCalendarComponent *component;
163   Class componentClass;
164   NSString *name, *etagLine, *calString;
165
166   name = [object objectForKey: @"c_name"];
167
168   if ([[object objectForKey: @"c_component"] isEqualToString: @"vevent"])
169     componentClass = [SOGoAppointmentObject class];
170   else
171     componentClass = [SOGoTaskObject class];
172
173   component = [componentClass objectWithName: name inContainer: self];
174
175   [r appendContentString: @"  <D:response>\r\n"];
176   [r appendContentString: @"    <D:href>"];
177   [r appendContentString: baseURL];
178   if (![baseURL hasSuffix: @"/"])
179     [r appendContentString: @"/"];
180   [r appendContentString: name];
181   [r appendContentString: @"</D:href>\r\n"];
182
183   [r appendContentString: @"    <D:propstat>\r\n"];
184   [r appendContentString: @"      <D:prop>\r\n"];
185   etagLine = [NSString stringWithFormat: @"        <D:getetag>%@</D:getetag>\r\n",
186                        [component davEntityTag]];
187   [r appendContentString: etagLine];
188   [r appendContentString: @"      </D:prop>\r\n"];
189   [r appendContentString: @"      <D:status>HTTP/1.1 200 OK</D:status>\r\n"];
190   [r appendContentString: @"    </D:propstat>\r\n"];
191   [r appendContentString: @"    <C:calendar-data>"];
192   calString = [[component contentAsString] stringByEscapingXMLString];
193   [r appendContentString: calString];
194   [r appendContentString: @"</C:calendar-data>\r\n"];
195   [r appendContentString: @"  </D:response>\r\n"];
196 }
197
198 - (void) _appendTimeRange: (id <DOMElement>) timeRangeElement
199                  toFilter: (NSMutableDictionary *) filter
200 {
201   NSCalendarDate *parsedDate;
202
203   parsedDate = [[timeRangeElement attribute: @"start"] asCalendarDate];
204   [filter setObject: parsedDate forKey: @"start"];
205   parsedDate = [[timeRangeElement attribute: @"end"] asCalendarDate];
206   [filter setObject: parsedDate forKey: @"end"];
207 }
208
209 - (NSDictionary *) _parseCalendarFilter: (id <DOMElement>) filterElement
210 {
211   NSMutableDictionary *filterData;
212   id <DOMNode> parentNode;
213   id <DOMNodeList> ranges;
214   NSString *componentName;
215
216   parentNode = [filterElement parentNode];
217   if ([[parentNode tagName] isEqualToString: @"comp-filter"]
218       && [[parentNode attribute: @"name"] isEqualToString: @"VCALENDAR"])
219     {
220       componentName = [[filterElement attribute: @"name"] lowercaseString];
221       filterData = [NSMutableDictionary new];
222       [filterData autorelease];
223       [filterData setObject: componentName forKey: @"name"];
224       ranges = [filterElement getElementsByTagName: @"time-range"];
225       if ([ranges count])
226         [self _appendTimeRange: [ranges objectAtIndex: 0]
227               toFilter: filterData];
228     }
229   else
230     filterData = nil;
231
232   return filterData;
233 }
234
235 - (NSArray *) _parseCalendarFilters: (id <DOMElement>) parentNode
236 {
237   NSEnumerator *children;
238   id<DOMElement> node;
239   NSMutableArray *filters;
240   NSDictionary *filter;
241
242   filters = [NSMutableArray array];
243   children = [[parentNode getElementsByTagName: @"comp-filter"]
244                objectEnumerator];
245   node = [children nextObject];
246   while (node)
247     {
248       filter = [self _parseCalendarFilter: node];
249       if (filter)
250         [filters addObject: filter];
251       node = [children nextObject];
252     }
253
254   return filters;
255 }
256
257 - (void) _appendComponentsMatchingFilters: (NSArray *) filters
258                                toResponse: (WOResponse *) response
259 {
260   NSArray *apts;
261   unsigned int count, max;
262   NSDictionary *currentFilter, *appointment;
263   NSEnumerator *appointments;
264   NSString *baseURL;
265
266   baseURL = [self baseURLInContext: context];
267
268   max = [filters count];
269   for (count = 0; count < max; count++)
270     {
271       currentFilter = [filters objectAtIndex: count];
272       apts = [self fetchCoreInfosFrom: [currentFilter objectForKey: @"start"]
273                    to: [currentFilter objectForKey: @"end"]
274                    title: [currentFilter objectForKey: @"title"]
275                    component: [currentFilter objectForKey: @"name"]];
276       appointments = [apts objectEnumerator];
277       appointment = [appointments nextObject];
278       while (appointment)
279         {
280           [self appendObject: appointment
281                 withBaseURL: baseURL
282                 toREPORTResponse: response];
283           appointment = [appointments nextObject];
284         }
285     }
286 }
287
288 - (NSArray *) davNamespaces
289 {
290   return [NSArray arrayWithObject: @"urn:ietf:params:xml:ns:caldav"];
291 }
292
293 - (id) davCalendarQuery: (id) queryContext
294 {
295   WOResponse *r;
296   NSArray *filters;
297   id <DOMDocument> document;
298
299   r = [context response];
300   [r setStatus: 207];
301   [r setContentEncoding: NSUTF8StringEncoding];
302   [r setHeader: @"text/xml; charset=\"utf-8\"" forKey: @"content-type"];
303   [r setHeader: @"no-cache" forKey: @"pragma"];
304   [r setHeader: @"no-cache" forKey: @"cache-control"];
305   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"];
306   [r appendContentString: @"<D:multistatus xmlns:D=\"DAV:\""
307      @" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\r\n"];
308
309   document = [[context request] contentAsDOMDocument];
310   filters = [self _parseCalendarFilters: [document documentElement]];
311   [self _appendComponentsMatchingFilters: filters
312         toResponse: r];
313   [r appendContentString:@"</D:multistatus>\r\n"];
314
315   return r;
316 }
317
318 - (Class) objectClassForContent: (NSString *) content
319 {
320   iCalCalendar *calendar;
321   NSArray *elements;
322   NSString *firstTag;
323   Class objectClass;
324
325   objectClass = Nil;
326
327   calendar = [iCalCalendar parseSingleFromSource: content];
328   if (calendar)
329     {
330       elements = [calendar allObjects];
331       if ([elements count])
332         {
333           firstTag = [[[elements objectAtIndex: 0] tag] uppercaseString];
334           if ([firstTag isEqualToString: @"VEVENT"])
335             objectClass = [SOGoAppointmentObject class];
336           else if ([firstTag isEqualToString: @"VTODO"])
337             objectClass = [SOGoTaskObject class];
338         }
339     }
340
341   return objectClass;
342 }
343
344 - (id) deduceObjectForName: (NSString *)_key
345                  inContext: (id)_ctx
346 {
347   WORequest *request;
348   NSString *method;
349   Class objectClass;
350   id obj;
351
352   request = [_ctx request];
353   method = [request method];
354   if ([method isEqualToString: @"PUT"])
355     objectClass = [self objectClassForContent: [request contentAsString]];
356   else
357     objectClass = [self objectClassForResourceNamed: _key];
358
359   if (objectClass)
360     obj = [objectClass objectWithName: _key inContainer: self];
361   else
362     obj = nil;
363
364   return obj;
365 }
366
367 - (BOOL) requestNamedIsHandledLater: (NSString *) name
368 {
369   return [name isEqualToString: @"OPTIONS"];
370 }
371
372 - (id) lookupName: (NSString *)_key
373         inContext: (id)_ctx
374           acquire: (BOOL)_flag
375 {
376   id obj;
377   NSString *url;
378   BOOL handledLater;
379
380   /* first check attributes directly bound to the application */
381   handledLater = [self requestNamedIsHandledLater: _key];
382   if (handledLater)
383     obj = nil;
384   else
385     {
386       obj = [super lookupName:_key inContext:_ctx acquire:NO];
387       if (!obj)
388         {
389           if ([self isValidAppointmentName:_key])
390             {
391               url = [[[_ctx request] uri] urlWithoutParameters];
392               if ([url hasSuffix: @"AsTask"])
393                 obj = [SOGoTaskObject objectWithName: _key
394                                       inContainer: self];
395               else if ([url hasSuffix: @"AsAppointment"])
396                 obj = [SOGoAppointmentObject objectWithName: _key
397                                              inContainer: self];
398               else
399                 obj = [self deduceObjectForName: _key
400                             inContext: _ctx];
401             }
402         }
403       if (!obj)
404         obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
405     }
406
407   if (obj)
408     [[SOGoCache sharedCache] registerObject: obj
409                              withName: _key
410                              inContainer: container];
411
412   return obj;
413 }
414
415 - (NSArray *) davComplianceClassesInContext: (id)_ctx
416 {
417   NSMutableArray *classes;
418   NSArray *primaryClasses;
419
420   classes = [NSMutableArray new];
421   [classes autorelease];
422
423   primaryClasses = [super davComplianceClassesInContext: _ctx];
424   if (primaryClasses)
425     [classes addObjectsFromArray: primaryClasses];
426   [classes addObject: @"access-control"];
427   [classes addObject: @"calendar-access"];
428
429   return classes;
430 }
431
432 - (NSArray *) groupDavResourceType
433 {
434   return [NSArray arrayWithObjects: @"vevent-collection",
435                   @"vtodo-collection", nil];
436 }
437
438 - (NSArray *) davResourceType
439 {
440   static NSArray *colType = nil;
441   NSArray *cdCol, *gdRT, *gdVEventCol, *gdVTodoCol;
442
443   if (!colType)
444     {
445       gdRT = [self groupDavResourceType];
446       gdVEventCol = [NSArray arrayWithObjects: [gdRT objectAtIndex: 0],
447                              XMLNS_GROUPDAV, nil];
448       gdVTodoCol = [NSArray arrayWithObjects: [gdRT objectAtIndex: 1],
449                             XMLNS_GROUPDAV, nil];
450       cdCol = [NSArray arrayWithObjects: @"calendar", XMLNS_CALDAV, nil];
451       colType = [NSArray arrayWithObjects: @"collection", cdCol,
452                          gdVEventCol, gdVTodoCol, nil];
453       [colType retain];
454     }
455
456   return colType;
457 }
458
459 /* vevent UID handling */
460
461 - (NSString *) resourceNameForEventUID: (NSString *)_u
462                               inFolder: (GCSFolder *)_f
463 {
464   static NSArray *nameFields = nil;
465   EOQualifier *qualifier;
466   NSArray     *records;
467   
468   if (![_u isNotNull]) return nil;
469   if (_f == nil) {
470     [self errorWithFormat:@"(%s): missing folder for fetch!",
471             __PRETTY_FUNCTION__];
472     return nil;
473   }
474   
475   if (nameFields == nil)
476     nameFields = [[NSArray alloc] initWithObjects: @"c_name", nil];
477   
478   qualifier = [EOQualifier qualifierWithQualifierFormat:@"c_uid = %@", _u];
479   records   = [_f fetchFields: nameFields matchingQualifier: qualifier];
480   
481   if ([records count] == 1)
482     return [[records objectAtIndex:0] valueForKey:@"c_name"];
483   if ([records count] == 0)
484     return nil;
485   
486   [self errorWithFormat:
487           @"The storage contains more than file with the same UID!"];
488   return [[records objectAtIndex:0] valueForKey:@"c_name"];
489 }
490
491 - (NSString *) resourceNameForEventUID: (NSString *) _uid
492 {
493   /* caches UIDs */
494   GCSFolder *folder;
495   NSString  *rname;
496   
497   if (![_uid isNotNull])
498     return nil;
499   if ((rname = [uidToFilename objectForKey:_uid]) != nil)
500     return [rname isNotNull] ? rname : nil;
501   
502   if ((folder = [self ocsFolder]) == nil) {
503     [self errorWithFormat:@"(%s): missing folder for fetch!",
504       __PRETTY_FUNCTION__];
505     return nil;
506   }
507
508   if (uidToFilename == nil)
509     uidToFilename = [[NSMutableDictionary alloc] initWithCapacity:16];
510   
511   if ((rname = [self resourceNameForEventUID:_uid inFolder:folder]) == nil)
512     [uidToFilename setObject:[NSNull null] forKey:_uid];
513   else
514     [uidToFilename setObject:rname forKey:_uid];
515   
516   return rname;
517 }
518
519 - (Class) objectClassForResourceNamed: (NSString *) name
520 {
521   EOQualifier *qualifier;
522   NSArray *records;
523   NSString *component;
524   Class objectClass;
525
526   qualifier = [EOQualifier qualifierWithQualifierFormat:@"c_name = %@", name];
527   records = [[self ocsFolder] fetchFields: [NSArray arrayWithObject: @"c_component"]
528                               matchingQualifier: qualifier];
529
530   if ([records count])
531     {
532       component = [[records objectAtIndex:0] valueForKey: @"c_component"];
533       if ([component isEqualToString: @"vevent"])
534         objectClass = [SOGoAppointmentObject class];
535       else if ([component isEqualToString: @"vtodo"])
536         objectClass = [SOGoTaskObject class];
537       else
538         objectClass = Nil;
539     }
540   else
541     objectClass = Nil;
542   
543   return objectClass;
544 }
545
546 /* fetching */
547
548 - (NSMutableDictionary *) fixupRecord: (NSDictionary *) _record
549                            fetchRange: (NGCalendarDateRange *) _r
550 {
551   NSMutableDictionary *md;
552   id tmp;
553   
554   md = [[_record mutableCopy] autorelease];
555  
556   if ((tmp = [_record objectForKey:@"c_startdate"])) {
557     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
558           (NSTimeInterval)[tmp unsignedIntValue]];
559     [tmp setTimeZone: timeZone];
560     if (tmp) [md setObject:tmp forKey:@"startDate"];
561     [tmp release];
562   }
563   else
564     [self logWithFormat:@"missing 'startdate' in record?"];
565
566   if ((tmp = [_record objectForKey:@"c_enddate"])) {
567     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
568           (NSTimeInterval)[tmp unsignedIntValue]];
569     [tmp setTimeZone: timeZone];
570     if (tmp) [md setObject:tmp forKey:@"endDate"];
571     [tmp release];
572   }
573   else
574     [self logWithFormat:@"missing 'enddate' in record?"];
575
576   return md;
577 }
578
579 - (NSMutableDictionary *) fixupCycleRecord: (NSDictionary *) _record
580                                 cycleRange: (NGCalendarDateRange *) _r
581 {
582   NSMutableDictionary *md;
583   id tmp;
584   
585   md = [[_record mutableCopy] autorelease];
586   
587   /* cycle is in _r. We also have to override the c_startdate/c_enddate with the date values of
588      the reccurence since we use those when displaying events in SOGo Web */
589   tmp = [_r startDate];
590   [tmp setTimeZone: timeZone];
591   [md setObject:tmp forKey:@"startDate"];
592   [md setObject: [NSNumber numberWithInt: [tmp timeIntervalSince1970]] forKey: @"c_startdate"];
593   tmp = [_r endDate];
594   [tmp setTimeZone: timeZone];
595   [md setObject:tmp forKey:@"endDate"];
596   [md setObject: [NSNumber numberWithInt: [tmp timeIntervalSince1970]] forKey: @"c_enddate"];
597   
598   return md;
599 }
600
601 - (NSArray *) fixupRecords: (NSArray *) records
602                 fetchRange: (NGCalendarDateRange *) r
603 {
604   // TODO: is the result supposed to be sorted by date?
605   NSMutableArray *ma;
606   unsigned count, max;
607   id row; // TODO: what is the type of the record?
608
609   if (records)
610     {
611       max = [records count];
612       ma = [NSMutableArray arrayWithCapacity: max];
613       for (count = 0; count < max; count++)
614         {
615           row = [self fixupRecord: [records objectAtIndex: count]
616                       fetchRange: r];
617           if (row)
618             [ma addObject: row];
619         }
620     }
621   else
622     ma = nil;
623
624   return ma;
625 }
626
627 - (void) _flattenCycleRecord: (NSDictionary *) _row
628                     forRange: (NGCalendarDateRange *) _r
629                    intoArray: (NSMutableArray *) _ma
630 {
631   NSMutableDictionary *row;
632   NSDictionary        *cycleinfo;
633   NSCalendarDate      *startDate, *endDate;
634   NGCalendarDateRange *fir;
635   NSArray             *rules, *exRules, *exDates, *ranges;
636   unsigned            i, count;
637
638   cycleinfo  = [[_row objectForKey:@"c_cycleinfo"] propertyList];
639   if (cycleinfo == nil) {
640     [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row];
641     return;
642   }
643
644   row = [self fixupRecord:_row fetchRange: _r];
645   [row removeObjectForKey: @"c_cycleinfo"];
646   [row setObject: sharedYes forKey:@"isRecurrentEvent"];
647
648   startDate = [row objectForKey:@"startDate"];
649   endDate   = [row objectForKey:@"endDate"];
650   fir       = [NGCalendarDateRange calendarDateRangeWithStartDate:startDate
651                                    endDate:endDate];
652   rules     = [cycleinfo objectForKey:@"rules"];
653   exRules   = [cycleinfo objectForKey:@"exRules"];
654   exDates   = [cycleinfo objectForKey:@"exDates"];
655   
656   ranges = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange:_r
657                                      firstInstanceCalendarDateRange:fir
658                                      recurrenceRules:rules
659                                      exceptionRules:exRules
660                                      exceptionDates:exDates];
661   count = [ranges count];
662
663   for (i = 0; i < count; i++) {
664     NGCalendarDateRange *rRange;
665     id fixedRow;
666     
667     rRange   = [ranges objectAtIndex:i];
668     fixedRow = [self fixupCycleRecord:row cycleRange:rRange];
669     if (fixedRow != nil)
670       {
671         [_ma addObject:fixedRow];
672       }
673   }
674 }
675
676 - (NSArray *) fixupCyclicRecords: (NSArray *) _records
677                       fetchRange: (NGCalendarDateRange *) _r
678 {
679   // TODO: is the result supposed to be sorted by date?
680   NSMutableArray *ma;
681   unsigned i, count;
682   
683   if (_records == nil) return nil;
684   if ((count = [_records count]) == 0)
685     return _records;
686   
687   ma = [NSMutableArray arrayWithCapacity:count];
688   for (i = 0; i < count; i++) {
689     id row; // TODO: what is the type of the record?
690     
691     row = [_records objectAtIndex:i];
692     [self _flattenCycleRecord:row forRange:_r intoArray:ma];
693   }
694   return ma;
695 }
696
697 - (NSString *) _sqlStringForComponent: (id) _component
698 {
699   NSString *sqlString;
700   NSArray *components;
701
702   if (_component)
703     {
704       if ([_component isKindOfClass: [NSArray class]])
705         components = _component;
706       else
707         components = [NSArray arrayWithObject: _component];
708
709       sqlString
710         = [NSString stringWithFormat: @" AND (c_component = '%@')",
711                     [components componentsJoinedByString: @"' OR c_component = '"]];
712     }
713   else
714     sqlString = @"";
715
716   return sqlString;
717 }
718
719 - (NSString *) _sqlStringRangeFrom: (NSCalendarDate *) _startDate
720                                 to: (NSCalendarDate *) _endDate
721 {
722   unsigned int start, end;
723
724   start = (unsigned int) [_startDate timeIntervalSince1970];
725   end = (unsigned int) [_endDate timeIntervalSince1970];
726
727   return [NSString stringWithFormat:
728                      @" AND (c_startdate <= %u) AND (c_enddate >= %u)",
729                    end, start];
730 }
731
732 - (NSString *) _privacyClassificationStringsForUID: (NSString *) uid
733 {
734   NSMutableString *classificationString;
735   NSString *currentRole;
736   unsigned int counter;
737   iCalAccessClass classes[] = {iCalAccessPublic, iCalAccessPrivate,
738                                iCalAccessConfidential};
739
740   classificationString = [NSMutableString string];
741   for (counter = 0; counter < 3; counter++)
742     {
743       currentRole = [self roleForComponentsWithAccessClass: classes[counter]
744                           forUser: uid];
745       if ([currentRole length] > 0)
746         [classificationString appendFormat: @"c_classification = %d or ",
747                               classes[counter]];
748     }
749
750   return classificationString;
751 }
752
753 - (NSString *) _privacySqlString
754 {
755   NSString *privacySqlString, *login, *email;
756   SOGoUser *activeUser;
757
758   activeUser = [context activeUser];
759   login = [activeUser login];
760
761   if ([login isEqualToString: owner])
762     privacySqlString = @"";
763   else if ([login isEqualToString: @"freebusy"])
764     privacySqlString = @"and (c_isopaque = 1)";
765   else
766     {
767 #warning we do not manage all the possible user emails
768       email = [[activeUser primaryIdentity] objectForKey: @"email"];
769       
770       privacySqlString
771         = [NSString stringWithFormat:
772                       @"(%@(c_orgmail = '%@')"
773                     @" or ((c_partmails caseInsensitiveLike '%@%%'"
774                     @" or c_partmails caseInsensitiveLike '%%\n%@%%')))",
775                     [self _privacyClassificationStringsForUID: login],
776                     email, email, email];
777     }
778
779   return privacySqlString;
780 }
781
782 - (NSArray *) subscriptionRoles
783 {
784   return [NSArray arrayWithObjects:
785                     SOGoRole_ObjectCreator, SOGoRole_ObjectEraser,
786                   SOGoCalendarRole_PublicResponder,
787                   SOGoCalendarRole_PublicModifier,
788                   SOGoCalendarRole_PublicViewer,
789                   SOGoCalendarRole_PublicDAndTViewer,
790                   SOGoCalendarRole_PrivateResponder,
791                   SOGoCalendarRole_PrivateModifier,
792                   SOGoCalendarRole_PrivateViewer,
793                   SOGoCalendarRole_PrivateDAndTViewer,
794                   SOGoCalendarRole_ConfidentialResponder,
795                   SOGoCalendarRole_ConfidentialModifier,
796                   SOGoCalendarRole_ConfidentialViewer,
797                   SOGoCalendarRole_ConfidentialDAndTViewer, nil];
798 }
799
800 - (NSString *) roleForComponentsWithAccessClass: (iCalAccessClass) accessClass
801                                         forUser: (NSString *) uid
802 {
803   NSString *accessRole, *prefix, *currentRole, *suffix;
804   NSEnumerator *acls;
805   NSMutableDictionary *userRoles;
806
807   accessRole = nil;
808
809   if (accessClass == iCalAccessPublic)
810     prefix = @"Public";
811   else if (accessClass == iCalAccessPrivate)
812     prefix = @"Private";
813   else
814     prefix = @"Confidential";
815
816   userRoles = [aclMatrix objectForKey: uid];
817   if (!userRoles)
818     {
819       userRoles = [NSMutableDictionary dictionaryWithCapacity: 3];
820       [aclMatrix setObject: userRoles forKey: uid];
821     }
822
823   accessRole = [userRoles objectForKey: prefix];
824   if (!accessRole)
825     {
826       acls = [[self aclsForUser: uid] objectEnumerator];
827       currentRole = [acls nextObject];
828       while (currentRole && !accessRole)
829         if ([currentRole hasPrefix: prefix])
830           {
831             suffix = [currentRole substringFromIndex: [prefix length]];
832             accessRole = [NSString stringWithFormat: @"Component%@", suffix];
833           }
834         else
835           currentRole = [acls nextObject];
836       if (!accessRole)
837         accessRole = @"";
838       [userRoles setObject: accessRole forKey: prefix];
839     }
840
841   return accessRole;
842 }
843
844 - (void) _buildStripFieldsFromFields: (NSArray *) fields
845 {
846   stripFields = [[NSMutableArray alloc] initWithCapacity: [fields count]];
847   [stripFields setArray: fields];
848   [stripFields removeObjectsInArray: [NSArray arrayWithObjects: @"c_name",
849                                               @"c_uid", @"c_startdate",
850                                               @"c_enddate", @"c_isallday",
851                                               @"c_iscycle",
852                                               @"c_classification",
853                                               @"c_component", nil]];
854 }
855
856 - (void) _fixupProtectedInformation: (NSEnumerator *) ma
857                            inFields: (NSArray *) fields
858                             forUser: (NSString *) uid
859 {
860   NSMutableDictionary *currentRecord;
861   NSString *roles[] = {nil, nil, nil};
862   iCalAccessClass accessClass;
863   NSString *fullRole, *role;
864
865   if (!stripFields)
866     [self _buildStripFieldsFromFields: fields];
867
868 #warning we do not take the participation status into account
869   while ((currentRecord = [ma nextObject]))
870     {
871       accessClass
872         = [[currentRecord objectForKey: @"c_classification"] intValue];
873       role = roles[accessClass];
874       if (!role)
875         {
876           fullRole = [self roleForComponentsWithAccessClass: accessClass
877                            forUser: uid];
878           if ([fullRole length] > 9)
879             role = [fullRole substringFromIndex: 9];
880           roles[accessClass] = role;
881         }
882       if ([role isEqualToString: @"DAndTViewer"])
883         [currentRecord removeObjectsForKeys: stripFields];
884     }
885 }
886
887 - (NSArray *) fetchFields: (NSArray *) _fields
888                fromFolder: (GCSFolder *) _folder
889                      from: (NSCalendarDate *) _startDate
890                        to: (NSCalendarDate *) _endDate 
891                     title: (NSString *) title
892                 component: (id) _component
893 {
894   EOQualifier *qualifier;
895   NSMutableArray *fields, *ma = nil;
896   NSArray *records;
897   NSString *sql, *dateSqlString, *titleSqlString, *componentSqlString,
898     *privacySqlString, *currentLogin;
899   NGCalendarDateRange *r;
900
901   if (!_folder)
902     {
903       [self errorWithFormat:@"(%s): missing folder for fetch!",
904             __PRETTY_FUNCTION__];
905       return nil;
906     }
907
908   if (_startDate && _endDate)
909     {
910       r = [NGCalendarDateRange calendarDateRangeWithStartDate: _startDate
911                                endDate: _endDate];
912       dateSqlString = [self _sqlStringRangeFrom: _startDate to: _endDate];
913     }
914   else
915     {
916       r = nil;
917       dateSqlString = @"";
918     }
919
920   if ([title length])
921     titleSqlString = [NSString stringWithFormat: @"AND (c_title"
922                                @" isCaseInsensitiveLike: '%%%@%%')", title];
923   else
924     titleSqlString = @"";
925
926   componentSqlString = [self _sqlStringForComponent: _component];
927   privacySqlString = [self _privacySqlString];
928
929   /* prepare mandatory fields */
930
931   fields = [NSMutableArray arrayWithArray: _fields];
932   [fields addObject: @"c_uid"];
933   [fields addObject: @"c_startdate"];
934   [fields addObject: @"c_enddate"];
935
936   if (logger)
937     [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate];
938
939   sql = [NSString stringWithFormat: @"(c_iscycle = 0)%@%@%@%@",
940                   dateSqlString, titleSqlString,
941                   componentSqlString, privacySqlString];
942
943   /* fetch non-recurrent apts first */
944   qualifier = [EOQualifier qualifierWithQualifierFormat: sql];
945
946   records = [_folder fetchFields: fields matchingQualifier: qualifier];
947   if (records)
948     {
949       if (r)
950         records = [self fixupRecords: records fetchRange: r];
951       if (logger)
952         [self debugWithFormat: @"fetched %i records: %@",
953               [records count], records];
954       ma = [NSMutableArray arrayWithArray: records];
955     }
956
957   /* fetch recurrent apts now. we do NOT consider the date range when doing that
958      as the c_startdate/c_enddate of a recurring event is always set to the first
959      recurrence - others are generated on the fly */
960   sql = [NSString stringWithFormat: @"(c_iscycle = 1)%@%@%@", titleSqlString,
961                   componentSqlString, privacySqlString];
962
963   qualifier = [EOQualifier qualifierWithQualifierFormat: sql];
964
965   records = [_folder fetchFields: fields matchingQualifier: qualifier];
966
967   if (records)
968     {
969       if (r) {
970         records = [self fixupCyclicRecords: records fetchRange: r];
971       }
972       if (!ma)
973         ma = [NSMutableArray arrayWithCapacity: [records count]];
974
975       [ma addObjectsFromArray: records];
976     }
977   else if (!ma)
978     {
979       [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__];
980       return nil;
981     }
982
983   if (logger)
984     [self debugWithFormat:@"returning %i records", [ma count]];
985
986   currentLogin = [[context activeUser] login];
987   if (![currentLogin isEqualToString: owner])
988     [self _fixupProtectedInformation: [ma objectEnumerator]
989           inFields: _fields
990           forUser: currentLogin];
991 //   [ma makeObjectsPerform: @selector (setObject:forKey:)
992 //       withObject: owner
993 //       withObject: @"owner"];
994
995   return ma;
996 }
997
998 /* override this in subclasses */
999 - (NSArray *) fetchFields: (NSArray *) _fields
1000                      from: (NSCalendarDate *) _startDate
1001                        to: (NSCalendarDate *) _endDate 
1002                     title: (NSString *) title
1003                 component: (id) _component
1004 {
1005   GCSFolder *folder;
1006   
1007   if ((folder = [self ocsFolder]) == nil) {
1008     [self errorWithFormat:@"(%s): missing folder for fetch!",
1009       __PRETTY_FUNCTION__];
1010     return nil;
1011   }
1012
1013   return [self fetchFields: _fields fromFolder: folder
1014                from: _startDate to: _endDate
1015                title: title
1016                component: _component];
1017 }
1018
1019 - (NSArray *) fetchFreeBusyInfosFrom: (NSCalendarDate *) _startDate
1020                                   to: (NSCalendarDate *) _endDate
1021 {
1022   static NSArray *infos = nil; // TODO: move to a plist file
1023   
1024   if (!infos)
1025     infos = [[NSArray alloc] initWithObjects: @"c_partmails", @"c_partstates",
1026                              @"c_isopaque", @"c_status", nil];
1027
1028   return [self fetchFields: infos from: _startDate to: _endDate
1029                title: nil
1030                component: @"vevent"];
1031 }
1032
1033 - (NSArray *) fetchCoreInfosFrom: (NSCalendarDate *) _startDate
1034                               to: (NSCalendarDate *) _endDate
1035                            title: (NSString *) title
1036                        component: (id) _component
1037 {
1038   static NSArray *infos = nil; // TODO: move to a plist file
1039
1040   if (!infos)
1041     infos = [[NSArray alloc] initWithObjects:
1042                                @"c_name", @"c_component",
1043                              @"c_title", @"c_location", @"c_orgmail",
1044                              @"c_status", @"c_classification",
1045                              @"c_isallday", @"c_isopaque",
1046                              @"c_participants", @"c_partmails",
1047                              @"c_partstates", @"c_sequence", @"c_priority", @"c_cycleinfo",
1048                              nil];
1049
1050   return [self fetchFields: infos from: _startDate to: _endDate title: title
1051                component: _component];
1052 }
1053
1054 /* URL generation */
1055
1056 - (NSString *) baseURLForAptWithUID: (NSString *)_uid
1057                           inContext: (id)_ctx
1058 {
1059   // TODO: who calls this?
1060   NSString *url;
1061   
1062   if ([_uid length] == 0)
1063     return nil;
1064   
1065   url = [self baseURLInContext:_ctx];
1066   if (![url hasSuffix: @"/"])
1067     url = [url stringByAppendingString: @"/"];
1068   
1069   // TODO: this should run a query to determine the uid!
1070   return [url stringByAppendingString:_uid];
1071 }
1072
1073 /* folder management */
1074 - (BOOL) create
1075 {
1076   BOOL rc;
1077   NSMutableArray *folderSubscription;
1078   NSUserDefaults *userSettings;
1079   NSMutableDictionary *calendarSettings;
1080   SOGoUser *ownerUser;
1081
1082   rc = [super create];
1083   if (rc)
1084     {
1085       ownerUser = [SOGoUser userWithLogin: [self ownerInContext: context]
1086                             roles: nil];
1087       userSettings = [ownerUser userSettings];
1088       calendarSettings = [userSettings objectForKey: @"Calendar"];
1089       if (!calendarSettings)
1090         {
1091           calendarSettings = [NSMutableDictionary dictionary];
1092           [userSettings setObject: calendarSettings forKey: @"Calendar"];
1093         }
1094       folderSubscription
1095         = [calendarSettings objectForKey: @"ActiveFolders"];
1096       if (!folderSubscription)
1097         {
1098           folderSubscription = [NSMutableArray array];
1099           [calendarSettings setObject: folderSubscription
1100                             forKey: @"ActiveFolders"];
1101         }
1102       [folderSubscription addObjectUniquely: nameInContainer];
1103       [userSettings synchronize];
1104     }
1105
1106   return rc;
1107 }
1108
1109 - (id) lookupHomeFolderForUID: (NSString *) _uid
1110                     inContext: (id)_ctx
1111 {
1112   // TODO: DUP to SOGoGroupFolder
1113   NSException *error = nil;
1114   NSArray     *path;
1115   id          ctx, result;
1116
1117   if (![_uid isNotNull])
1118     return nil;
1119
1120   /* create subcontext, so that we don't destroy our environment */
1121   
1122   if ((ctx = [context createSubContext]) == nil) {
1123     [self errorWithFormat:@"could not create SOPE subcontext!"];
1124     return nil;
1125   }
1126   
1127   /* build path */
1128   
1129   path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil;
1130   
1131   /* traverse path */
1132   
1133   result = [[ctx application] traversePathArray:path inContext:ctx
1134                               error:&error acquire:NO];
1135   if (error != nil) {
1136     [self errorWithFormat: @"folder lookup failed (c_uid=%@): %@",
1137             _uid, error];
1138     return nil;
1139   }
1140   
1141   [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@",
1142           _uid, [path componentsJoinedByString:@"=>"], result];
1143   return result;
1144 }
1145
1146 - (SOGoAppointmentFolder *) lookupCalendarFolderForUID: (NSString *) uid
1147 {
1148   SOGoFolder *currentContainer;
1149   SOGoAppointmentFolders *parent;
1150   NSException *error;
1151
1152   currentContainer = [[container container] container];
1153   currentContainer = [currentContainer lookupName: uid
1154                                        inContext: context
1155                                        acquire: NO];
1156   parent = [currentContainer lookupName: @"Calendar" inContext: context
1157                              acquire: NO];
1158   currentContainer = [parent lookupName: @"personal" inContext: context
1159                              acquire: NO];
1160   if (!currentContainer)
1161     {
1162       error = [parent newFolderWithName: [parent defaultFolderName]
1163                       andNameInContainer: @"personal"];
1164       if (!error)
1165         currentContainer = [parent lookupName: @"personal"
1166                                    inContext: context
1167                                    acquire: NO];
1168     }
1169
1170   return (SOGoAppointmentFolder *) currentContainer;
1171 }
1172
1173 - (NSArray *) lookupCalendarFoldersForUIDs: (NSArray *) _uids
1174                                  inContext: (id)_ctx
1175 {
1176   /* Note: can return NSNull objects in the array! */
1177   NSMutableArray *folders;
1178   NSEnumerator *e;
1179   NSString *uid, *ownerLogin;
1180   id folder;
1181
1182   ownerLogin = [self ownerInContext: context];
1183
1184   if ([_uids count] == 0) return nil;
1185   folders = [NSMutableArray arrayWithCapacity:16];
1186   e = [_uids objectEnumerator];
1187   while ((uid = [e nextObject]))
1188     {
1189       if ([uid isEqualToString: ownerLogin])
1190         folder = self;
1191       else
1192         {
1193           folder = [self lookupCalendarFolderForUID: uid];
1194           if (![folder isNotNull])
1195             [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid];
1196         }
1197
1198       if (folder)
1199         [folders addObject: folder];
1200     }
1201
1202   return folders;
1203 }
1204
1205 - (NSArray *) lookupFreeBusyObjectsForUIDs: (NSArray *) _uids
1206                                  inContext: (id) _ctx
1207 {
1208   /* Note: can return NSNull objects in the array! */
1209   NSMutableArray *objs;
1210   NSEnumerator   *e;
1211   NSString       *uid;
1212   
1213   if ([_uids count] == 0) return nil;
1214   objs = [NSMutableArray arrayWithCapacity:16];
1215   e    = [_uids objectEnumerator];
1216   while ((uid = [e nextObject])) {
1217     id obj;
1218     
1219     obj = [self lookupHomeFolderForUID:uid inContext:nil];
1220     if ([obj isNotNull]) {
1221       obj = [obj lookupName:@"freebusy.ifb" inContext:nil acquire:NO];
1222       if ([obj isKindOfClass:[NSException class]])
1223         obj = nil;
1224     }
1225     if (![obj isNotNull])
1226       [self logWithFormat:@"Note: did not find freebusy.ifb for uid: '%@'", uid];
1227     
1228     /* Note: intentionally add 'null' folders to allow a mapping */
1229     [objs addObject:obj ? obj : [NSNull null]];
1230   }
1231   return objs;
1232 }
1233
1234 - (NSArray *) uidsFromICalPersons: (NSArray *) _persons
1235 {
1236   /* Note: can return NSNull objects in the array! */
1237   NSMutableArray    *uids;
1238   LDAPUserManager *um;
1239   unsigned          i, count;
1240   
1241   if (_persons == nil)
1242     return nil;
1243
1244   count = [_persons count];
1245   uids  = [NSMutableArray arrayWithCapacity:count + 1];
1246   um    = [LDAPUserManager sharedUserManager];
1247   
1248   for (i = 0; i < count; i++) {
1249     iCalPerson *person;
1250     NSString   *email;
1251     NSString   *uid;
1252     
1253     person = [_persons objectAtIndex:i];
1254     email  = [person rfc822Email];
1255     if ([email isNotNull]) {
1256       uid = [um getUIDForEmail:email];
1257     }
1258     else
1259       uid = nil;
1260     
1261     [uids addObject:(uid != nil ? uid : (id)[NSNull null])];
1262   }
1263   return uids;
1264 }
1265
1266 - (NSArray *)lookupCalendarFoldersForICalPerson: (NSArray *) _persons
1267                                       inContext: (id) _ctx
1268 {
1269   /* Note: can return NSNull objects in the array! */
1270   NSArray *uids;
1271
1272   if ((uids = [self uidsFromICalPersons:_persons]) == nil)
1273     return nil;
1274   
1275   return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
1276 }
1277
1278 - (id) lookupGroupFolderForUIDs: (NSArray *) _uids
1279                       inContext: (id)_ctx
1280 {
1281   SOGoCustomGroupFolder *folder;
1282   
1283   if (_uids == nil)
1284     return nil;
1285
1286   folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self];
1287   return [folder autorelease];
1288 }
1289
1290 - (id) lookupGroupCalendarFolderForUIDs: (NSArray *) _uids
1291                               inContext: (id) _ctx
1292 {
1293   SOGoCustomGroupFolder *folder;
1294   
1295   if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil)
1296     return nil;
1297   
1298   folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO];
1299   if (![folder isNotNull])
1300     return nil;
1301   if ([folder isKindOfClass:[NSException class]]) {
1302     [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@",
1303             folder];
1304     return nil;
1305   }
1306   
1307   return folder;
1308 }
1309
1310 /* bulk fetches */
1311
1312 - (NSArray *) fetchAllSOGoAppointments
1313 {
1314   /* 
1315      Note: very expensive method, do not use unless absolutely required.
1316            returns an array of SOGoAppointment objects.
1317            
1318      Note that we can leave out the filenames, supposed to be stored
1319      in the 'uid' field of the iCalendar object!
1320   */
1321   NSMutableArray *events;
1322   NSDictionary *files;
1323   NSEnumerator *contents;
1324   NSString     *content;
1325   
1326   /* fetch all raw contents */
1327   
1328   files = [self fetchContentStringsAndNamesOfAllObjects];
1329   if (![files isNotNull]) return nil;
1330   if ([files isKindOfClass:[NSException class]]) return (id)files;
1331   
1332   /* transform to SOGo appointments */
1333   
1334   events   = [NSMutableArray arrayWithCapacity:[files count]];
1335   contents = [files objectEnumerator];
1336   while ((content = [contents nextObject]) != nil)
1337     [events addObject: [iCalCalendar parseSingleFromSource: content]];
1338   
1339   return events;
1340 }
1341
1342 // #warning We only support ONE calendar per user at this time
1343 // - (BOOL) _appendSubscribedFolders: (NSDictionary *) subscribedFolders
1344 //                   toFolderList: (NSMutableArray *) calendarFolders
1345 // {
1346 //   NSEnumerator *keys;
1347 //   NSString *currentKey;
1348 //   NSMutableDictionary *currentCalendar;
1349 //   BOOL firstShouldBeActive;
1350 //   unsigned int count;
1351
1352 //   firstShouldBeActive = YES;
1353
1354 //   keys = [[subscribedFolders allKeys] objectEnumerator];
1355 //   currentKey = [keys nextObject];
1356 //   count = 1;
1357 //   while (currentKey)
1358 //     {
1359 //       currentCalendar = [NSMutableDictionary new];
1360 //       [currentCalendar autorelease];
1361 //       [currentCalendar
1362 //      setDictionary: [subscribedFolders objectForKey: currentKey]];
1363 //       [currentCalendar setObject: currentKey forKey: @"folder"];
1364 //       [calendarFolders addObject: currentCalendar];
1365 //       if ([[currentCalendar objectForKey: @"active"] boolValue])
1366 //      firstShouldBeActive = NO;
1367 //       count++;
1368 //       currentKey = [keys nextObject];
1369 //     }
1370
1371 //   return firstShouldBeActive;
1372 // }
1373
1374 // - (NSArray *) calendarFolders
1375 // {
1376 //   NSMutableDictionary *userCalendar, *calendarDict;
1377 //   NSMutableArray *calendarFolders;
1378 //   SOGoUser *calendarUser;
1379 //   BOOL firstActive;
1380
1381 //   calendarFolders = [NSMutableArray new];
1382 //   [calendarFolders autorelease];
1383
1384 //   calendarUser = [SOGoUser userWithLogin: [self ownerInContext: context]
1385 //                         roles: nil];
1386 //   userCalendar = [NSMutableDictionary new];
1387 //   [userCalendar autorelease];
1388 //   [userCalendar setObject: @"/" forKey: @"folder"];
1389 //   [userCalendar setObject: @"Calendar" forKey: @"displayName"];
1390 //   [calendarFolders addObject: userCalendar];
1391
1392 //   calendarDict = [[calendarUser userSettings] objectForKey: @"Calendar"];
1393 //   firstActive = [[calendarDict objectForKey: @"activateUserFolder"] boolValue];
1394 //   firstActive = ([self _appendSubscribedFolders:
1395 //                       [calendarDict objectForKey: @"SubscribedFolders"]
1396 //                     toFolderList: calendarFolders]
1397 //               || firstActive);
1398 //   [userCalendar setObject: [NSNumber numberWithBool: firstActive]
1399 //              forKey: @"active"];
1400
1401 //   return calendarFolders;
1402 // }
1403
1404 // - (NSArray *) fetchContentObjectNames
1405 // {
1406 //   NSMutableArray *objectNames;
1407 //   NSArray *records;
1408 //   NSCalendarDate *today, *startDate, *endDate;
1409
1410 // #warning this should be user-configurable
1411 //   objectNames = [NSMutableArray array];
1412 //   today = [[NSCalendarDate calendarDate] beginOfDay];
1413 //   [today setTimeZone: timeZone];
1414
1415 //   startDate = [today dateByAddingYears: 0 months: 0 days: -1
1416 //                      hours: 0 minutes: 0 seconds: 0];
1417 //   endDate = [startDate dateByAddingYears: 0 months: 0 days: 2
1418 //                        hours: 0 minutes: 0 seconds: 0];
1419 //   records = [self fetchFields: [NSArray arrayWithObject: @"c_name"]
1420 //                from: startDate to: endDate
1421 //                component: @"vevent"];
1422 //   [objectNames addObjectsFromArray: [records valueForKey: @"c_name"]];
1423 //   records = [self fetchFields: [NSArray arrayWithObject: @"c_name"]
1424 //                from: startDate to: endDate
1425 //                component: @"vtodo"];
1426 //   [objectNames addObjectsFromArray: [records valueForKey: @"c_name"]];
1427
1428 //   return objectNames;
1429 // }
1430
1431 - (NSArray *) fetchContentObjectNames
1432 {
1433   static NSArray *cNameField = nil;
1434
1435   if (!cNameField)
1436     cNameField = [[NSArray alloc] initWithObjects: @"c_name", nil];
1437
1438   return [[self fetchFields: cNameField from: nil to: nil
1439                 title: nil component: nil] objectsForKey: @"c_name"];
1440 }
1441
1442 /* folder type */
1443
1444 - (NSString *) folderType
1445 {
1446   return @"Appointment";
1447 }
1448
1449 - (NSString *) outlookFolderClass
1450 {
1451   return @"IPF.Appointment";
1452 }
1453
1454 - (BOOL) isActive
1455 {
1456   NSUserDefaults *settings;
1457   NSArray *activeFolders;
1458
1459   settings = [[context activeUser] userSettings];
1460   activeFolders
1461     = [[settings objectForKey: @"Calendar"] objectForKey: @"ActiveFolders"];
1462
1463   return [activeFolders containsObject: nameInContainer];
1464 }
1465
1466 @end /* SOGoAppointmentFolder */