/* Copyright (C) 2002-2004 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. */ // $Id$ #include "SoObjectWebDAVDispatcher.h" #include "SoObject.h" #include "SoObject+SoDAV.h" #include "SoSecurityManager.h" #include "SoPermissions.h" #include "SoObjectRequestHandler.h" #include "SoSubscriptionManager.h" #include "SaxDAVHandler.h" #include "SoDAVLockManager.h" #include "EOFetchSpecification+SoDAV.h" #include "WOContext+SoObjects.h" #include #include #include #include #include #include #include #include "common.h" @interface WORequest(HackURI) - (void)_hackSetURI:(NSString *)_vuri; @end @implementation SoObjectWebDAVDispatcher static int debugOn = -1; static BOOL debugBulkTarget = NO; static NSNumber *yesNum = nil; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; static BOOL didInit = NO; if (didInit) return; didInit = YES; debugOn = [ud boolForKey:@"SoObjectDAVDispatcherDebugEnabled"] ? 1 : 0; if (debugOn) NSLog(@"Note: WebDAV dispatcher debugging is enabled."); if (yesNum == nil) yesNum = [[NSNumber numberWithBool:YES] retain]; } // THREAD static id xmlParser = nil; static SaxDAVHandler *davsax = nil; static NSTimeZone *gmt = nil; - (id)initWithObject:(id)_object { if ((self = [super init])) { self->object = [_object retain]; } return self; } - (void)dealloc { [self->object release]; [super dealloc]; } /* parser */ - (void)lockParser:(id)_sax { [_sax reset]; [xmlParser setContentHandler:_sax]; [xmlParser setErrorHandler:_sax]; } - (void)unlockParser:(id)_sax { [xmlParser setContentHandler:nil]; [xmlParser setErrorHandler:nil]; [_sax reset]; } /* common stuff */ - (NSException *)httpException:(int)_status reason:(NSString *)_reason { NSDictionary *ui; ui = [NSDictionary dictionaryWithObjectsAndKeys: self, @"dispatcher", [NSNumber numberWithInt:_status], @"http-status", nil]; return [NSException exceptionWithName: [NSString stringWithFormat:@"HTTP%i", _status] reason:_reason userInfo:ui]; } - (NSArray *)allowedMethods { static NSArray *defMethods = nil; NSMutableArray *allow; if (defMethods == nil) { defMethods = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SoWebDAVDefaultAllowMethods"] copy]; } allow = [NSMutableArray arrayWithCapacity:16]; if (defMethods) [allow addObjectsFromArray:defMethods]; if ([self->object respondsToSelector:@selector(performWebDAVQuery:inContext:)]) { [allow addObject:@"PROPFIND"]; [allow addObject:@"SEARCH"]; } if ([self->object respondsToSelector: @selector(davSetProperties:removePropertiesNamed:)]) [allow addObject:@"PROPPATCH"]; return allow; } - (NSString *)baseURLForContext:(WOContext *)_ctx { /* Note: Evolution doesn't correctly transfer the "Host:" header, it misses the port argument :-( */ NSString *baseURL; WORequest *rq; NSString *hostport; id tmp; rq = [_ctx request]; if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) { hostport = tmp; if ((tmp = [rq headerForKey:@"x-webobjects-server-port"])) hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp]; } else if ((tmp = [rq headerForKey:@"host"])) hostport = tmp; else hostport = [[NSHost currentHost] name]; baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]]; return baseURL; } - (id)primaryCallWebDAVMethod:(NSString *)_name inContext:(WOContext *)_ctx { id method; method = [self->object lookupName:_name inContext:_ctx acquire:NO]; if (method == nil) { return [self httpException:501 /* Not Implemented */ reason:@"target object does not support requested operation"]; } if ([method isKindOfClass:[NSException class]]) { [self logWithFormat:@"could not lookup method, got exception: %@", method]; return method; } [self debugWithFormat:@" %@ method: %@", _name, method]; return [method callOnObject:self->object inContext:_ctx]; } /* core HTTP methods */ - (id)doGET:(WOContext *)_ctx { NSException *e; id methodObject; if ((methodObject = [self->object lookupName:@"GET" inContext:_ctx acquire:NO]) == nil) methodObject = [self->object lookupDefaultMethod]; else { if ((e = [self->object validateName:@"GET" inContext:_ctx])) return e; } if (methodObject == nil) return self->object; if ([methodObject isKindOfClass:[NSException class]]) return methodObject; if ([methodObject respondsToSelector: @selector(takeValuesFromRequest:inContext:)]) [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx]; return [methodObject callOnObject:self->object inContext:_ctx]; } - (id)doPUT:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; NSString *pathInfo; pathInfo = [_ctx pathInfo]; [self debugWithFormat:@"doPUT (pathinfo='%@')", pathInfo]; /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission: ([pathInfo length] > 0) ? SoPerm_AddDocumentsImagesAndFiles : SoPerm_ChangeImagesAndFiles onObject:self->object inContext:_ctx]; if (e) return e; if ((e = [self->object validateName:@"PUT" inContext:_ctx])) return e; /* perform */ if ([pathInfo length] > 0) { /* check whether all the parent collections are available */ if ([pathInfo rangeOfString:@"/"].length > 0) { return [self httpException:409 /* Conflict */ reason: @"invalid WebDAV PUT request, first create all " @"parent collections !"]; } } return [self primaryCallWebDAVMethod:@"PUT" inContext:_ctx]; } - (id)doPOST:(WOContext *)_ctx { NSException *e; if ((e = [self->object validateName:@"POST" inContext:_ctx])) return e; return [self primaryCallWebDAVMethod:@"POST" inContext:_ctx]; } - (id)doDELETE:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:SoPerm_DeleteObjects onObject:self->object inContext:_ctx]; if (e) return e; if ((e = [self->object validateName:@"DELETE" inContext:_ctx])) return e; // TODO: IE WebFolders sent a "Destroy" header together with the // DELETE request, eg: // "Destroy: NoUndelete" return [self primaryCallWebDAVMethod:@"DELETE" inContext:_ctx]; } - (id)doOPTIONS:(WOContext *)_ctx { return [self allowedMethods]; } - (id)doHEAD:(WOContext *)_ctx { return [self doGET:_ctx]; } /* core WebDAV methods */ - (id)doMKCOL:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; NSString *pathInfo; pathInfo = [_ctx pathInfo]; if ([pathInfo length] == 0) { /* MKCOL target already exists ... */ WOResponse *r; [self logWithFormat:@"MKCOL target exists !"]; r = [_ctx response]; [r setStatus:405 /* method not allowed */]; [r appendContentString:@"collection already exists !"]; return r; } /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:SoPerm_AddFolders onObject:self->object inContext:_ctx]; if (e) return e; /* check whether all the parent collections are available */ if ([pathInfo rangeOfString:@"/"].length > 0) { return [self httpException:409 /* Conflict */ reason: @"invalid WebDAV MKCOL request, first create all " @"parent collections !"]; } /* check whether the object supports creating collections */ if (![self->object respondsToSelector: @selector(davCreateCollection:inContext:)]) { /* Note: this should never happen, as this is implemented on NSObject */ [self logWithFormat:@"MKCOL: object '%@' path-info '%@'", self->object, pathInfo]; return [self httpException:405 /* not allowed */ reason: @"this object cannot create a new collection with MKCOL"]; } if ((e = [self->object davCreateCollection:pathInfo inContext:_ctx])) { [self debugWithFormat:@"creation of collection '%@' failed: %@", pathInfo, e]; return e; } [self debugWithFormat:@"created collection."]; return [NSNumber numberWithBool:YES]; } - (NSString *)scopeForDepth:(NSString *)_depth inContext:(WOContext *)_ctx { NSString *scope; if ([_depth hasPrefix:@"0"]) scope = @"self"; else if ([_depth hasPrefix:@"1,noroot"]) scope = @"flat"; else if ([_depth hasPrefix:@"1"]) { NSString *ua; scope = @"flat+self"; /* some special handling for IE ... */ if ((ua = [[[_ctx request] clientCapabilities] userAgentType])) { if ([ua isEqualToString:@"Evolution"]) scope = @"flat"; else if ( [ua isEqualToString:@"WebFolder"]) scope = @"flat"; } } else if ([_depth hasPrefix:@"infinity"]) scope = @"deep"; else scope = @"deep"; return scope; } - (NSMutableDictionary *)hintsWithScope:(NSString *)_scope propNames:(NSArray *)_propNames findAll:(BOOL)_findAll namesOnly:(BOOL)_namesOnly { NSMutableDictionary *hints; hints = [NSMutableDictionary dictionaryWithCapacity:4]; if (_scope) [hints setObject:_scope forKey:@"scope"]; if (_propNames) [hints setObject:_propNames forKey:@"attributes"]; // else if (_findAll) ; /* empty attributes */ if (_namesOnly) [hints setObject:[NSNumber numberWithBool:YES] forKey:@"namesOnly"]; return hints; } - (id)doPROPFIND:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; EOFetchSpecification *fs; WORequest *rq; NSString *uri; NSString *depth; /* 0, 1, 1,noroot or infinity */ NSArray *propNames, *rtargets; BOOL findAll; BOOL findNames; id result; NSRange r; /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:SoPerm_AccessContentsInformation onObject:self->object inContext:_ctx]; if (e) return e; /* perform search */ if (![self->object respondsToSelector: @selector(performWebDAVQuery:inContext:)]) { return [self httpException:405 /* not allowed */ reason:@"this object cannot not execute a PROPFIND query"]; } rq = [_ctx request]; depth = [rq headerForKey:@"depth"]; uri = [rq uri]; if ([depth length] == 0) depth = @"infinity"; [self lockParser:davsax]; { [xmlParser parseFromSource:[rq content]]; propNames = [[davsax propFindQueriedNames] copy]; findAll = [davsax propFindAllProperties]; findNames = [davsax propFindPropertyNames]; } [self unlockParser:davsax]; propNames = [propNames autorelease]; /* check query all properties */ if (propNames == nil) propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx]; /* check for a ZideStore ranges query (a BPROPFIND "emulation") */ if (debugOn) [self logWithFormat:@"request uri: %@", uri]; r = [uri rangeOfString:@"_range"]; if (r.length > 0) { /* ZideStore range query */ NSString *s; NSArray *ids; if (debugOn) [self logWithFormat:@" detected a ZideStore range query: '%@'", uri]; s = [uri substringFromIndex:(r.location + r.length)]; if ([s hasSuffix:@"/"]) s = [s substringToIndex:([s length] - 1)]; if ([s hasPrefix:@"_"]) s = [s substringFromIndex:1]; ids = ([s length] == 0) ? [NSArray array] : [s componentsSeparatedByString:@"_"]; // TODO: should use -stringByUnescapingURL on IDs (not required for ints) rtargets = ids; if (debugOn) [self logWithFormat:@" IDs: %@", [ids componentsJoinedByString:@","]]; /* patch URI, could have side-effects ? */ [self logWithFormat: @"NOTE: hacked URI, _range_ part won't be visible in the HTTP " @"access log:\n%@", uri]; [rq _hackSetURI:[uri substringToIndex:r.location]]; } else rtargets = nil; /* build the fetch-spec */ { NSMutableDictionary *hints; hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx] propNames:propNames findAll:findAll namesOnly:findNames]; if (rtargets) /* range-query keys */ [hints setObject:rtargets forKey:@"bulkTargetKeys"]; fs = [EOFetchSpecification alloc]; fs = [fs initWithEntityName:[self baseURLForContext:_ctx] qualifier:nil sortOrderings:nil usesDistinct:NO isDeep:NO hints:hints]; fs = [fs autorelease]; if (debugOn) [self logWithFormat:@" propfind fetchspec: %@", fs]; } [_ctx setObject:fs forKey:@"DAVFetchSpecification"]; /* translate fetchspec if necessary */ { NSDictionary *map; if ((map = [self->object davAttributeMapInContext:_ctx])) { [_ctx setObject:map forKey:@"DAVPropertyMap"]; fs = [fs fetchSpecificationByApplyingKeyMap:map]; [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"]; } } /* perform */ if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) { return [self httpException:500 /* Server Error */ reason:@"could not perform query (object returned nil)"]; } if (debugOn) [self logWithFormat:@" propfind result: %@", result]; return result; } - (BOOL)allowDeletePropertiesOnNewObjectInContext:(WOContext *)_ctx { NSString *ua; ua = [[_ctx request] headerForKey:@"user-agent"]; if ([ua hasPrefix:@"Evolution"]) { /* if Evo creates tasks, it tries to delete some props at the same time */ return YES; } if ([ua hasPrefix:@"CFNetwork"]) { /* iSync trying to create a record ... */ return YES; } [self logWithFormat:@"do not allow delete properties on new object for: %@", ua]; return NO; } - (id)doPROPPATCH:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; NSMutableArray *resProps; NSArray *delProps; NSDictionary *setProps; NSString *pathInfo; pathInfo = [_ctx pathInfo]; /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:([pathInfo length] > 0) ? SoPerm_AddDocumentsImagesAndFiles : SoPerm_ChangeImagesAndFiles onObject:self->object inContext:_ctx]; if (e) return e; /* check for conflicts */ if ([pathInfo length] > 0) { /* check whether all the parent collections are available */ if ([pathInfo rangeOfString:@"/"].length > 0) { return [self httpException:409 /* Conflict */ reason: @"invalid WebDAV PROPPATCH request, first create all " @"parent collections !"]; } } /* check whether the object supports patching */ if ([pathInfo length] > 0) { if (![self->object respondsToSelector: @selector(davCreateObject:properties:inContext:)]) { [self debugWithFormat:@"cannot create new object via DAV on %@", self->object]; return [self httpException:405 /* not allowed */ reason: @"this object cannot create a new object with PROPPATCH"]; } } else { if (![self->object respondsToSelector: @selector(davSetProperties:removePropertiesNamed:inContext:)]) { [self debugWithFormat:@"cannot change object props via DAV on %@", self->object]; return [self httpException:405 /* not allowed */ reason:@"this object cannot PROPPATCH the attributes"]; } } /* parse request */ [self lockParser:davsax]; { [xmlParser parseFromSource:[[_ctx request] content]]; delProps = [[davsax propPatchPropertyNamesToRemove] copy]; setProps = [[davsax propPatchValues] copy]; } [self unlockParser:davsax]; delProps = [delProps autorelease]; setProps = [setProps autorelease]; if (delProps == nil && setProps == nil) { [self logWithFormat:@"WARNING: got no properties in PROPPATCH !"]; return [self httpException:400 /* bad request */ reason:@"got no properties in PROPPATCH !"]; } if ([pathInfo length] > 0) { /* a create object cannot delete props ... */ if ([delProps count] > 0) { if (![self allowDeletePropertiesOnNewObjectInContext:_ctx]) { [self logWithFormat:@"shall delete props in new object '%@': %@", pathInfo, delProps]; return [self httpException:400 /* bad request */ reason:@"cannot delete properties of a new object"]; } [self debugWithFormat:@"deleting properties on a new object: %@ ...", delProps]; } } resProps = [NSMutableArray arrayWithCapacity:16]; if (delProps) [resProps addObjectsFromArray:delProps]; if (setProps) [resProps addObjectsFromArray:[setProps allKeys]]; /* map attributes */ { NSDictionary *map; if ((map = [self->object davAttributeMapInContext:_ctx])) { unsigned count; [_ctx setObject:map forKey:@"DAVPropertyMap"]; if ((count = [delProps count]) > 0) { NSMutableArray *mappedDelProps; unsigned i; mappedDelProps = [NSMutableArray arrayWithCapacity:(count + 1)]; for (i = 0; i < count; i++) { NSString *k, *tk; k = [delProps objectAtIndex:i]; tk = [map valueForKey:k]; [mappedDelProps addObject:(tk ? tk : k)]; } delProps = mappedDelProps; } if ((count = [setProps count]) > 0) { NSMutableDictionary *mappedSetProps; NSEnumerator *keys; NSString *k; mappedSetProps = [NSMutableDictionary dictionaryWithCapacity:count]; keys = [setProps keyEnumerator]; while ((k = [keys nextObject])) { NSString *tk; tk = [map valueForKey:k]; [mappedSetProps setObject:[setProps objectForKey:k] forKey:(tk ? tk : k)]; } setProps = mappedSetProps; } } } if (debugOn) { [self debugWithFormat:@"PROPPATCH '%@': delete=%@, set=%@", pathInfo, delProps, setProps]; } if ([pathInfo length] == 0) { /* edit an object */ NSException *e; e = [self->object davSetProperties:setProps removePropertiesNamed:delProps inContext:_ctx]; if (e) return e; } else { /* create an object */ id newChild; newChild = [self->object davCreateObject:pathInfo properties:setProps inContext:_ctx]; if ([newChild isKindOfClass:[NSException class]]) return newChild; [self debugWithFormat:@"created: %@", newChild]; } /* generate response */ return resProps; } - (id)doLOCK:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; SoDAVLockManager *lockManager; WORequest *rq; WOResponse *r; NSString *ifValue, *lockDepth; id token; /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:SoPerm_WebDAVLockItems onObject:self->object inContext:_ctx]; if (e) return e; /* check lock manager */ if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) { return [self httpException:405 /* method not allowed */ reason:@"target object does not support locking !"]; } rq = [_ctx request]; r = [_ctx response]; lockDepth = [rq headerForKey:@"depth"]; ifValue = [rq headerForKey:@"if"]; if (lockDepth != nil && ![lockDepth isEqualToString:@"0"]) { [self logWithFormat: @"WARNING: 'depth' locking not supported yet (depth=%@)!", lockDepth]; } if (ifValue) { [self logWithFormat: @"WARNING: 'if' locking not supported yet, if: '%@'", ifValue]; } // need to parse lockinfo token = [lockManager lockURI:[rq uri] timeout:[rq headerForKey:@"timeout"] scope:@"exclusive" type:@"write" owner:nil]; if (token == nil) { /* already locked */ return [self httpException:423 /* locked */ reason:@"object locked, lock manager did not provide token."]; } [self debugWithFormat:@"locked: %@ (token %@)", [[_ctx request] uri], token]; return token; } - (id)doUNLOCK:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; SoDAVLockManager *lockManager; NSString *token; /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:SoPerm_WebDAVUnlockItems onObject:self->object inContext:_ctx]; if (e) return e; /* check lock manager */ if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) { return [self httpException:405 /* method not allowed */ reason:@"target object does not support locking."]; } token = [[_ctx request] headerForKey:@"lock-token"]; [lockManager unlockURI:[[_ctx request] uri] token:token]; [self debugWithFormat: @"unlocked: %@ (token %@)", [[_ctx request] uri], token]; [[_ctx response] setStatus:204 /* fake ok */]; return [_ctx response]; } - (NSException *)extractDestinationPath:(NSArray **)path_ fromContext:(WOContext *)_ctx { NSString *absDestURL; NSURL *destURL, *srvURL; if (path_) *path_ = nil; /* TODO: check proper permission prior attempting a move */ absDestURL = [[_ctx request] headerForKey:@"destination"]; if ([absDestURL length] == 0) { return [self httpException:400 /* Bad Request */ reason: @"the destination WebDAV header was missing " @"for the MOVE/COPY operation"]; } if ((destURL = [NSURL URLWithString:absDestURL]) == nil) { [self logWithFormat:@"MOVE: got invalid destination URL: '%@'", absDestURL]; return [self httpException:400 /* Bad Request */ reason:@"the MOVE/COPY destination is not a valid URL!"]; } srvURL = [_ctx serverURL]; [self debugWithFormat:@"move/copy:\n to: %@\n server: %@)", [destURL absoluteString], [srvURL absoluteString]]; /* check whether URL is on the same server ... */ if (![[srvURL host] isEqualToString:[destURL host]] || ![[srvURL port] isEqual:[destURL port]]) { /* The WebDAV spec is not really clear on what we should return in this case? Let me know if anybody has a suggestion ... */ [self logWithFormat:@"tried to do a cross server move (%@ vs %@)", [srvURL absoluteString], [destURL absoluteString]]; return [self httpException:403 /* Forbidden */ reason:@"MOVE destination is on a different host."]; } if (path_) { NSMutableArray *ma; unsigned i; /* TODO: hack hack hack */ ma = [[[destURL path] componentsSeparatedByString:@"/"] mutableCopy]; if ([ma count] > 0) // leading slash ("") [ma removeObjectAtIndex:0]; if ([ma count] > 0) // the appname (eg zidestore) [ma removeObjectAtIndex:0]; if ([ma count] > 0) // the request handler key (eg so) [ma removeObjectAtIndex:0]; /* unescape path components */ for (i = 0; i < [ma count]; i++) { NSString *s = [ma objectAtIndex:i], *ns; ns = [s stringByUnescapingURL]; if (ns != s) [ma replaceObjectAtIndex:i withObject:ns]; } *path_ = [ma copy]; [ma release]; } return nil; } - (NSException *)lookupDestinationObject:(id *)target_ andNewName:(NSString **)name_ inContext:(WOContext *)_ctx { NSException *error; NSArray *targetPath; id root; if ((error = [self extractDestinationPath:&targetPath fromContext:_ctx])) return error; if ((root = [_ctx application]) == nil) root = [WOApplication application]; if (root == nil) { return [self httpException:500 /* internal server error */ reason:@"did not find SOPE root object"]; } /* TODO: we should probably use a subcontext?! */ [_ctx setObject:yesNum forKey:@"isDestinationPathLookup"]; *target_ = [root traversePathArray:targetPath inContext:_ctx error:&error acquire:NO]; if (error) { [self logWithFormat:@"could not resolve destination object (%@): %@", [targetPath componentsJoinedByString:@" => "], error]; return error; } if (name_) *name_ = [[[_ctx pathInfo] copy] autorelease]; if (*target_ == nil) { [self debugWithFormat:@"MOVE/COPY destination could not be found."]; return [self httpException:404 /* Not Found */ reason:@"did not find target object"]; } [self debugWithFormat:@"SOURCE: %@", self->object]; [self debugWithFormat:@"TARGET: %@ (PI %@)", *target_, [_ctx pathInfo]]; return nil; } - (id)doCOPY:(WOContext *)_ctx { NSException *error; NSString *newName; id targetObject; /* TODO: check proper permission prior attempting a copy */ error = [self lookupDestinationObject:&targetObject andNewName:&newName inContext:_ctx]; if (error) return error; error = [self->object davCopyToTargetObject:targetObject newName:newName inContext:_ctx]; if (error) { [self debugWithFormat:@"WebDAV COPY operation failed: %@", error]; return error; } return ([newName length] > 0) ? [NSNumber numberWithBool:201 /* Created */] : [NSNumber numberWithBool:204 /* No Content */]; } - (id)doMOVE:(WOContext *)_ctx { NSException *error; NSString *newName; id targetObject; /* TODO: check proper permission prior attempting a move */ error = [self lookupDestinationObject:&targetObject andNewName:&newName inContext:_ctx]; if (error) return error; /* Note: more relevant headers: overwrite: T|F (overwrite target) [rc: 201 vs 204!] depth: infinity and locking tokens of course ... */ // TODO: should we check in this place for some constraints, // eg moving a collection to a non-collection or something // like that? error = [self->object davMoveToTargetObject:targetObject newName:newName inContext:_ctx]; if (error) { [self debugWithFormat:@"WebDAV MOVE operation failed: %@", error]; return error; } return ([newName length] > 0) ? [NSNumber numberWithBool:201 /* Created */] : [NSNumber numberWithBool:204 /* No Content */]; } /* WebDAV search methods */ - (id)doSEARCH:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; EOFetchSpecification *fs; NSString *baseURL; id result; NSString *range; /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:SoPerm_AccessContentsInformation onObject:self->object inContext:_ctx]; if (e) return e; /* perform search */ if (![self->object respondsToSelector:@selector(performWebDAVQuery:inContext:)]) { [[_ctx response] setStatus:405 /* not allowed */]; [[_ctx response] appendContentString: @"this object cannot not execute a SEARCH query"]; return [_ctx response]; } baseURL = [NSString stringWithFormat:@"http://%@%@", [[_ctx request] headerForKey:@"host"], [[_ctx request] uri]]; [self lockParser:davsax]; { [xmlParser parseFromSource:[[_ctx request] content]]; fs = [[davsax searchFetchSpecification] retain]; } [self unlockParser:davsax]; fs = [fs autorelease]; if (fs == nil) { [[_ctx response] setStatus:400 /* Bad Request */]; [[_ctx response] appendContentString: @"could not process SEARCH query specification"]; return [_ctx response]; } /* range */ if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) { /* TODO: parse range header and add to fetch-specification */ NSRange r; r = [range rangeOfString:@"rows="]; if (r.length > 0) { range = [range substringFromIndex:(r.location + r.length)]; [self debugWithFormat: @"Note: got a row range header (ignored): '%@'", range]; } else [self logWithFormat:@"Note: got a range header (ignored): '%@'", range]; } /* override entity name ... (FROM xxx isn't yet parsed correctly) */ [fs setEntityName:baseURL]; [self debugWithFormat:@"SEARCH: %@", fs]; [_ctx setObject:fs forKey:@"DAVFetchSpecification"]; /* translate fetchspec if necessary */ { NSDictionary *map; if ((map = [self->object davAttributeMapInContext:_ctx])) { [_ctx setObject:map forKey:@"DAVPropertyMap"]; fs = [fs fetchSpecificationByApplyingKeyMap:map]; [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"]; } } /* perform call */ if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) { return [self httpException:500 /* Server Error */ reason:@"could not execute SEARCH query (returned nil)"]; } return result; } /* Exchange WebDAV methods */ - (id)doNOTIFY:(WOContext *)_ctx { return [self httpException:403 reason:@"NOTIFY not yet implemented"]; } - (id)doPOLL:(WOContext *)_ctx { SoSubscriptionManager *sm; WORequest *rq; NSString *subscriptionID; NSArray *ids; NSURL *url; rq = [_ctx request]; sm = [SoSubscriptionManager sharedSubscriptionManager]; url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]]; if (url == nil) { return [self httpException:500 reason:@"could not calculate URL of WebDAV object !"]; } subscriptionID = [rq headerForKey:@"subscription-id"]; if ([subscriptionID length] == 0) { return [self httpException:400 /* Bad Request */ reason:@"did not find subscription-id header in POLL"]; } ids = [subscriptionID componentsSeparatedByString:@","]; return [sm pollSubscriptions:ids onURL:url]; } - (id)doSUBSCRIBE:(WOContext *)_ctx { SoSubscriptionManager *sm; WORequest *rq; WOResponse *r; NSURL *url; id callback; NSString *notificationType; NSString *notificationDelay; NSString *lifetime; NSString *subscriptionID; rq = [_ctx request]; r = [_ctx response]; sm = [SoSubscriptionManager sharedSubscriptionManager]; url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]]; if (url == nil) { return [self httpException:500 reason:@"could not calculate URL of WebDAV object !"]; } subscriptionID = [rq headerForKey:@"subscription-id"]; /* first check, whether it's an existing subscription to be renewed */ if ([subscriptionID length] > 0) { NSString *newId; if ((newId = [sm renewSubscription:subscriptionID onURL:url]) == nil) { return [self httpException:412 /* precondition failed */ reason:@"did not find provided subscription ID !"]; } return newId; } if ((callback = [rq headerForKey:@"call-back"])) { NSURL *url; if ((url = [NSURL URLWithString:[callback stringValue]]) == nil) { [self debugWithFormat:@"ERROR: could not parse callback URL '%@'", callback]; return [self httpException:400 /* Bad Request */ reason:@"missing valid callback URL !"]; } else callback = url; } /* TODO: add sanity checking of notification-type as described in docs */ /* TODO: check depth */ notificationDelay = [rq headerForKey:@"notification-delay"]; notificationType = [rq headerForKey:@"notification-type"]; lifetime = [rq headerForKey:@"subscription-lifetime"]; subscriptionID = [sm subscribeURL:url forObserver:callback type:notificationType delay:notificationDelay ? [notificationDelay doubleValue] : 0.0 lifetime:lifetime ? [lifetime doubleValue] : 0.0]; return subscriptionID; } - (id)doUNSUBSCRIBE:(WOContext *)_ctx { SoSubscriptionManager *sm; WORequest *rq; WOResponse *r; NSString *subscriptionID; NSURL *url; rq = [_ctx request]; r = [_ctx response]; sm = [SoSubscriptionManager sharedSubscriptionManager]; url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]]; if (url == nil) { return [self httpException:500 reason:@"could not calculate URL of WebDAV object !"]; } subscriptionID = [rq headerForKey:@"subscription-id"]; if ([subscriptionID length] == 0) { return [self httpException:400 /* Bad Request */ reason:@"missing subscription id !"]; } if ([sm unsubscribeID:subscriptionID onURL:url]) { [r setStatus:200]; return r; } else { return [self httpException:400 /* Bad Request */ reason:@"unsubscribe failed (invalid or old id ?)"]; } } /* Exchange bulk methods */ - (NSArray *)urlPartsForTargets:(NSArray *)_targets basePath:(NSString *)_base{ /* Transform the target URLs given to the BPROPFIND operation. This is a simplified implementation, for example we expect that the URLs are all located in the same URL space (on same host and port). */ NSMutableArray *ma; unsigned i, count; if ((count = [_targets count]) == 0) return [NSArray array]; ma = [NSMutableArray arrayWithCapacity:count]; for (i = 0; i < count; i++) { NSString *target; target = [_targets objectAtIndex:i]; if (debugBulkTarget) [self logWithFormat:@" MORPH target '%@'", target]; /* extract the path from full URLs */ if ([target isAbsoluteURL]) { NSURL *url; /* fix an Evolution bug, uses the 'unsafe' "@" in the URL ! */ if ([target rangeOfString:@"@"].length > 0) { target = [target stringByReplacingString:@"@" withString:@"%40"]; } if ((url = [NSURL URLWithString:target])) { if (debugBulkTarget) [self logWithFormat:@"got URL: %@", url]; target = [url path]; if (debugBulkTarget) [self logWithFormat:@"path: %@", target]; } else { [self logWithFormat:@"ERROR: could not parse BPROPFIND target '%@' !", target]; } } /* make the target name relative to the request URI */ if ([target hasPrefix:_base]) { target = [target substringFromIndex:[_base length]]; if ([target hasPrefix:@"/"]) target = [target substringFromIndex:1]; } /* add the target */ target = [target stringByUnescapingURL]; if (debugBulkTarget) [self logWithFormat:@" ADD target '%@'", target]; [ma addObject:target]; } return ma; } - (id)doBPROPFIND:(WOContext *)_ctx { /* TODO: could optimize a BPROPFIND on a single target to use PROPFIND How are BPROPFINDs mapped ? BPROPFIND corresponds to SKYRiX 4.1 "fetch-by-globalids" commands, that is, a search gets passed a list of primary keys to fetch. BPROPFIND is implemented in a similiar way, the target URLs are converted to be relative to the URI object and are passed to the query datasource using the "bulkTargetKeys" fetch hint. Important: the URI object *must* support the "bulkTargetKeys" fetch hint, otherwise the operation will run on the object itself. Note: Previously BPROPFIND was mapped to a set of individual requests, but obviously this doesn't match SQL very well (resulting in an individual SQL query for each entity ...) */ SoSecurityManager *sm; NSException *e; EOFetchSpecification *fs; WORequest *rq; NSString *depth; /* 0, 1, 1,noroot or infinity */ NSArray *propNames; NSArray *targets, *rtargets; BOOL findAll; BOOL findNames; id result; NSDictionary *map; /* check permissions */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:SoPerm_AccessContentsInformation onObject:self->object inContext:_ctx]; if (e) return e; /* perform search */ if (![self->object respondsToSelector:@selector(performWebDAVQuery:inContext:)]) { return [self httpException:405 /* not allowed */ reason:@"this object cannot not execute a PROPFIND query"]; } rq = [_ctx request]; depth = [rq headerForKey:@"depth"]; if ([depth length] == 0) depth = @"infinity"; [self lockParser:davsax]; { [xmlParser parseFromSource:[rq content]]; propNames = [[davsax propFindQueriedNames] copy]; findAll = [davsax propFindAllProperties]; findNames = [davsax propFindPropertyNames]; targets = [[davsax bpropFindTargets] copy]; } [self unlockParser:davsax]; propNames = [propNames autorelease]; targets = [targets autorelease]; if ([targets count] == 0) return [NSArray array]; /* check query all properties */ if (propNames == nil) propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx]; /* morph targets */ rtargets = [self urlPartsForTargets:targets basePath:[[rq uri] stringByUnescapingURL]]; [self debugWithFormat:@"BPROPFIND targets: %@", rtargets]; /* build the fetch-spec */ { NSMutableDictionary *hints; hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx] propNames:propNames findAll:findAll namesOnly:findNames]; [hints setObject:rtargets forKey:@"bulkTargetKeys"]; fs = [EOFetchSpecification alloc]; fs = [fs initWithEntityName:[self baseURLForContext:_ctx] qualifier:nil sortOrderings:nil usesDistinct:NO isDeep:NO hints:hints]; fs = [fs autorelease]; } [_ctx setObject:fs forKey:@"DAVFetchSpecification"]; /* translate fetchspec if necessary - we currently cannot allow a map for each target, so we use the map of the queried target. */ if ((map = [self->object davAttributeMapInContext:_ctx])) { [_ctx setObject:map forKey:@"DAVPropertyMap"]; fs = [fs fetchSpecificationByApplyingKeyMap:map]; [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"]; } /* perform */ if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) { return [self httpException:500 /* Server Error */ reason:@"could not perform query (object returned nil)"]; } return result; #if 0 /* now, for each BPROPFIND target ... */ { NSEnumerator *e; NSString *targetURL; result = [NSMutableArray arrayWithCapacity:32]; e = [targets objectEnumerator]; while ((targetURL = [e nextObject])) { NSAutoreleasePool *pool; WOContext *localContext; WORequest *localRequest; NSException *e; id targetObject; id targetResult; pool = [[NSAutoreleasePool alloc] init]; /* setup the "subrequest" */ if ([targetURL isAbsoluteURL]) { NSURL *url; if ((url = [NSURL URLWithString:targetURL])) targetURL = [url path]; else { [self logWithFormat:@"ERROR: could not parse target-url '%@'", targetURL]; } } localRequest = [[WORequest alloc] initWithMethod:@"PROPFIND" uri:targetURL httpVersion:[rq httpVersion] headers:[rq headers] content:nil userInfo:nil]; localContext = [[[WOContext alloc] initWithRequest:localRequest] autorelease]; [localRequest autorelease]; /* resetup fetchspec */ [fs setEntityName:targetURL]; /* traverse URL */ targetObject = [_ctx traversalRoot]; targetObject = [targetObject traversePathArray: [localRequest requestHandlerPathArray] inContext:localContext error:&e acquire:NO]; if (targetObject == nil) { [self logWithFormat:@"did not find BPROPFIND target: %@", targetURL]; [self logWithFormat:@" root: %@", [_ctx traversalRoot]]; [self logWithFormat:@" path: %@", [[localRequest requestHandlerPathArray] componentsJoinedByString:@"/"]]; [self logWithFormat:@" error: %@", e]; targetResult = e; } else { /* perform query */ targetResult = [targetObject performWebDAVQuery:fs inContext:localContext]; if (targetResult == nil) { targetResult = [self httpException:500 /* Server Error */ reason:@"could not perform query (object returned nil)"]; } } // do we need to distinguish the queries somehow ? (href generation) if ([targetResult isKindOfClass:[NSArray class]]) [result addObjectsFromArray:targetResult]; else if (targetResult) [result addObject:targetResult]; [pool release]; } } /* perform */ if (result) return result; #endif } - (id)doBCOPY:(WOContext *)_ctx { return [self httpException:403 /* forbidden */ reason:@"BCOPY not yet implemented."]; } - (id)doBDELETE:(WOContext *)_ctx { return [self httpException:403 /* forbidden */ reason:@"BDELETE not yet implemented."]; } - (id)doBMOVE:(WOContext *)_ctx { return [self httpException:403 /* forbidden */ reason:@"WebDAV operation not yet implemented."]; } - (id)doBPROPPATCH:(WOContext *)_ctx { return [self httpException:403 /* forbidden */ reason:@"WebDAV operation not yet implemented."]; } /* DAV access control lists */ - (id)doACL:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } /* DAV binding */ - (id)doBIND:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } /* DAV ordering */ - (id)doORDERPATCH:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } /* DAV deltav */ - (id)doCHECKOUT:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } - (id)doUNCHECKOUT:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } - (id)doCHECKIN:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } - (id)doMKWORKSPACE:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } - (id)doUPDATE:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } - (id)doMERGE:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } - (id)doVERSIONCONTROL:(WOContext *)_ctx { return [self httpException:405 /* method not allowed */ reason:@"WebDAV operation not yet implemented."]; } /* perform dispatch */ - (id)performMethod:(NSString *)_method inContext:(WOContext *)_ctx { SoSecurityManager *sm; NSException *e; NSString *s; SEL sel; /* check basic WebDAV permission */ sm = [_ctx soSecurityManager]; e = [sm validatePermission:SoPerm_WebDAVAccess onObject:self->object inContext:_ctx]; if (e) return e; /* perform search */ _method = [_method uppercaseString]; _method = [_method stringByReplacingString:@"-" withString:@""]; s = [NSString stringWithFormat:@"do%@:", _method]; sel = NSSelectorFromString(s); if (![self respondsToSelector:sel]) { [self logWithFormat:@"unknown WebDAV method: '%@'", _method]; [[_ctx response] setStatus:405 /* invalid method */]; return [_ctx response]; } return [self performSelector:sel withObject:_ctx]; } - (BOOL)setupXmlParser { if (xmlParser == nil) { xmlParser = [[[SaxXMLReaderFactory standardXMLReaderFactory] createXMLReaderForMimeType:@"text/xml"] retain]; if (xmlParser == nil) return NO; } if (davsax == nil) { if ((davsax = [[SaxDAVHandler alloc] init]) == nil) return NO; } return YES; } - (id)dispatchInContext:(WOContext *)_ctx { NSAutoreleasePool *pool; WOResponse *r; id result; if (gmt == nil) gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain]; /* setup XML parser */ if (![self setupXmlParser]) { r = [_ctx response]; [r setStatus:500 /* internal server error */]; [r appendContentString:@"did not find an XML parser, cannot process DAV."]; return r; } pool = [[NSAutoreleasePool alloc] init]; result = [[self performMethod:[[_ctx request] method] inContext:_ctx] retain]; [pool release]; return [result autorelease]; } /* logging */ - (NSString *)loggingPrefix { return @"[obj-dav-dispatch]"; } - (BOOL)isDebuggingEnabled { return debugOn ? YES : NO; } /* description */ - (NSString *)description { NSMutableString *ms; ms = [NSMutableString stringWithCapacity:64]; [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass((Class)*(void**)self)]; if (self->object) [ms appendFormat:@" object=%@", self->object]; else [ms appendString:@" "]; [ms appendString:@">"]; return ms; } @end /* SoObjectWebDAVDispatcher */