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