2a64c9ddca
Monotone-Revision: 14b1d75f21b83c7e79aaa832f13fcfd18393d8ea Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2008-03-28T14:15:46 Monotone-Branch: ca.inverse.sogo
1758 lines
50 KiB
Objective-C
1758 lines
50 KiB
Objective-C
/*
|
|
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 <Foundation/NSCalendarDate.h>
|
|
#import <Foundation/NSEnumerator.h>
|
|
#import <Foundation/NSURL.h>
|
|
#import <Foundation/NSUserDefaults.h>
|
|
#import <Foundation/NSValue.h>
|
|
|
|
#import <NGObjWeb/NSException+HTTP.h>
|
|
#import <NGObjWeb/SoObject+SoDAV.h>
|
|
#import <NGObjWeb/SoSecurityManager.h>
|
|
#import <NGObjWeb/WOContext+SoObjects.h>
|
|
#import <NGObjWeb/WOMessage.h>
|
|
#import <NGObjWeb/WORequest.h>
|
|
#import <NGObjWeb/WOResponse.h>
|
|
#import <NGExtensions/NGLoggerManager.h>
|
|
#import <NGExtensions/NSString+misc.h>
|
|
#import <GDLContentStore/GCSFolder.h>
|
|
#import <DOM/DOMProtocols.h>
|
|
#import <EOControl/EOQualifier.h>
|
|
#import <NGCards/iCalCalendar.h>
|
|
#import <NGCards/iCalDateTime.h>
|
|
#import <NGCards/iCalPerson.h>
|
|
#import <NGCards/iCalRecurrenceCalculator.h>
|
|
#import <NGCards/NSString+NGCards.h>
|
|
#import <NGExtensions/NGCalendarDateRange.h>
|
|
#import <NGExtensions/NSNull+misc.h>
|
|
#import <NGExtensions/NSObject+Logs.h>
|
|
#import <SaxObjC/SaxObjC.h>
|
|
#import <SaxObjC/XMLNamespaces.h>
|
|
|
|
// #import <NGObjWeb/SoClassSecurityInfo.h>
|
|
#import <SOGo/SOGoCache.h>
|
|
#import <SOGo/SOGoCustomGroupFolder.h>
|
|
#import <SOGo/LDAPUserManager.h>
|
|
#import <SOGo/SOGoPermissions.h>
|
|
#import <SOGo/NSArray+Utilities.h>
|
|
#import <SOGo/NSString+Utilities.h>
|
|
#import <SOGo/SOGoUser.h>
|
|
|
|
#import "SOGoAppointmentObject.h"
|
|
#import "SOGoAppointmentFolders.h"
|
|
#import "SOGoTaskObject.h"
|
|
|
|
#import "SOGoAppointmentFolder.h"
|
|
|
|
#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;
|
|
|
|
+ (int) version
|
|
{
|
|
return [super version] + 1 /* v1 */;
|
|
}
|
|
|
|
+ (void) initialize
|
|
{
|
|
NGLoggerManager *lm;
|
|
static BOOL didInit = NO;
|
|
// SoClassSecurityInfo *securityInfo;
|
|
|
|
if (didInit) return;
|
|
didInit = YES;
|
|
|
|
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];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* name lookup */
|
|
|
|
- (void) _appendPropstat: (NSDictionary *) propstat
|
|
toResponse: (WOResponse *) r
|
|
{
|
|
NSArray *properties;
|
|
unsigned int count, max;
|
|
|
|
[r appendContentString: @"<D:propstat><D:prop>"];
|
|
properties = [propstat objectForKey: @"properties"];
|
|
max = [properties count];
|
|
for (count = 0; count < max; count++)
|
|
[r appendContentString: [properties objectAtIndex: count]];
|
|
[r appendContentString: @"</D:prop><D:status>"];
|
|
[r appendContentString: [propstat objectForKey: @"status"]];
|
|
[r appendContentString: @"</D:status></D:propstat>"];
|
|
}
|
|
|
|
#warning we should use the EOFetchSpecification for that!!! (see doPROPFIND:)
|
|
|
|
#warning components in calendar-data query are ignored
|
|
- (NSString *) _property: (NSObject <DOMElement> *) property
|
|
ofObject: (SOGoObject *) sogoObject
|
|
{
|
|
NSDictionary *map;
|
|
NSString *value, *propName, *methodName;
|
|
SEL methodSel;
|
|
|
|
value = nil;
|
|
|
|
propName = [NSString stringWithFormat: @"{%@}%@",
|
|
[property namespaceURI],
|
|
[property nodeName]];
|
|
map = [[self class] defaultWebDAVAttributeMap];
|
|
methodName = [map objectForKey: propName];
|
|
if (methodName)
|
|
{
|
|
methodSel = NSSelectorFromString(methodName);
|
|
if ([sogoObject respondsToSelector: methodSel])
|
|
value = [[sogoObject performSelector: methodSel]
|
|
stringByEscapingXMLString];
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
- (NSString *) _namespaceRep: (NSString *) namespace
|
|
{
|
|
NSString *rep;
|
|
|
|
if ([namespace isEqualToString: @"urn:ietf:params:xml:ns:caldav"])
|
|
rep = @"C";
|
|
else
|
|
rep = @"D";
|
|
|
|
return rep;
|
|
}
|
|
|
|
- (NSString *) _nodeTag: (NSObject <DOMElement> *) property
|
|
{
|
|
NSMutableString *nodeTag;
|
|
NSString *nsRep;
|
|
|
|
nodeTag = [NSMutableString string];
|
|
nsRep = [self _namespaceRep: [property namespaceURI]];
|
|
if (nsRep)
|
|
[nodeTag appendFormat: @"%@:", nsRep];
|
|
[nodeTag appendString: [property nodeName]];
|
|
|
|
return nodeTag;
|
|
}
|
|
|
|
- (NSString *) _representProperty: (NSDictionary *) property
|
|
{
|
|
NSMutableString *propertyValue;
|
|
NSString *content, *nodeTag;
|
|
|
|
propertyValue = [NSMutableString string];
|
|
nodeTag = [self _nodeTag: [property objectForKey: @"property"]];
|
|
content = [property objectForKey: @"content"];
|
|
if (content)
|
|
[propertyValue appendFormat: @"<%@>%@</%@>", nodeTag, content, nodeTag];
|
|
else
|
|
[propertyValue appendFormat: @"<%@/>", nodeTag];
|
|
|
|
return propertyValue;
|
|
}
|
|
|
|
- (NSArray *) _properties: (NSArray *) properties
|
|
ofObject: (NSDictionary *) object
|
|
{
|
|
NSMutableArray *values;
|
|
NSEnumerator *list;
|
|
NSObject <DOMElement> *currentProperty;
|
|
NSMutableDictionary *currentValue;
|
|
SOGoObject *sogoObject;
|
|
NSString *content;
|
|
SoSecurityManager *mgr;
|
|
|
|
values = [NSMutableArray array];
|
|
|
|
#warning this check should be done directly in the query... we should fix this sometime
|
|
mgr = [SoSecurityManager sharedSecurityManager];
|
|
sogoObject = [self lookupName: [object objectForKey: @"c_name"]
|
|
inContext: context
|
|
acquire: NO];
|
|
if (!([mgr validatePermission: SOGoPerm_AccessObject
|
|
onObject: sogoObject
|
|
inContext: context]))
|
|
{
|
|
list = [properties objectEnumerator];
|
|
while ((currentProperty = [list nextObject]))
|
|
{
|
|
currentValue = [NSMutableDictionary dictionary];
|
|
[currentValue setObject: currentProperty
|
|
forKey: @"property"];
|
|
content = [self _property: currentProperty
|
|
ofObject: sogoObject];
|
|
if (content)
|
|
[currentValue setObject: content
|
|
forKey: @"content"];
|
|
[values addObject: currentValue];
|
|
}
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
- (NSArray *) _propstats: (NSArray *) properties
|
|
ofObject: (NSDictionary *) object
|
|
{
|
|
NSMutableArray *propstats, *properties200, *properties404;
|
|
NSEnumerator *values;
|
|
NSDictionary *currentProperty;
|
|
NSString *content, *propertyValue;
|
|
|
|
propstats = [NSMutableArray array];
|
|
|
|
properties200 = [NSMutableArray new];
|
|
properties404 = [NSMutableArray new];
|
|
|
|
values = [[self _properties: properties ofObject: object]
|
|
objectEnumerator];
|
|
while ((currentProperty = [values nextObject]))
|
|
{
|
|
content = [currentProperty objectForKey: @"content"];
|
|
propertyValue = [self _representProperty: currentProperty];
|
|
if (content)
|
|
[properties200 addObject: propertyValue];
|
|
else
|
|
[properties404 addObject: propertyValue];
|
|
}
|
|
|
|
if ([properties200 count])
|
|
{
|
|
[propstats addObject: [NSDictionary dictionaryWithObjectsAndKeys:
|
|
properties200, @"properties",
|
|
@"HTTP/1.1 200 OK", @"status",
|
|
nil]];
|
|
[properties200 autorelease];
|
|
}
|
|
else
|
|
[properties200 release];
|
|
|
|
if ([properties404 count])
|
|
{
|
|
[propstats addObject: [NSDictionary dictionaryWithObjectsAndKeys:
|
|
properties404, @"properties",
|
|
@"HTTP/1.1 404 Not Found", @"status",
|
|
nil]];
|
|
[properties404 autorelease];
|
|
}
|
|
else
|
|
[properties404 release];
|
|
|
|
return propstats;
|
|
}
|
|
|
|
- (void) appendObject: (NSDictionary *) object
|
|
properties: (NSArray *) properties
|
|
withBaseURL: (NSString *) baseURL
|
|
toComplexResponse: (WOResponse *) r
|
|
{
|
|
NSEnumerator *propstats;
|
|
NSDictionary *propstat;
|
|
|
|
[r appendContentString: @"<D:response><D:href>"];
|
|
[r appendContentString: baseURL];
|
|
if (![baseURL hasSuffix: @"/"])
|
|
[r appendContentString: @"/"];
|
|
[r appendContentString: [object objectForKey: @"c_name"]];
|
|
[r appendContentString: @"</D:href>"];
|
|
|
|
propstats = [[self _propstats: properties ofObject: object]
|
|
objectEnumerator];
|
|
while ((propstat = [propstats nextObject]))
|
|
[self _appendPropstat: propstat toResponse: r];
|
|
|
|
[r appendContentString: @"</D:response>\r\n"];
|
|
}
|
|
|
|
- (void) appendMissingObjectRef: (NSString *) href
|
|
toComplexResponse: (WOResponse *) r
|
|
{
|
|
[r appendContentString: @"<D:response><D:href>"];
|
|
[r appendContentString: href];
|
|
[r appendContentString: @"</D:href><D:status>HTTP/1.1 404 Not Found</D:status></D:response>\r\n"];
|
|
}
|
|
|
|
- (void) _appendTimeRange: (id <DOMElement>) 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"];
|
|
}
|
|
|
|
- (NSDictionary *) _parseCalendarFilter: (id <DOMElement>) filterElement
|
|
{
|
|
NSMutableDictionary *filterData;
|
|
id <DOMElement> parentNode;
|
|
id <DOMNodeList> ranges;
|
|
NSString *componentName;
|
|
|
|
parentNode = (id <DOMElement>) [filterElement parentNode];
|
|
if ([[parentNode tagName] isEqualToString: @"comp-filter"]
|
|
&& [[parentNode attribute: @"name"] isEqualToString: @"VCALENDAR"])
|
|
{
|
|
componentName = [[filterElement attribute: @"name"] lowercaseString];
|
|
filterData = [NSMutableDictionary new];
|
|
[filterData autorelease];
|
|
[filterData setObject: componentName forKey: @"name"];
|
|
ranges = [filterElement getElementsByTagName: @"time-range"];
|
|
if ([ranges length])
|
|
[self _appendTimeRange: [ranges objectAtIndex: 0]
|
|
toFilter: filterData];
|
|
}
|
|
else
|
|
filterData = nil;
|
|
|
|
return filterData;
|
|
}
|
|
|
|
- (NSArray *) _parseRequestedProperties: (id <DOMElement>) parentNode
|
|
{
|
|
NSMutableArray *properties;
|
|
NSObject <DOMNodeList> *propList, *children;
|
|
NSObject <DOMNode> *currentChild;
|
|
unsigned int count, max, count2, max2;
|
|
|
|
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)])
|
|
[properties addObject: currentChild];
|
|
}
|
|
|
|
// while ([children hasChildNodes])
|
|
// [properties addObject: [children next]];
|
|
}
|
|
|
|
return properties;
|
|
}
|
|
|
|
- (NSArray *) _parseCalendarFilters: (id <DOMElement>) parentNode
|
|
{
|
|
id <DOMNodeList> children;
|
|
id <DOMElement> node;
|
|
NSMutableArray *filters;
|
|
NSDictionary *filter;
|
|
unsigned int count, max;
|
|
|
|
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];
|
|
}
|
|
|
|
return filters;
|
|
}
|
|
|
|
- (void) _appendComponentProperties: (NSArray *) properties
|
|
matchingFilters: (NSArray *) filters
|
|
toResponse: (WOResponse *) response
|
|
{
|
|
NSArray *apts;
|
|
unsigned int count, max;
|
|
NSDictionary *currentFilter, *appointment;
|
|
NSEnumerator *appointments;
|
|
NSString *baseURL;
|
|
|
|
baseURL = [self baseURLInContext: context];
|
|
|
|
max = [filters count];
|
|
for (count = 0; count < max; count++)
|
|
{
|
|
currentFilter = [filters objectAtIndex: count];
|
|
apts = [self fetchCoreInfosFrom: [currentFilter objectForKey: @"start"]
|
|
to: [currentFilter objectForKey: @"end"]
|
|
title: [currentFilter objectForKey: @"title"]
|
|
component: [currentFilter objectForKey: @"name"]];
|
|
appointments = [apts objectEnumerator];
|
|
appointment = [appointments nextObject];
|
|
while (appointment)
|
|
{
|
|
[self appendObject: appointment
|
|
properties: properties
|
|
withBaseURL: baseURL
|
|
toComplexResponse: response];
|
|
appointment = [appointments nextObject];
|
|
}
|
|
}
|
|
}
|
|
|
|
#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
|
|
|
|
- (NSDictionary *) _componentMatchingURL: (NSString *) url
|
|
inBaseURL: (NSString *) baseURL
|
|
{
|
|
NSDictionary *component;
|
|
NSURL *componentURL, *realBaseURL;
|
|
NSArray *urlComponents;
|
|
NSString *componentURLPath, *cName;
|
|
|
|
component = nil;
|
|
|
|
realBaseURL = [NSURL URLWithString: baseURL];
|
|
componentURL = [[NSURL URLWithString: url
|
|
relativeToURL: realBaseURL]
|
|
standardizedURL];
|
|
componentURLPath = [componentURL absoluteString];
|
|
if ([componentURLPath rangeOfString: [realBaseURL absoluteString]].location
|
|
!= NSNotFound)
|
|
{
|
|
urlComponents = [componentURLPath componentsSeparatedByString: @"/"];
|
|
cName = [urlComponents objectAtIndex: [urlComponents count] - 1];
|
|
component = [NSDictionary dictionaryWithObject: cName forKey: @"c_name"];
|
|
}
|
|
|
|
return component;
|
|
}
|
|
|
|
- (void) _appendComponentProperties: (NSArray *) properties
|
|
matchingURLs: (id <DOMNodeList>) refs
|
|
toResponse: (WOResponse *) response
|
|
{
|
|
NSObject <DOMElement> *element;
|
|
NSDictionary *currentComponent;
|
|
NSString *baseURL, *currentURL;
|
|
unsigned int count, max;
|
|
|
|
baseURL = [self baseURLInContext: context];
|
|
|
|
max = [refs length];
|
|
for (count = 0; count < max; count++)
|
|
{
|
|
element = [refs objectAtIndex: count];
|
|
currentURL = [[element firstChild] nodeValue];
|
|
currentComponent = [self _componentMatchingURL: currentURL
|
|
inBaseURL: baseURL];
|
|
if (currentComponent)
|
|
[self appendObject: currentComponent
|
|
properties: properties
|
|
withBaseURL: baseURL
|
|
toComplexResponse: response];
|
|
else
|
|
[self appendMissingObjectRef: currentURL
|
|
toComplexResponse: response];
|
|
}
|
|
}
|
|
|
|
- (NSArray *) davNamespaces
|
|
{
|
|
NSMutableArray *ns;
|
|
|
|
ns = [NSMutableArray arrayWithArray: [super davNamespaces]];
|
|
[ns addObjectUniquely: @"urn:ietf:params:xml:ns:caldav"];
|
|
|
|
return ns;
|
|
}
|
|
|
|
- (id) davCalendarQuery: (id) queryContext
|
|
{
|
|
WOResponse *r;
|
|
id <DOMDocument> document;
|
|
id <DOMElement> documentElement;
|
|
|
|
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:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"];
|
|
[r appendContentString: @"<D:multistatus xmlns:D=\"DAV:\""
|
|
@" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\r\n"];
|
|
|
|
document = [[context request] contentAsDOMDocument];
|
|
documentElement = [document documentElement];
|
|
[self _appendComponentProperties: [self _parseRequestedProperties: documentElement]
|
|
matchingFilters: [self _parseCalendarFilters: documentElement]
|
|
toResponse: r];
|
|
[r appendContentString:@"</D:multistatus>\r\n"];
|
|
|
|
return r;
|
|
}
|
|
|
|
- (id) davCalendarMultiget: (id) queryContext
|
|
{
|
|
WOResponse *r;
|
|
id <DOMDocument> document;
|
|
id <DOMElement> documentElement;
|
|
|
|
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:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"];
|
|
[r appendContentString: @"<D:multistatus xmlns:D=\"DAV:\""
|
|
@" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\r\n"];
|
|
|
|
document = [[context request] contentAsDOMDocument];
|
|
documentElement = [document documentElement];
|
|
[self _appendComponentProperties: [self _parseRequestedProperties: documentElement]
|
|
matchingURLs: [documentElement getElementsByTagName: @"href"]
|
|
toResponse: r];
|
|
[r appendContentString:@"</D:multistatus>\r\n"];
|
|
|
|
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;
|
|
}
|
|
|
|
- (id) deduceObjectForName: (NSString *)_key
|
|
inContext: (id)_ctx
|
|
{
|
|
WORequest *request;
|
|
NSString *method;
|
|
Class objectClass;
|
|
id obj;
|
|
|
|
request = [_ctx request];
|
|
method = [request method];
|
|
if ([method isEqualToString: @"PUT"])
|
|
objectClass = [self objectClassForContent: [request contentAsString]];
|
|
else
|
|
objectClass = [self objectClassForResourceNamed: _key];
|
|
|
|
if (objectClass)
|
|
obj = [objectClass objectWithName: _key inContainer: self];
|
|
else
|
|
obj = nil;
|
|
|
|
return obj;
|
|
}
|
|
|
|
- (BOOL) requestNamedIsHandledLater: (NSString *) name
|
|
{
|
|
return [name isEqualToString: @"OPTIONS"];
|
|
}
|
|
|
|
- (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];
|
|
else
|
|
obj = [self deduceObjectForName: _key
|
|
inContext: _ctx];
|
|
}
|
|
}
|
|
if (!obj)
|
|
obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
|
|
}
|
|
|
|
if (obj)
|
|
[[SOGoCache sharedCache] registerObject: obj
|
|
withName: _key
|
|
inContainer: container];
|
|
|
|
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: @"access-control"];
|
|
[classes addObject: @"calendar-access"];
|
|
|
|
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;
|
|
}
|
|
|
|
/* vevent UID handling */
|
|
|
|
- (NSString *) resourceNameForEventUID: (NSString *)_u
|
|
inFolder: (GCSFolder *)_f
|
|
{
|
|
static NSArray *nameFields = nil;
|
|
EOQualifier *qualifier;
|
|
NSArray *records;
|
|
|
|
if (![_u isNotNull]) return nil;
|
|
if (_f == nil) {
|
|
[self errorWithFormat:@"(%s): missing folder for fetch!",
|
|
__PRETTY_FUNCTION__];
|
|
return nil;
|
|
}
|
|
|
|
if (nameFields == nil)
|
|
nameFields = [[NSArray alloc] initWithObjects: @"c_name", nil];
|
|
|
|
qualifier = [EOQualifier qualifierWithQualifierFormat:@"c_uid = %@", _u];
|
|
records = [_f fetchFields: nameFields matchingQualifier: qualifier];
|
|
|
|
if ([records count] == 1)
|
|
return [[records objectAtIndex:0] valueForKey:@"c_name"];
|
|
if ([records count] == 0)
|
|
return nil;
|
|
|
|
[self errorWithFormat:
|
|
@"The storage contains more than file with the same UID!"];
|
|
return [[records objectAtIndex:0] valueForKey:@"c_name"];
|
|
}
|
|
|
|
- (NSString *) resourceNameForEventUID: (NSString *) _uid
|
|
{
|
|
/* caches UIDs */
|
|
GCSFolder *folder;
|
|
NSString *rname;
|
|
|
|
if (![_uid isNotNull])
|
|
return nil;
|
|
if ((rname = [uidToFilename objectForKey:_uid]) != nil)
|
|
return [rname isNotNull] ? rname : nil;
|
|
|
|
if ((folder = [self ocsFolder]) == nil) {
|
|
[self errorWithFormat:@"(%s): missing folder for fetch!",
|
|
__PRETTY_FUNCTION__];
|
|
return nil;
|
|
}
|
|
|
|
if (uidToFilename == nil)
|
|
uidToFilename = [[NSMutableDictionary alloc] initWithCapacity:16];
|
|
|
|
if ((rname = [self resourceNameForEventUID:_uid inFolder:folder]) == nil)
|
|
[uidToFilename setObject:[NSNull null] forKey:_uid];
|
|
else
|
|
[uidToFilename setObject:rname forKey:_uid];
|
|
|
|
return rname;
|
|
}
|
|
|
|
- (Class) objectClassForResourceNamed: (NSString *) name
|
|
{
|
|
EOQualifier *qualifier;
|
|
NSArray *records;
|
|
NSString *component;
|
|
Class objectClass;
|
|
|
|
qualifier = [EOQualifier qualifierWithQualifierFormat:@"c_name = %@", name];
|
|
records = [[self ocsFolder] fetchFields: [NSArray arrayWithObject: @"c_component"]
|
|
matchingQualifier: qualifier];
|
|
|
|
if ([records count])
|
|
{
|
|
component = [[records objectAtIndex:0] valueForKey: @"c_component"];
|
|
if ([component isEqualToString: @"vevent"])
|
|
objectClass = [SOGoAppointmentObject class];
|
|
else if ([component isEqualToString: @"vtodo"])
|
|
objectClass = [SOGoTaskObject class];
|
|
else
|
|
objectClass = Nil;
|
|
}
|
|
else
|
|
objectClass = Nil;
|
|
|
|
return objectClass;
|
|
}
|
|
|
|
/* fetching */
|
|
|
|
- (NSMutableDictionary *) fixupRecord: (NSDictionary *) _record
|
|
fetchRange: (NGCalendarDateRange *) _r
|
|
{
|
|
NSMutableDictionary *md;
|
|
id tmp;
|
|
|
|
md = [[_record mutableCopy] autorelease];
|
|
|
|
if ((tmp = [_record objectForKey:@"c_startdate"])) {
|
|
tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
|
|
(NSTimeInterval)[tmp unsignedIntValue]];
|
|
[tmp setTimeZone: timeZone];
|
|
if (tmp) [md setObject:tmp forKey:@"startDate"];
|
|
[tmp release];
|
|
}
|
|
else
|
|
[self logWithFormat:@"missing 'startdate' in record?"];
|
|
|
|
if ((tmp = [_record objectForKey:@"c_enddate"])) {
|
|
tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
|
|
(NSTimeInterval)[tmp unsignedIntValue]];
|
|
[tmp setTimeZone: timeZone];
|
|
if (tmp) [md setObject:tmp forKey:@"endDate"];
|
|
[tmp release];
|
|
}
|
|
else
|
|
[self logWithFormat:@"missing 'enddate' in record?"];
|
|
|
|
return md;
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
|
|
- (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 ([[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 *) 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;
|
|
}
|
|
|
|
- (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
|
|
fromFolder: (GCSFolder *) _folder
|
|
from: (NSCalendarDate *) _startDate
|
|
to: (NSCalendarDate *) _endDate
|
|
title: (NSString *) title
|
|
component: (id) _component
|
|
{
|
|
EOQualifier *qualifier;
|
|
NSMutableArray *fields, *ma = nil;
|
|
NSArray *records;
|
|
NSString *sql, *dateSqlString, *titleSqlString, *componentSqlString,
|
|
*privacySqlString, *currentLogin;
|
|
NGCalendarDateRange *r;
|
|
|
|
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];
|
|
|
|
/* 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];
|
|
|
|
/* 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];
|
|
|
|
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"];
|
|
|
|
return ma;
|
|
}
|
|
|
|
/* override this in subclasses */
|
|
- (NSArray *) fetchFields: (NSArray *) _fields
|
|
from: (NSCalendarDate *) _startDate
|
|
to: (NSCalendarDate *) _endDate
|
|
title: (NSString *) title
|
|
component: (id) _component
|
|
{
|
|
GCSFolder *folder;
|
|
|
|
if ((folder = [self ocsFolder]) == nil) {
|
|
[self errorWithFormat:@"(%s): missing folder for fetch!",
|
|
__PRETTY_FUNCTION__];
|
|
return nil;
|
|
}
|
|
|
|
return [self fetchFields: _fields fromFolder: folder
|
|
from: _startDate to: _endDate
|
|
title: title
|
|
component: _component];
|
|
}
|
|
|
|
- (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", nil];
|
|
|
|
return [self fetchFields: infos from: _startDate to: _endDate
|
|
title: nil
|
|
component: @"vevent"];
|
|
}
|
|
|
|
- (NSArray *) fetchCoreInfosFrom: (NSCalendarDate *) _startDate
|
|
to: (NSCalendarDate *) _endDate
|
|
title: (NSString *) title
|
|
component: (id) _component
|
|
{
|
|
static NSArray *infos = nil; // TODO: move to a plist file
|
|
|
|
if (!infos)
|
|
infos = [[NSArray alloc] initWithObjects:
|
|
@"c_name", @"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];
|
|
}
|
|
|
|
/* 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;
|
|
NSMutableArray *folderSubscription;
|
|
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"];
|
|
}
|
|
folderSubscription
|
|
= [calendarSettings objectForKey: @"ActiveFolders"];
|
|
if (!folderSubscription)
|
|
{
|
|
folderSubscription = [NSMutableArray array];
|
|
[calendarSettings setObject: folderSubscription
|
|
forKey: @"ActiveFolders"];
|
|
}
|
|
[folderSubscription addObjectUniquely: nameInContainer];
|
|
[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 */
|
|
[objs addObject:obj ? obj : [NSNull null]];
|
|
}
|
|
return objs;
|
|
}
|
|
|
|
- (NSArray *) uidsFromICalPersons: (NSArray *) _persons
|
|
{
|
|
/* Note: can return NSNull objects in the array! */
|
|
NSMutableArray *uids;
|
|
LDAPUserManager *um;
|
|
unsigned i, count;
|
|
|
|
if (_persons == nil)
|
|
return nil;
|
|
|
|
count = [_persons count];
|
|
uids = [NSMutableArray arrayWithCapacity:count + 1];
|
|
um = [LDAPUserManager sharedUserManager];
|
|
|
|
for (i = 0; i < count; i++) {
|
|
iCalPerson *person;
|
|
NSString *email;
|
|
NSString *uid;
|
|
|
|
person = [_persons objectAtIndex:i];
|
|
email = [person rfc822Email];
|
|
if ([email isNotNull]) {
|
|
uid = [um getUIDForEmail:email];
|
|
}
|
|
else
|
|
uid = nil;
|
|
|
|
[uids addObject:(uid != nil ? uid : (id)[NSNull null])];
|
|
}
|
|
return uids;
|
|
}
|
|
|
|
- (NSArray *)lookupCalendarFoldersForICalPerson: (NSArray *) _persons
|
|
inContext: (id) _ctx
|
|
{
|
|
/* Note: can return NSNull objects in the array! */
|
|
NSArray *uids;
|
|
|
|
if ((uids = [self uidsFromICalPersons:_persons]) == nil)
|
|
return nil;
|
|
|
|
return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
|
|
}
|
|
|
|
- (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 */
|
|
|
|
- (NSArray *) fetchAllSOGoAppointments
|
|
{
|
|
/*
|
|
Note: very expensive method, do not use unless absolutely required.
|
|
returns an array of SOGoAppointment objects.
|
|
|
|
Note that we can leave out the filenames, supposed to be stored
|
|
in the 'uid' field of the iCalendar object!
|
|
*/
|
|
NSMutableArray *events;
|
|
NSDictionary *files;
|
|
NSEnumerator *contents;
|
|
NSString *content;
|
|
|
|
/* fetch all raw contents */
|
|
|
|
files = [self fetchContentStringsAndNamesOfAllObjects];
|
|
if (![files isNotNull]) return nil;
|
|
if ([files isKindOfClass:[NSException class]]) return (id)files;
|
|
|
|
/* transform to SOGo appointments */
|
|
|
|
events = [NSMutableArray arrayWithCapacity:[files count]];
|
|
contents = [files objectEnumerator];
|
|
while ((content = [contents nextObject]) != nil)
|
|
[events addObject: [iCalCalendar parseSingleFromSource: content]];
|
|
|
|
return events;
|
|
}
|
|
|
|
// #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;
|
|
// }
|
|
|
|
- (NSArray *) fetchContentObjectNames
|
|
{
|
|
static NSArray *cNameField = nil;
|
|
|
|
if (!cNameField)
|
|
cNameField = [[NSArray alloc] initWithObjects: @"c_name", nil];
|
|
|
|
return [[self fetchFields: cNameField from: nil to: nil
|
|
title: nil component: nil] objectsForKey: @"c_name"];
|
|
}
|
|
|
|
/* folder type */
|
|
|
|
- (NSString *) folderType
|
|
{
|
|
return @"Appointment";
|
|
}
|
|
|
|
- (NSString *) outlookFolderClass
|
|
{
|
|
return @"IPF.Appointment";
|
|
}
|
|
|
|
- (BOOL) isActive
|
|
{
|
|
NSUserDefaults *settings;
|
|
NSArray *activeFolders;
|
|
|
|
settings = [[context activeUser] userSettings];
|
|
activeFolders
|
|
= [[settings objectForKey: @"Calendar"] objectForKey: @"ActiveFolders"];
|
|
|
|
return [activeFolders containsObject: nameInContainer];
|
|
}
|
|
|
|
@end /* SOGoAppointmentFolder */
|