2 Copyright (C) 2002-2006 SKYRIX Software AG
3 Copyright (C) 2006 Helge Hess
5 This file is part of SOPE.
7 SOPE is free software; you can redistribute it and/or modify it under
8 the terms of the GNU Lesser General Public License as published by the
9 Free Software Foundation; either version 2, or (at your option) any
12 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
13 WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
15 License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with SOPE; see the file COPYING. If not, write to the
19 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 "NSException+HTTP.h"
35 #include <NGObjWeb/WOApplication.h>
36 #include <NGObjWeb/WORequest.h>
37 #include <NGObjWeb/WOResponse.h>
38 #include <NGObjWeb/WOContext.h>
39 #include <NGObjWeb/WEClientCapabilities.h>
40 #include <SaxObjC/SaxObjC.h>
41 #include <SaxObjC/XMLNamespaces.h>
42 #include <DOM/DOMDocument.h>
43 #include <NGExtensions/NSString+Ext.h>
46 @interface WORequest(HackURI)
47 - (void)_hackSetURI:(NSString *)_vuri;
50 @implementation SoObjectWebDAVDispatcher
52 static int debugOn = -1;
53 static BOOL debugBulkTarget = NO;
54 static BOOL disableCrossHostMoveCheck = NO;
55 static NSNumber *yesNum = nil;
58 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
59 static BOOL didInit = NO;
63 debugOn = [ud boolForKey:@"SoObjectDAVDispatcherDebugEnabled"] ? 1 : 0;
64 if (debugOn) NSLog(@"Note: WebDAV dispatcher debugging is enabled.");
65 if (yesNum == nil) yesNum = [[NSNumber numberWithBool:YES] retain];
67 disableCrossHostMoveCheck =
68 [ud boolForKey:@"SoWebDAVDisableCrossHostMoveCheck"];
72 static id<NSObject,SaxXMLReader> xmlParser = nil;
73 static SaxDAVHandler *davsax = nil;
74 static NSTimeZone *gmt = nil;
76 - (id)initWithObject:(id)_object {
77 if ((self = [super init])) {
78 self->object = [_object retain];
83 [self->object release];
89 - (void)lockParser:(id)_sax {
91 [xmlParser setContentHandler:_sax];
92 [xmlParser setErrorHandler:_sax];
94 - (void)unlockParser:(id)_sax {
95 [xmlParser setContentHandler:nil];
96 [xmlParser setErrorHandler:nil];
102 - (NSException *)httpException:(int)_status reason:(NSString *)_reason {
105 ui = [NSDictionary dictionaryWithObjectsAndKeys:
107 [NSNumber numberWithInt:_status], @"http-status",
109 return [NSException exceptionWithName:
110 [NSString stringWithFormat:@"HTTP%i", _status]
115 - (NSString *)baseURLForContext:(WOContext *)_ctx {
116 extern NSString *SoObjectRootURLInContext
117 (WOContext *_ctx, id logobj, BOOL withAppPart);
120 rootURL = SoObjectRootURLInContext(_ctx, self, NO);
121 return [rootURL stringByAppendingString:[[_ctx request] uri]];
124 - (id)primaryCallWebDAVMethod:(NSString *)_name inContext:(WOContext *)_ctx {
127 method = [self->object lookupName:_name inContext:_ctx acquire:NO];
129 return [self httpException:501 /* Not Implemented */
130 reason:@"target object does not support requested operation"];
132 if ([method isKindOfClass:[NSException class]]) {
133 [self logWithFormat:@"could not lookup method, got exception: %@", method];
137 [self debugWithFormat:@" %@ method: %@", _name, method];
138 return [method callOnObject:self->object inContext:_ctx];
141 /* core HTTP methods */
143 - (id)_callObjectMethod:(NSString *)_method inContext:(WOContext *)_ctx {
144 /* returns 'nil' if the object had no such method */
150 [self->object lookupName:_method inContext:_ctx acquire:NO];
151 if (![methodObject isNotNull])
153 if ([methodObject isKindOfClass:[NSException class]]) {
154 if ([(NSException *)methodObject httpStatus] == 404 /* Not Found */) {
158 return methodObject; /* the exception */
160 if ((e = [self->object validateName:_method inContext:_ctx]) != nil)
163 if ([methodObject respondsToSelector:
164 @selector(takeValuesFromRequest:inContext:)])
165 [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx];
167 result = [methodObject callOnObject:self->object inContext:_ctx];
168 return (result != nil) ? result : (id)[NSNull null];
171 - (id)doGET:(WOContext *)_ctx {
175 methodObject = [self->object lookupName:@"GET" inContext:_ctx acquire:NO];
176 if (methodObject == nil)
177 methodObject = [self->object lookupDefaultMethod];
179 if ((e = [self->object validateName:@"GET" inContext:_ctx]) != nil)
183 if (methodObject == nil)
185 if ([methodObject isKindOfClass:[NSException class]])
188 if ([methodObject respondsToSelector:
189 @selector(takeValuesFromRequest:inContext:)])
190 [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx];
192 return [methodObject callOnObject:self->object inContext:_ctx];
195 - (id)doPUT:(WOContext *)_ctx {
196 SoSecurityManager *sm;
200 pathInfo = [_ctx pathInfo];
201 [self debugWithFormat:@"doPUT (pathinfo='%@')", pathInfo];
203 /* check permissions */
205 sm = [_ctx soSecurityManager];
206 e = [sm validatePermission:
207 [pathInfo isNotEmpty]
208 ? SoPerm_AddDocumentsImagesAndFiles
209 : SoPerm_ChangeImagesAndFiles
210 onObject:self->object
212 if (e != nil) return e;
214 if ((e = [self->object validateName:@"PUT" inContext:_ctx]))
219 if ([pathInfo isNotEmpty]) {
220 /* check whether all the parent collections are available */
221 // TODO: we might also want to check for a 'create' permission
222 if ([pathInfo rangeOfString:@"/"].length > 0) {
223 return [self httpException:409 /* Conflict */
225 @"invalid WebDAV PUT request, first create all "
226 @"parent collections !"];
230 return [self primaryCallWebDAVMethod:@"PUT" inContext:_ctx];
233 - (id)doPOST:(WOContext *)_ctx {
236 if ((e = [self->object validateName:@"POST" inContext:_ctx]))
239 return [self primaryCallWebDAVMethod:@"POST" inContext:_ctx];
242 - (id)doDELETE:(WOContext *)_ctx {
243 SoSecurityManager *sm;
246 /* check permissions */
248 sm = [_ctx soSecurityManager];
249 e = [sm validatePermission:SoPerm_DeleteObjects
250 onObject:self->object
254 if ((e = [self->object validateName:@"DELETE" inContext:_ctx]) != nil)
257 // TODO: IE WebFolders sent a "Destroy" header together with the
258 // DELETE request, eg:
259 // "Destroy: NoUndelete"
261 return [self primaryCallWebDAVMethod:@"DELETE" inContext:_ctx];
264 - (id)doOPTIONS:(WOContext *)_ctx {
265 WOResponse *response;
269 /* this checks whether the object provides a specific OPTIONS method */
270 if ((result = [self _callObjectMethod:@"OPTIONS" inContext:_ctx]) != nil)
273 response = [_ctx response];
274 [response setStatus:200 /* OK */];
276 if ((tmp = [self->object davAllowedMethodsInContext:_ctx]) != nil)
277 [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"allow"];
279 if ([[[_ctx request] clientCapabilities] isWebFolder]) {
281 As described over here:
282 http://teyc.editthispage.com/2005/06/02
284 This page also says that: "MS-Auth-Via header is not required to work
287 [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"public"];
290 if ((tmp = [self->object davComplianceClassesInContext:_ctx]) != nil)
291 [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"dav"];
296 - (id)doHEAD:(WOContext *)_ctx {
297 return [self doGET:_ctx];
300 /* core WebDAV methods */
302 - (id)doMKCOL:(WOContext *)_ctx {
303 SoSecurityManager *sm;
307 pathInfo = [_ctx pathInfo];
308 if (![pathInfo isNotEmpty]) {
309 /* MKCOL target already exists ... */
312 [self logWithFormat:@"MKCOL target exists !"];
315 [r setStatus:405 /* method not allowed */];
316 [r appendContentString:@"collection already exists !"];
320 /* check permissions */
322 sm = [_ctx soSecurityManager];
323 e = [sm validatePermission:SoPerm_AddFolders
324 onObject:self->object
326 if (e != nil) return e;
328 /* check whether all the parent collections are available */
329 if ([pathInfo rangeOfString:@"/"].length > 0) {
330 return [self httpException:409 /* Conflict */
332 @"invalid WebDAV MKCOL request, first create all "
333 @"parent collections !"];
336 /* check whether the object supports creating collections */
338 if (![self->object respondsToSelector:
339 @selector(davCreateCollection:inContext:)]) {
340 /* Note: this should never happen, as this is implemented on NSObject */
342 [self logWithFormat:@"MKCOL: object '%@' path-info '%@'",
343 self->object, pathInfo];
344 return [self httpException:405 /* not allowed */
346 @"this object cannot create a new collection with MKCOL"];
349 if ((e = [self->object davCreateCollection:pathInfo inContext:_ctx])) {
350 [self debugWithFormat:@"creation of collection '%@' failed: %@",
355 [self debugWithFormat:@"created collection."];
356 return [NSNumber numberWithBool:YES];
359 - (NSString *)scopeForDepth:(NSString *)_depth inContext:(WOContext *)_ctx {
362 if ([_depth hasPrefix:@"0"])
364 else if ([_depth hasPrefix:@"1,noroot"])
366 else if ([_depth hasPrefix:@"1"]) {
369 scope = @"flat+self";
371 /* some special handling for IE ... */
372 if ((ua = [[[_ctx request] clientCapabilities] userAgentType])) {
373 if ([ua isEqualToString:@"Evolution"])
375 else if ( [ua isEqualToString:@"WebFolder"])
379 else if ([_depth hasPrefix:@"infinity"])
387 - (NSMutableDictionary *)hintsWithScope:(NSString *)_scope
388 propNames:(NSArray *)_propNames
389 findAll:(BOOL)_findAll
390 namesOnly:(BOOL)_namesOnly
392 NSMutableDictionary *hints;
394 hints = [NSMutableDictionary dictionaryWithCapacity:4];
397 [hints setObject:_scope forKey:@"scope"];
399 [hints setObject:_propNames forKey:@"attributes"];
400 // else if (_findAll) ; /* empty attributes */
403 [hints setObject:[NSNumber numberWithBool:YES] forKey:@"namesOnly"];
407 - (id)doPROPFIND:(WOContext *)_ctx {
408 SoSecurityManager *sm;
410 EOFetchSpecification *fs;
413 NSString *depth; /* 0, 1, 1,noroot or infinity */
414 NSArray *propNames, *rtargets;
420 /* check permissions */
422 sm = [_ctx soSecurityManager];
423 e = [sm validatePermission:SoPerm_AccessContentsInformation
424 onObject:self->object
426 if (e != nil) return e;
430 if (![self->object respondsToSelector:
431 @selector(performWebDAVQuery:inContext:)]) {
432 return [self httpException:405 /* not allowed */
433 reason:@"this object cannot not execute a PROPFIND query"];
437 depth = [rq headerForKey:@"depth"];
440 if (![depth isNotEmpty]) depth = @"infinity";
442 if ([[rq content] isNotEmpty]) {
443 [self lockParser:davsax];
445 [xmlParser parseFromSource:[rq content]];
446 propNames = [[davsax propFindQueriedNames] copy];
447 findAll = [davsax propFindAllProperties];
448 findNames = [davsax propFindPropertyNames];
450 [self unlockParser:davsax];
451 propNames = [propNames autorelease];
456 "A client may choose not to submit a request body. An empty PROPFIND
457 request body MUST be treated as a request for the names and values of
459 TODO: means, an empty request is to be handled as <allprop/>?
468 Hack up request to include 'brief'. This elimates the reporting of 404
469 properties in the renderer. Its necessary because some objects may not
470 properly report their default properties (they sometimes report missing
473 [[_ctx request] setHeader:@"true" forKey:@"brief"];
476 /* check query all properties */
478 if (propNames == nil)
479 propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx];
481 /* check for a ZideStore ranges query (a BPROPFIND "emulation") */
483 if (debugOn) [self logWithFormat:@"request uri: %@", uri];
484 r = [uri rangeOfString:@"_range"];
485 if (r.length > 0) { /* ZideStore range query */
490 [self logWithFormat:@" detected a ZideStore range query: '%@'", uri];
492 s = [uri substringFromIndex:(r.location + r.length)];
493 if ([s hasSuffix:@"/"]) s = [s substringToIndex:([s length] - 1)];
494 if ([s hasPrefix:@"_"]) s = [s substringFromIndex:1];
497 ? [s componentsSeparatedByString:@"_"]
498 : (NSArray *)[NSArray array];
500 // TODO: should use -stringByUnescapingURL on IDs (not required for ints)
504 [self logWithFormat:@" IDs: %@", [ids componentsJoinedByString:@","]];
506 /* patch URI, could have side-effects ? */
508 @"NOTE: hacked URI, _range_ part won't be visible in the HTTP "
509 @"access log:\n%@", uri];
510 [rq _hackSetURI:[uri substringToIndex:r.location]];
515 /* build the fetch-spec */
517 NSMutableDictionary *hints;
519 hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx]
520 propNames:propNames findAll:findAll namesOnly:findNames];
521 if (rtargets != nil) /* range-query keys */
522 [hints setObject:rtargets forKey:@"bulkTargetKeys"];
524 fs = [EOFetchSpecification alloc];
525 fs = [fs initWithEntityName:[self baseURLForContext:_ctx]
528 usesDistinct:NO isDeep:NO hints:hints];
529 fs = [fs autorelease];
531 if (debugOn) [self logWithFormat:@" propfind fetchspec: %@", fs];
534 [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
536 /* translate fetchspec if necessary */
540 if ((map = [self->object davAttributeMapInContext:_ctx]) != nil) {
541 [_ctx setObject:map forKey:@"DAVPropertyMap"];
542 fs = [fs fetchSpecificationByApplyingKeyMap:map];
543 [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"];
544 if (debugOn) [self logWithFormat:@" remapped fetchspec: %@", fs];
550 if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
551 return [self httpException:500 /* Server Error */
552 reason:@"could not perform query (object returned nil)"];
555 if (debugOn) [self logWithFormat:@" propfind result: %@", result];
560 - (BOOL)allowDeletePropertiesOnNewObjectInContext:(WOContext *)_ctx {
563 ua = [[_ctx request] headerForKey:@"user-agent"];
564 if ([ua hasPrefix:@"Evolution"]) {
565 /* if Evo creates tasks, it tries to delete some props at the same time */
568 if ([ua hasPrefix:@"CFNetwork"]) {
569 /* iSync trying to create a record ... */
573 [self logWithFormat:@"do not allow delete properties on new object for: %@",
578 - (id)doPROPPATCH:(WOContext *)_ctx {
579 SoSecurityManager *sm;
581 NSMutableArray *resProps;
583 NSDictionary *setProps;
586 pathInfo = [_ctx pathInfo];
588 /* check permissions */
590 sm = [_ctx soSecurityManager];
591 e = [sm validatePermission:[pathInfo isNotEmpty]
592 ? SoPerm_AddDocumentsImagesAndFiles
593 : SoPerm_ChangeImagesAndFiles
594 onObject:self->object
596 if (e != nil) return e;
598 /* check for conflicts */
600 if ([pathInfo isNotEmpty]) {
601 /* check whether all the parent collections are available */
602 if ([pathInfo rangeOfString:@"/"].length > 0) {
603 return [self httpException:409 /* Conflict */
605 @"invalid WebDAV PROPPATCH request, first create all "
606 @"parent collections !"];
610 /* check whether the object supports patching */
612 if ([pathInfo isNotEmpty]) {
613 if (![self->object respondsToSelector:
614 @selector(davCreateObject:properties:inContext:)]) {
615 [self debugWithFormat:@"cannot create new object via DAV on %@",
617 return [self httpException:405 /* not allowed */
619 @"this object cannot create a new object with PROPPATCH"];
623 if (![self->object respondsToSelector:
624 @selector(davSetProperties:removePropertiesNamed:inContext:)]) {
625 [self debugWithFormat:@"cannot change object props via DAV on %@",
627 return [self httpException:405 /* not allowed */
628 reason:@"this object cannot PROPPATCH the attributes"];
634 [self lockParser:davsax];
636 [xmlParser parseFromSource:[[_ctx request] content]];
637 delProps = [[davsax propPatchPropertyNamesToRemove] copy];
638 setProps = [[davsax propPatchValues] copy];
640 [self unlockParser:davsax];
641 delProps = [delProps autorelease];
642 setProps = [setProps autorelease];
644 if (delProps == nil && setProps == nil) {
645 [self warnWithFormat:@"got no properties in PROPPATCH !"];
646 return [self httpException:400 /* bad request */
647 reason:@"got no properties in PROPPATCH !"];
650 if ([pathInfo isNotEmpty]) {
651 /* a create object cannot delete props ... */
652 if ([delProps isNotEmpty]) {
653 if (![self allowDeletePropertiesOnNewObjectInContext:_ctx]) {
654 [self logWithFormat:@"shall delete props in new object '%@': %@",
656 return [self httpException:400 /* bad request */
657 reason:@"cannot delete properties of a new object"];
659 [self debugWithFormat:@"deleting properties on a new object: %@ ...",
664 resProps = [NSMutableArray arrayWithCapacity:16];
665 if (delProps) [resProps addObjectsFromArray:delProps];
666 if (setProps) [resProps addObjectsFromArray:[setProps allKeys]];
672 if ((map = [self->object davAttributeMapInContext:_ctx])) {
675 [_ctx setObject:map forKey:@"DAVPropertyMap"];
677 if ((count = [delProps count]) > 0) {
678 NSMutableArray *mappedDelProps;
681 mappedDelProps = [NSMutableArray arrayWithCapacity:(count + 1)];
682 for (i = 0; i < count; i++) {
685 k = [delProps objectAtIndex:i];
686 tk = [map valueForKey:k];
688 [mappedDelProps addObject:(tk ? tk : k)];
690 delProps = mappedDelProps;
692 if ((count = [setProps count]) > 0) {
693 NSMutableDictionary *mappedSetProps;
697 mappedSetProps = [NSMutableDictionary dictionaryWithCapacity:count];
698 keys = [setProps keyEnumerator];
699 while ((k = [keys nextObject])) {
702 tk = [map valueForKey:k];
703 [mappedSetProps setObject:[setProps objectForKey:k]
704 forKey:(tk ? tk : k)];
706 setProps = mappedSetProps;
712 [self debugWithFormat:@"PROPPATCH '%@': delete=%@, set=%@",
713 pathInfo, delProps, setProps];
716 if (![pathInfo isNotEmpty]) {
721 davSetProperties:setProps
722 removePropertiesNamed:delProps
724 if (e != nil) return e;
727 /* create an object */
730 newChild = [self->object
731 davCreateObject:pathInfo
734 if ([newChild isKindOfClass:[NSException class]])
737 [self debugWithFormat:@"created: %@", newChild];
740 /* generate response */
744 - (id)doLOCK:(WOContext *)_ctx {
745 SoSecurityManager *sm;
747 SoDAVLockManager *lockManager;
750 NSString *ifValue, *lockDepth;
753 /* check permissions */
755 sm = [_ctx soSecurityManager];
756 e = [sm validatePermission:SoPerm_WebDAVLockItems
757 onObject:self->object
759 if (e != nil) return e;
761 /* check lock manager */
763 if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) {
764 return [self httpException:405 /* method not allowed */
765 reason:@"target object does not support locking !"];
771 lockDepth = [rq headerForKey:@"depth"];
772 ifValue = [rq headerForKey:@"if"];
774 if (lockDepth != nil && ![lockDepth isEqualToString:@"0"]) {
775 [self warnWithFormat:@"'depth' locking not supported yet (depth=%@)!",
778 if (ifValue != nil) {
779 [self warnWithFormat:@"'if' locking not supported yet, if: '%@'", ifValue];
783 TODO: parse lockinfo:
784 <?xml version="1.0" encoding="UTF-8" ?>
785 <lockinfo xmlns="DAV:">
786 <locktype><write/></locktype>
787 <lockscope><exclusive/></lockscope>
791 Currently we assume exclusive/write ... (also see SoWebDAVRenderer)
794 /* Sample timeout: Second-180 */
796 token = [lockManager lockURI:[rq uri]
797 timeout:[rq headerForKey:@"timeout"]
798 scope:@"exclusive" // TODO
799 type:@"write" // TODO
803 return [self httpException:423 /* locked */
804 reason:@"object locked, lock manager did not provide token."];
807 [self debugWithFormat:@"locked: %@ (token %@)", [[_ctx request] uri], token];
811 - (id)doUNLOCK:(WOContext *)_ctx {
812 SoSecurityManager *sm;
814 SoDAVLockManager *lockManager;
817 /* check permissions */
819 sm = [_ctx soSecurityManager];
820 e = [sm validatePermission:SoPerm_WebDAVUnlockItems
821 onObject:self->object
823 if (e != nil) return e;
825 /* check lock manager */
827 if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) {
828 return [self httpException:405 /* method not allowed */
829 reason:@"target object does not support locking."];
832 token = [[_ctx request] headerForKey:@"lock-token"];
834 [lockManager unlockURI:[[_ctx request] uri] token:token];
836 [self debugWithFormat:
837 @"unlocked: %@ (token %@)", [[_ctx request] uri], token];
839 [[_ctx response] setStatus:204 /* fake ok */];
840 return [_ctx response];
843 - (NSException *)extractDestinationPath:(NSArray **)path_
844 fromContext:(WOContext *)_ctx
846 NSString *absDestURL;
847 NSURL *destURL, *srvURL;
849 if (path_) *path_ = nil;
851 /* TODO: check proper permission prior attempting a move */
853 absDestURL = [[_ctx request] headerForKey:@"destination"];
854 if (![absDestURL isNotEmpty]) {
855 return [self httpException:400 /* Bad Request */
857 @"the destination WebDAV header was missing "
858 @"for the MOVE/COPY operation"];
860 if ((destURL = [NSURL URLWithString:absDestURL]) == nil) {
861 [self logWithFormat:@"MOVE: got invalid destination URL: '%@'",
863 return [self httpException:400 /* Bad Request */
864 reason:@"the MOVE/COPY destination is not a valid URL!"];
867 srvURL = [_ctx serverURL];
869 [self debugWithFormat:@"move/copy:\n to: %@ (%@)\n server: %@)",
870 [destURL absoluteString], absDestURL,
871 [srvURL absoluteString]];
873 /* check whether URL is on the same server ... */
874 if (![[srvURL host] isEqualToString:[destURL host]] ||
875 ![[srvURL port] isEqual:[destURL port]]) {
877 The WebDAV spec is not really clear on what we should return in this
878 case? Let me know if anybody has a suggestion ...
880 Note: This is easy to confuse if you don't use the Apache server name
881 to access Apache (eg just the IP). Which is why we allow to
884 [self logWithFormat:@"tried to do a cross server move (%@ vs %@)",
885 [srvURL absoluteString], [destURL absoluteString]];
886 if (!disableCrossHostMoveCheck) {
887 return [self httpException:403 /* Forbidden */
888 reason:@"MOVE destination is on a different host."];
896 /* TODO: hack hack hack */
897 ma = [[[destURL path] componentsSeparatedByString:@"/"] mutableCopy];
898 if ([ma isNotEmpty]) // leading slash ("")
899 [ma removeObjectAtIndex:0];
900 if ([ma isNotEmpty]) // the appname (eg zidestore)
901 [ma removeObjectAtIndex:0];
902 if ([ma isNotEmpty]) // the request handler key (eg so)
903 [ma removeObjectAtIndex:0];
905 /* unescape path components */
906 for (i = 0; i < [ma count]; i++) {
909 s = [ma objectAtIndex:i];
910 ns = [s stringByUnescapingURL];
912 [ma replaceObjectAtIndex:i withObject:ns];
920 - (NSException *)lookupDestinationObject:(id *)target_
921 andNewName:(NSString **)name_
922 inContext:(WOContext *)_ctx
928 if ((error = [self extractDestinationPath:&targetPath fromContext:_ctx]))
931 if ((root = [_ctx application]) == nil)
932 root = [WOApplication application];
934 return [self httpException:500 /* internal server error */
935 reason:@"did not find SOPE root object"];
938 /* TODO: we should probably use a subcontext?! */
939 [_ctx setObject:yesNum forKey:@"isDestinationPathLookup"];
940 *target_ = [root traversePathArray:targetPath
944 if ([*target_ isKindOfClass:[NSException class]])
947 [self logWithFormat:@"could not resolve destination object (%@): %@",
948 [targetPath componentsJoinedByString:@" => "],
953 if (name_ != NULL) *name_ = [[[_ctx pathInfo] copy] autorelease];
955 if (*target_ == nil) {
956 [self debugWithFormat:@"MOVE/COPY destination could not be found."];
957 return [self httpException:404 /* Not Found */
958 reason:@"did not find target object"];
961 [self debugWithFormat:@"SOURCE: %@", self->object];
962 [self debugWithFormat:@"TARGET: %@ (pathinfo %@)",
963 *target_, [_ctx pathInfo]];
967 - (id)doCOPY:(WOContext *)_ctx {
972 /* TODO: check proper permission prior attempting a copy */
974 error = [self lookupDestinationObject:&targetObject andNewName:&newName
976 if (error) return error;
978 error = [self->object
979 davCopyToTargetObject:targetObject newName:newName
982 [self debugWithFormat:@"WebDAV COPY operation failed: %@", error];
986 return [newName isNotEmpty]
987 ? [NSNumber numberWithBool:201 /* Created */]
988 : [NSNumber numberWithBool:204 /* No Content */];
991 - (id)doMOVE:(WOContext *)_ctx {
996 /* TODO: check proper permission prior attempting a move */
998 error = [self lookupDestinationObject:&targetObject andNewName:&newName
1000 if (error) return error;
1003 Note: more relevant headers:
1004 overwrite: T|F (overwrite target) [rc: 201 vs 204!]
1006 and locking tokens of course ...
1009 // TODO: should we check in this place for some constraints,
1010 // eg moving a collection to a non-collection or something
1013 error = [self->object
1014 davMoveToTargetObject:targetObject newName:newName
1017 [self debugWithFormat:@"WebDAV MOVE operation failed: %@", error];
1021 return [newName isNotEmpty]
1022 ? [NSNumber numberWithBool:201 /* Created */]
1023 : [NSNumber numberWithBool:204 /* No Content */];
1026 /* WebDAV search methods */
1028 - (id)doSEARCH:(WOContext *)_ctx {
1029 SoSecurityManager *sm;
1031 EOFetchSpecification *fs;
1036 /* check permissions */
1038 sm = [_ctx soSecurityManager];
1039 e = [sm validatePermission:SoPerm_AccessContentsInformation
1040 onObject:self->object
1042 if (e != nil) return e;
1044 /* perform search */
1047 respondsToSelector:@selector(performWebDAVQuery:inContext:)]) {
1048 [[_ctx response] setStatus:405 /* not allowed */];
1049 [[_ctx response] appendContentString:
1050 @"this object cannot not execute a SEARCH query"];
1051 return [_ctx response];
1054 // TODO: whats that? VERY bad, maybe use -baseURLForContext:?
1055 baseURL = [NSString stringWithFormat:@"http://%@%@",
1056 [[_ctx request] headerForKey:@"host"],
1057 [[_ctx request] uri]];
1059 [self lockParser:davsax];
1061 [xmlParser parseFromSource:[[_ctx request] content]];
1062 fs = [[davsax searchFetchSpecification] retain];
1064 [self unlockParser:davsax];
1066 fs = [fs autorelease];
1068 [[_ctx response] setStatus:400 /* Bad Request */];
1069 [[_ctx response] appendContentString:
1070 @"could not process SEARCH query specification"];
1071 return [_ctx response];
1075 if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
1076 /* TODO: parse range header and add to fetch-specification */
1079 r = [range rangeOfString:@"rows="];
1081 range = [range substringFromIndex:(r.location + r.length)];
1082 [self debugWithFormat:
1083 @"Note: got a row range header (ignored): '%@'", range];
1086 [self logWithFormat:@"Note: got a range header (ignored): '%@'", range];
1089 /* override entity name ... (FROM xxx isn't yet parsed correctly) */
1090 [fs setEntityName:baseURL];
1092 [self debugWithFormat:@"SEARCH: %@", fs];
1094 [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
1096 /* translate fetchspec if necessary */
1100 if ((map = [self->object davAttributeMapInContext:_ctx])) {
1101 [_ctx setObject:map forKey:@"DAVPropertyMap"];
1102 fs = [fs fetchSpecificationByApplyingKeyMap:map];
1103 [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"];
1109 if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
1110 return [self httpException:500 /* Server Error */
1111 reason:@"could not execute SEARCH query (returned nil)"];
1117 /* Exchange WebDAV methods */
1119 - (id)doNOTIFY:(WOContext *)_ctx {
1120 return [self httpException:403 reason:@"NOTIFY not yet implemented"];
1123 - (id)doPOLL:(WOContext *)_ctx {
1124 SoSubscriptionManager *sm;
1126 NSString *subscriptionID;
1130 rq = [_ctx request];
1131 sm = [SoSubscriptionManager sharedSubscriptionManager];
1132 url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1135 return [self httpException:500
1136 reason:@"could not calculate URL of WebDAV object !"];
1139 subscriptionID = [rq headerForKey:@"subscription-id"];
1140 if (![subscriptionID isNotEmpty]) {
1141 return [self httpException:400 /* Bad Request */
1142 reason:@"did not find subscription-id header in POLL"];
1145 ids = [subscriptionID componentsSeparatedByString:@","];
1147 return [sm pollSubscriptions:ids onURL:url];
1150 - (id)doSUBSCRIBE:(WOContext *)_ctx {
1151 SoSubscriptionManager *sm;
1156 NSString *notificationType;
1157 NSString *notificationDelay;
1159 NSString *subscriptionID;
1161 rq = [_ctx request];
1162 r = [_ctx response];
1163 sm = [SoSubscriptionManager sharedSubscriptionManager];
1164 url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1167 return [self httpException:500
1168 reason:@"could not calculate URL of WebDAV object !"];
1171 subscriptionID = [rq headerForKey:@"subscription-id"];
1173 /* first check, whether it's an existing subscription to be renewed */
1175 if ([subscriptionID isNotEmpty]) {
1178 if ((newId = [sm renewSubscription:subscriptionID onURL:url]) == nil) {
1179 return [self httpException:412 /* precondition failed */
1180 reason:@"did not find provided subscription ID !"];
1185 if ((callback = [rq headerForKey:@"call-back"])) {
1188 if ((url = [NSURL URLWithString:[callback stringValue]]) == nil) {
1189 [self errorWithFormat:@"could not parse callback URL '%@'",
1191 return [self httpException:400 /* Bad Request */
1192 reason:@"missing valid callback URL !"];
1198 /* TODO: add sanity checking of notification-type as described in docs */
1199 /* TODO: check depth */
1201 notificationDelay = [rq headerForKey:@"notification-delay"];
1202 notificationType = [rq headerForKey:@"notification-type"];
1203 lifetime = [rq headerForKey:@"subscription-lifetime"];
1205 subscriptionID = [sm subscribeURL:url forObserver:callback
1206 type:notificationType
1207 delay:notificationDelay
1208 ? [notificationDelay doubleValue] : 0.0
1209 lifetime:lifetime ? [lifetime doubleValue] : 0.0];
1210 return subscriptionID;
1212 - (id)doUNSUBSCRIBE:(WOContext *)_ctx {
1213 SoSubscriptionManager *sm;
1216 NSString *subscriptionID;
1219 rq = [_ctx request];
1220 r = [_ctx response];
1221 sm = [SoSubscriptionManager sharedSubscriptionManager];
1222 url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1225 return [self httpException:500
1226 reason:@"could not calculate URL of WebDAV object !"];
1229 subscriptionID = [rq headerForKey:@"subscription-id"];
1230 if (![subscriptionID isNotEmpty]) {
1231 return [self httpException:400 /* Bad Request */
1232 reason:@"missing subscription id !"];
1235 if ([sm unsubscribeID:subscriptionID onURL:url]) {
1240 return [self httpException:400 /* Bad Request */
1241 reason:@"unsubscribe failed (invalid or old id ?)"];
1244 /* Exchange bulk methods */
1246 - (NSArray *)urlPartsForTargets:(NSArray *)_targets basePath:(NSString *)_base{
1248 Transform the target URLs given to the BPROPFIND operation. This is a
1249 simplified implementation, for example we expect that the URLs are all
1250 located in the same URL space (on same host and port).
1255 if ((count = [_targets count]) == 0)
1256 return [NSArray array];
1258 ma = [NSMutableArray arrayWithCapacity:count];
1259 for (i = 0; i < count; i++) {
1262 target = [_targets objectAtIndex:i];
1263 if (debugBulkTarget)
1264 [self logWithFormat:@" MORPH target '%@'", target];
1266 /* extract the path from full URLs */
1267 if ([target isAbsoluteURL]) {
1270 /* fix an Evolution bug, uses the 'unsafe' "@" in the URL ! */
1271 if ([target rangeOfString:@"@"].length > 0) {
1272 target = [target stringByReplacingString:@"@"
1276 if ((url = [NSURL URLWithString:target])) {
1277 if (debugBulkTarget) [self logWithFormat:@"got URL: %@", url];
1278 target = [url path];
1279 if (debugBulkTarget) [self logWithFormat:@"path: %@", target];
1282 [self errorWithFormat:@"could not parse BPROPFIND target '%@' !",
1287 /* make the target name relative to the request URI */
1288 if ([target hasPrefix:_base]) {
1289 target = [target substringFromIndex:[_base length]];
1290 if ([target hasPrefix:@"/"])
1291 target = [target substringFromIndex:1];
1294 /* add the target */
1295 target = [target stringByUnescapingURL];
1296 if (debugBulkTarget) [self logWithFormat:@" ADD target '%@'", target];
1297 [ma addObject:target];
1302 - (id)doBPROPFIND:(WOContext *)_ctx {
1304 TODO: could optimize a BPROPFIND on a single target to use PROPFIND
1306 How are BPROPFINDs mapped ? BPROPFIND corresponds to SKYRiX 4.1
1307 "fetch-by-globalids" commands, that is, a search gets passed a list
1308 of primary keys to fetch.
1309 BPROPFIND is implemented in a similiar way, the target URLs are converted
1310 to be relative to the URI object and are passed to the query datasource
1311 using the "bulkTargetKeys" fetch hint.
1313 Important: the URI object *must* support the "bulkTargetKeys" fetch hint,
1314 otherwise the operation will run on the object itself.
1316 Note: Previously BPROPFIND was mapped to a set of individual requests,
1317 but obviously this doesn't match SQL very well (resulting in an individual
1318 SQL query for each entity ...)
1320 SoSecurityManager *sm;
1322 EOFetchSpecification *fs;
1324 NSString *depth; /* 0, 1, 1,noroot or infinity */
1326 NSArray *targets, *rtargets;
1332 /* check permissions */
1334 sm = [_ctx soSecurityManager];
1335 e = [sm validatePermission:SoPerm_AccessContentsInformation
1336 onObject:self->object
1338 if (e != nil) return e;
1340 /* perform search */
1342 if (![self->object respondsToSelector:@selector(performWebDAVQuery:inContext:)]) {
1343 return [self httpException:405 /* not allowed */
1344 reason:@"this object cannot not execute a PROPFIND query"];
1347 rq = [_ctx request];
1348 depth = [rq headerForKey:@"depth"];
1349 if (![depth isNotEmpty]) depth = @"infinity";
1351 [self lockParser:davsax];
1353 [xmlParser parseFromSource:[rq content]];
1354 propNames = [[davsax propFindQueriedNames] copy];
1355 findAll = [davsax propFindAllProperties];
1356 findNames = [davsax propFindPropertyNames];
1357 targets = [[davsax bpropFindTargets] copy];
1359 [self unlockParser:davsax];
1360 propNames = [propNames autorelease];
1361 targets = [targets autorelease];
1363 if (![targets isNotEmpty])
1364 return [NSArray array];
1366 /* check query all properties */
1368 if (propNames == nil)
1369 propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx];
1373 rtargets = [self urlPartsForTargets:targets
1374 basePath:[[rq uri] stringByUnescapingURL]];
1376 [self debugWithFormat:@"BPROPFIND targets: %@", rtargets];
1378 /* build the fetch-spec */
1380 NSMutableDictionary *hints;
1382 hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx]
1383 propNames:propNames findAll:findAll namesOnly:findNames];
1384 [hints setObject:rtargets forKey:@"bulkTargetKeys"];
1386 fs = [EOFetchSpecification alloc];
1387 fs = [fs initWithEntityName:[self baseURLForContext:_ctx]
1390 usesDistinct:NO isDeep:NO hints:hints];
1391 fs = [fs autorelease];
1394 [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
1397 translate fetchspec if necessary - we currently cannot allow a map
1398 for each target, so we use the map of the queried target.
1400 if ((map = [self->object davAttributeMapInContext:_ctx])) {
1401 [_ctx setObject:map forKey:@"DAVPropertyMap"];
1402 fs = [fs fetchSpecificationByApplyingKeyMap:map];
1403 [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"];
1408 if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
1409 return [self httpException:500 /* Server Error */
1410 reason:@"could not perform query (object returned nil)"];
1415 /* now, for each BPROPFIND target ... */
1418 NSString *targetURL;
1420 result = [NSMutableArray arrayWithCapacity:32];
1422 e = [targets objectEnumerator];
1423 while ((targetURL = [e nextObject])) {
1424 NSAutoreleasePool *pool;
1425 WOContext *localContext;
1426 WORequest *localRequest;
1431 pool = [[NSAutoreleasePool alloc] init];
1433 /* setup the "subrequest" */
1435 if ([targetURL isAbsoluteURL]) {
1438 if ((url = [NSURL URLWithString:targetURL]))
1439 targetURL = [url path];
1441 [self errorWithFormat:@"could not parse target-url '%@'", targetURL];
1445 localRequest = [[WORequest alloc] initWithMethod:@"PROPFIND"
1447 httpVersion:[rq httpVersion]
1448 headers:[rq headers]
1452 [[[WOContext alloc] initWithRequest:localRequest] autorelease];
1453 [localRequest autorelease];
1455 /* resetup fetchspec */
1456 [fs setEntityName:targetURL];
1460 targetObject = [_ctx traversalRoot];
1461 targetObject = [targetObject traversePathArray:
1462 [localRequest requestHandlerPathArray]
1463 inContext:localContext
1466 if (targetObject == nil) {
1467 [self logWithFormat:@"did not find BPROPFIND target: %@", targetURL];
1468 [self logWithFormat:@" root: %@", [_ctx traversalRoot]];
1469 [self logWithFormat:@" path: %@",
1470 [[localRequest requestHandlerPathArray]
1471 componentsJoinedByString:@"/"]];
1472 [self logWithFormat:@" error: %@", e];
1478 targetResult = [targetObject performWebDAVQuery:fs
1479 inContext:localContext];
1480 if (targetResult == nil) {
1482 [self httpException:500 /* Server Error */
1483 reason:@"could not perform query (object returned nil)"];
1487 // do we need to distinguish the queries somehow ? (href generation)
1488 if ([targetResult isKindOfClass:[NSArray class]])
1489 [result addObjectsFromArray:targetResult];
1490 else if (targetResult)
1491 [result addObject:targetResult];
1499 if (result) return result;
1503 - (id)doBCOPY:(WOContext *)_ctx {
1504 return [self httpException:403 /* forbidden */
1505 reason:@"BCOPY not yet implemented."];
1507 - (id)doBDELETE:(WOContext *)_ctx {
1508 return [self httpException:403 /* forbidden */
1509 reason:@"BDELETE not yet implemented."];
1511 - (id)doBMOVE:(WOContext *)_ctx {
1512 return [self httpException:403 /* forbidden */
1513 reason:@"WebDAV operation not yet implemented."];
1516 - (id)doBPROPPATCH:(WOContext *)_ctx {
1517 return [self httpException:403 /* forbidden */
1518 reason:@"WebDAV operation not yet implemented."];
1523 - (id)doREPORT:(WOContext *)_ctx {
1524 id<DOMDocument> domDocument;
1527 id method, resultObject;
1529 rq = [_ctx request];
1533 if (![[rq headerForKey:@"content-type"] hasPrefix:@"text/xml"]) {
1534 return [self httpException:400 /* invalid request */
1535 reason:@"XML entity expected for WebDAV REPORT."];
1540 if ((domDocument = [rq contentAsDOMDocument]) == nil) {
1541 return [self httpException:400 /* invalid request */
1542 reason:@"Could not parse XML of WebDAV REPORT."];
1545 /* first try to lookup method with fully qualified name */
1547 mname = [NSString stringWithFormat:@"{%@}%@",
1548 [[domDocument documentElement] namespaceURI],
1549 [[domDocument documentElement] localName]];
1550 method = [self->object lookupName:mname inContext:_ctx acquire:NO];
1552 if (method == nil || [method isKindOfClass:[NSException class]]) {
1553 /* then try to lookup by simplified name */
1556 m2 = [self->object lookupName:[[domDocument documentElement] localName]
1557 inContext:_ctx acquire:NO];
1560 else if ([m2 isKindOfClass:[NSException class]]) {
1562 method = m2; /* use the second exceptions */
1566 mname = [[domDocument documentElement] localName];
1570 // TODO: what I would really like to have here is a pluggable dispatcher
1571 // mechanism which translates the report payload into a customized
1574 /* check for lookup errors */
1576 if (method == nil || [method isKindOfClass:[NSException class]]) {
1577 [self logWithFormat:@"did not find a method to server the REPORT"];
1578 return [NSException exceptionWithHTTPStatus:501 /* not implemented */
1579 reason:@"did not find the specified REPORT"];
1581 else if ([method isKindOfClass:[NSException class]]) {
1582 [self logWithFormat:@"failed to lookup the REPORT: %@", method];
1585 else if (![method isCallable]) {
1586 [self warnWithFormat:
1587 @"object found for REPORT '%@' is not callable: %@",
1590 [self debugWithFormat:@"REPORT method: %@", method];
1594 resultObject = [method callOnObject:[_ctx clientObject] inContext:_ctx];
1595 if (debugOn) [self debugWithFormat:@"got REPORT result: %@", resultObject];
1596 return resultObject;
1601 - (id)doMKCALENDAR:(WOContext *)_ctx {
1602 return [self httpException:405 /* method not allowed */
1603 reason:@"CalDAV calendar creation not yet implemented."];
1606 /* DAV access control lists */
1608 - (id)doACL:(WOContext *)_ctx {
1609 return [self httpException:405 /* method not allowed */
1610 reason:@"WebDAV operation not yet implemented."];
1615 - (id)doBIND:(WOContext *)_ctx {
1616 return [self httpException:405 /* method not allowed */
1617 reason:@"WebDAV operation not yet implemented."];
1622 - (id)doORDERPATCH:(WOContext *)_ctx {
1623 return [self httpException:405 /* method not allowed */
1624 reason:@"WebDAV operation not yet implemented."];
1629 - (id)doCHECKOUT:(WOContext *)_ctx {
1630 return [self httpException:405 /* method not allowed */
1631 reason:@"WebDAV operation not yet implemented."];
1633 - (id)doUNCHECKOUT:(WOContext *)_ctx {
1634 return [self httpException:405 /* method not allowed */
1635 reason:@"WebDAV operation not yet implemented."];
1637 - (id)doCHECKIN:(WOContext *)_ctx {
1638 return [self httpException:405 /* method not allowed */
1639 reason:@"WebDAV operation not yet implemented."];
1641 - (id)doMKWORKSPACE:(WOContext *)_ctx {
1642 return [self httpException:405 /* method not allowed */
1643 reason:@"WebDAV operation not yet implemented."];
1645 - (id)doUPDATE:(WOContext *)_ctx {
1646 return [self httpException:405 /* method not allowed */
1647 reason:@"WebDAV operation not yet implemented."];
1649 - (id)doMERGE:(WOContext *)_ctx {
1650 return [self httpException:405 /* method not allowed */
1651 reason:@"WebDAV operation not yet implemented."];
1653 - (id)doVERSIONCONTROL:(WOContext *)_ctx {
1654 return [self httpException:405 /* method not allowed */
1655 reason:@"WebDAV operation not yet implemented."];
1658 /* perform dispatch */
1660 - (id)performMethod:(NSString *)_method inContext:(WOContext *)_ctx {
1661 SoSecurityManager *sm;
1666 /* check basic WebDAV permission */
1668 sm = [_ctx soSecurityManager];
1669 e = [sm validatePermission:SoPerm_WebDAVAccess
1670 onObject:self->object
1672 if (e != nil) return e;
1674 /* perform search */
1676 _method = [_method uppercaseString];
1677 _method = [_method stringByReplacingString:@"-" withString:@""];
1678 s = [NSString stringWithFormat:@"do%@:", _method];
1679 sel = NSSelectorFromString(s);
1681 if (![self respondsToSelector:sel]) {
1682 [self logWithFormat:@"unknown WebDAV method: '%@'", _method];
1683 [[_ctx response] setStatus:405 /* invalid method */];
1684 return [_ctx response];
1687 return [self performSelector:sel withObject:_ctx];
1690 - (BOOL)setupXmlParser {
1691 if (xmlParser == nil) {
1693 [[[SaxXMLReaderFactory standardXMLReaderFactory]
1694 createXMLReaderForMimeType:@"text/xml"]
1696 if (xmlParser == nil)
1699 if (davsax == nil) {
1700 if ((davsax = [[SaxDAVHandler alloc] init]) == nil)
1706 - (id)dispatchInContext:(WOContext *)_ctx {
1707 NSAutoreleasePool *pool;
1711 if (gmt == nil) gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
1713 /* setup XML parser */
1714 if (![self setupXmlParser]) {
1715 r = [_ctx response];
1716 [r setStatus:500 /* internal server error */];
1717 [r appendContentString:@"did not find an XML parser, cannot process DAV."];
1721 pool = [[NSAutoreleasePool alloc] init];
1722 result = [[self performMethod:[[_ctx request] method] inContext:_ctx] retain];
1724 return [result autorelease];
1729 - (NSString *)loggingPrefix {
1730 return @"[obj-dav-dispatch]";
1732 - (BOOL)isDebuggingEnabled {
1733 return debugOn ? YES : NO;
1738 - (NSString *)description {
1739 NSMutableString *ms;
1741 ms = [NSMutableString stringWithCapacity:64];
1742 [ms appendFormat:@"<0x%p[%@]:", self,
1743 NSStringFromClass((Class)*(void**)self)];
1746 [ms appendFormat:@" object=%@", self->object];
1748 [ms appendString:@" <no object>"];
1750 [ms appendString:@">"];
1754 @end /* SoObjectWebDAVDispatcher */