/*
- Copyright (C) 2002-2005 SKYRIX Software AG
+ Copyright (C) 2002-2006 SKYRIX Software AG
+ Copyright (C) 2006 Helge Hess
This file is part of SOPE.
#include "SoDAVLockManager.h"
#include "EOFetchSpecification+SoDAV.h"
#include "WOContext+SoObjects.h"
+#include "NSException+HTTP.h"
#include <NGObjWeb/WOApplication.h>
#include <NGObjWeb/WORequest.h>
#include <NGObjWeb/WOResponse.h>
#include <NGObjWeb/WEClientCapabilities.h>
#include <SaxObjC/SaxObjC.h>
#include <SaxObjC/XMLNamespaces.h>
+#include <DOM/DOMDocument.h>
#include <NGExtensions/NSString+Ext.h>
#include "common.h"
static int debugOn = -1;
static BOOL debugBulkTarget = NO;
+static BOOL disableCrossHostMoveCheck = NO;
static NSNumber *yesNum = nil;
+ (void)initialize {
debugOn = [ud boolForKey:@"SoObjectDAVDispatcherDebugEnabled"] ? 1 : 0;
if (debugOn) NSLog(@"Note: WebDAV dispatcher debugging is enabled.");
if (yesNum == nil) yesNum = [[NSNumber numberWithBool:YES] retain];
+
+ disableCrossHostMoveCheck =
+ [ud boolForKey:@"SoWebDAVDisableCrossHostMoveCheck"];
}
// THREAD
ui = [NSDictionary dictionaryWithObjectsAndKeys:
self, @"dispatcher",
[NSNumber numberWithInt:_status], @"http-status",
- nil];
+ 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];
+ extern NSString *SoObjectRootURLInContext
+ (WOContext *_ctx, id logobj, BOOL withAppPart);
+ NSString *rootURL;
- baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
- return baseURL;
+ rootURL = SoObjectRootURLInContext(_ctx, self, NO);
+ return [rootURL stringByAppendingString:[[_ctx request] uri]];
}
- (id)primaryCallWebDAVMethod:(NSString *)_name inContext:(WOContext *)_ctx {
/* core HTTP methods */
-- (id)doGET:(WOContext *)_ctx {
+- (id)_callObjectMethod:(NSString *)_method inContext:(WOContext *)_ctx {
+ /* returns 'nil' if the object had no such method */
NSException *e;
id methodObject;
+ id result;
+ methodObject =
+ [self->object lookupName:_method inContext:_ctx acquire:NO];
+ if (![methodObject isNotNull])
+ return nil;
+ if ([methodObject isKindOfClass:[NSException class]]) {
+ if ([(NSException *)methodObject httpStatus] == 404 /* Not Found */) {
+ /* not found */
+ return nil;
+ }
+ return methodObject; /* the exception */
+ }
+ if ((e = [self->object validateName:_method inContext:_ctx]) != nil)
+ return e;
- if ((methodObject =
- [self->object lookupName:@"GET" inContext:_ctx acquire:NO]) == nil)
+ if ([methodObject respondsToSelector:
+ @selector(takeValuesFromRequest:inContext:)])
+ [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx];
+
+ result = [methodObject callOnObject:self->object inContext:_ctx];
+ return (result != nil) ? result : (id)[NSNull null];
+}
+
+- (id)doGET:(WOContext *)_ctx {
+ NSException *e;
+ id methodObject;
+
+ methodObject = [self->object lookupName:@"GET" inContext:_ctx acquire:NO];
+ if (methodObject == nil)
methodObject = [self->object lookupDefaultMethod];
else {
- if ((e = [self->object validateName:@"GET" inContext:_ctx]))
+ if ((e = [self->object validateName:@"GET" inContext:_ctx]) != nil)
return e;
}
sm = [_ctx soSecurityManager];
e = [sm validatePermission:
- ([pathInfo length] > 0)
+ [pathInfo isNotEmpty]
? SoPerm_AddDocumentsImagesAndFiles
: SoPerm_ChangeImagesAndFiles
onObject:self->object
/* perform */
- if ([pathInfo length] > 0) {
+ if ([pathInfo isNotEmpty]) {
/* check whether all the parent collections are available */
+ // TODO: we might also want to check for a 'create' permission
if ([pathInfo rangeOfString:@"/"].length > 0) {
return [self httpException:409 /* Conflict */
reason:
}
- (id)doOPTIONS:(WOContext *)_ctx {
- return [self allowedMethods];
+ WOResponse *response;
+ NSArray *tmp;
+ id result;
+
+ /* this checks whether the object provides a specific OPTIONS method */
+ if ((result = [self _callObjectMethod:@"OPTIONS" inContext:_ctx]) != nil)
+ return result;
+
+ response = [_ctx response];
+ [response setStatus:200 /* OK */];
+
+ if ((tmp = [self->object davAllowedMethodsInContext:_ctx]) != nil)
+ [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"allow"];
+
+ if ([[[_ctx request] clientCapabilities] isWebFolder]) {
+ /*
+ As described over here:
+ http://teyc.editthispage.com/2005/06/02
+
+ This page also says that: "MS-Auth-Via header is not required to work
+ with Web Folders".
+ */
+ [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"public"];
+ }
+
+ if ((tmp = [self->object davComplianceClassesInContext:_ctx]) != nil)
+ [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"dav"];
+
+ return response;
}
- (id)doHEAD:(WOContext *)_ctx {
NSString *pathInfo;
pathInfo = [_ctx pathInfo];
- if ([pathInfo length] == 0) {
+ if (![pathInfo isNotEmpty]) {
/* MKCOL target already exists ... */
WOResponse *r;
depth = [rq headerForKey:@"depth"];
uri = [rq uri];
- if ([depth length] == 0) depth = @"infinity";
+ if (![depth isNotEmpty]) depth = @"infinity";
- if ([[rq content] length] > 0) {
+ if ([[rq content] isNotEmpty]) {
[self lockParser:davsax];
{
[xmlParser parseFromSource:[rq content]];
"A client may choose not to submit a request body. An empty PROPFIND
request body MUST be treated as a request for the names and values of
all properties."
- TODO: means, an empty request is to be treated as allprop?
+ TODO: means, an empty request is to be handled as <allprop/>?
*/
propNames = nil;
findAll = YES;
findNames = NO;
}
+ if (findAll) {
+ /*
+ Hack up request to include 'brief'. This elimates the reporting of 404
+ properties in the renderer. Its necessary because some objects may not
+ properly report their default properties (they sometimes report missing
+ properties).
+ */
+ [[_ctx request] setHeader:@"true" forKey:@"brief"];
+ }
+
/* check query all properties */
if (propNames == nil)
if ([s hasSuffix:@"/"]) s = [s substringToIndex:([s length] - 1)];
if ([s hasPrefix:@"_"]) s = [s substringFromIndex:1];
- ids = ([s length] == 0)
- ? [NSArray array]
- : [s componentsSeparatedByString:@"_"];
+ ids = [s isNotEmpty]
+ ? [s componentsSeparatedByString:@"_"]
+ : (NSArray *)[NSArray array];
// TODO: should use -stringByUnescapingURL on IDs (not required for ints)
/* check permissions */
sm = [_ctx soSecurityManager];
- e = [sm validatePermission:([pathInfo length] > 0)
+ e = [sm validatePermission:[pathInfo isNotEmpty]
? SoPerm_AddDocumentsImagesAndFiles
: SoPerm_ChangeImagesAndFiles
onObject:self->object
/* check for conflicts */
- if ([pathInfo length] > 0) {
+ if ([pathInfo isNotEmpty]) {
/* check whether all the parent collections are available */
if ([pathInfo rangeOfString:@"/"].length > 0) {
return [self httpException:409 /* Conflict */
/* check whether the object supports patching */
- if ([pathInfo length] > 0) {
+ if ([pathInfo isNotEmpty]) {
if (![self->object respondsToSelector:
@selector(davCreateObject:properties:inContext:)]) {
[self debugWithFormat:@"cannot create new object via DAV on %@",
reason:@"got no properties in PROPPATCH !"];
}
- if ([pathInfo length] > 0) {
+ if ([pathInfo isNotEmpty]) {
/* a create object cannot delete props ... */
- if ([delProps count] > 0) {
+ if ([delProps isNotEmpty]) {
if (![self allowDeletePropertiesOnNewObjectInContext:_ctx]) {
[self logWithFormat:@"shall delete props in new object '%@': %@",
pathInfo, delProps];
pathInfo, delProps, setProps];
}
- if ([pathInfo length] == 0) {
+ if (![pathInfo isNotEmpty]) {
/* edit an object */
NSException *e;
[self warnWithFormat:@"'depth' locking not supported yet (depth=%@)!",
lockDepth];
}
- if (ifValue) {
+ if (ifValue != nil) {
[self warnWithFormat:@"'if' locking not supported yet, if: '%@'", ifValue];
}
- // need to parse lockinfo
+ /*
+ TODO: parse lockinfo:
+ <?xml version="1.0" encoding="UTF-8" ?>
+ <lockinfo xmlns="DAV:">
+ <locktype><write/></locktype>
+ <lockscope><exclusive/></lockscope>
+ <owner>helge</owner>
+ </lockinfo>
+
+ Currently we assume exclusive/write ... (also see SoWebDAVRenderer)
+ */
+
+ /* Sample timeout: Second-180 */
token = [lockManager lockURI:[rq uri]
timeout:[rq headerForKey:@"timeout"]
- scope:@"exclusive"
- type:@"write"
- owner:nil];
+ scope:@"exclusive" // TODO
+ type:@"write" // TODO
+ owner:nil]; // TODO
if (token == nil) {
/* already locked */
return [self httpException:423 /* locked */
/* TODO: check proper permission prior attempting a move */
absDestURL = [[_ctx request] headerForKey:@"destination"];
- if ([absDestURL length] == 0) {
+ if (![absDestURL isNotEmpty]) {
return [self httpException:400 /* Bad Request */
reason:
@"the destination WebDAV header was missing "
/*
The WebDAV spec is not really clear on what we should return in this
case? Let me know if anybody has a suggestion ...
+
+ Note: This is easy to confuse if you don't use the Apache server name
+ to access Apache (eg just the IP). Which is why we allow to
+ disable this check.
*/
[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 (!disableCrossHostMoveCheck) {
+ return [self httpException:403 /* Forbidden */
+ reason:@"MOVE destination is on a different host."];
+ }
}
- if (path_) {
+ if (path_ != NULL) {
NSMutableArray *ma;
unsigned i;
/* TODO: hack hack hack */
ma = [[[destURL path] componentsSeparatedByString:@"/"] mutableCopy];
- if ([ma count] > 0) // leading slash ("")
+ if ([ma isNotEmpty]) // leading slash ("")
[ma removeObjectAtIndex:0];
- if ([ma count] > 0) // the appname (eg zidestore)
+ if ([ma isNotEmpty]) // the appname (eg zidestore)
[ma removeObjectAtIndex:0];
- if ([ma count] > 0) // the request handler key (eg so)
+ if ([ma isNotEmpty]) // 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;
+ NSString *s, *ns;
+ s = [ma objectAtIndex:i];
ns = [s stringByUnescapingURL];
if (ns != s)
[ma replaceObjectAtIndex:i withObject:ns];
return error;
}
- return ([newName length] > 0)
+ return [newName isNotEmpty]
? [NSNumber numberWithBool:201 /* Created */]
: [NSNumber numberWithBool:204 /* No Content */];
}
return error;
}
- return ([newName length] > 0)
+ return [newName isNotEmpty]
? [NSNumber numberWithBool:201 /* Created */]
: [NSNumber numberWithBool:204 /* No Content */];
}
return [_ctx response];
}
+ // TODO: whats that? VERY bad, maybe use -baseURLForContext:?
baseURL = [NSString stringWithFormat:@"http://%@%@",
[[_ctx request] headerForKey:@"host"],
[[_ctx request] uri]];
}
subscriptionID = [rq headerForKey:@"subscription-id"];
- if ([subscriptionID length] == 0) {
+ if (![subscriptionID isNotEmpty]) {
return [self httpException:400 /* Bad Request */
reason:@"did not find subscription-id header in POLL"];
}
/* first check, whether it's an existing subscription to be renewed */
- if ([subscriptionID length] > 0) {
+ if ([subscriptionID isNotEmpty]) {
NSString *newId;
if ((newId = [sm renewSubscription:subscriptionID onURL:url]) == nil) {
}
subscriptionID = [rq headerForKey:@"subscription-id"];
- if ([subscriptionID length] == 0) {
+ if (![subscriptionID isNotEmpty]) {
return [self httpException:400 /* Bad Request */
reason:@"missing subscription id !"];
}
[r setStatus:200];
return r;
}
- else {
- return [self httpException:400 /* Bad Request */
- reason:@"unsubscribe failed (invalid or old id ?)"];
- }
+
+ return [self httpException:400 /* Bad Request */
+ reason:@"unsubscribe failed (invalid or old id ?)"];
}
/* Exchange bulk methods */
rq = [_ctx request];
depth = [rq headerForKey:@"depth"];
- if ([depth length] == 0) depth = @"infinity";
+ if (![depth isNotEmpty]) depth = @"infinity";
[self lockParser:davsax];
{
propNames = [propNames autorelease];
targets = [targets autorelease];
- if ([targets count] == 0)
+ if (![targets isNotEmpty])
return [NSArray array];
/* check query all properties */
/* DAV reports */
- (id)doREPORT:(WOContext *)_ctx {
+ id<DOMDocument> domDocument;
+ WORequest *rq;
+ NSString *mname;
+ id method, resultObject;
+
+ rq = [_ctx request];
+
+ /* ensure XML */
+
+ if (![[rq headerForKey:@"content-type"] hasPrefix:@"text/xml"]) {
+ return [self httpException:400 /* invalid request */
+ reason:@"XML entity expected for WebDAV REPORT."];
+ }
+
+ /* retrieve XML */
+
+ if ((domDocument = [rq contentAsDOMDocument]) == nil) {
+ return [self httpException:400 /* invalid request */
+ reason:@"Could not parse XML of WebDAV REPORT."];
+ }
+
+ /* first try to lookup method with fully qualified name */
+
+ mname = [NSString stringWithFormat:@"{%@}%@",
+ [[domDocument documentElement] namespaceURI],
+ [[domDocument documentElement] localName]];
+ method = [self->object lookupName:mname inContext:_ctx acquire:NO];
+
+ if (method == nil || [method isKindOfClass:[NSException class]]) {
+ /* then try to lookup by simplified name */
+ id m2;
+
+ m2 = [self->object lookupName:[[domDocument documentElement] localName]
+ inContext:_ctx acquire:NO];
+ if (m2 == nil)
+ ; /* failed */
+ else if ([m2 isKindOfClass:[NSException class]]) {
+ if (method == nil)
+ method = m2; /* use the second exceptions */
+ }
+ else {
+ method = m2;
+ mname = [[domDocument documentElement] localName];
+ }
+ }
+
+ // TODO: what I would really like to have here is a pluggable dispatcher
+ // mechanism which translates the report payload into a customized
+ // method call.
+
+ /* check for lookup errors */
+
+ if (method == nil || [method isKindOfClass:[NSException class]]) {
+ [self logWithFormat:@"did not find a method to server the REPORT"];
+ return [NSException exceptionWithHTTPStatus:501 /* not implemented */
+ reason:@"did not find the specified REPORT"];
+ }
+ else if ([method isKindOfClass:[NSException class]]) {
+ [self logWithFormat:@"failed to lookup the REPORT: %@", method];
+ return method;
+ }
+ else if (![method isCallable]) {
+ [self warnWithFormat:
+ @"object found for REPORT '%@' is not callable: %@",
+ mname, method];
+ }
+ [self debugWithFormat:@"REPORT method: %@", method];
+
+ /* perform call */
+
+ resultObject = [method callOnObject:[_ctx clientObject] inContext:_ctx];
+ if (debugOn) [self debugWithFormat:@"got REPORT result: %@", resultObject];
+ return resultObject;
+}
+
+/* CalDAV */
+
+- (id)doMKCALENDAR:(WOContext *)_ctx {
return [self httpException:405 /* method not allowed */
- reason:@"WebDAV reports not yet implemented."];
+ reason:@"CalDAV calendar creation not yet implemented."];
}
/* DAV access control lists */
NSMutableString *ms;
ms = [NSMutableString stringWithCapacity:64];
- [ms appendFormat:@"<0x%08X[%@]:", self,
+ [ms appendFormat:@"<0x%p[%@]:", self,
NSStringFromClass((Class)*(void**)self)];
if (self->object)