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