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