]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentFolder.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1146 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       email = [activeUser primaryEmail];
750       
751       privacySqlString
752         = [NSString stringWithFormat:
753                       @"(%@(c_orgmail = '%@')"
754                     @" or ((c_partmails caseInsensitiveLike '%@%%'"
755                     @" or c_partmails caseInsensitiveLike '%%\n%@%%')))",
756                     [self _privacyClassificationStringsForUID: login],
757                     email, email, email];
758     }
759   
760   return privacySqlString;
761 }
762
763 - (NSString *) roleForComponentsWithAccessClass: (iCalAccessClass) accessClass
764                                         forUser: (NSString *) uid
765 {
766   NSString *accessRole, *prefix, *currentRole, *suffix;
767   NSEnumerator *acls;
768
769   accessRole = nil;
770
771   if (accessClass == iCalAccessPublic)
772     prefix = @"Public";
773   else if (accessClass == iCalAccessPrivate)
774     prefix = @"Private";
775   else
776     prefix = @"Confidential";
777
778   acls = [[self aclsForUser: uid] objectEnumerator];
779   currentRole = [acls nextObject];
780   while (currentRole && !accessRole)
781     if ([currentRole hasPrefix: prefix])
782       {
783         suffix = [currentRole substringFromIndex: [prefix length]];
784         accessRole = [NSString stringWithFormat: @"Component%@", suffix];
785       }
786     else
787       currentRole = [acls nextObject];
788
789   return accessRole;
790 }
791
792 - (NSArray *) fetchFields: (NSArray *) _fields
793                fromFolder: (GCSFolder *) _folder
794                      from: (NSCalendarDate *) _startDate
795                        to: (NSCalendarDate *) _endDate 
796                 component: (id) _component
797 {
798   EOQualifier *qualifier;
799   NSMutableArray *fields, *ma = nil;
800   NSArray *records;
801   NSString *sql, *dateSqlString, *componentSqlString, *privacySqlString;
802   NGCalendarDateRange *r;
803
804   if (_folder == nil) {
805     [self errorWithFormat:@"(%s): missing folder for fetch!",
806             __PRETTY_FUNCTION__];
807     return nil;
808   }
809   
810   if (_startDate && _endDate)
811     {
812       r = [NGCalendarDateRange calendarDateRangeWithStartDate: _startDate
813                                endDate: _endDate];
814       dateSqlString = [self _sqlStringRangeFrom: _startDate to: _endDate];
815     }
816   else
817     {
818       r = nil;
819       dateSqlString = @"";
820     }
821
822   componentSqlString = [self _sqlStringForComponent: _component];
823   privacySqlString = [self _privacySqlString];
824
825   /* prepare mandatory fields */
826
827   fields = [NSMutableArray arrayWithArray: _fields];
828   [fields addObject: @"c_uid"];
829   [fields addObject: @"c_startdate"];
830   [fields addObject: @"c_enddate"];
831
832   if (logger)
833     [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate];
834
835   sql = [NSString stringWithFormat: @"(c_iscycle = 0)%@%@%@",
836                   dateSqlString, componentSqlString, privacySqlString];
837
838   /* fetch non-recurrent apts first */
839   qualifier = [EOQualifier qualifierWithQualifierFormat: sql];
840
841   records = [_folder fetchFields: fields matchingQualifier: qualifier];
842   if (records)
843     {
844       if (r)
845         records = [self fixupRecords: records fetchRange: r];
846       if (logger)
847         [self debugWithFormat: @"fetched %i records: %@",
848               [records count], records];
849       ma = [NSMutableArray arrayWithArray: records];
850     }
851
852   /* fetch recurrent apts now */
853   sql = [NSString stringWithFormat: @"(c_iscycle = 1)%@%@%@",
854                   dateSqlString, componentSqlString, privacySqlString];
855   qualifier = [EOQualifier qualifierWithQualifierFormat: sql];
856
857   [fields addObject: @"c_cycleinfo"];
858
859   records = [_folder fetchFields: fields matchingQualifier: qualifier];
860   if (records)
861     {
862       if (logger)
863         [self debugWithFormat: @"fetched %i cyclic records: %@",
864               [records count], records];
865       if (r)
866         records = [self fixupCyclicRecords: records fetchRange: r];
867       if (!ma)
868         ma = [NSMutableArray arrayWithCapacity: [records count]];
869
870       [ma addObjectsFromArray: records];
871     }
872   else if (!ma)
873     {
874       [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__];
875       return nil;
876     }
877
878   if (logger)
879     [self debugWithFormat:@"returning %i records", [ma count]];
880
881 //   [ma makeObjectsPerform: @selector (setObject:forKey:)
882 //       withObject: owner
883 //       withObject: @"owner"];
884
885   return ma;
886 }
887
888 /* override this in subclasses */
889 - (NSArray *) fetchFields: (NSArray *) _fields
890                      from: (NSCalendarDate *) _startDate
891                        to: (NSCalendarDate *) _endDate 
892                 component: (id) _component
893 {
894   GCSFolder *folder;
895   
896   if ((folder = [self ocsFolder]) == nil) {
897     [self errorWithFormat:@"(%s): missing folder for fetch!",
898       __PRETTY_FUNCTION__];
899     return nil;
900   }
901
902   return [self fetchFields: _fields fromFolder: folder
903                from: _startDate to: _endDate
904                component: _component];
905 }
906
907
908 - (NSArray *) fetchFreeBusyInfosFrom: (NSCalendarDate *) _startDate
909                                   to: (NSCalendarDate *) _endDate
910 {
911   static NSArray *infos = nil; // TODO: move to a plist file
912   
913   if (!infos)
914     infos = [[NSArray alloc] initWithObjects: @"c_partmails", @"c_partstates",
915                              @"c_isopaque", @"c_status", nil];
916
917   return [self fetchFields: infos from: _startDate to: _endDate
918                component: @"vevent"];
919 }
920
921 - (NSArray *) fetchCoreInfosFrom: (NSCalendarDate *) _startDate
922                               to: (NSCalendarDate *) _endDate
923                        component: (id) _component
924 {
925   static NSArray *infos = nil; // TODO: move to a plist file
926
927   if (!infos)
928     infos = [[NSArray alloc] initWithObjects:
929                                @"c_name", @"c_component",
930                              @"c_title", @"c_location", @"c_orgmail",
931                              @"c_status", @"c_classification",
932                              @"c_isallday", @"c_isopaque",
933                              @"c_participants", @"c_partmails",
934                              @"c_partstates", @"c_sequence", @"c_priority",
935                              nil];
936
937   return [self fetchFields: infos from: _startDate to: _endDate
938                component: _component];
939 }
940
941 - (void) deleteEntriesWithIds: (NSArray *) ids
942 {
943   Class objectClass;
944   unsigned int count, max;
945   NSString *currentId;
946   id deleteObject;
947
948   max = [ids count];
949   for (count = 0; count < max; count++)
950     {
951       currentId = [ids objectAtIndex: count];
952       objectClass
953         = [self objectClassForResourceNamed: currentId];
954       deleteObject = [objectClass objectWithName: currentId
955                                   inContainer: self];
956       [deleteObject delete];
957       [deleteObject primaryDelete];
958     }
959 }
960
961 /* URL generation */
962
963 - (NSString *) baseURLForAptWithUID: (NSString *)_uid
964                           inContext: (id)_ctx
965 {
966   // TODO: who calls this?
967   NSString *url;
968   
969   if ([_uid length] == 0)
970     return nil;
971   
972   url = [self baseURLInContext:_ctx];
973   if (![url hasSuffix: @"/"])
974     url = [url stringByAppendingString: @"/"];
975   
976   // TODO: this should run a query to determine the uid!
977   return [url stringByAppendingString:_uid];
978 }
979
980 /* folder management */
981
982 - (id) lookupHomeFolderForUID: (NSString *) _uid
983                     inContext: (id)_ctx
984 {
985   // TODO: DUP to SOGoGroupFolder
986   NSException *error = nil;
987   NSArray     *path;
988   id          ctx, result;
989
990   if (![_uid isNotNull])
991     return nil;
992
993   /* create subcontext, so that we don't destroy our environment */
994   
995   if ((ctx = [context createSubContext]) == nil) {
996     [self errorWithFormat:@"could not create SOPE subcontext!"];
997     return nil;
998   }
999   
1000   /* build path */
1001   
1002   path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil;
1003   
1004   /* traverse path */
1005   
1006   result = [[ctx application] traversePathArray:path inContext:ctx
1007                               error:&error acquire:NO];
1008   if (error != nil) {
1009     [self errorWithFormat: @"folder lookup failed (c_uid=%@): %@",
1010             _uid, error];
1011     return nil;
1012   }
1013   
1014   [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@",
1015           _uid, [path componentsJoinedByString:@"=>"], result];
1016   return result;
1017 }
1018
1019 - (SOGoAppointmentFolder *) lookupCalendarFolderForUID: (NSString *) uid
1020 {
1021   SOGoFolder *upperContainer;
1022   SOGoUserFolder *userFolder;
1023   SOGoAppointmentFolder *calendarFolder;
1024
1025   upperContainer = [[self container] container];
1026   userFolder = [SOGoUserFolder objectWithName: uid
1027                                inContainer: upperContainer];
1028   calendarFolder = [SOGoAppointmentFolder objectWithName: @"Calendar"
1029                                           inContainer: userFolder];
1030   [calendarFolder
1031     setOCSPath: [NSString stringWithFormat: @"/Users/%@/Calendar", uid]];
1032   [calendarFolder setOwner: uid];
1033
1034   return calendarFolder;
1035 }
1036
1037 - (NSArray *) lookupCalendarFoldersForUIDs: (NSArray *) _uids
1038                                  inContext: (id)_ctx
1039 {
1040   /* Note: can return NSNull objects in the array! */
1041   NSMutableArray *folders;
1042   NSEnumerator *e;
1043   NSString     *uid;
1044   
1045   if ([_uids count] == 0) return nil;
1046   folders = [NSMutableArray arrayWithCapacity:16];
1047   e = [_uids objectEnumerator];
1048   while ((uid = [e nextObject])) {
1049     id folder;
1050     
1051     folder = [self lookupCalendarFolderForUID: uid];
1052     if (![folder isNotNull])
1053       [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid];
1054     
1055     /* Note: intentionally add 'null' folders to allow a mapping */
1056     [folders addObject:folder ? folder : [NSNull null]];
1057   }
1058   return folders;
1059 }
1060
1061 - (NSArray *) lookupFreeBusyObjectsForUIDs: (NSArray *) _uids
1062                                  inContext: (id) _ctx
1063 {
1064   /* Note: can return NSNull objects in the array! */
1065   NSMutableArray *objs;
1066   NSEnumerator   *e;
1067   NSString       *uid;
1068   
1069   if ([_uids count] == 0) return nil;
1070   objs = [NSMutableArray arrayWithCapacity:16];
1071   e    = [_uids objectEnumerator];
1072   while ((uid = [e nextObject])) {
1073     id obj;
1074     
1075     obj = [self lookupHomeFolderForUID:uid inContext:nil];
1076     if ([obj isNotNull]) {
1077       obj = [obj lookupName:@"freebusy.ifb" inContext:nil acquire:NO];
1078       if ([obj isKindOfClass:[NSException class]])
1079         obj = nil;
1080     }
1081     if (![obj isNotNull])
1082       [self logWithFormat:@"Note: did not find freebusy.ifb for uid: '%@'", uid];
1083     
1084     /* Note: intentionally add 'null' folders to allow a mapping */
1085     [objs addObject:obj ? obj : [NSNull null]];
1086   }
1087   return objs;
1088 }
1089
1090 - (NSArray *) uidsFromICalPersons: (NSArray *) _persons
1091 {
1092   /* Note: can return NSNull objects in the array! */
1093   NSMutableArray    *uids;
1094   LDAPUserManager *um;
1095   unsigned          i, count;
1096   
1097   if (_persons == nil)
1098     return nil;
1099
1100   count = [_persons count];
1101   uids  = [NSMutableArray arrayWithCapacity:count + 1];
1102   um    = [LDAPUserManager sharedUserManager];
1103   
1104   for (i = 0; i < count; i++) {
1105     iCalPerson *person;
1106     NSString   *email;
1107     NSString   *uid;
1108     
1109     person = [_persons objectAtIndex:i];
1110     email  = [person rfc822Email];
1111     if ([email isNotNull]) {
1112       uid = [um getUIDForEmail:email];
1113     }
1114     else
1115       uid = nil;
1116     
1117     [uids addObject:(uid != nil ? uid : (id)[NSNull null])];
1118   }
1119   return uids;
1120 }
1121
1122 - (NSArray *)lookupCalendarFoldersForICalPerson: (NSArray *) _persons
1123                                       inContext: (id) _ctx
1124 {
1125   /* Note: can return NSNull objects in the array! */
1126   NSArray *uids;
1127
1128   if ((uids = [self uidsFromICalPersons:_persons]) == nil)
1129     return nil;
1130   
1131   return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
1132 }
1133
1134 - (id) lookupGroupFolderForUIDs: (NSArray *) _uids
1135                       inContext: (id)_ctx
1136 {
1137   SOGoCustomGroupFolder *folder;
1138   
1139   if (_uids == nil)
1140     return nil;
1141
1142   folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self];
1143   return [folder autorelease];
1144 }
1145
1146 - (id) lookupGroupCalendarFolderForUIDs: (NSArray *) _uids
1147                               inContext: (id) _ctx
1148 {
1149   SOGoCustomGroupFolder *folder;
1150   
1151   if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil)
1152     return nil;
1153   
1154   folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO];
1155   if (![folder isNotNull])
1156     return nil;
1157   if ([folder isKindOfClass:[NSException class]]) {
1158     [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@",
1159             folder];
1160     return nil;
1161   }
1162   
1163   return folder;
1164 }
1165
1166 /* bulk fetches */
1167
1168 - (NSArray *) fetchAllSOGoAppointments
1169 {
1170   /* 
1171      Note: very expensive method, do not use unless absolutely required.
1172            returns an array of SOGoAppointment objects.
1173            
1174      Note that we can leave out the filenames, supposed to be stored
1175      in the 'uid' field of the iCalendar object!
1176   */
1177   NSMutableArray *events;
1178   NSDictionary *files;
1179   NSEnumerator *contents;
1180   NSString     *content;
1181   
1182   /* fetch all raw contents */
1183   
1184   files = [self fetchContentStringsAndNamesOfAllObjects];
1185   if (![files isNotNull]) return nil;
1186   if ([files isKindOfClass:[NSException class]]) return (id)files;
1187   
1188   /* transform to SOGo appointments */
1189   
1190   events   = [NSMutableArray arrayWithCapacity:[files count]];
1191   contents = [files objectEnumerator];
1192   while ((content = [contents nextObject]) != nil)
1193     [events addObject: [iCalCalendar parseSingleFromSource: content]];
1194   
1195   return events;
1196 }
1197
1198 #warning We only support ONE calendar per user at this time
1199 - (BOOL) _appendSubscribedFolders: (NSDictionary *) subscribedFolders
1200                      toFolderList: (NSMutableArray *) calendarFolders
1201 {
1202   NSEnumerator *keys;
1203   NSString *currentKey;
1204   NSMutableDictionary *currentCalendar;
1205   BOOL firstShouldBeActive;
1206   unsigned int count;
1207
1208   firstShouldBeActive = YES;
1209
1210   keys = [[subscribedFolders allKeys] objectEnumerator];
1211   currentKey = [keys nextObject];
1212   count = 1;
1213   while (currentKey)
1214     {
1215       currentCalendar = [NSMutableDictionary new];
1216       [currentCalendar autorelease];
1217       [currentCalendar
1218         setDictionary: [subscribedFolders objectForKey: currentKey]];
1219       [currentCalendar setObject: currentKey forKey: @"folder"];
1220       [calendarFolders addObject: currentCalendar];
1221       if ([[currentCalendar objectForKey: @"active"] boolValue])
1222         firstShouldBeActive = NO;
1223       count++;
1224       currentKey = [keys nextObject];
1225     }
1226
1227   return firstShouldBeActive;
1228 }
1229
1230 - (NSArray *) calendarFolders
1231 {
1232   NSMutableDictionary *userCalendar, *calendarDict;
1233   NSMutableArray *calendarFolders;
1234   SOGoUser *calendarUser;
1235   BOOL firstActive;
1236
1237   calendarFolders = [NSMutableArray new];
1238   [calendarFolders autorelease];
1239
1240   calendarUser = [SOGoUser userWithLogin: [self ownerInContext: context]
1241                            roles: nil];
1242   userCalendar = [NSMutableDictionary new];
1243   [userCalendar autorelease];
1244   [userCalendar setObject: @"/" forKey: @"folder"];
1245   [userCalendar setObject: @"Calendar" forKey: @"displayName"];
1246   [calendarFolders addObject: userCalendar];
1247
1248   calendarDict = [[calendarUser userSettings] objectForKey: @"Calendar"];
1249   firstActive = [[calendarDict objectForKey: @"activateUserFolder"] boolValue];
1250   firstActive = ([self _appendSubscribedFolders:
1251                          [calendarDict objectForKey: @"SubscribedFolders"]
1252                        toFolderList: calendarFolders]
1253                  || firstActive);
1254   [userCalendar setObject: [NSNumber numberWithBool: firstActive]
1255                 forKey: @"active"];
1256
1257   return calendarFolders;
1258 }
1259
1260 // - (NSArray *) fetchContentObjectNames
1261 // {
1262 //   NSMutableArray *objectNames;
1263 //   NSArray *records;
1264 //   NSCalendarDate *today, *startDate, *endDate;
1265
1266 // #warning this should be user-configurable
1267 //   objectNames = [NSMutableArray array];
1268 //   today = [[NSCalendarDate calendarDate] beginOfDay];
1269 //   [today setTimeZone: timeZone];
1270
1271 //   startDate = [today dateByAddingYears: 0 months: 0 days: -1
1272 //                      hours: 0 minutes: 0 seconds: 0];
1273 //   endDate = [startDate dateByAddingYears: 0 months: 0 days: 2
1274 //                        hours: 0 minutes: 0 seconds: 0];
1275 //   records = [self fetchFields: [NSArray arrayWithObject: @"c_name"]
1276 //                from: startDate to: endDate
1277 //                component: @"vevent"];
1278 //   [objectNames addObjectsFromArray: [records valueForKey: @"c_name"]];
1279 //   records = [self fetchFields: [NSArray arrayWithObject: @"c_name"]
1280 //                from: startDate to: endDate
1281 //                component: @"vtodo"];
1282 //   [objectNames addObjectsFromArray: [records valueForKey: @"c_name"]];
1283
1284 //   return objectNames;
1285 // }
1286
1287 /* folder type */
1288
1289 - (NSString *) folderType
1290 {
1291   return @"Appointment";
1292 }
1293
1294 - (NSString *) outlookFolderClass
1295 {
1296   return @"IPF.Appointment";
1297 }
1298
1299 @end /* SOGoAppointmentFolder */