]> err.no Git - sope/blobdiff - sope-appserver/NGObjWeb/WebDAV/SoObjectWebDAVDispatcher.m
improved WebDAV property handling
[sope] / sope-appserver / NGObjWeb / WebDAV / SoObjectWebDAVDispatcher.m
index 733cb29f5f339a6e4bd86b430a32c5a5fb5e76d3..6bc5e857174fc91e22d20466bd7695c98e7fbe0f 100644 (file)
@@ -1,5 +1,6 @@
 /*
-  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.
 
@@ -38,6 +39,7 @@
 #include <NGObjWeb/WEClientCapabilities.h>
 #include <SaxObjC/SaxObjC.h>
 #include <SaxObjC/XMLNamespaces.h>
+#include <DOM/DOMDocument.h>
 #include <NGExtensions/NSString+Ext.h>
 #include "common.h"
 
@@ -49,6 +51,7 @@
 
 static int      debugOn = -1;
 static BOOL     debugBulkTarget = NO;
+static BOOL     disableCrossHostMoveCheck = NO;
 static NSNumber *yesNum = nil;
 
 + (void)initialize {
@@ -60,6 +63,9 @@ static NSNumber *yesNum = nil;
   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
@@ -99,7 +105,7 @@ static NSTimeZone                *gmt      = nil;
   ui = [NSDictionary dictionaryWithObjectsAndKeys:
                       self, @"dispatcher",
                       [NSNumber numberWithInt:_status], @"http-status",
-                      nil];
+                    nil];
   return [NSException exceptionWithName:
                        [NSString stringWithFormat:@"HTTP%i", _status]
                      reason:_reason
@@ -107,7 +113,8 @@ static NSTimeZone                *gmt      = nil;
 }
 
 - (NSString *)baseURLForContext:(WOContext *)_ctx {
-  extern NSString *SoObjectRootURLInContext(WOContext *_ctx, id logobj, BOOL withAppPart);
+  extern NSString *SoObjectRootURLInContext
+    (WOContext *_ctx, id logobj, BOOL withAppPart);
   NSString *rootURL;
   
   rootURL = SoObjectRootURLInContext(_ctx, self, NO);
@@ -144,11 +151,11 @@ static NSTimeZone                *gmt      = nil;
   if (![methodObject isNotNull])
     return nil;
   if ([methodObject isKindOfClass:[NSException class]]) {
-    if ([(NSException *)methodObject httpStatus] == 404) {
+    if ([(NSException *)methodObject httpStatus] == 404 /* Not Found */) {
       /* not found */
       return nil;
     }
-    return methodObject;
+    return methodObject; /* the exception */
   }
   if ((e = [self->object validateName:_method inContext:_ctx]) != nil)
     return e;
@@ -158,7 +165,7 @@ static NSTimeZone                *gmt      = nil;
     [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx];
   
   result = [methodObject callOnObject:self->object inContext:_ctx];
-  return (result != nil) ? result : [NSNull null];
+  return (result != nil) ? result : (id)[NSNull null];
 }
 
 - (id)doGET:(WOContext *)_ctx {
@@ -197,7 +204,7 @@ static NSTimeZone                *gmt      = nil;
   
   sm = [_ctx soSecurityManager];
   e  = [sm validatePermission:
-            ([pathInfo length] > 0)
+            [pathInfo isNotEmpty]
             ? SoPerm_AddDocumentsImagesAndFiles
             : SoPerm_ChangeImagesAndFiles
           onObject:self->object
@@ -209,8 +216,9 @@ static NSTimeZone                *gmt      = nil;
   
   /* 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:
@@ -258,6 +266,7 @@ static NSTimeZone                *gmt      = nil;
   NSArray    *tmp;
   id         result;
   
+  /* this checks whether the object provides a specific OPTIONS method */
   if ((result = [self _callObjectMethod:@"OPTIONS" inContext:_ctx]) != nil)
     return result;
   
@@ -267,6 +276,17 @@ static NSTimeZone                *gmt      = nil;
   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"];
   
@@ -285,7 +305,7 @@ static NSTimeZone                *gmt      = nil;
   NSString          *pathInfo;
   
   pathInfo = [_ctx pathInfo];
-  if ([pathInfo length] == 0) {
+  if (![pathInfo isNotEmpty]) {
     /* MKCOL target already exists ... */
     WOResponse *r;
 
@@ -417,9 +437,9 @@ static NSTimeZone                *gmt      = nil;
   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]];
@@ -436,13 +456,23 @@ static NSTimeZone                *gmt      = nil;
       "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)
@@ -463,9 +493,9 @@ static NSTimeZone                *gmt      = 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)
     
@@ -558,7 +588,7 @@ static NSTimeZone                *gmt      = nil;
   /* check permissions */
   
   sm = [_ctx soSecurityManager];
-  e  = [sm validatePermission:([pathInfo length] > 0)
+  e  = [sm validatePermission:[pathInfo isNotEmpty]
             ? SoPerm_AddDocumentsImagesAndFiles
             : SoPerm_ChangeImagesAndFiles
           onObject:self->object
@@ -567,7 +597,7 @@ static NSTimeZone                *gmt      = nil;
   
   /* 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 */
@@ -579,7 +609,7 @@ static NSTimeZone                *gmt      = nil;
   
   /* 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 %@",
@@ -617,9 +647,9 @@ static NSTimeZone                *gmt      = nil;
                 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];
@@ -683,7 +713,7 @@ static NSTimeZone                *gmt      = nil;
            pathInfo, delProps, setProps];
   }
   
-  if ([pathInfo length] == 0) {
+  if (![pathInfo isNotEmpty]) {
     /* edit an object */
     NSException *e;
     
@@ -745,17 +775,29 @@ static NSTimeZone                *gmt      = nil;
     [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 */
@@ -809,7 +851,7 @@ static NSTimeZone                *gmt      = nil;
   /* 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 "
@@ -834,30 +876,37 @@ static NSTimeZone                *gmt      = nil;
     /* 
        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];
@@ -934,7 +983,7 @@ static NSTimeZone                *gmt      = nil;
     return error;
   }
   
-  return ([newName length] > 0)
+  return [newName isNotEmpty]
     ? [NSNumber numberWithBool:201 /* Created */]
     : [NSNumber numberWithBool:204 /* No Content */];
 }
@@ -969,7 +1018,7 @@ static NSTimeZone                *gmt      = nil;
     return error;
   }
   
-  return ([newName length] > 0)
+  return [newName isNotEmpty]
     ? [NSNumber numberWithBool:201 /* Created */]
     : [NSNumber numberWithBool:204 /* No Content */];
 }
@@ -1002,6 +1051,7 @@ static NSTimeZone                *gmt      = nil;
     return [_ctx response];
   }
   
+  // TODO: whats that? VERY bad, maybe use -baseURLForContext:?
   baseURL = [NSString stringWithFormat:@"http://%@%@",
                        [[_ctx request] headerForKey:@"host"],
                        [[_ctx request] uri]];
@@ -1087,7 +1137,7 @@ static NSTimeZone                *gmt      = nil;
   }
   
   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"];
   }
@@ -1122,7 +1172,7 @@ static NSTimeZone                *gmt      = nil;
   
   /* 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) {
@@ -1177,7 +1227,7 @@ static NSTimeZone                *gmt      = nil;
   }
   
   subscriptionID = [rq headerForKey:@"subscription-id"];
-  if ([subscriptionID length] == 0) {
+  if (![subscriptionID isNotEmpty]) {
     return [self httpException:400 /* Bad Request */
                 reason:@"missing subscription id !"];
   }
@@ -1186,10 +1236,9 @@ static NSTimeZone                *gmt      = nil;
     [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 */
@@ -1297,7 +1346,7 @@ static NSTimeZone                *gmt      = nil;
   
   rq = [_ctx request];
   depth = [rq headerForKey:@"depth"];
-  if ([depth length] == 0) depth = @"infinity";
+  if (![depth isNotEmpty]) depth = @"infinity";
   
   [self lockParser:davsax];
   {
@@ -1311,7 +1360,7 @@ static NSTimeZone                *gmt      = nil;
   propNames = [propNames autorelease];
   targets   = [targets   autorelease];
   
-  if ([targets count] == 0)
+  if (![targets isNotEmpty])
     return [NSArray array];
   
   /* check query all properties */
@@ -1472,8 +1521,79 @@ static NSTimeZone                *gmt      = nil;
 /* DAV reports */
 
 - (id)doREPORT:(WOContext *)_ctx {
-  return [self httpException:405 /* method not allowed */
-              reason:@"WebDAV reports not yet implemented."];
+  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 */
@@ -1619,7 +1739,7 @@ static NSTimeZone                *gmt      = nil;
   NSMutableString *ms;
   
   ms = [NSMutableString stringWithCapacity:64];
-  [ms appendFormat:@"<0x%08X[%@]:", self,
+  [ms appendFormat:@"<0x%p[%@]:", self,
         NSStringFromClass((Class)*(void**)self)];
   
   if (self->object)