/* Copyright (C) 2002-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. */ #include "SxFolder.h" #include "SxObject.h" #include "OLDavPropMapper.h" #include "NGResourceLocator+ZSF.h" #include
#include #include #include "common.h" #include @interface NSObject(RSS) - (id)rssInContext:(id)_ctx; @end @interface SxFolder(ZL2) - (id)lookupRangeQueryFolder:(NSString *)_name inContext:(id)_ctx; @end @implementation SxFolder static BOOL explainOn = YES; static BOOL debugLookup = NO; static BOOL debugOn = NO; static NSString *cachePath = nil; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; explainOn = [ud boolForKey:@"SxExplain"]; debugOn = [ud boolForKey:@"SxFolderDebugEnabled"]; cachePath = [ud stringForKey:@"SxCachePath"]; if (cachePath == nil) cachePath = @"/var/cache/zidestore"; } - (NSArray *)subPropMapper { int DidCheckClass = -1; Class clazz; id pm; if (self->subPropMapper) return self->subPropMapper; if (DidCheckClass == 0) return nil; if ((clazz = NSClassFromString(@"ZLPropMapper")) == Nil) { static BOOL didNote = NO; if (!didNote) [self logWithFormat:@"Note: no ZideLook support installed."]; didNote = YES; DidCheckClass = 0; return nil; } else DidCheckClass = 1; if ((pm = [[[clazz alloc] initWithDictionary:nil] autorelease]) == nil) { [self logWithFormat:@"could not instantiate prop mapper: %@", clazz]; return nil; } if ([pm respondsToSelector:@selector(propertySetNamed:)]) self->subPropMapper = [[NSArray alloc] initWithObjects:&pm count:1]; else [self logWithFormat:@"ZLPropMapper has not propertySetNamed: ?"]; return self->subPropMapper; } - (id)initWithName:(NSString *)_name inContainer:(id)_container { if ((self = [super init])) { self->nameInContainer = [_name copy]; [self setContainer:_container]; } return self; } - (void)dealloc { if ([self->container shouldRetainAsSoContainer]) [self->container release]; [self->subPropMapper release]; [self->baseURL release]; [self->nameInContainer release]; [super dealloc]; } /* accessors */ - (BOOL)doExplainQueries { return explainOn; } /* hierarchy */ - (void)setContainer:(id)_container { id tmp; if (self->container == _container) return; tmp = _container; if ([tmp shouldRetainAsSoContainer]) tmp = [tmp retain]; if ([self->container shouldRetainAsSoContainer]) [self->container release]; self->container = tmp; } - (id)container { return self->container; } - (void)setNameInContainer:(NSString *)_name { ASSIGNCOPY(self->nameInContainer, _name); } - (NSString *)nameInContainer { return self->nameInContainer; } /* OGo entry point */ - (LSCommandContext *)commandContextInContext:(id)_ctx { SxAuthenticator *auth; LSCommandContext *ctx; if (_ctx == nil) _ctx = [(WOApplication *)[WOApplication application] context]; if ((auth = [self authenticatorInContext:_ctx]) == nil) { [self logWithFormat:@"ERROR: got no authenticator for context: %@", _ctx]; return nil; } if ((ctx = [auth commandContextInContext:_ctx]) == nil) { [self logWithFormat:@"ERROR: got no OGo context from authenticator: %@", auth]; return nil; } return ctx; } /* attribute mappings */ - (id)davAttributeMapInContext:(id)_ctx { static OLDavPropMapper *davMap = nil; if (davMap == nil) { davMap = [[OLDavPropMapper alloc] initWithDictionary: [[self class] defaultWebDAVAttributeMap]]; } return davMap; } /* ZideLook specialties */ - (NSString *)personalFolderInfoKey { WOContext *ctx; NSString *login; NSString *key; id auth; if ((ctx = [(WOApplication *)[WOApplication application] context]) == nil) return nil; if ((auth = [self authenticatorInContext:ctx]) == nil) return nil; if ((login = [[auth userInContext:ctx] login]) == nil) return nil; key = [NSString stringWithFormat:@"%@::%@", login, [self baseURLInContext:ctx]]; [self logWithFormat:@"made key: %@", key]; return key; } - (NSString *)associatedContentsPath { return [cachePath stringByAppendingPathComponent: @"AssociatedContents.plist"]; } - (void)setAssociatedContents:(NSString *)_value { NSMutableDictionary *dict; NSString *path; [self logWithFormat:@"set assoc contents: %@", _value]; path = [self associatedContentsPath]; dict = [[NSMutableDictionary alloc] initWithContentsOfFile:path]; if (dict == nil) dict = [[NSMutableDictionary alloc] initWithCapacity:4]; if ([_value isNotNull]) [dict setObject:_value forKey:[self personalFolderInfoKey]]; else [dict removeObjectForKey:[self personalFolderInfoKey]]; if (![dict writeToFile:path atomically:YES]) [self logWithFormat:@"WARNING: could not write assoc contents: %@", path]; [dict release]; } - (NSString *)associatedContents { NSDictionary *dict; NSString *value; NSString *path; path = [self associatedContentsPath]; dict = [[NSDictionary alloc] initWithContentsOfFile:path]; if (dict == nil) return nil; value = [[[dict objectForKey:[self personalFolderInfoKey]] copy] autorelease]; [dict release]; [self logWithFormat:@"assoc-contents: %@", value]; return value; } /* Exchange properties */ - (NSString *)outlookFolderClass { return @"IPF.Folder"; } - (BOOL)isReadAllowed { return YES; } - (BOOL)isModificationAllowed { return YES; } - (BOOL)isItemCreationAllowed { return YES; } - (BOOL)isFolderCreationAllowed { return NO; } - (BOOL)isDeletionAllowed { /* folders should not be deleted */ return NO; } /* file extension */ - (NSString *)fileExtensionForOutlook { /* either Evolution or Outlook */ return @"EML"; } - (NSString *)fileExtensionForFileSystem { /* Cadaver, MacOSX, Nautilus ... */ return nil; } - (NSString *)fileExtensionForChildrenInContext:(id)_ctx { NSString *ua; ua = [[[(WOContext *)_ctx request] clientCapabilities] userAgentType]; if ([ua hasPrefix:@"Cadaver"]) return [self fileExtensionForFileSystem]; if ([ua hasPrefix:@"MacOSXDAVFS"]) return [self fileExtensionForFileSystem]; if ([ua hasPrefix:@"GNOME-VFS"]) return [self fileExtensionForFileSystem]; if ([ua hasPrefix:@"MSOutlook"]) return [self fileExtensionForOutlook]; if ([ua hasPrefix:@"Evolution"]) return [self fileExtensionForOutlook]; [self logWithFormat:@"delivering raw filename to: %@", ua]; return nil; } /* URL processing */ - (NSString *)baseURLInContext:(id)_ctx { if (self->baseURL != nil) { if (_ctx != nil && _ctx != self->baseContext) { /* context changed */ [self->baseURL release]; self->baseURL = nil; } } if (self->baseURL == nil) { NSString *b; if (_ctx == nil) _ctx = [(WOApplication *)[WOApplication application] context]; if ((b = [super baseURLInContext:_ctx])) { if ([b hasSuffix:@"/"]) self->baseURL = [b copy]; else self->baseURL = [[b stringByAppendingString:@"/"] copy]; self->baseContext = _ctx; } } return self->baseURL; } - (NSString *)baseURL { return [self baseURLInContext: [(WOApplication *)[WOApplication application] context]]; } /* name lookup */ - (NSString *)normalizeKey:(NSString *)_key { /* useful for content-negotiation */ NSString *pe; if ([(pe = [_key pathExtension]) length] > 0) _key = [_key stringByDeletingPathExtension]; return _key; } - (BOOL)isNewKey:(NSString *)_key inContext:(id)_ctx { if ([_key length] == 0) return YES; if ([_key rangeOfString:@" "].length > 0) return YES; if ([_key rangeOfString:@"@"].length > 0) return YES; if ([_key rangeOfString:@"-"].length > 0) return YES; if (!isdigit([_key characterAtIndex:0])) /* as long as we don't allow login names as keys ... */ return YES; // TODO: what exactly is the gain of not fetching the EO? In which cases is // the EO not used? // maybe we should use the access manager to check for existance? return NO; } - (Class)recordClassForKey:(NSString *)_key { [self logWithFormat:@"Note: class does not specify class for key: '%@'", _key]; return Nil; } - (id)childForNewKey:(NSString *)_key inContext:(id)_ctx { [self logWithFormat:@"Note: class does not specify object for new key: '%@'", _key]; return nil; } - (id)childForExistingKey:(NSString *)_key inContext:(id)_ctx { Class recClass; id value; recClass = [self recordClassForKey:_key]; if ([recClass instancesRespondToSelector:@selector(initWithName:inFolder:)]) // TODO: deprecated?! value = [[recClass alloc] initWithName:_key inFolder:self]; else value = [[recClass alloc] initWithName:_key inContainer:self]; return [value autorelease]; } - (BOOL)shouldIgnoreName:(NSString *)_name inContext:(id)_ctx { /* check some artificial keys ... */ unsigned len = [_name length]; if (len == 0) return NO; if ([_name characterAtIndex:0] == '.') { if ([_name hasPrefix:@"._"]) /* OSX resource forks */ return YES; if ([_name isEqualToString:@".DS_Store"]) /* OSX indexer */ return YES; if ([_name isEqualToString:@".hidden"]) /* OSX .hidden file */ return YES; if ([_name isEqualToString:@".autodiskmounted"]) /* OSX */ return YES; } return NO; } /* IDs and Versions */ - (NSString *)getIDsAndVersionsInContext:(id)_ctx { // return all IDs and Versions in this format: // ID:Version\n [self logWithFormat:@"TODO: implement -getIDsAndVersionsInContext: !"]; return @""; } - (id)getIDsAndVersionsAction:(id)_ctx { WOResponse *response; NSString *s; if ((s = [self getIDsAndVersionsInContext:_ctx]) == nil) return nil; response = [(WOContext *)_ctx response]; [response setStatus:200]; /* OK */ [response setHeader:@"text/plain" forKey:@"content-type"]; [response setHeader:@"close" forKey:@"connection"]; [response appendContentString:s]; return response; } - (id)performETagsQuery:(EOFetchSpecification *)_fspec inContext:(id)_ctx { // TODO: rewrite getIDsAndVersionsInContext: to return some array/dict NSMutableArray *entries; NSString *csv; NSArray *lines; unsigned i, count; if ((csv = [self getIDsAndVersionsInContext:_ctx]) == nil) return nil; if ([csv length] == 0) return [NSArray array]; lines = [csv componentsSeparatedByString:@"\n"]; count = [lines count]; entries = [NSMutableArray arrayWithCapacity:count]; // [self logWithFormat:@"process lines: %@", lines]; for (i = 0; i < count; i++) { NSDictionary *record; NSString *line, *pkey, *etag, *url; id keys[2], values[2]; NSRange r; line = [lines objectAtIndex:i]; r = [line rangeOfString:@":"]; if (r.length == 0) { [self logWithFormat:@"ERROR: malformed getIDsAndVersions file!"]; continue; } /* NOTE: do _not_ change etag, used in other places! */ pkey = [line substringToIndex:r.location]; etag = line; url = [[NSString alloc] initWithFormat:@"%@%@.ics", [self baseURL], pkey]; keys[0] = @"{DAV:}href"; values[0] = url; keys[1] = @"davEntityTag"; values[1] = etag; record = [[NSDictionary alloc] initWithObjects:values forKeys:keys count:2]; [entries addObject:record]; [record release]; record = nil; [url release]; url = nil; } return entries; } /* name lookup */ - (id)lookupName:(NSString *)_name inContext:(id)_ctx acquire:(BOOL)_ac { NSString *nkey; id value; /* some special ZideStore things ... */ if ([_name hasPrefix:@"_range_"]) return [self lookupRangeQueryFolder:_name inContext:_ctx]; if ([_name isEqualToString:@"getIDsAndVersions"]) return [self getIDsAndVersionsAction:_ctx]; if ([self shouldIgnoreName:_name inContext:_ctx]) return nil; /* not found */ if ([_name hasSuffix:@".rss"] || [_name hasSuffix:@".xml"]) { if ([self respondsToSelector:@selector(rssInContext:)]) return [self rssInContext:_ctx]; } /* check methods */ if ((value = [super lookupName:_name inContext:_ctx acquire:_ac])) { if (debugLookup) [self debugWithFormat:@" value from superclass: %@", value]; return value; } /* normalize key */ nkey = [self normalizeKey:_name]; //[self debugWithFormat:@" normalized '%@'=>'%@'", _name, nkey]; // TODO: add some validity checking based on type-cache /* check cache */ // TODO: add a cache ... /* perform query */ value = [self isNewKey:nkey inContext:_ctx] ? [self childForNewKey:nkey inContext:_ctx] : [self childForExistingKey:nkey inContext:_ctx]; if (value == nil) { [self logWithFormat:@"ERROR(%s): got no record for key %@", __PRETTY_FUNCTION__, nkey]; return [NSException exceptionWithHTTPStatus:404 /* Not Found */ reason:@"could not resolve given key"]; } // TODO: add to cache return value; } /* actions */ - (NSString *)defaultMethodNameInContext:(id)_ctx { if ([[self soClass] hasKey:@"view" inContext:_ctx]) return @"view"; return nil; } - (id)GETAction:(id)_ctx { /* per default, return nothing ... */ WOResponse *r = [(WOContext *)_ctx response]; NSString *defName; if ((defName = [self defaultMethodNameInContext:_ctx]) != nil) { [r setStatus:302 /* moved */]; [r setHeader:[[self baseURL] stringByAppendingPathComponent:defName] forKey:@"location"]; return r; } [r setStatus:200 /* Ok */]; [self logWithFormat:@"GET on folder, returning nothing."]; return r; } - (id)PUTAction:(id)_ctx { /* per default, return nothing ... */ WOResponse *r; r = [(WOContext *)_ctx response]; [r setStatus:405 /* forbidden */]; [self logWithFormat: @"PUT on folder (attempt to create an object?), path-info: '%@'", [_ctx pathInfo]]; return r; } - (id)DELETEAction:(id)_ctx { if (![self isDeletionAllowed]) { [self logWithFormat:@"tried to delete protected folder"]; return [NSException exceptionWithHTTPStatus:403 /* forbidden */ reason:@"folder is protected against deletion"]; } return [NSException exceptionWithHTTPStatus:500 reason:@"folder deletion is not implemented"]; } /* property sets */ - (NSDictionary *)propsetPlist { static NSDictionary *plist = nil; NGResourceLocator *locator; NSString *path; if (plist != nil) return plist; locator = [NGResourceLocator zsfResourceLocator]; if ((path = [locator lookupFileWithName:@"DAVPropSets.plist"]) != nil) plist = [[NSDictionary alloc] initWithContentsOfFile:path]; else [self logWithFormat:@"ERROR: did not find DAVPropSets.plist!"]; return plist; } - (NSSet *)propertySetNamed:(NSString *)_name { static NSMutableDictionary *propsets = nil; NSDictionary *plist; NSArray *array; NSSet *set; NSEnumerator *enumerator; id obj; enumerator = [[self subPropMapper] objectEnumerator]; while ((obj = [enumerator nextObject]) != nil) { id res; if ((res = [obj propertySetNamed:_name])) return res; } if ((set = [propsets objectForKey:_name]) != nil) return set; if (propsets == nil) propsets = [[NSMutableDictionary alloc] init]; if ((plist = [self propsetPlist]) == nil) return nil; if ((array = [plist objectForKey:_name]) == nil) { static NSMutableSet *warnSets = nil; if (warnSets == nil) warnSets = [[NSMutableSet alloc] initWithCapacity:8]; if (![warnSets containsObject:_name]) { [self logWithFormat:@"Note: did not find property set '%@'", _name]; [warnSets addObject:_name]; } // TODO: we might want to fallback to some standard DAV property set return nil; } set = [[NSSet alloc] initWithArray:array]; [propsets setObject:set forKey:_name]; return [set autorelease]; } /* queries common for all folders */ - (BOOL)isETagsQuery:(EOFetchSpecification *)_fs { // subfolders static NSSet *listSet = nil; id propNames; if (listSet == nil) listSet = [[self propertySetNamed:@"QueryETagsSet"] retain]; if ((propNames = [_fs selectedWebDAVPropertyNames]) == nil) return NO; if ([propNames count] > [listSet count]) return NO; propNames = [NSSet setWithArray:propNames]; return [propNames isSubsetOfSet:listSet]; } - (BOOL)isWebDAVListQuery:(EOFetchSpecification *)_fs { // subfolders static NSSet *listSet = nil; id propNames; if (listSet == nil) listSet = [[self propertySetNamed:@"CadaverListSet"] retain]; if ((propNames = [_fs selectedWebDAVPropertyNames]) == nil) return NO; if ([propNames count] > [listSet count]) return NO; propNames = [NSSet setWithArray:propNames]; return [propNames isSubsetOfSet:listSet]; } /* KVC */ - (id)valueForUndefinedKey:(NSString *)_key { if (debugOn) [self debugWithFormat:@"queried undefined KVC key: '%@'", _key]; return nil; } /* debugging */ - (BOOL)isDebuggingEnabled { return debugOn; } - (NSString *)loggingPrefix { return [NSString stringWithFormat:@"|%@:%@|", NSStringFromClass([self class]), [self nameInContainer]]; } @end /* SxFolder */ @implementation NSObject(ContainerRetainManagement) /* Whether a child is supposed to retain this container. */ - (BOOL)shouldRetainAsSoContainer { return YES; } @end /* NSObject(ContainerRetainManagement) */