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