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