/* Copyright (C) 2004-2005 SKYRIX Software AG This file is part of OpenGroupware.org. OGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. OGo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with OGo; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import // #import #import #import #import #import // #import #import #import #import #import #import #import #import "SOGoAppointmentObject.h" #import "SOGoAppointmentFolders.h" #import "SOGoFreeBusyObject.h" #import "SOGoTaskObject.h" #import "SOGoAppointmentFolder.h" #define defaultColor @"#AAAAAA" #if APPLE_Foundation_LIBRARY || NeXT_Foundation_LIBRARY @interface NSDate(UsedPrivates) - (id)initWithTimeIntervalSince1970:(NSTimeInterval)_interval; @end #endif @implementation SOGoAppointmentFolder static NGLogger *logger = nil; static NSNumber *sharedYes = nil; static NSArray *reportQueryFields = nil; static Class sogoAppointmentFolderKlass = Nil; + (void) initialize { NGLoggerManager *lm; static BOOL didInit = NO; // SoClassSecurityInfo *securityInfo; if (didInit) return; didInit = YES; if (!sogoAppointmentFolderKlass) sogoAppointmentFolderKlass = self; if (!reportQueryFields) reportQueryFields = [[NSArray alloc] initWithObjects: @"c_name", @"c_content", @"c_creationdate", @"c_lastmodified", @"c_version", @"c_component", nil]; NSAssert2([super version] == 0, @"invalid superclass (%@) version %i !", NSStringFromClass([self superclass]), [super version]); lm = [NGLoggerManager defaultLoggerManager]; logger = [lm loggerForDefaultKey: @"SOGoAppointmentFolderDebugEnabled"]; // securityInfo = [self soClassSecurityInfo]; // [securityInfo declareRole: SOGoRole_Delegate // asDefaultForPermission: SoPerm_AddDocumentsImagesAndFiles]; // [securityInfo declareRole: SOGoRole_Delegate // asDefaultForPermission: SoPerm_ChangeImagesAndFiles]; // [securityInfo declareRoles: [NSArray arrayWithObjects: // SOGoRole_Delegate, // SOGoRole_Assistant, nil] // asDefaultForPermission: SoPerm_View]; sharedYes = [[NSNumber numberWithBool: YES] retain]; } + (SOGoWebDAVAclManager *) webdavAclManager { SOGoWebDAVAclManager *webdavAclManager = nil; NSString *nsI; if (!webdavAclManager) { nsI = @"urn:inverse:params:xml:ns:inverse-dav"; webdavAclManager = [SOGoWebDAVAclManager new]; [webdavAclManager registerDAVPermission: davElement (@"read", XMLNS_WEBDAV) abstract: YES withEquivalent: SoPerm_WebDAVAccess asChildOf: davElement (@"all", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"read-current-user-privilege-set", XMLNS_WEBDAV) abstract: YES withEquivalent: SoPerm_WebDAVAccess asChildOf: davElement (@"read", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"read-free-busy", XMLNS_WEBDAV) abstract: NO withEquivalent: SoPerm_AccessContentsInformation asChildOf: davElement (@"read", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"write", XMLNS_WEBDAV) abstract: YES withEquivalent: nil asChildOf: davElement (@"all", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"bind", XMLNS_WEBDAV) abstract: NO withEquivalent: SoPerm_AddDocumentsImagesAndFiles asChildOf: davElement (@"write", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"bind", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-post", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-post-vevent", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-post", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-post-vtodo", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-post", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-post-vjournal", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-post", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-post-vfreebusy", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-post", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-deliver", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-deliver-vevent", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-deliver", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-deliver-vtodo", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-deliver", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-deliver-vjournal", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-deliver", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-deliver-vfreebusy", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-deliver", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-respond", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-respond-vevent", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-respond", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"schedule-respond-vtodo", XMLNS_CALDAV) abstract: NO withEquivalent: nil asChildOf: davElement (@"schedule-respond", XMLNS_CALDAV)]; [webdavAclManager registerDAVPermission: davElement (@"unbind", XMLNS_WEBDAV) abstract: NO withEquivalent: SoPerm_DeleteObjects asChildOf: davElement (@"write", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"write-properties", XMLNS_WEBDAV) abstract: YES withEquivalent: SoPerm_ChangePermissions /* hackish */ asChildOf: davElement (@"write", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"write-content", XMLNS_WEBDAV) abstract: YES withEquivalent: nil asChildOf: davElement (@"write", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"admin", nsI) abstract: YES withEquivalent: nil asChildOf: davElement (@"all", XMLNS_WEBDAV)]; [webdavAclManager registerDAVPermission: davElement (@"read-acl", XMLNS_WEBDAV) abstract: YES withEquivalent: SOGoPerm_ReadAcls asChildOf: davElement (@"admin", nsI)]; [webdavAclManager registerDAVPermission: davElement (@"write-acl", XMLNS_WEBDAV) abstract: YES withEquivalent: SoPerm_ChangePermissions asChildOf: davElement (@"admin", nsI)]; } return webdavAclManager; } - (id) initWithName: (NSString *) name inContainer: (id) newContainer { if ((self = [super initWithName: name inContainer: newContainer])) { timeZone = [[context activeUser] timeZone]; aclMatrix = [NSMutableDictionary new]; stripFields = nil; uidToFilename = nil; } return self; } - (void) dealloc { [aclMatrix release]; [stripFields release]; [uidToFilename release]; [super dealloc]; } - (Class) objectClassForComponentName: (NSString *) componentName { Class objectClass; if ([componentName isEqualToString: @"vevent"]) objectClass = [SOGoAppointmentObject class]; else if ([componentName isEqualToString: @"vtodo"]) objectClass = [SOGoTaskObject class]; else objectClass = Nil; return objectClass; } - (NSString *) calendarColor { NSUserDefaults *settings; NSDictionary *colors; NSString *color; settings = [[context activeUser] userSettings]; colors = [[settings objectForKey: @"Calendar"] objectForKey: @"FolderColors"]; color = [colors objectForKey: [self folderReference]]; if (!color) color = defaultColor; return color; } - (void) setCalendarColor: (NSString *) newColor { NSUserDefaults *settings; NSMutableDictionary *calendarSettings; NSMutableDictionary *colors; settings = [[context activeUser] userSettings]; calendarSettings = [settings objectForKey: @"Calendar"]; if (!calendarSettings) { calendarSettings = [NSMutableDictionary dictionary]; [settings setObject: calendarSettings forKey: @"Calendar"]; } colors = [calendarSettings objectForKey: @"FolderColors"]; if (!colors) { colors = [NSMutableDictionary dictionary]; [calendarSettings setObject: colors forKey: @"FolderColors"]; } [colors setObject: newColor forKey: [self folderReference]]; [settings synchronize]; } - (NSString *) syncTag { NSUserDefaults *settings; NSDictionary *syncTags; NSString *syncTag; settings = [[context activeUser] userSettings]; syncTags = [[settings objectForKey: @"Calendar"] objectForKey: @"FolderSyncTags"]; syncTag = [syncTags objectForKey: [self folderReference]]; if (!syncTag) syncTag = @""; return syncTag; } #warning this code shares a lot with the colour code - (void) setSyncTag: (NSString *) newSyncTag { NSUserDefaults *settings; NSMutableDictionary *calendarSettings; NSMutableDictionary *syncTags; settings = [[context activeUser] userSettings]; calendarSettings = [settings objectForKey: @"Calendar"]; if (!calendarSettings) { calendarSettings = [NSMutableDictionary dictionary]; [settings setObject: calendarSettings forKey: @"Calendar"]; } syncTags = [calendarSettings objectForKey: @"FolderSyncTags"]; if (!syncTags) { syncTags = [NSMutableDictionary dictionary]; [calendarSettings setObject: syncTags forKey: @"FolderSyncTags"]; } [syncTags setObject: newSyncTag forKey: [self folderReference]]; [settings synchronize]; } /* logging */ - (id) debugLogger { return logger; } /* selection */ - (NSArray *) calendarUIDs { /* this is used for group calendars (this folder just returns itself) */ NSString *s; s = [[self container] nameInContainer]; // [self logWithFormat:@"CAL UID: %@", s]; return [s isNotNull] ? [NSArray arrayWithObjects:&s count:1] : nil; } /* fetching */ - (NSString *) _sqlStringForComponent: (id) _component { NSString *sqlString; NSArray *components; if (_component) { if ([_component isKindOfClass: [NSArray class]]) components = _component; else components = [NSArray arrayWithObject: _component]; sqlString = [NSString stringWithFormat: @"AND (c_component = '%@')", [components componentsJoinedByString: @"' OR c_component = '"]]; } else sqlString = @""; return sqlString; } - (NSString *) _sqlStringRangeFrom: (NSCalendarDate *) _startDate to: (NSCalendarDate *) _endDate { unsigned int start, end; start = (unsigned int) [_startDate timeIntervalSince1970]; end = (unsigned int) [_endDate timeIntervalSince1970]; return [NSString stringWithFormat: @" AND (c_startdate <= %u) AND (c_enddate >= %u)", end, start]; } - (NSString *) _privacyClassificationStringsForUID: (NSString *) uid { NSMutableString *classificationString; NSString *currentRole; unsigned int counter; iCalAccessClass classes[] = {iCalAccessPublic, iCalAccessPrivate, iCalAccessConfidential}; classificationString = [NSMutableString string]; for (counter = 0; counter < 3; counter++) { currentRole = [self roleForComponentsWithAccessClass: classes[counter] forUser: uid]; if ([currentRole length] > 0) [classificationString appendFormat: @"c_classification = %d or ", classes[counter]]; } return classificationString; } - (NSString *) _privacySqlString { NSString *privacySqlString, *email, *login; SOGoUser *activeUser; activeUser = [context activeUser]; if (activeUserIsOwner || [[self ownerInContext: context] isEqualToString: [[context activeUser] login]] || [[activeUser rolesForObject: self inContext: context] containsObject: SoRole_Owner]) privacySqlString = @""; else if ([[activeUser login] isEqualToString: @"freebusy"]) privacySqlString = @"and (c_isopaque = 1)"; else { #warning we do not manage all the possible user emails email = [[activeUser primaryIdentity] objectForKey: @"email"]; login = [activeUser login]; privacySqlString = [NSString stringWithFormat: @"(%@(c_orgmail = '%@')" @" or ((c_partmails caseInsensitiveLike '%@%%'" @" or c_partmails caseInsensitiveLike '%%\n%@%%')))", [self _privacyClassificationStringsForUID: login], email, email, email]; } return privacySqlString; } - (NSArray *) bareFetchFields: (NSArray *) fields from: (NSCalendarDate *) startDate to: (NSCalendarDate *) endDate title: (NSString *) title component: (id) component additionalFilters: (NSString *) filters { EOQualifier *qualifier; GCSFolder *folder; NSString *sql, *dateSqlString, *titleSqlString, *componentSqlString, *filterSqlString; NGCalendarDateRange *r; folder = [self ocsFolder]; if (startDate && endDate) { r = [NGCalendarDateRange calendarDateRangeWithStartDate: startDate endDate: endDate]; dateSqlString = [self _sqlStringRangeFrom: startDate to: endDate]; } else { r = nil; dateSqlString = @""; } if ([title length]) titleSqlString = [NSString stringWithFormat: @"AND (c_title" @" isCaseInsensitiveLike: '%%%@%%')", title]; else titleSqlString = @""; componentSqlString = [self _sqlStringForComponent: component]; if (filters) filterSqlString = [NSString stringWithFormat: @"AND (%@)", filters]; else filterSqlString = @""; /* prepare mandatory fields */ sql = [[NSString stringWithFormat: @"%@%@%@%@", dateSqlString, titleSqlString, componentSqlString, filterSqlString] substringFromIndex: 4]; /* fetch non-recurrent apts first */ qualifier = [EOQualifier qualifierWithQualifierFormat: sql]; return [folder fetchFields: fields matchingQualifier: qualifier]; } - (BOOL) _checkIfWeCanRememberRecords: (NSArray *) fields { return ([fields containsObject: @"c_name"] && [fields containsObject: @"c_version"] && [fields containsObject: @"c_lastmodified"] && [fields containsObject: @"c_creationdate"] && [fields containsObject: @"c_component"]); } - (void) _rememberRecords: (NSArray *) records { NSEnumerator *recordsEnum; NSDictionary *currentRecord; recordsEnum = [records objectEnumerator]; while ((currentRecord = [recordsEnum nextObject])) [childRecords setObject: currentRecord forKey: [currentRecord objectForKey: @"c_name"]]; } - (NSMutableDictionary *) fixupRecord: (NSDictionary *) _record fetchRange: (NGCalendarDateRange *) _r { NSMutableDictionary *md; static NSString *fields[] = { @"c_startdate", @"startDate", @"c_enddate", @"endDate" }; unsigned int count; NSCalendarDate *date; NSNumber *dateValue; md = [[_record mutableCopy] autorelease]; for (count = 0; count < 2; count++) { dateValue = [_record objectForKey: fields[count * 2]]; if (dateValue) { date = [NSCalendarDate dateWithTimeIntervalSince1970: (NSTimeInterval) [dateValue unsignedIntValue]]; if (date) { [date setTimeZone: timeZone]; [md setObject: date forKey: fields[count * 2 + 1]]; } } else [self logWithFormat:@"missing '%@' in record?", fields[count * 2]]; } return md; } - (NSArray *) fixupRecords: (NSArray *) records fetchRange: (NGCalendarDateRange *) r { // TODO: is the result supposed to be sorted by date? NSMutableArray *ma; unsigned count, max; id row; // TODO: what is the type of the record? if (records) { max = [records count]; ma = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { row = [self fixupRecord: [records objectAtIndex: count] fetchRange: r]; if (row) [ma addObject: row]; } } else ma = nil; return ma; } - (NSMutableDictionary *) fixupCycleRecord: (NSDictionary *) _record cycleRange: (NGCalendarDateRange *) _r { NSMutableDictionary *md; id tmp; md = [[_record mutableCopy] autorelease]; /* cycle is in _r. We also have to override the c_startdate/c_enddate with the date values of the reccurence since we use those when displaying events in SOGo Web */ tmp = [_r startDate]; [tmp setTimeZone: timeZone]; [md setObject:tmp forKey:@"startDate"]; [md setObject: [NSNumber numberWithInt: [tmp timeIntervalSince1970]] forKey: @"c_startdate"]; tmp = [_r endDate]; [tmp setTimeZone: timeZone]; [md setObject:tmp forKey:@"endDate"]; [md setObject: [NSNumber numberWithInt: [tmp timeIntervalSince1970]] forKey: @"c_enddate"]; return md; } - (void) _flattenCycleRecord: (NSDictionary *) _row forRange: (NGCalendarDateRange *) _r intoArray: (NSMutableArray *) _ma { NSMutableDictionary *row, *fixedRow; NSDictionary *cycleinfo; NSCalendarDate *startDate, *endDate; NGCalendarDateRange *fir, *rRange; NSArray *rules, *exRules, *exDates, *ranges; unsigned i, count; NSString *content; content = [_row objectForKey: @"c_cycleinfo"]; if (![content isNotNull]) { [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row]; return; } cycleinfo = [content propertyList]; if (!cycleinfo) { [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row]; return; } row = [self fixupRecord:_row fetchRange: _r]; [row removeObjectForKey: @"c_cycleinfo"]; [row setObject: sharedYes forKey: @"isRecurrentEvent"]; startDate = [row objectForKey: @"startDate"]; endDate = [row objectForKey: @"endDate"]; fir = [NGCalendarDateRange calendarDateRangeWithStartDate: startDate endDate: endDate]; rules = [cycleinfo objectForKey: @"rules"]; exRules = [cycleinfo objectForKey: @"exRules"]; exDates = [cycleinfo objectForKey: @"exDates"]; ranges = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: _r firstInstanceCalendarDateRange: fir recurrenceRules: rules exceptionRules: exRules exceptionDates: exDates]; count = [ranges count]; for (i = 0; i < count; i++) { rRange = [ranges objectAtIndex:i]; fixedRow = [self fixupCycleRecord: row cycleRange: rRange]; if (fixedRow) [_ma addObject:fixedRow]; } } - (NSArray *) fixupCyclicRecords: (NSArray *) _records fetchRange: (NGCalendarDateRange *) _r { // TODO: is the result supposed to be sorted by date? NSMutableArray *ma; NSDictionary *row; unsigned int i, count; count = [_records count]; ma = [NSMutableArray arrayWithCapacity: count]; if (count > 0) for (i = 0; i < count; i++) { row = [_records objectAtIndex: i]; [self _flattenCycleRecord: row forRange: _r intoArray: ma]; } return ma; } - (void) _buildStripFieldsFromFields: (NSArray *) fields { stripFields = [[NSMutableArray alloc] initWithCapacity: [fields count]]; [stripFields setArray: fields]; [stripFields removeObjectsInArray: [NSArray arrayWithObjects: @"c_name", @"c_uid", @"c_startdate", @"c_enddate", @"c_isallday", @"c_iscycle", @"c_classification", @"c_component", nil]]; } - (void) _fixupProtectedInformation: (NSEnumerator *) ma inFields: (NSArray *) fields forUser: (NSString *) uid { NSMutableDictionary *currentRecord; NSString *roles[] = {nil, nil, nil}; iCalAccessClass accessClass; NSString *fullRole, *role; if (!stripFields) [self _buildStripFieldsFromFields: fields]; #warning we do not take the participation status into account while ((currentRecord = [ma nextObject])) { accessClass = [[currentRecord objectForKey: @"c_classification"] intValue]; role = roles[accessClass]; if (!role) { fullRole = [self roleForComponentsWithAccessClass: accessClass forUser: uid]; if ([fullRole length] > 9) role = [fullRole substringFromIndex: 9]; roles[accessClass] = role; } if ([role isEqualToString: @"DAndTViewer"]) [currentRecord removeObjectsForKeys: stripFields]; } } - (NSArray *) fetchFields: (NSArray *) _fields from: (NSCalendarDate *) _startDate to: (NSCalendarDate *) _endDate title: (NSString *) title component: (id) _component additionalFilters: (NSString *) filters { EOQualifier *qualifier; GCSFolder *folder; NSMutableArray *fields, *ma = nil; NSArray *records; NSString *sql, *dateSqlString, *titleSqlString, *componentSqlString, *privacySqlString, *currentLogin, *filterSqlString; NGCalendarDateRange *r; BOOL rememberRecords; rememberRecords = [self _checkIfWeCanRememberRecords: _fields]; // if (rememberRecords) // NSLog (@"we will remember those records!"); folder = [self ocsFolder]; if (!folder) { [self errorWithFormat:@"(%s): missing folder for fetch!", __PRETTY_FUNCTION__]; return nil; } if (_startDate && _endDate) { r = [NGCalendarDateRange calendarDateRangeWithStartDate: _startDate endDate: _endDate]; dateSqlString = [self _sqlStringRangeFrom: _startDate to: _endDate]; } else { r = nil; dateSqlString = @""; } if ([title length]) titleSqlString = [NSString stringWithFormat: @"AND (c_title" @" isCaseInsensitiveLike: '%%%@%%')", title]; else titleSqlString = @""; componentSqlString = [self _sqlStringForComponent: _component]; privacySqlString = [self _privacySqlString]; if (filters) filterSqlString = [NSString stringWithFormat: @"AND (%@)", filters]; else filterSqlString = @""; /* prepare mandatory fields */ fields = [NSMutableArray arrayWithArray: _fields]; [fields addObject: @"c_uid"]; [fields addObject: @"c_startdate"]; [fields addObject: @"c_enddate"]; if (logger) [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate]; sql = [NSString stringWithFormat: @"(c_iscycle = 0)%@%@%@%@%@", dateSqlString, titleSqlString, componentSqlString, privacySqlString, filterSqlString]; /* fetch non-recurrent apts first */ qualifier = [EOQualifier qualifierWithQualifierFormat: sql]; records = [folder fetchFields: fields matchingQualifier: qualifier]; if (records) { if (r) records = [self fixupRecords: records fetchRange: r]; if (logger) [self debugWithFormat: @"fetched %i records: %@", [records count], records]; ma = [NSMutableArray arrayWithArray: records]; } /* fetch recurrent apts now. we do NOT consider the date range when doing that as the c_startdate/c_enddate of a recurring event is always set to the first recurrence - others are generated on the fly */ sql = [NSString stringWithFormat: @"(c_iscycle = 1)%@%@%@%@", titleSqlString, componentSqlString, privacySqlString, filterSqlString]; qualifier = [EOQualifier qualifierWithQualifierFormat: sql]; records = [folder fetchFields: fields matchingQualifier: qualifier]; if (records) { if (r) records = [self fixupCyclicRecords: records fetchRange: r]; if (!ma) ma = [NSMutableArray arrayWithCapacity: [records count]]; [ma addObjectsFromArray: records]; } else if (!ma) { [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__]; return nil; } if (logger) [self debugWithFormat:@"returning %i records", [ma count]]; currentLogin = [[context activeUser] login]; if (![currentLogin isEqualToString: owner]) [self _fixupProtectedInformation: [ma objectEnumerator] inFields: _fields forUser: currentLogin]; // [ma makeObjectsPerform: @selector (setObject:forKey:) // withObject: owner // withObject: @"owner"]; if (rememberRecords) [self _rememberRecords: ma]; return ma; } - (void) _appendPropstat: (NSDictionary *) propstat toResponse: (WOResponse *) r { NSArray *properties; unsigned int count, max; [r appendContentString: @""]; properties = [propstat objectForKey: @"properties"]; max = [properties count]; for (count = 0; count < max; count++) [r appendContentString: [properties objectAtIndex: count]]; [r appendContentString: @""]; [r appendContentString: [propstat objectForKey: @"status"]]; [r appendContentString: @""]; } #warning we should use the EOFetchSpecification for that!!! (see doPROPFIND:) #warning components in calendar-data query are ignored static inline SEL _selectorForProperty (NSString *property) { static NSMutableDictionary *methodMap = nil; SEL propSel; NSValue *propPtr; NSDictionary *map; NSString *methodName; if (!methodMap) methodMap = [NSMutableDictionary new]; propPtr = [methodMap objectForKey: property]; if (propPtr) propSel = [propPtr pointerValue]; else { map = [sogoAppointmentFolderKlass defaultWebDAVAttributeMap]; methodName = [map objectForKey: property]; if (methodName) { propSel = NSSelectorFromString (methodName); if (propSel) [methodMap setObject: [NSValue valueWithPointer: propSel] forKey: property]; } } return propSel; } - (NSString *) _nodeTagForProperty: (NSString *) property { NSString *namespace, *nodeName, *nsRep; NSRange nsEnd; nsEnd = [property rangeOfString: @"}"]; namespace = [property substringFromRange: NSMakeRange (1, nsEnd.location - 1)]; nodeName = [property substringFromIndex: nsEnd.location + 1]; if ([namespace isEqualToString: XMLNS_CALDAV]) nsRep = @"C"; else nsRep = @"D"; return [NSString stringWithFormat: @"%@:%@", nsRep, nodeName]; } - (NSString *) _nodeTag: (NSString *) property { static NSMutableDictionary *tags = nil; NSString *nodeTag; if (!tags) tags = [NSMutableDictionary new]; nodeTag = [tags objectForKey: property]; if (!nodeTag) { nodeTag = [self _nodeTagForProperty: property]; [tags setObject: nodeTag forKey: property]; } return nodeTag; } - (NSString **) _properties: (NSString **) properties ofObject: (NSDictionary *) object { NSString **currentProperty; NSString **values, **currentValue; SOGoObject *sogoObject; SoSecurityManager *mgr; SEL methodSel; #warning things may crash here... values = calloc (100, sizeof (NSMutableString *)); // NSLog (@"_properties:ofObject:: %@", [NSDate date]); #warning this check should be done directly in the query... we should fix this sometime mgr = [SoSecurityManager sharedSecurityManager]; sogoObject = [self _createChildComponentWithRecord: object]; if (activeUserIsOwner || [[self ownerInContext: context] isEqualToString: [[context activeUser] login]] || !([mgr validatePermission: SOGoPerm_AccessObject onObject: sogoObject inContext: context])) { currentProperty = properties; currentValue = values; while (*currentProperty) { methodSel = _selectorForProperty (*currentProperty); if (methodSel && [sogoObject respondsToSelector: methodSel]) *currentValue = [[sogoObject performSelector: methodSel] stringByEscapingXMLString]; currentProperty++; currentValue++; } } // NSLog (@"/_properties:ofObject:: %@", [NSDate date]); return values; } - (NSArray *) _propstats: (NSString **) properties ofObject: (NSDictionary *) object { NSMutableArray *propstats, *properties200, *properties404, *propDict; NSString **property, **values, **currentValue; NSString *propertyValue, *nodeTag; // NSLog (@"_propstats:ofObject:: %@", [NSDate date]); propstats = [NSMutableArray array]; properties200 = [NSMutableArray new]; properties404 = [NSMutableArray new]; values = [self _properties: properties ofObject: object]; currentValue = values; property = properties; while (*property) { nodeTag = [self _nodeTag: *property]; if (*currentValue) { propertyValue = [NSString stringWithFormat: @"<%@>%@", nodeTag, *currentValue, nodeTag]; propDict = properties200; } else { propertyValue = [NSString stringWithFormat: @"<%@/>", nodeTag]; propDict = properties404; } [propDict addObject: propertyValue]; property++; currentValue++; } free (values); if ([properties200 count]) [propstats addObject: [NSDictionary dictionaryWithObjectsAndKeys: properties200, @"properties", @"HTTP/1.1 200 OK", @"status", nil]]; [properties200 release]; if ([properties404 count]) [propstats addObject: [NSDictionary dictionaryWithObjectsAndKeys: properties404, @"properties", @"HTTP/1.1 404 Not Found", @"status", nil]]; [properties404 release]; // NSLog (@"/_propstats:ofObject:: %@", [NSDate date]); return propstats; } #warning We need to use the new DAV utilities here... - (void) appendObject: (NSDictionary *) object properties: (NSString **) properties withBaseURL: (NSString *) baseURL toComplexResponse: (WOResponse *) r { NSArray *propstats; unsigned int count, max; [r appendContentString: @""]; [r appendContentString: baseURL]; // if (![baseURL hasSuffix: @"/"]) // [r appendContentString: @"/"]; [r appendContentString: [object objectForKey: @"c_name"]]; [r appendContentString: @""]; // NSLog (@"(appendPropstats...): %@", [NSDate date]); propstats = [self _propstats: properties ofObject: object]; max = [propstats count]; for (count = 0; count < max; count++) [self _appendPropstat: [propstats objectAtIndex: count] toResponse: r]; // NSLog (@"/(appendPropstats...): %@", [NSDate date]); [r appendContentString: @"\r\n"]; } - (void) appendMissingObjectRef: (NSString *) href toComplexResponse: (WOResponse *) r { [r appendContentString: @""]; [r appendContentString: href]; [r appendContentString: @"HTTP/1.1 404 Not Found\r\n"]; } - (void) _appendTimeRange: (id ) timeRangeElement toFilter: (NSMutableDictionary *) filter { NSCalendarDate *parsedDate; parsedDate = [[timeRangeElement attribute: @"start"] asCalendarDate]; [filter setObject: parsedDate forKey: @"start"]; parsedDate = [[timeRangeElement attribute: @"end"] asCalendarDate]; [filter setObject: parsedDate forKey: @"end"]; } #warning This method lacks support for timeranges - (void) _appendPropertyFilter: (id ) propFilter toFilter: (NSMutableDictionary *) filter { NSString *propName, *textMatch; id matches; propName = [[propFilter attribute: @"name"] lowercaseString]; matches = [propFilter getElementsByTagName: @"text-match"]; if ([matches length]) textMatch = [[matches objectAtIndex: 0] textValue]; else { matches = [propFilter getElementsByTagName: @"is-not-defined"]; if ([matches length]) textMatch = @"NULL"; else textMatch = @""; } [filter setObject: textMatch forKey: propName]; } - (NSDictionary *) _parseCalendarFilter: (id ) filterElement { NSMutableDictionary *filterData; id parentNode; id elements; NSString *componentName; parentNode = (id ) [filterElement parentNode]; if ([[parentNode tagName] isEqualToString: @"comp-filter"] && [[parentNode attribute: @"name"] isEqualToString: @"VCALENDAR"]) { componentName = [[filterElement attribute: @"name"] lowercaseString]; filterData = [NSMutableDictionary dictionary]; [filterData setObject: componentName forKey: @"name"]; elements = [filterElement getElementsByTagName: @"time-range"]; if ([elements length]) [self _appendTimeRange: [elements objectAtIndex: 0] toFilter: filterData]; elements = [filterElement getElementsByTagName: @"prop-filter"]; if ([elements length]) [self _appendPropertyFilter: [elements objectAtIndex: 0] toFilter: filterData]; } else filterData = nil; return filterData; } - (NSArray *) _parseRequestedProperties: (id ) parentNode { NSMutableArray *properties; NSObject *propList, *children; NSObject *currentChild; unsigned int count, max, count2, max2; NSString *flatProperty; // NSLog (@"parseRequestProperties: %@", [NSDate date]); properties = [NSMutableArray array]; propList = [parentNode getElementsByTagName: @"prop"]; max = [propList length]; for (count = 0; count < max; count++) { children = [[propList objectAtIndex: count] childNodes]; max2 = [children length]; for (count2 = 0; count2 < max2; count2++) { currentChild = [children objectAtIndex: count2]; if ([currentChild conformsToProtocol: @protocol (DOMElement)]) { flatProperty = [NSString stringWithFormat: @"{%@}%@", [currentChild namespaceURI], [currentChild nodeName]]; [properties addObject: flatProperty]; } } // while ([children hasChildNodes]) // [properties addObject: [children next]]; } // NSLog (@"/parseRequestProperties: %@", [NSDate date]); return properties; } - (NSArray *) _parseCalendarFilters: (id ) parentNode { id children; id node; NSMutableArray *filters; NSDictionary *filter; unsigned int count, max; // NSLog (@"parseCalendarFilter: %@", [NSDate date]); filters = [NSMutableArray array]; children = [parentNode getElementsByTagName: @"comp-filter"]; max = [children length]; for (count = 0; count < max; count++) { node = [children objectAtIndex: count]; filter = [self _parseCalendarFilter: node]; if (filter) [filters addObject: filter]; } // NSLog (@"/parseCalendarFilter: %@", [NSDate date]); return filters; } - (NSString *) _additionalFilterKey: (NSString *) key value: (NSString *) value { NSString *filterString; if ([value length]) { if ([value isEqualToString: @"NULL"]) filterString = [NSString stringWithFormat: @"(%@ = '')", key]; else filterString = [NSString stringWithFormat: @"(%@ like '%%%@%%')", key, value]; } else filterString = [NSString stringWithFormat: @"(%@ != '')", key]; return filterString; } - (NSString *) _composeAdditionalFilters: (NSDictionary *) filter { NSString *additionalFilter; NSEnumerator *keys; NSString *currentKey, *keyField, *filterString; static NSArray *fields = nil; NSMutableArray *filters; #warning the list of fields should be taken from the .ocs description file if (!fields) { fields = [NSArray arrayWithObject: @"c_uid"]; [fields retain]; } filters = [NSMutableArray new]; keys = [[filter allKeys] objectEnumerator]; while ((currentKey = [keys nextObject])) { keyField = [NSString stringWithFormat: @"c_%@", currentKey]; if ([fields containsObject: keyField]) { filterString = [self _additionalFilterKey: keyField value: [filter objectForKey: currentKey]]; [filters addObject: filterString]; } } if ([filters count]) additionalFilter = [filters componentsJoinedByString: @" and "]; else additionalFilter = nil; [filters release]; return additionalFilter; } #warning this is baddddd because we return a single-valued dictionary containing \ a cname which may not event exist... the logic behind appendObject:... should be \ rethought, especially since we may start using SQL views - (NSString *) davCalendarColor { NSString *color; color = [[self calendarColor] uppercaseString]; return [NSString stringWithFormat: @"%@FF", color]; } - (NSException *) setDavCalendarColor: (NSString *) newColor { NSException *error; NSString *realColor; if ([newColor length] == 9 && [newColor hasPrefix: @"#"]) { realColor = [newColor substringToIndex: 7]; [self setCalendarColor: realColor]; error = nil; } else error = [NSException exceptionWithHTTPStatus: 400 reason: @"Bad color format (should be '#XXXXXXXX')."]; return error; } - (void) _appendComponentProperties: (NSString **) properties matchingFilters: (NSArray *) filters toResponse: (WOResponse *) response { NSArray *apts; NSDictionary *currentFilter; NSEnumerator *filterList; NSString *additionalFilters, *baseURL; unsigned int count, max; baseURL = [[self davURL] absoluteString]; filterList = [filters objectEnumerator]; while ((currentFilter = [filterList nextObject])) { additionalFilters = [self _composeAdditionalFilters: currentFilter]; // NSLog(@"query"); apts = [self bareFetchFields: reportQueryFields from: [currentFilter objectForKey: @"start"] to: [currentFilter objectForKey: @"end"] title: [currentFilter objectForKey: @"title"] component: [currentFilter objectForKey: @"name"] additionalFilters: additionalFilters]; // NSLog(@"adding properties"); max = [apts count]; for (count = 0; count < max; count++) [self appendObject: [apts objectAtIndex: count] properties: properties withBaseURL: baseURL toComplexResponse: response]; // NSLog(@"done"); } } - (id) davCalendarQuery: (id) queryContext { WOResponse *r; id document; id documentElement; NSString **properties; r = [context response]; [r setStatus: 207]; [r setContentEncoding: NSUTF8StringEncoding]; [r setHeader: @"text/xml; charset=\"utf-8\"" forKey: @"content-type"]; [r setHeader: @"no-cache" forKey: @"pragma"]; [r setHeader: @"no-cache" forKey: @"cache-control"]; [r appendContentString:@"\r\n"]; [r appendContentString: @"\r\n"]; document = [[context request] contentAsDOMDocument]; documentElement = [document documentElement]; properties = [[self _parseRequestedProperties: documentElement] asPointersOfObjects]; [self _appendComponentProperties: properties matchingFilters: [self _parseCalendarFilters: documentElement] toResponse: r]; [r appendContentString:@"\r\n"]; free (properties); return r; } - (NSDictionary *) _deduceObjectNamesFromURLs: (NSArray *) urls { unsigned int count, max; NSString *url, *componentURLPath, *cName, *baseURLString; NSMutableDictionary *cNames; NSURL *componentURL, *baseURL; NSArray *urlComponents; max = [urls count]; cNames = [NSMutableDictionary dictionaryWithCapacity: max]; baseURL = [self davURL]; baseURLString = [baseURL absoluteString]; for (count = 0; count < max; count++) { url = [urls objectAtIndex: count]; componentURL = [[NSURL URLWithString: url relativeToURL: baseURL] standardizedURL]; componentURLPath = [componentURL absoluteString]; if ([componentURLPath rangeOfString: baseURLString].location != NSNotFound) { urlComponents = [componentURLPath componentsSeparatedByString: @"/"]; cName = [urlComponents objectAtIndex: [urlComponents count] - 1]; [cNames setObject: url forKey: cName]; } } return cNames; } - (NSArray *) _fetchComponentsWithNames: (NSArray *) cNames { NSMutableString *filterString; NSArray *records; // NSLog (@"fetchComponentsWithNames"); filterString = [NSMutableString new]; [filterString appendFormat: @"c_name='%@'", [cNames componentsJoinedByString: @"' OR c_name='"]]; // NSLog (@"fetchComponentsWithNames: query"); records = [self bareFetchFields: reportQueryFields from: nil to: nil title: nil component: nil additionalFilters: filterString]; [filterString release]; // NSLog (@"/fetchComponentsWithNames"); return records; } #define maxQuerySize 2500 #define baseQuerySize 160 #define idQueryOverhead 13 - (NSArray *) _fetchComponentsMatchingObjectNames: (NSArray *) cNames { NSMutableArray *components; NSArray *records; NSMutableArray *currentNames; unsigned int count, max, currentSize, queryNameLength; NSString *currentName; // NSLog (@"fetching components matching names"); currentNames = [NSMutableArray new]; currentSize = baseQuerySize; max = [cNames count]; components = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { currentName = [cNames objectAtIndex: count]; queryNameLength = idQueryOverhead + [currentName length]; if ((currentSize + queryNameLength) > maxQuerySize) { records = [self _fetchComponentsWithNames: currentNames]; [components addObjectsFromArray: records]; [currentNames removeAllObjects]; currentSize = baseQuerySize; } [currentNames addObject: currentName]; currentSize += queryNameLength; } records = [self _fetchComponentsWithNames: currentNames]; [components addObjectsFromArray: records]; [currentNames release]; // NSLog (@"/fetching components matching names"); return components; } - (NSDictionary *) _fetchComponentsMatchingURLs: (NSArray *) urls { NSMutableDictionary *components; NSDictionary *cnames, *record; NSString *recordURL; NSArray *records; unsigned int count, max; components = [NSMutableDictionary dictionary]; cnames = [self _deduceObjectNamesFromURLs: urls]; records = [self _fetchComponentsMatchingObjectNames: [cnames allKeys]]; max = [records count]; for (count = 0; count < max; count++) { record = [records objectAtIndex: count]; recordURL = [cnames objectForKey: [record objectForKey: @"c_name"]]; [components setObject: record forKey: recordURL]; } return components; } - (void) _appendComponentProperties: (NSString **) properties matchingURLs: (id ) refs toResponse: (WOResponse *) response { NSObject *element; NSDictionary *currentComponent, *components; NSString *currentURL, *baseURL; NSMutableArray *urls; unsigned int count, max; baseURL = [[self davURL] absoluteString]; urls = [NSMutableArray new]; max = [refs length]; for (count = 0; count < max; count++) { element = [refs objectAtIndex: count]; currentURL = [[element firstChild] nodeValue]; [urls addObject: currentURL]; } components = [self _fetchComponentsMatchingURLs: urls]; max = [urls count]; // NSLog (@"adding properties with url"); for (count = 0; count < max; count++) { currentComponent = [components objectForKey: [urls objectAtIndex: count]]; if (currentComponent) [self appendObject: currentComponent properties: properties withBaseURL: baseURL toComplexResponse: response]; else [self appendMissingObjectRef: currentURL toComplexResponse: response]; } // NSLog (@"/adding properties with url"); [urls release]; } - (id) davCalendarMultiget: (id) queryContext { WOResponse *r; id document; id documentElement; NSString **properties; r = [context response]; [r setStatus: 207]; [r setContentEncoding: NSUTF8StringEncoding]; [r setHeader: @"text/xml; charset=\"utf-8\"" forKey: @"content-type"]; [r setHeader: @"no-cache" forKey: @"pragma"]; [r setHeader: @"no-cache" forKey: @"cache-control"]; [r appendContentString:@"\r\n"]; [r appendContentString: @"\r\n"]; document = [[context request] contentAsDOMDocument]; documentElement = [document documentElement]; properties = [[self _parseRequestedProperties: documentElement] asPointersOfObjects]; [self _appendComponentProperties: properties matchingURLs: [documentElement getElementsByTagName: @"href"] toResponse: r]; [r appendContentString:@"\r\n"]; free (properties); return r; } - (Class) objectClassForContent: (NSString *) content { iCalCalendar *calendar; NSArray *elements; NSString *firstTag; Class objectClass; objectClass = Nil; calendar = [iCalCalendar parseSingleFromSource: content]; if (calendar) { elements = [calendar allObjects]; if ([elements count]) { firstTag = [[[elements objectAtIndex: 0] tag] uppercaseString]; if ([firstTag isEqualToString: @"VEVENT"]) objectClass = [SOGoAppointmentObject class]; else if ([firstTag isEqualToString: @"VTODO"]) objectClass = [SOGoTaskObject class]; } } return objectClass; } - (BOOL) requestNamedIsHandledLater: (NSString *) name { return [name isEqualToString: @"OPTIONS"]; } - (id) lookupComponentByUID: (NSString *) uid { NSString *filename; id component; filename = [self resourceNameForEventUID: uid]; if (filename) { component = [self lookupName: filename inContext: context acquire: NO]; if ([component isKindOfClass: [NSException class]]) component = nil; } else component = nil; return nil; } - (id) lookupName: (NSString *)_key inContext: (id)_ctx acquire: (BOOL)_flag { id obj; NSString *url; BOOL handledLater; /* first check attributes directly bound to the application */ handledLater = [self requestNamedIsHandledLater: _key]; if (handledLater) obj = nil; else { obj = [super lookupName:_key inContext:_ctx acquire:NO]; if (!obj) { if ([self isValidContentName:_key]) { url = [[[_ctx request] uri] urlWithoutParameters]; if ([url hasSuffix: @"AsTask"]) obj = [SOGoTaskObject objectWithName: _key inContainer: self]; else if ([url hasSuffix: @"AsAppointment"]) obj = [SOGoAppointmentObject objectWithName: _key inContainer: self]; } } if (!obj) obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */]; } if (obj) [[SOGoCache sharedCache] registerObject: obj withName: _key inContainer: container]; return obj; } - (NSDictionary *) freebusyResponseForRecipient: (NSString *) recipient withUser: (SOGoUser *) user andCalendarData: (NSString *) calendarData { NSDictionary *response; NSMutableArray *content; content = [NSMutableArray new]; [content addObject: davElementWithContent (@"recipient", XMLNS_CALDAV, recipient)]; if (user) { [content addObject: davElementWithContent (@"request-status", XMLNS_CALDAV, @"2.0;Success")]; [content addObject: davElementWithContent (@"calendar-data", XMLNS_CALDAV, calendarData)]; } else [content addObject: davElementWithContent (@"request-status", XMLNS_CALDAV, @"3.7;Invalid Calendar User")]; response = davElementWithContent (@"response", XMLNS_CALDAV, content); [content release]; return response; } - (NSDictionary *) caldavFreeBusyRequestOnRecipient: (NSString *) recipient from: (NSCalendarDate *) start to: (NSCalendarDate *) to { LDAPUserManager *um; SOGoUser *user; NSString *lRecipient, *login, *calendarData; SOGoFreeBusyObject *freebusy; user = nil; calendarData = nil; lRecipient = [recipient lowercaseString]; if ([lRecipient hasPrefix: @"mailto:"]) { um = [LDAPUserManager sharedUserManager]; login = [um getUIDForEmail: [lRecipient substringFromIndex: 7]]; if ([login length]) { user = [SOGoUser userWithLogin: login roles: nil]; freebusy = [[user homeFolderInContext: context] freeBusyObject: @"freebusy.ifb" inContext: context]; calendarData = [freebusy contentAsStringWithMethod: @"REPLY" from: start to: to]; } } return [self freebusyResponseForRecipient: recipient withUser: user andCalendarData: calendarData]; } - (NSDictionary *) caldavFreeBusyRequest: (iCalFreeBusy *) freebusy from: (NSString *) originator to: (NSArray *) recipients { NSDictionary *responseElement; NSMutableArray *elements; NSString *recipient; unsigned int count, max; NSCalendarDate *startDate, *endDate; elements = [NSMutableArray new]; [freebusy fillStartDate: &startDate andEndDate: &endDate]; max = [recipients count]; for (count = 0; count < max; count++) { recipient = [recipients objectAtIndex: count]; [elements addObject: [self caldavFreeBusyRequestOnRecipient: recipient from: startDate to: endDate]]; } responseElement = davElementWithContent (@"schedule-response", XMLNS_CALDAV, elements); [elements release]; return responseElement; } - (NSDictionary *) _postCalDAVEventRequest: (iCalEvent *) event to: (NSArray *) recipients { NSString *filename, *iCalString; SOGoAppointmentObject *apt; NSDictionary *responseElement; NSMutableArray *elements, *content; NSString *recipient; unsigned int count, max; apt = [self lookupComponentByUID: [event uid]]; if (!apt) { filename = [NSString stringWithFormat: @"%@.ics", [event uid]]; iCalString = [[event parent] versitString]; apt = [self _createChildComponentWithName: filename andContent: iCalString]; } #warning cleanup: add a method to POST requests from CalDAV from SOGoAppointmentObject [apt saveComponent: event]; elements = [NSMutableArray new]; max = [recipients count]; for (count = 0; count < max; count++) { /* this is a fake success status */ recipient = [recipients objectAtIndex: count]; content = [NSMutableArray new]; [content addObject: davElementWithContent (@"recipient", XMLNS_CALDAV, recipient)]; [content addObject: davElementWithContent (@"request-status", XMLNS_CALDAV, @"2.0;Success")]; [elements addObject: davElementWithContent (@"response", XMLNS_CALDAV, content)]; [content release]; } responseElement = davElementWithContent (@"schedule-response", XMLNS_CALDAV, elements); [elements release]; return responseElement; } - (NSDictionary *) _postCalDAVEventReply: (iCalEvent *) event from: (NSString *) originator { NSDictionary *responseElement, *attendeeCode; NSMutableArray *content; iCalPerson *attendee; NSException *ex; SOGoAppointmentObject *apt; /* this is a fake success status */ apt = [self lookupComponentByUID: [event uid]]; if ([apt isKindOfClass: [SOGoAppointmentObject class]]) { content = [NSMutableArray new]; [content addObject: davElementWithContent (@"recipient", XMLNS_CALDAV, originator)]; #warning cleanup: add a method to POST replies from CalDAV from SOGoAppointmentObject attendee = [event findParticipant: [context activeUser]]; if (attendee) { ex = [apt changeParticipationStatus: [attendee partStat]]; if (ex) attendeeCode = davElementWithContent (@"request-status", XMLNS_CALDAV, @"3.1;Invalid property value"); else attendeeCode = davElementWithContent (@"request-status", XMLNS_CALDAV, @"2.0;Success"); } else attendeeCode = davElementWithContent (@"request-status", XMLNS_CALDAV, @"3.7;Invalid Calendar User"); [content addObject: attendeeCode]; responseElement = davElementWithContent (@"schedule-response", XMLNS_CALDAV, davElementWithContent (@"response", XMLNS_CALDAV, content)); [content release]; } else responseElement = nil; return responseElement; } - (NSDictionary *) caldavEventRequest: (iCalEvent *) event from: (NSString *) originator to: (NSArray *) recipients { NSDictionary *responseElement; NSString *method; method = [[event parent] method]; if ([method isEqualToString: @"REQUEST"]) responseElement = [self _postCalDAVEventRequest: event to: recipients]; else if ([method isEqualToString: @"REPLY"]) responseElement = [self _postCalDAVEventReply: event from: originator]; else responseElement = nil; return responseElement; } - (WOResponse *) _caldavScheduleResponse: (NSDictionary *) tags { WOResponse *response; response = [context response]; if (tags) { [response setStatus: 200]; [response appendContentString:@"\r\n"]; [response setHeader: @"application/xml; charset=utf-8" forKey: @"Content-Type"]; [response appendContentString: [tags asWebDavStringWithNamespaces: nil]]; } else [response setStatus: 415]; return response; } - (WOResponse *) caldavScheduleRequest: (WORequest *) rq withCalendar: (iCalCalendar *) calendar { NSString *tag, *originator; NSArray *recipients, *elements; iCalEntityObject *element; NSDictionary *tags; elements = [calendar allObjects]; if ([elements count]) { element = [elements objectAtIndex: 0]; originator = [rq headerForKey: @"originator"]; recipients = [[rq headerForKey: @"recipient"] componentsSeparatedByString: @", "]; tag = [[element tag] uppercaseString]; if ([tag isEqualToString: @"VFREEBUSY"]) tags = [self caldavFreeBusyRequest: (iCalFreeBusy *) element from: originator to: recipients]; else if ([tag isEqualToString: @"VEVENT"]) tags = [self caldavEventRequest: (iCalEvent *) element from: originator to: recipients]; else tags = nil; #warning needs to handle errors } return [self _caldavScheduleResponse: tags]; } - (id) POSTAction: (id) localContext { id obj; NSString *cType; WORequest *rq; iCalCalendar *calendar; obj = nil; rq = [localContext request]; if ([rq isSoWebDAVRequest]) { cType = [rq headerForKey: @"content-type"]; if ([cType hasPrefix: @"text/calendar"]) { calendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]]; obj = [self caldavScheduleRequest: rq withCalendar: calendar]; } } return obj; } - (NSArray *) davComplianceClassesInContext: (id)_ctx { NSMutableArray *classes; NSArray *primaryClasses; classes = [NSMutableArray new]; [classes autorelease]; primaryClasses = [super davComplianceClassesInContext: _ctx]; if (primaryClasses) [classes addObjectsFromArray: primaryClasses]; [classes addObject: @"calendar-access"]; [classes addObject: @"calendar-schedule"]; return classes; } - (NSArray *) groupDavResourceType { return [NSArray arrayWithObjects: @"vevent-collection", @"vtodo-collection", nil]; } - (NSArray *) davResourceType { static NSArray *colType = nil; NSArray *cdCol, *gdRT, *gdVEventCol, *gdVTodoCol; if (!colType) { gdRT = [self groupDavResourceType]; gdVEventCol = [NSArray arrayWithObjects: [gdRT objectAtIndex: 0], XMLNS_GROUPDAV, nil]; gdVTodoCol = [NSArray arrayWithObjects: [gdRT objectAtIndex: 1], XMLNS_GROUPDAV, nil]; cdCol = [NSArray arrayWithObjects: @"calendar", XMLNS_CALDAV, nil]; colType = [NSArray arrayWithObjects: @"collection", cdCol, gdVEventCol, gdVTodoCol, nil]; [colType retain]; } return colType; } - (NSString *) davCollectionTag { NSArray *records; GCSFolder *folder; static EOFetchSpecification *cTagSpec = nil; EOSortOrdering *ordering; NSNumber *lastModified; NSString *cTag; folder = [self ocsFolder]; if (!cTagSpec) { ordering = [EOSortOrdering sortOrderingWithKey: @"c_lastmodified" selector: EOCompareDescending]; cTagSpec = [EOFetchSpecification fetchSpecificationWithEntityName: [folder folderName] qualifier: nil sortOrderings: [NSArray arrayWithObject: ordering]]; [cTagSpec retain]; } records = [folder fetchFields: [NSArray arrayWithObject: @"c_lastmodified"] fetchSpecification: cTagSpec]; if ([records count]) { lastModified = [[records objectAtIndex: 0] objectForKey: @"c_lastmodified"]; cTag = [lastModified stringValue]; } else cTag = @"-1"; return cTag; } - (NSString *) davDescription { return @""; } /* vevent UID handling */ - (NSString *) resourceNameForEventUID: (NSString *) uid inFolder: (GCSFolder *) folder { static NSArray *nameFields = nil; EOQualifier *qualifier; NSArray *records; NSString *filename; unsigned int count; filename = nil; if (!nameFields) nameFields = [[NSArray alloc] initWithObjects: @"c_name", nil]; if (uid && folder) { qualifier = [EOQualifier qualifierWithQualifierFormat: @"c_uid = %@", uid]; records = [folder fetchFields: nameFields matchingQualifier: qualifier]; count = [records count]; if (count) { filename = [[records objectAtIndex:0] valueForKey:@"c_name"]; if (count > 1) [self errorWithFormat: @"The storage contains more than file with UID '%@'", uid]; } } return filename; } - (NSString *) resourceNameForEventUID: (NSString *) uid { /* caches UIDs */ GCSFolder *folder; NSString *rname; rname = nil; if (uid) { if (!uidToFilename) uidToFilename = [NSMutableDictionary new]; rname = [uidToFilename objectForKey: uid]; if (!rname) { folder = [self ocsFolder]; rname = [self resourceNameForEventUID: uid inFolder: folder]; if (rname) [uidToFilename setObject: rname forKey: uid]; } } return rname; } - (NSArray *) subscriptionRoles { return [NSArray arrayWithObjects: SOGoRole_ObjectCreator, SOGoRole_ObjectEraser, SOGoCalendarRole_PublicResponder, SOGoCalendarRole_PublicModifier, SOGoCalendarRole_PublicViewer, SOGoCalendarRole_PublicDAndTViewer, SOGoCalendarRole_PrivateResponder, SOGoCalendarRole_PrivateModifier, SOGoCalendarRole_PrivateViewer, SOGoCalendarRole_PrivateDAndTViewer, SOGoCalendarRole_ConfidentialResponder, SOGoCalendarRole_ConfidentialModifier, SOGoCalendarRole_ConfidentialViewer, SOGoCalendarRole_ConfidentialDAndTViewer, nil]; } - (NSString *) roleForComponentsWithAccessClass: (iCalAccessClass) accessClass forUser: (NSString *) uid { NSString *accessRole, *prefix, *currentRole, *suffix; NSEnumerator *acls; NSMutableDictionary *userRoles; accessRole = nil; if (accessClass == iCalAccessPublic) prefix = @"Public"; else if (accessClass == iCalAccessPrivate) prefix = @"Private"; else prefix = @"Confidential"; userRoles = [aclMatrix objectForKey: uid]; if (!userRoles) { userRoles = [NSMutableDictionary dictionaryWithCapacity: 3]; [aclMatrix setObject: userRoles forKey: uid]; } accessRole = [userRoles objectForKey: prefix]; if (!accessRole) { acls = [[self aclsForUser: uid] objectEnumerator]; currentRole = [acls nextObject]; while (currentRole && !accessRole) if ([currentRole hasPrefix: prefix]) { suffix = [currentRole substringFromIndex: [prefix length]]; accessRole = [NSString stringWithFormat: @"Component%@", suffix]; } else currentRole = [acls nextObject]; if (!accessRole) accessRole = @""; [userRoles setObject: accessRole forKey: prefix]; } return accessRole; } - (NSArray *) fetchFreeBusyInfosFrom: (NSCalendarDate *) _startDate to: (NSCalendarDate *) _endDate { static NSArray *infos = nil; // TODO: move to a plist file if (!infos) infos = [[NSArray alloc] initWithObjects: @"c_partmails", @"c_partstates", @"c_isopaque", @"c_status", @"c_cycleinfo", nil]; return [self fetchFields: infos from: _startDate to: _endDate title: nil component: @"vevent" additionalFilters: nil]; } - (NSArray *) fetchCoreInfosFrom: (NSCalendarDate *) _startDate to: (NSCalendarDate *) _endDate title: (NSString *) title component: (id) _component { return [self fetchCoreInfosFrom: _startDate to: _endDate title: title component: _component additionalFilters: nil]; } - (NSArray *) fetchCoreInfosFrom: (NSCalendarDate *) _startDate to: (NSCalendarDate *) _endDate title: (NSString *) title component: (id) _component additionalFilters: (NSString *) filters { static NSArray *infos = nil; // TODO: move to a plist file if (!infos) infos = [[NSArray alloc] initWithObjects: @"c_name", @"c_content", @"c_creationdate", @"c_lastmodified", @"c_version", @"c_component", @"c_title", @"c_location", @"c_orgmail", @"c_status", @"c_classification", @"c_isallday", @"c_isopaque", @"c_participants", @"c_partmails", @"c_partstates", @"c_sequence", @"c_priority", @"c_cycleinfo", nil]; return [self fetchFields: infos from: _startDate to: _endDate title: title component: _component additionalFilters: filters]; } /* URL generation */ - (NSString *) baseURLForAptWithUID: (NSString *)_uid inContext: (id)_ctx { // TODO: who calls this? NSString *url; if ([_uid length] == 0) return nil; url = [self baseURLInContext:_ctx]; if (![url hasSuffix: @"/"]) url = [url stringByAppendingString: @"/"]; // TODO: this should run a query to determine the uid! return [url stringByAppendingString:_uid]; } /* folder management */ - (BOOL) create { BOOL rc; NSUserDefaults *userSettings; NSMutableDictionary *calendarSettings; SOGoUser *ownerUser; rc = [super create]; if (rc) { ownerUser = [SOGoUser userWithLogin: [self ownerInContext: context] roles: nil]; userSettings = [ownerUser userSettings]; calendarSettings = [userSettings objectForKey: @"Calendar"]; if (!calendarSettings) { calendarSettings = [NSMutableDictionary dictionary]; [userSettings setObject: calendarSettings forKey: @"Calendar"]; } [userSettings synchronize]; } return rc; } - (id) lookupHomeFolderForUID: (NSString *) _uid inContext: (id)_ctx { // TODO: DUP to SOGoGroupFolder NSException *error = nil; NSArray *path; id ctx, result; if (![_uid isNotNull]) return nil; /* create subcontext, so that we don't destroy our environment */ if ((ctx = [context createSubContext]) == nil) { [self errorWithFormat:@"could not create SOPE subcontext!"]; return nil; } /* build path */ path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil; /* traverse path */ result = [[ctx application] traversePathArray:path inContext:ctx error:&error acquire:NO]; if (error != nil) { [self errorWithFormat: @"folder lookup failed (c_uid=%@): %@", _uid, error]; return nil; } [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@", _uid, [path componentsJoinedByString:@"=>"], result]; return result; } - (SOGoAppointmentFolder *) lookupCalendarFolderForUID: (NSString *) uid { SOGoFolder *currentContainer; SOGoAppointmentFolders *parent; NSException *error; currentContainer = [[container container] container]; currentContainer = [currentContainer lookupName: uid inContext: context acquire: NO]; parent = [currentContainer lookupName: @"Calendar" inContext: context acquire: NO]; currentContainer = [parent lookupName: @"personal" inContext: context acquire: NO]; if (!currentContainer) { error = [parent newFolderWithName: [parent defaultFolderName] andNameInContainer: @"personal"]; if (!error) currentContainer = [parent lookupName: @"personal" inContext: context acquire: NO]; } return (SOGoAppointmentFolder *) currentContainer; } - (NSArray *) lookupCalendarFoldersForUIDs: (NSArray *) _uids inContext: (id)_ctx { /* Note: can return NSNull objects in the array! */ NSMutableArray *folders; NSEnumerator *e; NSString *uid, *ownerLogin; id folder; ownerLogin = [self ownerInContext: context]; if ([_uids count] == 0) return nil; folders = [NSMutableArray arrayWithCapacity:16]; e = [_uids objectEnumerator]; while ((uid = [e nextObject])) { if ([uid isEqualToString: ownerLogin]) folder = self; else { folder = [self lookupCalendarFolderForUID: uid]; if (![folder isNotNull]) [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid]; } if (folder) [folders addObject: folder]; } return folders; } - (NSArray *) lookupFreeBusyObjectsForUIDs: (NSArray *) _uids inContext: (id) _ctx { /* Note: can return NSNull objects in the array! */ NSMutableArray *objs; NSEnumerator *e; NSString *uid; if ([_uids count] == 0) return nil; objs = [NSMutableArray arrayWithCapacity:16]; e = [_uids objectEnumerator]; while ((uid = [e nextObject])) { id obj; obj = [self lookupHomeFolderForUID:uid inContext:nil]; if ([obj isNotNull]) { obj = [obj lookupName: @"freebusy.ifb" inContext: nil acquire: NO]; if ([obj isKindOfClass: [NSException class]]) obj = nil; } if (![obj isNotNull]) [self logWithFormat: @"Note: did not find freebusy.ifb for uid: '%@'", uid]; /* Note: intentionally add 'null' folders to allow a mapping */ if (!obj) obj = [NSNull null]; [objs addObject: obj]; } return objs; } - (NSArray *) uidsFromICalPersons: (NSArray *) _persons { /* Note: can return NSNull objects in the array! */ NSMutableArray *uids; LDAPUserManager *um; unsigned i, count; iCalPerson *person; NSString *email; NSString *uid; if (_persons) { count = [_persons count]; uids = [NSMutableArray arrayWithCapacity:count + 1]; um = [LDAPUserManager sharedUserManager]; for (i = 0; i < count; i++) { person = [_persons objectAtIndex:i]; email = [person rfc822Email]; if ([email isNotNull]) uid = [um getUIDForEmail:email]; else uid = nil; if (!uid) uid = (NSString *) [NSNull null]; [uids addObject: uid]; } } else uids = nil; return uids; } - (NSArray *) lookupCalendarFoldersForICalPerson: (NSArray *) _persons inContext: (id) _ctx { /* Note: can return NSNull objects in the array! */ NSArray *uids, *folders; uids = [self uidsFromICalPersons: _persons]; if (uids) folders = [self lookupCalendarFoldersForUIDs: uids inContext: _ctx]; else folders = nil; return folders; } // - (id) lookupGroupFolderForUIDs: (NSArray *) _uids // inContext: (id)_ctx // { // SOGoCustomGroupFolder *folder; // if (_uids == nil) // return nil; // folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self]; // return [folder autorelease]; // } // - (id) lookupGroupCalendarFolderForUIDs: (NSArray *) _uids // inContext: (id) _ctx // { // SOGoCustomGroupFolder *folder; // if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil) // return nil; // folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO]; // if (![folder isNotNull]) // return nil; // if ([folder isKindOfClass:[NSException class]]) { // [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@", // folder]; // return nil; // } // return folder; // } /* bulk fetches */ // #warning We only support ONE calendar per user at this time // - (BOOL) _appendSubscribedFolders: (NSDictionary *) subscribedFolders // toFolderList: (NSMutableArray *) calendarFolders // { // NSEnumerator *keys; // NSString *currentKey; // NSMutableDictionary *currentCalendar; // BOOL firstShouldBeActive; // unsigned int count; // firstShouldBeActive = YES; // keys = [[subscribedFolders allKeys] objectEnumerator]; // currentKey = [keys nextObject]; // count = 1; // while (currentKey) // { // currentCalendar = [NSMutableDictionary new]; // [currentCalendar autorelease]; // [currentCalendar // setDictionary: [subscribedFolders objectForKey: currentKey]]; // [currentCalendar setObject: currentKey forKey: @"folder"]; // [calendarFolders addObject: currentCalendar]; // if ([[currentCalendar objectForKey: @"active"] boolValue]) // firstShouldBeActive = NO; // count++; // currentKey = [keys nextObject]; // } // return firstShouldBeActive; // } // - (NSArray *) calendarFolders // { // NSMutableDictionary *userCalendar, *calendarDict; // NSMutableArray *calendarFolders; // SOGoUser *calendarUser; // BOOL firstActive; // calendarFolders = [NSMutableArray new]; // [calendarFolders autorelease]; // calendarUser = [SOGoUser userWithLogin: [self ownerInContext: context] // roles: nil]; // userCalendar = [NSMutableDictionary new]; // [userCalendar autorelease]; // [userCalendar setObject: @"/" forKey: @"folder"]; // [userCalendar setObject: @"Calendar" forKey: @"displayName"]; // [calendarFolders addObject: userCalendar]; // calendarDict = [[calendarUser userSettings] objectForKey: @"Calendar"]; // firstActive = [[calendarDict objectForKey: @"activateUserFolder"] boolValue]; // firstActive = ([self _appendSubscribedFolders: // [calendarDict objectForKey: @"SubscribedFolders"] // toFolderList: calendarFolders] // || firstActive); // [userCalendar setObject: [NSNumber numberWithBool: firstActive] // forKey: @"active"]; // return calendarFolders; // } // - (NSArray *) fetchContentObjectNames // { // NSMutableArray *objectNames; // NSArray *records; // NSCalendarDate *today, *startDate, *endDate; // #warning this should be user-configurable // objectNames = [NSMutableArray array]; // today = [[NSCalendarDate calendarDate] beginOfDay]; // [today setTimeZone: timeZone]; // startDate = [today dateByAddingYears: 0 months: 0 days: -1 // hours: 0 minutes: 0 seconds: 0]; // endDate = [startDate dateByAddingYears: 0 months: 0 days: 2 // hours: 0 minutes: 0 seconds: 0]; // records = [self fetchFields: [NSArray arrayWithObject: @"c_name"] // from: startDate to: endDate // component: @"vevent"]; // [objectNames addObjectsFromArray: [records valueForKey: @"c_name"]]; // records = [self fetchFields: [NSArray arrayWithObject: @"c_name"] // from: startDate to: endDate // component: @"vtodo"]; // [objectNames addObjectsFromArray: [records valueForKey: @"c_name"]]; // return objectNames; // } /* folder type */ - (NSString *) folderType { return @"Appointment"; } - (NSString *) outlookFolderClass { return @"IPF.Appointment"; } - (BOOL) isActive { NSUserDefaults *settings; NSArray *inactiveFolders; settings = [[context activeUser] userSettings]; inactiveFolders = [[settings objectForKey: @"Calendar"] objectForKey: @"InactiveFolders"]; return (![inactiveFolders containsObject: nameInContainer]); } @end /* SOGoAppointmentFolder */