2 Copyright (C) 2002-2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
23 #include "SoObjectWebDAVDispatcher.h"
25 #include "SoObject+SoDAV.h"
26 #include "SoSecurityManager.h"
27 #include "SoPermissions.h"
28 #include "SoObjectRequestHandler.h"
29 #include "SoSubscriptionManager.h"
30 #include "SaxDAVHandler.h"
31 #include "SoDAVLockManager.h"
32 #include "EOFetchSpecification+SoDAV.h"
33 #include "WOContext+SoObjects.h"
34 #include <NGObjWeb/WORequest.h>
35 #include <NGObjWeb/WOResponse.h>
36 #include <NGObjWeb/WOContext.h>
37 #include <NGObjWeb/WEClientCapabilities.h>
38 #include <SaxObjC/SaxObjC.h>
39 #include <SaxObjC/XMLNamespaces.h>
40 #include <NGExtensions/NSString+Ext.h>
43 @interface WORequest(HackURI)
44 - (void)_hackSetURI:(NSString *)_vuri;
47 @implementation SoObjectWebDAVDispatcher
49 static int debugOn = -1;
50 static BOOL debugBulkTarget = NO;
51 static NSNumber *yesNum = nil;
54 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
55 static BOOL didInit = NO;
59 debugOn = [ud boolForKey:@"SoObjectDAVDispatcherDebugEnabled"] ? 1 : 0;
60 if (debugOn) NSLog(@"Note: WebDAV dispatcher debugging is enabled.");
61 if (yesNum == nil) yesNum = [[NSNumber numberWithBool:YES] retain];
65 static id<NSObject,SaxXMLReader> xmlParser = nil;
66 static SaxDAVHandler *davsax = nil;
67 static NSTimeZone *gmt = nil;
69 - (id)initWithObject:(id)_object {
70 if ((self = [super init])) {
71 self->object = [_object retain];
76 [self->object release];
82 - (void)lockParser:(id)_sax {
84 [xmlParser setContentHandler:_sax];
85 [xmlParser setErrorHandler:_sax];
87 - (void)unlockParser:(id)_sax {
88 [xmlParser setContentHandler:nil];
89 [xmlParser setErrorHandler:nil];
95 - (NSException *)httpException:(int)_status reason:(NSString *)_reason {
98 ui = [NSDictionary dictionaryWithObjectsAndKeys:
100 [NSNumber numberWithInt:_status], @"http-status",
102 return [NSException exceptionWithName:
103 [NSString stringWithFormat:@"HTTP%i", _status]
108 - (NSArray *)allowedMethods {
109 static NSArray *defMethods = nil;
110 NSMutableArray *allow;
112 if (defMethods == nil) {
113 defMethods = [[[NSUserDefaults standardUserDefaults]
114 arrayForKey:@"SoWebDAVDefaultAllowMethods"]
118 allow = [NSMutableArray arrayWithCapacity:16];
119 if (defMethods) [allow addObjectsFromArray:defMethods];
122 respondsToSelector:@selector(performWebDAVQuery:inContext:)]) {
123 [allow addObject:@"PROPFIND"];
124 [allow addObject:@"SEARCH"];
126 if ([self->object respondsToSelector:
127 @selector(davSetProperties:removePropertiesNamed:)])
128 [allow addObject:@"PROPPATCH"];
133 - (NSString *)baseURLForContext:(WOContext *)_ctx {
135 Note: Evolution doesn't correctly transfer the "Host:" header, it
136 misses the port argument :-(
145 if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
147 if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
148 hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
150 else if ((tmp = [rq headerForKey:@"host"]))
153 hostport = [[NSHost currentHost] name];
155 baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
159 - (id)primaryCallWebDAVMethod:(NSString *)_name inContext:(WOContext *)_ctx {
162 method = [self->object lookupName:_name inContext:_ctx acquire:NO];
164 return [self httpException:501 /* Not Implemented */
165 reason:@"target object does not support requested operation"];
167 if ([method isKindOfClass:[NSException class]]) {
168 [self logWithFormat:@"could not lookup method, got exception: %@", method];
172 [self debugWithFormat:@" %@ method: %@", _name, method];
173 return [method callOnObject:self->object inContext:_ctx];
176 /* core HTTP methods */
178 - (id)doGET:(WOContext *)_ctx {
184 [self->object lookupName:@"GET" inContext:_ctx acquire:NO]) == nil)
185 methodObject = [self->object lookupDefaultMethod];
187 if ((e = [self->object validateName:@"GET" inContext:_ctx]))
191 if (methodObject == nil)
193 if ([methodObject isKindOfClass:[NSException class]])
196 if ([methodObject respondsToSelector:
197 @selector(takeValuesFromRequest:inContext:)])
198 [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx];
200 return [methodObject callOnObject:self->object inContext:_ctx];
203 - (id)doPUT:(WOContext *)_ctx {
204 SoSecurityManager *sm;
208 pathInfo = [_ctx pathInfo];
209 [self debugWithFormat:@"doPUT (pathinfo='%@')", pathInfo];
211 /* check permissions */
213 sm = [_ctx soSecurityManager];
214 e = [sm validatePermission:
215 ([pathInfo length] > 0)
216 ? SoPerm_AddDocumentsImagesAndFiles
217 : SoPerm_ChangeImagesAndFiles
218 onObject:self->object
222 if ((e = [self->object validateName:@"PUT" inContext:_ctx]))
227 if ([pathInfo length] > 0) {
228 /* check whether all the parent collections are available */
229 if ([pathInfo rangeOfString:@"/"].length > 0) {
230 return [self httpException:409 /* Conflict */
232 @"invalid WebDAV PUT request, first create all "
233 @"parent collections !"];
237 return [self primaryCallWebDAVMethod:@"PUT" inContext:_ctx];
240 - (id)doPOST:(WOContext *)_ctx {
243 if ((e = [self->object validateName:@"POST" inContext:_ctx]))
246 return [self primaryCallWebDAVMethod:@"POST" inContext:_ctx];
249 - (id)doDELETE:(WOContext *)_ctx {
250 SoSecurityManager *sm;
253 /* check permissions */
255 sm = [_ctx soSecurityManager];
256 e = [sm validatePermission:SoPerm_DeleteObjects
257 onObject:self->object
260 if ((e = [self->object validateName:@"DELETE" inContext:_ctx]))
263 // TODO: IE WebFolders sent a "Destroy" header together with the
264 // DELETE request, eg:
265 // "Destroy: NoUndelete"
267 return [self primaryCallWebDAVMethod:@"DELETE" inContext:_ctx];
270 - (id)doOPTIONS:(WOContext *)_ctx {
271 return [self allowedMethods];
274 - (id)doHEAD:(WOContext *)_ctx {
275 return [self doGET:_ctx];
278 /* core WebDAV methods */
280 - (id)doMKCOL:(WOContext *)_ctx {
281 SoSecurityManager *sm;
285 pathInfo = [_ctx pathInfo];
286 if ([pathInfo length] == 0) {
287 /* MKCOL target already exists ... */
290 [self logWithFormat:@"MKCOL target exists !"];
293 [r setStatus:405 /* method not allowed */];
294 [r appendContentString:@"collection already exists !"];
298 /* check permissions */
300 sm = [_ctx soSecurityManager];
301 e = [sm validatePermission:SoPerm_AddFolders
302 onObject:self->object
306 /* check whether all the parent collections are available */
307 if ([pathInfo rangeOfString:@"/"].length > 0) {
308 return [self httpException:409 /* Conflict */
310 @"invalid WebDAV MKCOL request, first create all "
311 @"parent collections !"];
314 /* check whether the object supports creating collections */
316 if (![self->object respondsToSelector:
317 @selector(davCreateCollection:inContext:)]) {
318 /* Note: this should never happen, as this is implemented on NSObject */
320 [self logWithFormat:@"MKCOL: object '%@' path-info '%@'",
321 self->object, pathInfo];
322 return [self httpException:405 /* not allowed */
324 @"this object cannot create a new collection with MKCOL"];
327 if ((e = [self->object davCreateCollection:pathInfo inContext:_ctx])) {
328 [self debugWithFormat:@"creation of collection '%@' failed: %@",
333 [self debugWithFormat:@"created collection."];
334 return [NSNumber numberWithBool:YES];
337 - (NSString *)scopeForDepth:(NSString *)_depth inContext:(WOContext *)_ctx {
340 if ([_depth hasPrefix:@"0"])
342 else if ([_depth hasPrefix:@"1,noroot"])
344 else if ([_depth hasPrefix:@"1"]) {
347 scope = @"flat+self";
349 /* some special handling for IE ... */
350 if ((ua = [[[_ctx request] clientCapabilities] userAgentType])) {
351 if ([ua isEqualToString:@"Evolution"])
353 else if ( [ua isEqualToString:@"WebFolder"])
357 else if ([_depth hasPrefix:@"infinity"])
365 - (NSMutableDictionary *)hintsWithScope:(NSString *)_scope
366 propNames:(NSArray *)_propNames
367 findAll:(BOOL)_findAll
368 namesOnly:(BOOL)_namesOnly
370 NSMutableDictionary *hints;
372 hints = [NSMutableDictionary dictionaryWithCapacity:4];
375 [hints setObject:_scope forKey:@"scope"];
377 [hints setObject:_propNames forKey:@"attributes"];
378 // else if (_findAll) ; /* empty attributes */
381 [hints setObject:[NSNumber numberWithBool:YES] forKey:@"namesOnly"];
385 - (id)doPROPFIND:(WOContext *)_ctx {
386 SoSecurityManager *sm;
388 EOFetchSpecification *fs;
391 NSString *depth; /* 0, 1, 1,noroot or infinity */
392 NSArray *propNames, *rtargets;
398 /* check permissions */
400 sm = [_ctx soSecurityManager];
401 e = [sm validatePermission:SoPerm_AccessContentsInformation
402 onObject:self->object
408 if (![self->object respondsToSelector:
409 @selector(performWebDAVQuery:inContext:)]) {
410 return [self httpException:405 /* not allowed */
411 reason:@"this object cannot not execute a PROPFIND query"];
415 depth = [rq headerForKey:@"depth"];
418 if ([depth length] == 0) depth = @"infinity";
420 [self lockParser:davsax];
422 [xmlParser parseFromSource:[rq content]];
423 propNames = [[davsax propFindQueriedNames] copy];
424 findAll = [davsax propFindAllProperties];
425 findNames = [davsax propFindPropertyNames];
427 [self unlockParser:davsax];
428 propNames = [propNames autorelease];
430 /* check query all properties */
432 if (propNames == nil)
433 propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx];
435 /* check for a ZideStore ranges query (a BPROPFIND "emulation") */
437 if (debugOn) [self logWithFormat:@"request uri: %@", uri];
438 r = [uri rangeOfString:@"_range"];
439 if (r.length > 0) { /* ZideStore range query */
444 [self logWithFormat:@" detected a ZideStore range query: '%@'", uri];
446 s = [uri substringFromIndex:(r.location + r.length)];
447 if ([s hasSuffix:@"/"]) s = [s substringToIndex:([s length] - 1)];
448 if ([s hasPrefix:@"_"]) s = [s substringFromIndex:1];
450 ids = ([s length] == 0)
452 : [s componentsSeparatedByString:@"_"];
454 // TODO: should use -stringByUnescapingURL on IDs (not required for ints)
458 [self logWithFormat:@" IDs: %@", [ids componentsJoinedByString:@","]];
460 /* patch URI, could have side-effects ? */
462 @"NOTE: hacked URI, _range_ part won't be visible in the HTTP "
463 @"access log:\n%@", uri];
464 [rq _hackSetURI:[uri substringToIndex:r.location]];
469 /* build the fetch-spec */
471 NSMutableDictionary *hints;
473 hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx]
474 propNames:propNames findAll:findAll namesOnly:findNames];
475 if (rtargets) /* range-query keys */
476 [hints setObject:rtargets forKey:@"bulkTargetKeys"];
478 fs = [EOFetchSpecification alloc];
479 fs = [fs initWithEntityName:[self baseURLForContext:_ctx]
482 usesDistinct:NO isDeep:NO hints:hints];
483 fs = [fs autorelease];
485 if (debugOn) [self logWithFormat:@" propfind fetchspec: %@", fs];
488 [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
490 /* translate fetchspec if necessary */
494 if ((map = [self->object davAttributeMapInContext:_ctx])) {
495 [_ctx setObject:map forKey:@"DAVPropertyMap"];
496 fs = [fs fetchSpecificationByApplyingKeyMap:map];
497 [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"];
503 if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
504 return [self httpException:500 /* Server Error */
505 reason:@"could not perform query (object returned nil)"];
508 if (debugOn) [self logWithFormat:@" propfind result: %@", result];
513 - (BOOL)allowDeletePropertiesOnNewObjectInContext:(WOContext *)_ctx {
516 ua = [[_ctx request] headerForKey:@"user-agent"];
517 if ([ua hasPrefix:@"Evolution"]) {
518 /* if Evo creates tasks, it tries to delete some props at the same time */
521 if ([ua hasPrefix:@"CFNetwork"]) {
522 /* iSync trying to create a record ... */
526 [self logWithFormat:@"do not allow delete properties on new object for: %@",
531 - (id)doPROPPATCH:(WOContext *)_ctx {
532 SoSecurityManager *sm;
534 NSMutableArray *resProps;
536 NSDictionary *setProps;
539 pathInfo = [_ctx pathInfo];
541 /* check permissions */
543 sm = [_ctx soSecurityManager];
544 e = [sm validatePermission:([pathInfo length] > 0)
545 ? SoPerm_AddDocumentsImagesAndFiles
546 : SoPerm_ChangeImagesAndFiles
547 onObject:self->object
551 /* check for conflicts */
553 if ([pathInfo length] > 0) {
554 /* check whether all the parent collections are available */
555 if ([pathInfo rangeOfString:@"/"].length > 0) {
556 return [self httpException:409 /* Conflict */
558 @"invalid WebDAV PROPPATCH request, first create all "
559 @"parent collections !"];
563 /* check whether the object supports patching */
565 if ([pathInfo length] > 0) {
566 if (![self->object respondsToSelector:
567 @selector(davCreateObject:properties:inContext:)]) {
568 [self debugWithFormat:@"cannot create new object via DAV on %@",
570 return [self httpException:405 /* not allowed */
572 @"this object cannot create a new object with PROPPATCH"];
576 if (![self->object respondsToSelector:
577 @selector(davSetProperties:removePropertiesNamed:inContext:)]) {
578 [self debugWithFormat:@"cannot change object props via DAV on %@",
580 return [self httpException:405 /* not allowed */
581 reason:@"this object cannot PROPPATCH the attributes"];
587 [self lockParser:davsax];
589 [xmlParser parseFromSource:[[_ctx request] content]];
590 delProps = [[davsax propPatchPropertyNamesToRemove] copy];
591 setProps = [[davsax propPatchValues] copy];
593 [self unlockParser:davsax];
594 delProps = [delProps autorelease];
595 setProps = [setProps autorelease];
597 if (delProps == nil && setProps == nil) {
598 [self logWithFormat:@"WARNING: got no properties in PROPPATCH !"];
599 return [self httpException:400 /* bad request */
600 reason:@"got no properties in PROPPATCH !"];
603 if ([pathInfo length] > 0) {
604 /* a create object cannot delete props ... */
605 if ([delProps count] > 0) {
606 if (![self allowDeletePropertiesOnNewObjectInContext:_ctx]) {
607 [self logWithFormat:@"shall delete props in new object '%@': %@",
609 return [self httpException:400 /* bad request */
610 reason:@"cannot delete properties of a new object"];
612 [self debugWithFormat:@"deleting properties on a new object: %@ ...",
617 resProps = [NSMutableArray arrayWithCapacity:16];
618 if (delProps) [resProps addObjectsFromArray:delProps];
619 if (setProps) [resProps addObjectsFromArray:[setProps allKeys]];
625 if ((map = [self->object davAttributeMapInContext:_ctx])) {
628 [_ctx setObject:map forKey:@"DAVPropertyMap"];
630 if ((count = [delProps count]) > 0) {
631 NSMutableArray *mappedDelProps;
634 mappedDelProps = [NSMutableArray arrayWithCapacity:(count + 1)];
635 for (i = 0; i < count; i++) {
638 k = [delProps objectAtIndex:i];
639 tk = [map valueForKey:k];
641 [mappedDelProps addObject:(tk ? tk : k)];
643 delProps = mappedDelProps;
645 if ((count = [setProps count]) > 0) {
646 NSMutableDictionary *mappedSetProps;
650 mappedSetProps = [NSMutableDictionary dictionaryWithCapacity:count];
651 keys = [setProps keyEnumerator];
652 while ((k = [keys nextObject])) {
655 tk = [map valueForKey:k];
656 [mappedSetProps setObject:[setProps objectForKey:k]
657 forKey:(tk ? tk : k)];
659 setProps = mappedSetProps;
665 [self debugWithFormat:@"PROPPATCH '%@': delete=%@, set=%@",
666 pathInfo, delProps, setProps];
669 if ([pathInfo length] == 0) {
674 davSetProperties:setProps
675 removePropertiesNamed:delProps
680 /* create an object */
683 newChild = [self->object
684 davCreateObject:pathInfo
687 if ([newChild isKindOfClass:[NSException class]])
690 [self debugWithFormat:@"created: %@", newChild];
693 /* generate response */
697 - (id)doLOCK:(WOContext *)_ctx {
698 SoSecurityManager *sm;
700 SoDAVLockManager *lockManager;
703 NSString *ifValue, *lockDepth;
706 /* check permissions */
708 sm = [_ctx soSecurityManager];
709 e = [sm validatePermission:SoPerm_WebDAVLockItems
710 onObject:self->object
714 /* check lock manager */
716 if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) {
717 return [self httpException:405 /* method not allowed */
718 reason:@"target object does not support locking !"];
724 lockDepth = [rq headerForKey:@"depth"];
725 ifValue = [rq headerForKey:@"if"];
727 if (lockDepth != nil && ![lockDepth isEqualToString:@"0"]) {
729 @"WARNING: 'depth' locking not supported yet (depth=%@)!",
734 @"WARNING: 'if' locking not supported yet, if: '%@'", ifValue];
737 // need to parse lockinfo
739 token = [lockManager lockURI:[rq uri]
740 timeout:[rq headerForKey:@"timeout"]
746 return [self httpException:423 /* locked */
747 reason:@"object locked, lock manager did not provide token."];
750 [self debugWithFormat:@"locked: %@ (token %@)", [[_ctx request] uri], token];
754 - (id)doUNLOCK:(WOContext *)_ctx {
755 SoSecurityManager *sm;
757 SoDAVLockManager *lockManager;
760 /* check permissions */
762 sm = [_ctx soSecurityManager];
763 e = [sm validatePermission:SoPerm_WebDAVUnlockItems
764 onObject:self->object
768 /* check lock manager */
770 if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) {
771 return [self httpException:405 /* method not allowed */
772 reason:@"target object does not support locking."];
775 token = [[_ctx request] headerForKey:@"lock-token"];
777 [lockManager unlockURI:[[_ctx request] uri] token:token];
779 [self debugWithFormat:
780 @"unlocked: %@ (token %@)", [[_ctx request] uri], token];
782 [[_ctx response] setStatus:204 /* fake ok */];
783 return [_ctx response];
786 - (NSException *)extractDestinationPath:(NSArray **)path_
787 fromContext:(WOContext *)_ctx
789 NSString *absDestURL;
790 NSURL *destURL, *srvURL;
792 if (path_) *path_ = nil;
794 /* TODO: check proper permission prior attempting a move */
796 absDestURL = [[_ctx request] headerForKey:@"destination"];
797 if ([absDestURL length] == 0) {
798 return [self httpException:400 /* Bad Request */
800 @"the destination WebDAV header was missing "
801 @"for the MOVE/COPY operation"];
803 if ((destURL = [NSURL URLWithString:absDestURL]) == nil) {
804 [self logWithFormat:@"MOVE: got invalid destination URL: '%@'",
806 return [self httpException:400 /* Bad Request */
807 reason:@"the MOVE/COPY destination is not a valid URL!"];
810 srvURL = [_ctx serverURL];
812 [self debugWithFormat:@"move/copy:\n to: %@\n server: %@)",
813 [destURL absoluteString], [srvURL absoluteString]];
815 /* check whether URL is on the same server ... */
816 if (![[srvURL host] isEqualToString:[destURL host]] ||
817 ![[srvURL port] isEqual:[destURL port]]) {
819 The WebDAV spec is not really clear on what we should return in this
820 case? Let me know if anybody has a suggestion ...
822 [self logWithFormat:@"tried to do a cross server move (%@ vs %@)",
823 [srvURL absoluteString], [destURL absoluteString]];
824 return [self httpException:403 /* Forbidden */
825 reason:@"MOVE destination is on a different host."];
832 /* TODO: hack hack hack */
833 ma = [[[destURL path] componentsSeparatedByString:@"/"] mutableCopy];
834 if ([ma count] > 0) // leading slash ("")
835 [ma removeObjectAtIndex:0];
836 if ([ma count] > 0) // the appname (eg zidestore)
837 [ma removeObjectAtIndex:0];
838 if ([ma count] > 0) // the request handler key (eg so)
839 [ma removeObjectAtIndex:0];
841 /* unescape path components */
842 for (i = 0; i < [ma count]; i++) {
843 NSString *s = [ma objectAtIndex:i], *ns;
845 ns = [s stringByUnescapingURL];
847 [ma replaceObjectAtIndex:i withObject:ns];
855 - (NSException *)lookupDestinationObject:(id *)target_
856 andNewName:(NSString **)name_
857 inContext:(WOContext *)_ctx
863 if ((error = [self extractDestinationPath:&targetPath fromContext:_ctx]))
866 if ((root = [_ctx application]) == nil)
867 root = [WOApplication application];
869 return [self httpException:500 /* internal server error */
870 reason:@"did not find SOPE root object"];
873 /* TODO: we should probably use a subcontext?! */
874 [_ctx setObject:yesNum forKey:@"isDestinationPathLookup"];
875 *target_ = [root traversePathArray:targetPath
880 [self logWithFormat:@"could not resolve destination object (%@): %@",
881 [targetPath componentsJoinedByString:@" => "],
886 if (name_) *name_ = [[[_ctx pathInfo] copy] autorelease];
888 if (*target_ == nil) {
889 [self debugWithFormat:@"MOVE/COPY destination could not be found."];
890 return [self httpException:404 /* Not Found */
891 reason:@"did not find target object"];
894 [self debugWithFormat:@"SOURCE: %@", self->object];
895 [self debugWithFormat:@"TARGET: %@ (PI %@)", *target_, [_ctx pathInfo]];
899 - (id)doCOPY:(WOContext *)_ctx {
904 /* TODO: check proper permission prior attempting a copy */
906 error = [self lookupDestinationObject:&targetObject andNewName:&newName
908 if (error) return error;
910 error = [self->object
911 davCopyToTargetObject:targetObject newName:newName
914 [self debugWithFormat:@"WebDAV COPY operation failed: %@", error];
918 return ([newName length] > 0)
919 ? [NSNumber numberWithBool:201 /* Created */]
920 : [NSNumber numberWithBool:204 /* No Content */];
923 - (id)doMOVE:(WOContext *)_ctx {
928 /* TODO: check proper permission prior attempting a move */
930 error = [self lookupDestinationObject:&targetObject andNewName:&newName
932 if (error) return error;
935 Note: more relevant headers:
936 overwrite: T|F (overwrite target) [rc: 201 vs 204!]
938 and locking tokens of course ...
941 // TODO: should we check in this place for some constraints,
942 // eg moving a collection to a non-collection or something
945 error = [self->object
946 davMoveToTargetObject:targetObject newName:newName
949 [self debugWithFormat:@"WebDAV MOVE operation failed: %@", error];
953 return ([newName length] > 0)
954 ? [NSNumber numberWithBool:201 /* Created */]
955 : [NSNumber numberWithBool:204 /* No Content */];
958 /* WebDAV search methods */
960 - (id)doSEARCH:(WOContext *)_ctx {
961 SoSecurityManager *sm;
963 EOFetchSpecification *fs;
968 /* check permissions */
970 sm = [_ctx soSecurityManager];
971 e = [sm validatePermission:SoPerm_AccessContentsInformation
972 onObject:self->object
979 respondsToSelector:@selector(performWebDAVQuery:inContext:)]) {
980 [[_ctx response] setStatus:405 /* not allowed */];
981 [[_ctx response] appendContentString:
982 @"this object cannot not execute a SEARCH query"];
983 return [_ctx response];
986 baseURL = [NSString stringWithFormat:@"http://%@%@",
987 [[_ctx request] headerForKey:@"host"],
988 [[_ctx request] uri]];
990 [self lockParser:davsax];
992 [xmlParser parseFromSource:[[_ctx request] content]];
993 fs = [[davsax searchFetchSpecification] retain];
995 [self unlockParser:davsax];
997 fs = [fs autorelease];
999 [[_ctx response] setStatus:400 /* Bad Request */];
1000 [[_ctx response] appendContentString:
1001 @"could not process SEARCH query specification"];
1002 return [_ctx response];
1006 if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
1007 /* TODO: parse range header and add to fetch-specification */
1010 r = [range rangeOfString:@"rows="];
1012 range = [range substringFromIndex:(r.location + r.length)];
1013 [self debugWithFormat:
1014 @"Note: got a row range header (ignored): '%@'", range];
1017 [self logWithFormat:@"Note: got a range header (ignored): '%@'", range];
1020 /* override entity name ... (FROM xxx isn't yet parsed correctly) */
1021 [fs setEntityName:baseURL];
1023 [self debugWithFormat:@"SEARCH: %@", fs];
1025 [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
1027 /* translate fetchspec if necessary */
1031 if ((map = [self->object davAttributeMapInContext:_ctx])) {
1032 [_ctx setObject:map forKey:@"DAVPropertyMap"];
1033 fs = [fs fetchSpecificationByApplyingKeyMap:map];
1034 [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"];
1040 if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
1041 return [self httpException:500 /* Server Error */
1042 reason:@"could not execute SEARCH query (returned nil)"];
1048 /* Exchange WebDAV methods */
1050 - (id)doNOTIFY:(WOContext *)_ctx {
1051 return [self httpException:403 reason:@"NOTIFY not yet implemented"];
1054 - (id)doPOLL:(WOContext *)_ctx {
1055 SoSubscriptionManager *sm;
1057 NSString *subscriptionID;
1061 rq = [_ctx request];
1062 sm = [SoSubscriptionManager sharedSubscriptionManager];
1063 url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1066 return [self httpException:500
1067 reason:@"could not calculate URL of WebDAV object !"];
1070 subscriptionID = [rq headerForKey:@"subscription-id"];
1071 if ([subscriptionID length] == 0) {
1072 return [self httpException:400 /* Bad Request */
1073 reason:@"did not find subscription-id header in POLL"];
1076 ids = [subscriptionID componentsSeparatedByString:@","];
1078 return [sm pollSubscriptions:ids onURL:url];
1081 - (id)doSUBSCRIBE:(WOContext *)_ctx {
1082 SoSubscriptionManager *sm;
1087 NSString *notificationType;
1088 NSString *notificationDelay;
1090 NSString *subscriptionID;
1092 rq = [_ctx request];
1093 r = [_ctx response];
1094 sm = [SoSubscriptionManager sharedSubscriptionManager];
1095 url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1098 return [self httpException:500
1099 reason:@"could not calculate URL of WebDAV object !"];
1102 subscriptionID = [rq headerForKey:@"subscription-id"];
1104 /* first check, whether it's an existing subscription to be renewed */
1106 if ([subscriptionID length] > 0) {
1109 if ((newId = [sm renewSubscription:subscriptionID onURL:url]) == nil) {
1110 return [self httpException:412 /* precondition failed */
1111 reason:@"did not find provided subscription ID !"];
1116 if ((callback = [rq headerForKey:@"call-back"])) {
1119 if ((url = [NSURL URLWithString:[callback stringValue]]) == nil) {
1120 [self debugWithFormat:@"ERROR: could not parse callback URL '%@'",
1122 return [self httpException:400 /* Bad Request */
1123 reason:@"missing valid callback URL !"];
1129 /* TODO: add sanity checking of notification-type as described in docs */
1130 /* TODO: check depth */
1132 notificationDelay = [rq headerForKey:@"notification-delay"];
1133 notificationType = [rq headerForKey:@"notification-type"];
1134 lifetime = [rq headerForKey:@"subscription-lifetime"];
1136 subscriptionID = [sm subscribeURL:url forObserver:callback
1137 type:notificationType
1138 delay:notificationDelay
1139 ? [notificationDelay doubleValue] : 0.0
1140 lifetime:lifetime ? [lifetime doubleValue] : 0.0];
1141 return subscriptionID;
1143 - (id)doUNSUBSCRIBE:(WOContext *)_ctx {
1144 SoSubscriptionManager *sm;
1147 NSString *subscriptionID;
1150 rq = [_ctx request];
1151 r = [_ctx response];
1152 sm = [SoSubscriptionManager sharedSubscriptionManager];
1153 url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1156 return [self httpException:500
1157 reason:@"could not calculate URL of WebDAV object !"];
1160 subscriptionID = [rq headerForKey:@"subscription-id"];
1161 if ([subscriptionID length] == 0) {
1162 return [self httpException:400 /* Bad Request */
1163 reason:@"missing subscription id !"];
1166 if ([sm unsubscribeID:subscriptionID onURL:url]) {
1171 return [self httpException:400 /* Bad Request */
1172 reason:@"unsubscribe failed (invalid or old id ?)"];
1176 /* Exchange bulk methods */
1178 - (NSArray *)urlPartsForTargets:(NSArray *)_targets basePath:(NSString *)_base{
1180 Transform the target URLs given to the BPROPFIND operation. This is a
1181 simplified implementation, for example we expect that the URLs are all
1182 located in the same URL space (on same host and port).
1187 if ((count = [_targets count]) == 0)
1188 return [NSArray array];
1190 ma = [NSMutableArray arrayWithCapacity:count];
1191 for (i = 0; i < count; i++) {
1194 target = [_targets objectAtIndex:i];
1195 if (debugBulkTarget)
1196 [self logWithFormat:@" MORPH target '%@'", target];
1198 /* extract the path from full URLs */
1199 if ([target isAbsoluteURL]) {
1202 /* fix an Evolution bug, uses the 'unsafe' "@" in the URL ! */
1203 if ([target rangeOfString:@"@"].length > 0) {
1204 target = [target stringByReplacingString:@"@"
1208 if ((url = [NSURL URLWithString:target])) {
1209 if (debugBulkTarget) [self logWithFormat:@"got URL: %@", url];
1210 target = [url path];
1211 if (debugBulkTarget) [self logWithFormat:@"path: %@", target];
1214 [self logWithFormat:@"ERROR: could not parse BPROPFIND target '%@' !",
1219 /* make the target name relative to the request URI */
1220 if ([target hasPrefix:_base]) {
1221 target = [target substringFromIndex:[_base length]];
1222 if ([target hasPrefix:@"/"])
1223 target = [target substringFromIndex:1];
1226 /* add the target */
1227 target = [target stringByUnescapingURL];
1228 if (debugBulkTarget) [self logWithFormat:@" ADD target '%@'", target];
1229 [ma addObject:target];
1234 - (id)doBPROPFIND:(WOContext *)_ctx {
1236 TODO: could optimize a BPROPFIND on a single target to use PROPFIND
1238 How are BPROPFINDs mapped ? BPROPFIND corresponds to SKYRiX 4.1
1239 "fetch-by-globalids" commands, that is, a search gets passed a list
1240 of primary keys to fetch.
1241 BPROPFIND is implemented in a similiar way, the target URLs are converted
1242 to be relative to the URI object and are passed to the query datasource
1243 using the "bulkTargetKeys" fetch hint.
1245 Important: the URI object *must* support the "bulkTargetKeys" fetch hint,
1246 otherwise the operation will run on the object itself.
1248 Note: Previously BPROPFIND was mapped to a set of individual requests,
1249 but obviously this doesn't match SQL very well (resulting in an individual
1250 SQL query for each entity ...)
1252 SoSecurityManager *sm;
1254 EOFetchSpecification *fs;
1256 NSString *depth; /* 0, 1, 1,noroot or infinity */
1258 NSArray *targets, *rtargets;
1264 /* check permissions */
1266 sm = [_ctx soSecurityManager];
1267 e = [sm validatePermission:SoPerm_AccessContentsInformation
1268 onObject:self->object
1272 /* perform search */
1274 if (![self->object respondsToSelector:@selector(performWebDAVQuery:inContext:)]) {
1275 return [self httpException:405 /* not allowed */
1276 reason:@"this object cannot not execute a PROPFIND query"];
1279 rq = [_ctx request];
1280 depth = [rq headerForKey:@"depth"];
1281 if ([depth length] == 0) depth = @"infinity";
1283 [self lockParser:davsax];
1285 [xmlParser parseFromSource:[rq content]];
1286 propNames = [[davsax propFindQueriedNames] copy];
1287 findAll = [davsax propFindAllProperties];
1288 findNames = [davsax propFindPropertyNames];
1289 targets = [[davsax bpropFindTargets] copy];
1291 [self unlockParser:davsax];
1292 propNames = [propNames autorelease];
1293 targets = [targets autorelease];
1295 if ([targets count] == 0)
1296 return [NSArray array];
1298 /* check query all properties */
1300 if (propNames == nil)
1301 propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx];
1305 rtargets = [self urlPartsForTargets:targets
1306 basePath:[[rq uri] stringByUnescapingURL]];
1308 [self debugWithFormat:@"BPROPFIND targets: %@", rtargets];
1310 /* build the fetch-spec */
1312 NSMutableDictionary *hints;
1314 hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx]
1315 propNames:propNames findAll:findAll namesOnly:findNames];
1316 [hints setObject:rtargets forKey:@"bulkTargetKeys"];
1318 fs = [EOFetchSpecification alloc];
1319 fs = [fs initWithEntityName:[self baseURLForContext:_ctx]
1322 usesDistinct:NO isDeep:NO hints:hints];
1323 fs = [fs autorelease];
1326 [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
1329 translate fetchspec if necessary - we currently cannot allow a map
1330 for each target, so we use the map of the queried target.
1332 if ((map = [self->object davAttributeMapInContext:_ctx])) {
1333 [_ctx setObject:map forKey:@"DAVPropertyMap"];
1334 fs = [fs fetchSpecificationByApplyingKeyMap:map];
1335 [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"];
1340 if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
1341 return [self httpException:500 /* Server Error */
1342 reason:@"could not perform query (object returned nil)"];
1347 /* now, for each BPROPFIND target ... */
1350 NSString *targetURL;
1352 result = [NSMutableArray arrayWithCapacity:32];
1354 e = [targets objectEnumerator];
1355 while ((targetURL = [e nextObject])) {
1356 NSAutoreleasePool *pool;
1357 WOContext *localContext;
1358 WORequest *localRequest;
1363 pool = [[NSAutoreleasePool alloc] init];
1365 /* setup the "subrequest" */
1367 if ([targetURL isAbsoluteURL]) {
1370 if ((url = [NSURL URLWithString:targetURL]))
1371 targetURL = [url path];
1373 [self logWithFormat:@"ERROR: could not parse target-url '%@'",
1378 localRequest = [[WORequest alloc] initWithMethod:@"PROPFIND"
1380 httpVersion:[rq httpVersion]
1381 headers:[rq headers]
1385 [[[WOContext alloc] initWithRequest:localRequest] autorelease];
1386 [localRequest autorelease];
1388 /* resetup fetchspec */
1389 [fs setEntityName:targetURL];
1393 targetObject = [_ctx traversalRoot];
1394 targetObject = [targetObject traversePathArray:
1395 [localRequest requestHandlerPathArray]
1396 inContext:localContext
1399 if (targetObject == nil) {
1400 [self logWithFormat:@"did not find BPROPFIND target: %@", targetURL];
1401 [self logWithFormat:@" root: %@", [_ctx traversalRoot]];
1402 [self logWithFormat:@" path: %@",
1403 [[localRequest requestHandlerPathArray]
1404 componentsJoinedByString:@"/"]];
1405 [self logWithFormat:@" error: %@", e];
1411 targetResult = [targetObject performWebDAVQuery:fs
1412 inContext:localContext];
1413 if (targetResult == nil) {
1415 [self httpException:500 /* Server Error */
1416 reason:@"could not perform query (object returned nil)"];
1420 // do we need to distinguish the queries somehow ? (href generation)
1421 if ([targetResult isKindOfClass:[NSArray class]])
1422 [result addObjectsFromArray:targetResult];
1423 else if (targetResult)
1424 [result addObject:targetResult];
1432 if (result) return result;
1436 - (id)doBCOPY:(WOContext *)_ctx {
1437 return [self httpException:403 /* forbidden */
1438 reason:@"BCOPY not yet implemented."];
1440 - (id)doBDELETE:(WOContext *)_ctx {
1441 return [self httpException:403 /* forbidden */
1442 reason:@"BDELETE not yet implemented."];
1444 - (id)doBMOVE:(WOContext *)_ctx {
1445 return [self httpException:403 /* forbidden */
1446 reason:@"WebDAV operation not yet implemented."];
1449 - (id)doBPROPPATCH:(WOContext *)_ctx {
1450 return [self httpException:403 /* forbidden */
1451 reason:@"WebDAV operation not yet implemented."];
1454 /* DAV access control lists */
1456 - (id)doACL:(WOContext *)_ctx {
1457 return [self httpException:405 /* method not allowed */
1458 reason:@"WebDAV operation not yet implemented."];
1463 - (id)doBIND:(WOContext *)_ctx {
1464 return [self httpException:405 /* method not allowed */
1465 reason:@"WebDAV operation not yet implemented."];
1470 - (id)doORDERPATCH:(WOContext *)_ctx {
1471 return [self httpException:405 /* method not allowed */
1472 reason:@"WebDAV operation not yet implemented."];
1477 - (id)doCHECKOUT:(WOContext *)_ctx {
1478 return [self httpException:405 /* method not allowed */
1479 reason:@"WebDAV operation not yet implemented."];
1481 - (id)doUNCHECKOUT:(WOContext *)_ctx {
1482 return [self httpException:405 /* method not allowed */
1483 reason:@"WebDAV operation not yet implemented."];
1485 - (id)doCHECKIN:(WOContext *)_ctx {
1486 return [self httpException:405 /* method not allowed */
1487 reason:@"WebDAV operation not yet implemented."];
1489 - (id)doMKWORKSPACE:(WOContext *)_ctx {
1490 return [self httpException:405 /* method not allowed */
1491 reason:@"WebDAV operation not yet implemented."];
1493 - (id)doUPDATE:(WOContext *)_ctx {
1494 return [self httpException:405 /* method not allowed */
1495 reason:@"WebDAV operation not yet implemented."];
1497 - (id)doMERGE:(WOContext *)_ctx {
1498 return [self httpException:405 /* method not allowed */
1499 reason:@"WebDAV operation not yet implemented."];
1501 - (id)doVERSIONCONTROL:(WOContext *)_ctx {
1502 return [self httpException:405 /* method not allowed */
1503 reason:@"WebDAV operation not yet implemented."];
1506 /* perform dispatch */
1508 - (id)performMethod:(NSString *)_method inContext:(WOContext *)_ctx {
1509 SoSecurityManager *sm;
1514 /* check basic WebDAV permission */
1516 sm = [_ctx soSecurityManager];
1517 e = [sm validatePermission:SoPerm_WebDAVAccess
1518 onObject:self->object
1522 /* perform search */
1524 _method = [_method uppercaseString];
1525 _method = [_method stringByReplacingString:@"-" withString:@""];
1526 s = [NSString stringWithFormat:@"do%@:", _method];
1527 sel = NSSelectorFromString(s);
1529 if (![self respondsToSelector:sel]) {
1530 [self logWithFormat:@"unknown WebDAV method: '%@'", _method];
1531 [[_ctx response] setStatus:405 /* invalid method */];
1532 return [_ctx response];
1535 return [self performSelector:sel withObject:_ctx];
1538 - (BOOL)setupXmlParser {
1539 if (xmlParser == nil) {
1541 [[[SaxXMLReaderFactory standardXMLReaderFactory]
1542 createXMLReaderForMimeType:@"text/xml"]
1544 if (xmlParser == nil)
1547 if (davsax == nil) {
1548 if ((davsax = [[SaxDAVHandler alloc] init]) == nil)
1554 - (id)dispatchInContext:(WOContext *)_ctx {
1555 NSAutoreleasePool *pool;
1559 if (gmt == nil) gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
1561 /* setup XML parser */
1562 if (![self setupXmlParser]) {
1563 r = [_ctx response];
1564 [r setStatus:500 /* internal server error */];
1565 [r appendContentString:@"did not find an XML parser, cannot process DAV."];
1569 pool = [[NSAutoreleasePool alloc] init];
1570 result = [[self performMethod:[[_ctx request] method] inContext:_ctx] retain];
1572 return [result autorelease];
1577 - (NSString *)loggingPrefix {
1578 return @"[obj-dav-dispatch]";
1580 - (BOOL)isDebuggingEnabled {
1581 return debugOn ? YES : NO;
1586 - (NSString *)description {
1587 NSMutableString *ms;
1589 ms = [NSMutableString stringWithCapacity:64];
1590 [ms appendFormat:@"<0x%08X[%@]:", self,
1591 NSStringFromClass((Class)*(void**)self)];
1594 [ms appendFormat:@" object=%@", self->object];
1596 [ms appendString:@" <no object>"];
1598 [ms appendString:@">"];
1602 @end /* SoObjectWebDAVDispatcher */