]> err.no Git - sope/blob - sope-appserver/NGObjWeb/WebDAV/SoObjectWebDAVDispatcher.m
improved WebDAV property handling
[sope] / sope-appserver / NGObjWeb / WebDAV / SoObjectWebDAVDispatcher.m
1 /*
2   Copyright (C) 2002-2006 SKYRIX Software AG
3   Copyright (C) 2006      Helge Hess
4
5   This file is part of SOPE.
6
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
10   later version.
11
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.
16
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
20   02111-1307, USA.
21 */
22
23 #include "SoObjectWebDAVDispatcher.h"
24 #include "SoObject.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>
44 #include "common.h"
45
46 @interface WORequest(HackURI)
47 - (void)_hackSetURI:(NSString *)_vuri;
48 @end
49
50 @implementation SoObjectWebDAVDispatcher
51
52 static int      debugOn = -1;
53 static BOOL     debugBulkTarget = NO;
54 static BOOL     disableCrossHostMoveCheck = NO;
55 static NSNumber *yesNum = nil;
56
57 + (void)initialize {
58   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
59   static BOOL didInit = NO;
60   if (didInit) return;
61   didInit = YES;
62   
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];
66   
67   disableCrossHostMoveCheck =
68     [ud boolForKey:@"SoWebDAVDisableCrossHostMoveCheck"];
69 }
70
71 // THREAD
72 static id<NSObject,SaxXMLReader> xmlParser = nil;
73 static SaxDAVHandler             *davsax   = nil;
74 static NSTimeZone                *gmt      = nil;
75
76 - (id)initWithObject:(id)_object {
77   if ((self = [super init])) {
78     self->object = [_object retain];
79   }
80   return self;
81 }
82 - (void)dealloc {
83   [self->object release];
84   [super dealloc];
85 }
86
87 /* parser */
88
89 - (void)lockParser:(id)_sax {
90   [_sax reset];
91   [xmlParser setContentHandler:_sax];
92   [xmlParser setErrorHandler:_sax];
93 }
94 - (void)unlockParser:(id)_sax {
95   [xmlParser setContentHandler:nil];
96   [xmlParser setErrorHandler:nil];
97   [_sax reset];
98 }
99
100 /* common stuff */
101
102 - (NSException *)httpException:(int)_status reason:(NSString *)_reason {
103   NSDictionary *ui;
104
105   ui = [NSDictionary dictionaryWithObjectsAndKeys:
106                        self, @"dispatcher",
107                        [NSNumber numberWithInt:_status], @"http-status",
108                      nil];
109   return [NSException exceptionWithName:
110                         [NSString stringWithFormat:@"HTTP%i", _status]
111                       reason:_reason
112                       userInfo:ui];
113 }
114
115 - (NSString *)baseURLForContext:(WOContext *)_ctx {
116   extern NSString *SoObjectRootURLInContext
117     (WOContext *_ctx, id logobj, BOOL withAppPart);
118   NSString *rootURL;
119   
120   rootURL = SoObjectRootURLInContext(_ctx, self, NO);
121   return [rootURL stringByAppendingString:[[_ctx request] uri]];
122 }
123
124 - (id)primaryCallWebDAVMethod:(NSString *)_name inContext:(WOContext *)_ctx {
125   id method;
126   
127   method = [self->object lookupName:_name inContext:_ctx acquire:NO];
128   if (method == nil) {
129     return [self httpException:501 /* Not Implemented */
130                  reason:@"target object does not support requested operation"];
131   }
132   if ([method isKindOfClass:[NSException class]]) {
133     [self logWithFormat:@"could not lookup method, got exception: %@", method];
134     return method;
135   }
136   
137   [self debugWithFormat:@"  %@ method: %@", _name, method];
138   return [method callOnObject:self->object inContext:_ctx];
139 }
140
141 /* core HTTP methods */
142
143 - (id)_callObjectMethod:(NSString *)_method inContext:(WOContext *)_ctx {
144   /* returns 'nil' if the object had no such method */
145   NSException *e;
146   id methodObject;
147   id result;
148   
149   methodObject =
150     [self->object lookupName:_method inContext:_ctx acquire:NO];
151   if (![methodObject isNotNull])
152     return nil;
153   if ([methodObject isKindOfClass:[NSException class]]) {
154     if ([(NSException *)methodObject httpStatus] == 404 /* Not Found */) {
155       /* not found */
156       return nil;
157     }
158     return methodObject; /* the exception */
159   }
160   if ((e = [self->object validateName:_method inContext:_ctx]) != nil)
161     return e;
162   
163   if ([methodObject respondsToSelector:
164                       @selector(takeValuesFromRequest:inContext:)])
165     [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx];
166   
167   result = [methodObject callOnObject:self->object inContext:_ctx];
168   return (result != nil) ? result : (id)[NSNull null];
169 }
170
171 - (id)doGET:(WOContext *)_ctx {
172   NSException *e;
173   id methodObject;
174   
175   methodObject = [self->object lookupName:@"GET" inContext:_ctx acquire:NO];
176   if (methodObject == nil)
177     methodObject = [self->object lookupDefaultMethod];
178   else {
179     if ((e = [self->object validateName:@"GET" inContext:_ctx]) != nil)
180       return e;
181   }
182   
183   if (methodObject == nil)
184     return self->object;
185   if ([methodObject isKindOfClass:[NSException class]])
186     return methodObject;
187   
188   if ([methodObject respondsToSelector:
189                       @selector(takeValuesFromRequest:inContext:)])
190     [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx];
191   
192   return [methodObject callOnObject:self->object inContext:_ctx];
193 }
194
195 - (id)doPUT:(WOContext *)_ctx {
196   SoSecurityManager *sm;
197   NSException       *e;
198   NSString          *pathInfo;
199   
200   pathInfo = [_ctx pathInfo];
201   [self debugWithFormat:@"doPUT (pathinfo='%@')", pathInfo];
202   
203   /* check permissions */
204   
205   sm = [_ctx soSecurityManager];
206   e  = [sm validatePermission:
207              [pathInfo isNotEmpty]
208              ? SoPerm_AddDocumentsImagesAndFiles
209              : SoPerm_ChangeImagesAndFiles
210            onObject:self->object
211            inContext:_ctx];
212   if (e != nil) return e;
213   
214   if ((e = [self->object validateName:@"PUT" inContext:_ctx]))
215     return e;
216   
217   /* perform */
218   
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 */
224                    reason:
225                      @"invalid WebDAV PUT request, first create all "
226                      @"parent collections !"];
227     }
228   }
229
230   return [self primaryCallWebDAVMethod:@"PUT" inContext:_ctx];
231 }
232
233 - (id)doPOST:(WOContext *)_ctx {
234   NSException *e;
235   
236   if ((e = [self->object validateName:@"POST" inContext:_ctx]))
237     return e;
238   
239   return [self primaryCallWebDAVMethod:@"POST" inContext:_ctx];
240 }
241
242 - (id)doDELETE:(WOContext *)_ctx {
243   SoSecurityManager *sm;
244   NSException       *e;
245   
246   /* check permissions */
247   
248   sm = [_ctx soSecurityManager];
249   e  = [sm validatePermission:SoPerm_DeleteObjects
250            onObject:self->object
251            inContext:_ctx];
252   if (e != nil) 
253     return e;
254   if ((e = [self->object validateName:@"DELETE" inContext:_ctx]) != nil)
255     return e;
256   
257   // TODO: IE WebFolders sent a "Destroy" header together with the
258   //       DELETE request, eg:
259   //       "Destroy: NoUndelete"
260   
261   return [self primaryCallWebDAVMethod:@"DELETE" inContext:_ctx];
262 }
263
264 - (id)doOPTIONS:(WOContext *)_ctx {
265   WOResponse *response;
266   NSArray    *tmp;
267   id         result;
268   
269   /* this checks whether the object provides a specific OPTIONS method */
270   if ((result = [self _callObjectMethod:@"OPTIONS" inContext:_ctx]) != nil)
271     return result;
272   
273   response = [_ctx response];
274   [response setStatus:200 /* OK */];
275   
276   if ((tmp = [self->object davAllowedMethodsInContext:_ctx]) != nil) 
277     [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"allow"];
278   
279   if ([[[_ctx request] clientCapabilities] isWebFolder]) {
280     /*
281        As described over here:
282          http://teyc.editthispage.com/2005/06/02
283        
284        This page also says that: "MS-Auth-Via header is not required to work
285        with Web Folders".
286     */
287     [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"public"];
288   }
289   
290   if ((tmp = [self->object davComplianceClassesInContext:_ctx]) != nil) 
291     [response setHeader:[tmp componentsJoinedByString:@", "] forKey:@"dav"];
292   
293   return response;
294 }
295
296 - (id)doHEAD:(WOContext *)_ctx {
297   return [self doGET:_ctx];
298 }
299
300 /* core WebDAV methods */
301
302 - (id)doMKCOL:(WOContext *)_ctx {
303   SoSecurityManager *sm;
304   NSException       *e;
305   NSString          *pathInfo;
306   
307   pathInfo = [_ctx pathInfo];
308   if (![pathInfo isNotEmpty]) {
309     /* MKCOL target already exists ... */
310     WOResponse *r;
311
312     [self logWithFormat:@"MKCOL target exists !"];
313     
314     r = [_ctx response];
315     [r setStatus:405 /* method not allowed */];
316     [r appendContentString:@"collection already exists !"];
317     return r;
318   }
319   
320   /* check permissions */
321   
322   sm = [_ctx soSecurityManager];
323   e  = [sm validatePermission:SoPerm_AddFolders 
324            onObject:self->object
325            inContext:_ctx];
326   if (e != nil) return e;
327
328   /* check whether all the parent collections are available */
329   if ([pathInfo rangeOfString:@"/"].length > 0) {
330     return [self httpException:409 /* Conflict */
331                  reason:
332                    @"invalid WebDAV MKCOL request, first create all "
333                    @"parent collections !"];
334   }
335   
336   /* check whether the object supports creating collections */
337
338   if (![self->object respondsToSelector:
339               @selector(davCreateCollection:inContext:)]) {
340     /* Note: this should never happen, as this is implemented on NSObject */
341     
342     [self logWithFormat:@"MKCOL: object '%@' path-info '%@'", 
343             self->object, pathInfo];
344     return [self httpException:405 /* not allowed */
345                  reason:
346                    @"this object cannot create a new collection with MKCOL"];
347   }
348   
349   if ((e = [self->object davCreateCollection:pathInfo inContext:_ctx])) {
350     [self debugWithFormat:@"creation of collection '%@' failed: %@",
351             pathInfo, e];
352     return e;
353   }
354   
355   [self debugWithFormat:@"created collection."];
356   return [NSNumber numberWithBool:YES];
357 }
358
359 - (NSString *)scopeForDepth:(NSString *)_depth inContext:(WOContext *)_ctx {
360   NSString *scope;
361   
362   if ([_depth hasPrefix:@"0"])
363     scope = @"self";
364   else if ([_depth hasPrefix:@"1,noroot"])
365     scope = @"flat";
366   else if ([_depth hasPrefix:@"1"]) {
367     NSString *ua;
368     
369     scope = @"flat+self";
370     
371     /* some special handling for IE ... */
372     if ((ua = [[[_ctx request] clientCapabilities] userAgentType])) {
373       if ([ua isEqualToString:@"Evolution"])
374         scope = @"flat";
375       else if ( [ua isEqualToString:@"WebFolder"])
376         scope = @"flat";
377     }
378   }
379   else if ([_depth hasPrefix:@"infinity"])
380     scope = @"deep";
381   else
382     scope = @"deep";
383
384   return scope;
385 }
386
387 - (NSMutableDictionary *)hintsWithScope:(NSString *)_scope
388   propNames:(NSArray *)_propNames
389   findAll:(BOOL)_findAll
390   namesOnly:(BOOL)_namesOnly
391 {
392   NSMutableDictionary *hints;
393   
394   hints = [NSMutableDictionary dictionaryWithCapacity:4];
395
396   if (_scope)
397     [hints setObject:_scope forKey:@"scope"];
398   if (_propNames)
399     [hints setObject:_propNames forKey:@"attributes"];
400   // else if (_findAll) ; /* empty attributes */
401   
402   if (_namesOnly)
403     [hints setObject:[NSNumber numberWithBool:YES] forKey:@"namesOnly"];
404   return hints;
405 }
406
407 - (id)doPROPFIND:(WOContext *)_ctx {
408   SoSecurityManager    *sm;
409   NSException          *e;
410   EOFetchSpecification *fs;
411   WORequest *rq;
412   NSString  *uri;
413   NSString  *depth; /* 0, 1, 1,noroot or infinity */
414   NSArray   *propNames, *rtargets;
415   BOOL      findAll;
416   BOOL      findNames;
417   id        result;
418   NSRange   r;
419   
420   /* check permissions */
421   
422   sm = [_ctx soSecurityManager];
423   e  = [sm validatePermission:SoPerm_AccessContentsInformation 
424            onObject:self->object
425            inContext:_ctx];
426   if (e != nil) return e;
427   
428   /* perform search */
429   
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"];
434   }
435   
436   rq    = [_ctx request];
437   depth = [rq headerForKey:@"depth"];
438   uri   = [rq uri];
439   
440   if (![depth isNotEmpty]) depth = @"infinity";
441   
442   if ([[rq content] isNotEmpty]) {
443     [self lockParser:davsax];
444     {
445       [xmlParser parseFromSource:[rq content]];
446       propNames = [[davsax propFindQueriedNames] copy];
447       findAll   = [davsax  propFindAllProperties];
448       findNames = [davsax  propFindPropertyNames];
449     }
450     [self unlockParser:davsax];
451     propNames = [propNames autorelease];
452   }
453   else {
454     /*
455       8.1 PROPFIND
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
458        all properties."
459       TODO: means, an empty request is to be handled as <allprop/>?
460     */
461     propNames = nil;
462     findAll   = YES;
463     findNames = NO;
464   }
465   
466   if (findAll) {
467     /* 
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
471        properties).
472     */
473     [[_ctx request] setHeader:@"true" forKey:@"brief"];
474   }
475   
476   /* check query all properties */
477   
478   if (propNames == nil)
479     propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx];
480   
481   /* check for a ZideStore ranges query (a BPROPFIND "emulation") */
482   
483   if (debugOn) [self logWithFormat:@"request uri: %@", uri];
484   r = [uri rangeOfString:@"_range"];
485   if (r.length > 0) { /* ZideStore range query */
486     NSString *s;
487     NSArray  *ids;
488     
489     if (debugOn)
490       [self logWithFormat:@"  detected a ZideStore range query: '%@'", uri];
491     
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];
495     
496     ids = [s isNotEmpty]
497       ? [s componentsSeparatedByString:@"_"]
498       : (NSArray *)[NSArray array];
499     
500     // TODO: should use -stringByUnescapingURL on IDs (not required for ints)
501     
502     rtargets = ids;
503     if (debugOn) 
504       [self logWithFormat:@"  IDs: %@", [ids componentsJoinedByString:@","]];
505     
506     /* patch URI, could have side-effects ? */
507     [self logWithFormat:
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]];
511   }
512   else
513     rtargets = nil;
514   
515   /* build the fetch-spec */
516   {
517     NSMutableDictionary *hints;
518     
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"];
523     
524     fs = [EOFetchSpecification alloc];
525     fs = [fs initWithEntityName:[self baseURLForContext:_ctx]
526              qualifier:nil
527              sortOrderings:nil
528              usesDistinct:NO isDeep:NO hints:hints];
529     fs = [fs autorelease];
530
531     if (debugOn) [self logWithFormat:@"  propfind fetchspec: %@", fs];
532   }
533   
534   [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
535   
536   /* translate fetchspec if necessary */
537   {
538     NSDictionary *map;
539     
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];
545     }
546   }
547   
548   /* perform */
549   
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)"];
553   }
554   
555   if (debugOn) [self logWithFormat:@"  propfind result: %@", result];
556   
557   return result;
558 }
559
560 - (BOOL)allowDeletePropertiesOnNewObjectInContext:(WOContext *)_ctx {
561   NSString *ua;
562   
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 */
566     return YES;
567   }
568   if ([ua hasPrefix:@"CFNetwork"]) {
569     /* iSync trying to create a record ... */
570     return YES;
571   }
572   
573   [self logWithFormat:@"do not allow delete properties on new object for: %@",
574           ua];
575   return NO;
576 }
577
578 - (id)doPROPPATCH:(WOContext *)_ctx {
579   SoSecurityManager *sm;
580   NSException       *e;
581   NSMutableArray    *resProps;
582   NSArray           *delProps;
583   NSDictionary      *setProps;
584   NSString          *pathInfo;
585   
586   pathInfo = [_ctx pathInfo];
587   
588   /* check permissions */
589   
590   sm = [_ctx soSecurityManager];
591   e  = [sm validatePermission:[pathInfo isNotEmpty]
592              ? SoPerm_AddDocumentsImagesAndFiles
593              : SoPerm_ChangeImagesAndFiles
594            onObject:self->object
595            inContext:_ctx];
596   if (e != nil) return e;
597   
598   /* check for conflicts */
599
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 */
604                    reason:
605                      @"invalid WebDAV PROPPATCH request, first create all "
606                      @"parent collections !"];
607     }
608   }
609   
610   /* check whether the object supports patching */
611
612   if ([pathInfo isNotEmpty]) {
613     if (![self->object respondsToSelector:
614                 @selector(davCreateObject:properties:inContext:)]) {
615       [self debugWithFormat:@"cannot create new object via DAV on %@",
616               self->object];
617       return [self httpException:405 /* not allowed */
618                    reason:
619                      @"this object cannot create a new object with PROPPATCH"];
620     }
621   }
622   else {
623     if (![self->object respondsToSelector:
624             @selector(davSetProperties:removePropertiesNamed:inContext:)]) {
625       [self debugWithFormat:@"cannot change object props via DAV on %@",
626               self->object];
627       return [self httpException:405 /* not allowed */
628                    reason:@"this object cannot PROPPATCH the attributes"];
629     }
630   }
631   
632   /* parse request */
633   
634   [self lockParser:davsax];
635   {
636     [xmlParser parseFromSource:[[_ctx request] content]];
637     delProps = [[davsax propPatchPropertyNamesToRemove] copy];
638     setProps = [[davsax propPatchValues] copy];
639   }
640   [self unlockParser:davsax];
641   delProps = [delProps autorelease];
642   setProps = [setProps autorelease];
643   
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 !"];
648   }
649   
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 '%@': %@",
655                 pathInfo, delProps];
656         return [self httpException:400 /* bad request */
657                      reason:@"cannot delete properties of a new object"];
658       }
659       [self debugWithFormat:@"deleting properties on a new object: %@ ...",
660               delProps];
661     }
662   }
663   
664   resProps = [NSMutableArray arrayWithCapacity:16];
665   if (delProps) [resProps addObjectsFromArray:delProps];
666   if (setProps) [resProps addObjectsFromArray:[setProps allKeys]];
667   
668   /* map attributes */
669   {
670     NSDictionary *map;
671     
672     if ((map = [self->object davAttributeMapInContext:_ctx])) {
673       unsigned count;
674       
675       [_ctx setObject:map forKey:@"DAVPropertyMap"];
676       
677       if ((count = [delProps count]) > 0) {
678         NSMutableArray *mappedDelProps;
679         unsigned i;
680         
681         mappedDelProps = [NSMutableArray arrayWithCapacity:(count + 1)];
682         for (i = 0; i < count; i++) {
683           NSString *k, *tk;
684           
685           k  = [delProps objectAtIndex:i];
686           tk = [map valueForKey:k];
687           
688           [mappedDelProps addObject:(tk ? tk : k)];
689         }
690         delProps = mappedDelProps;
691       }
692       if ((count = [setProps count]) > 0) {
693         NSMutableDictionary *mappedSetProps;
694         NSEnumerator *keys;
695         NSString *k;
696         
697         mappedSetProps = [NSMutableDictionary dictionaryWithCapacity:count];
698         keys = [setProps keyEnumerator];
699         while ((k = [keys nextObject])) {
700           NSString *tk;
701           
702           tk = [map valueForKey:k];
703           [mappedSetProps setObject:[setProps objectForKey:k]
704                           forKey:(tk ? tk : k)];
705         }
706         setProps = mappedSetProps;
707       }
708     }
709   }
710   
711   if (debugOn) {
712     [self debugWithFormat:@"PROPPATCH '%@': delete=%@, set=%@",
713             pathInfo, delProps, setProps];
714   }
715   
716   if (![pathInfo isNotEmpty]) {
717     /* edit an object */
718     NSException *e;
719     
720     e = [self->object 
721              davSetProperties:setProps
722              removePropertiesNamed:delProps
723              inContext:_ctx];
724     if (e != nil) return e;
725   }
726   else {
727     /* create an object */
728     id newChild;
729     
730     newChild = [self->object 
731                     davCreateObject:pathInfo
732                     properties:setProps
733                     inContext:_ctx];
734     if ([newChild isKindOfClass:[NSException class]]) 
735       return newChild;
736     
737     [self debugWithFormat:@"created: %@", newChild];
738   }
739   
740   /* generate response */
741   return resProps;
742 }
743
744 - (id)doLOCK:(WOContext *)_ctx {
745   SoSecurityManager *sm;
746   NSException       *e;
747   SoDAVLockManager *lockManager;
748   WORequest  *rq;
749   WOResponse *r;
750   NSString   *ifValue, *lockDepth;
751   id token;
752   
753   /* check permissions */
754   
755   sm = [_ctx soSecurityManager];
756   e  = [sm validatePermission:SoPerm_WebDAVLockItems
757            onObject:self->object
758            inContext:_ctx];
759   if (e != nil) return e;
760   
761   /* check lock manager */
762   
763   if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) {
764     return [self httpException:405 /* method not allowed */
765                  reason:@"target object does not support locking !"];
766   }
767   
768   rq = [_ctx request];
769   r  = [_ctx response];
770   
771   lockDepth = [rq headerForKey:@"depth"];
772   ifValue   = [rq headerForKey:@"if"];
773   
774   if (lockDepth != nil && ![lockDepth isEqualToString:@"0"]) {
775     [self warnWithFormat:@"'depth' locking not supported yet (depth=%@)!", 
776             lockDepth];
777   }
778   if (ifValue != nil) {
779     [self warnWithFormat:@"'if' locking not supported yet, if: '%@'", ifValue];
780   }
781   
782   /*
783     TODO: parse lockinfo:
784       <?xml version="1.0" encoding="UTF-8" ?>
785       <lockinfo xmlns="DAV:">
786         <locktype><write/></locktype>
787         <lockscope><exclusive/></lockscope>
788         <owner>helge</owner>
789       </lockinfo>
790     
791     Currently we assume exclusive/write ... (also see SoWebDAVRenderer)
792   */
793   
794   /* Sample timeout: Second-180 */
795   
796   token = [lockManager lockURI:[rq uri]
797                        timeout:[rq headerForKey:@"timeout"]
798                        scope:@"exclusive" // TODO
799                        type:@"write"      // TODO
800                        owner:nil];        // TODO
801   if (token == nil) {
802     /* already locked */
803     return [self httpException:423 /* locked */
804                  reason:@"object locked, lock manager did not provide token."];
805   }
806   
807   [self debugWithFormat:@"locked: %@ (token %@)", [[_ctx request] uri], token];
808   return token;
809 }
810
811 - (id)doUNLOCK:(WOContext *)_ctx {
812   SoSecurityManager *sm;
813   NSException       *e;
814   SoDAVLockManager  *lockManager;
815   NSString *token;
816   
817   /* check permissions */
818   
819   sm = [_ctx soSecurityManager];
820   e  = [sm validatePermission:SoPerm_WebDAVUnlockItems
821            onObject:self->object
822            inContext:_ctx];
823   if (e != nil) return e;
824   
825   /* check lock manager */
826   
827   if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) {
828     return [self httpException:405 /* method not allowed */
829                  reason:@"target object does not support locking."];
830   }
831   
832   token = [[_ctx request] headerForKey:@"lock-token"];
833   
834   [lockManager unlockURI:[[_ctx request] uri] token:token];
835   
836   [self debugWithFormat:
837           @"unlocked: %@ (token %@)", [[_ctx request] uri], token];
838   
839   [[_ctx response] setStatus:204 /* fake ok */];
840   return [_ctx response];
841 }
842
843 - (NSException *)extractDestinationPath:(NSArray **)path_
844   fromContext:(WOContext *)_ctx
845 {
846   NSString *absDestURL;
847   NSURL    *destURL, *srvURL;
848   
849   if (path_) *path_ = nil;
850   
851   /* TODO: check proper permission prior attempting a move */
852   
853   absDestURL = [[_ctx request] headerForKey:@"destination"];
854   if (![absDestURL isNotEmpty]) {
855     return [self httpException:400 /* Bad Request */
856                  reason:
857                    @"the destination WebDAV header was missing "
858                    @"for the MOVE/COPY operation"];
859   }
860   if ((destURL = [NSURL URLWithString:absDestURL]) == nil) {
861     [self logWithFormat:@"MOVE: got invalid destination URL: '%@'", 
862             absDestURL];
863     return [self httpException:400 /* Bad Request */
864                  reason:@"the MOVE/COPY destination is not a valid URL!"];
865   }
866   
867   srvURL = [_ctx serverURL];
868   
869   [self debugWithFormat:@"move/copy:\n  to:    %@ (%@)\n  server: %@)", 
870           [destURL absoluteString], absDestURL,
871           [srvURL absoluteString]];
872   
873   /* check whether URL is on the same server ... */
874   if (![[srvURL host] isEqualToString:[destURL host]] ||
875       ![[srvURL port] isEqual:[destURL port]]) {
876     /* 
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 ...
879
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
882              disable this check.
883     */
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."];
889     }
890   }
891   
892   if (path_ != NULL) {
893     NSMutableArray *ma;
894     unsigned i;
895     
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];
904     
905     /* unescape path components */
906     for (i = 0; i < [ma count]; i++) {
907       NSString *s, *ns;
908       
909       s = [ma objectAtIndex:i];
910       ns = [s stringByUnescapingURL];
911       if (ns != s)
912         [ma replaceObjectAtIndex:i withObject:ns];
913     }
914     
915     *path_ = [ma copy];
916     [ma release];
917   }
918   return nil;
919 }
920 - (NSException *)lookupDestinationObject:(id *)target_ 
921   andNewName:(NSString **)name_
922   inContext:(WOContext *)_ctx
923 {
924   NSException *error;
925   NSArray     *targetPath;
926   id          root;
927   
928   if ((error = [self extractDestinationPath:&targetPath fromContext:_ctx]))
929     return error;
930
931   if ((root = [_ctx application]) == nil)
932     root = [WOApplication application];
933   if (root == nil) {
934     return [self httpException:500 /* internal server error */
935                  reason:@"did not find SOPE root object"];
936   }
937   
938   /* TODO: we should probably use a subcontext?! */
939   [_ctx setObject:yesNum forKey:@"isDestinationPathLookup"];
940   *target_ = [root traversePathArray:targetPath
941                    inContext:_ctx
942                    error:&error
943                    acquire:NO];
944   if ([*target_ isKindOfClass:[NSException class]])
945     error = *target_;
946   if (error != nil) {
947     [self logWithFormat:@"could not resolve destination object (%@): %@",
948             [targetPath componentsJoinedByString:@" => "],
949             error];
950     return error;
951   }
952   
953   if (name_ != NULL) *name_ = [[[_ctx pathInfo] copy] autorelease];
954   
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"];
959   }
960   
961   [self debugWithFormat:@"SOURCE: %@", self->object];
962   [self debugWithFormat:@"TARGET: %@ (pathinfo %@)", 
963         *target_, [_ctx pathInfo]];
964   return nil;
965 }
966
967 - (id)doCOPY:(WOContext *)_ctx {
968   NSException *error;
969   NSString    *newName;
970   id          targetObject;
971   
972   /* TODO: check proper permission prior attempting a copy */
973   
974   error = [self lookupDestinationObject:&targetObject andNewName:&newName
975                 inContext:_ctx];
976   if (error) return error;
977   
978   error = [self->object 
979                davCopyToTargetObject:targetObject newName:newName
980                inContext:_ctx];
981   if (error) {
982     [self debugWithFormat:@"WebDAV COPY operation failed: %@", error];
983     return error;
984   }
985   
986   return [newName isNotEmpty]
987     ? [NSNumber numberWithBool:201 /* Created */]
988     : [NSNumber numberWithBool:204 /* No Content */];
989 }
990
991 - (id)doMOVE:(WOContext *)_ctx {
992   NSException *error;
993   NSString    *newName;
994   id          targetObject;
995   
996   /* TODO: check proper permission prior attempting a move */
997   
998   error = [self lookupDestinationObject:&targetObject andNewName:&newName
999                 inContext:_ctx];
1000   if (error) return error;
1001   
1002   /*
1003     Note: more relevant headers:
1004       overwrite: T|F      (overwrite target) [rc: 201 vs 204!]
1005       depth:     infinity
1006       and locking tokens of course ...
1007   */
1008   
1009   // TODO: should we check in this place for some constraints,
1010   //       eg moving a collection to a non-collection or something
1011   //       like that?
1012   
1013   error = [self->object 
1014                davMoveToTargetObject:targetObject newName:newName
1015                inContext:_ctx];
1016   if (error) {
1017     [self debugWithFormat:@"WebDAV MOVE operation failed: %@", error];
1018     return error;
1019   }
1020   
1021   return [newName isNotEmpty]
1022     ? [NSNumber numberWithBool:201 /* Created */]
1023     : [NSNumber numberWithBool:204 /* No Content */];
1024 }
1025
1026 /* WebDAV search methods */
1027
1028 - (id)doSEARCH:(WOContext *)_ctx {
1029   SoSecurityManager    *sm;
1030   NSException          *e;
1031   EOFetchSpecification *fs;
1032   NSString *baseURL;
1033   id       result;
1034   NSString *range;
1035   
1036   /* check permissions */
1037   
1038   sm = [_ctx soSecurityManager];
1039   e  = [sm validatePermission:SoPerm_AccessContentsInformation 
1040            onObject:self->object
1041            inContext:_ctx];
1042   if (e != nil) return e;
1043
1044   /* perform search */
1045   
1046   if (![self->object 
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];
1052   }
1053   
1054   // TODO: whats that? VERY bad, maybe use -baseURLForContext:?
1055   baseURL = [NSString stringWithFormat:@"http://%@%@",
1056                         [[_ctx request] headerForKey:@"host"],
1057                         [[_ctx request] uri]];
1058   
1059   [self lockParser:davsax];
1060   {
1061     [xmlParser parseFromSource:[[_ctx request] content]];
1062     fs = [[davsax searchFetchSpecification] retain];
1063   }
1064   [self unlockParser:davsax];
1065   
1066   fs = [fs autorelease];
1067   if (fs == nil) {
1068     [[_ctx response] setStatus:400 /* Bad Request */];
1069     [[_ctx response] appendContentString:
1070                        @"could not process SEARCH query specification"];
1071     return [_ctx response];
1072   }
1073
1074   /* range */
1075   if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
1076     /* TODO: parse range header and add to fetch-specification */
1077     NSRange r;
1078     
1079     r = [range rangeOfString:@"rows="];
1080     if (r.length > 0) {
1081       range = [range substringFromIndex:(r.location + r.length)];
1082       [self debugWithFormat:
1083               @"Note: got a row range header (ignored): '%@'", range];
1084     }
1085     else
1086       [self logWithFormat:@"Note: got a range header (ignored): '%@'", range];
1087   }
1088   
1089   /* override entity name ... (FROM xxx isn't yet parsed correctly) */
1090   [fs setEntityName:baseURL];
1091   
1092   [self debugWithFormat:@"SEARCH: %@", fs];
1093   
1094   [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
1095
1096   /* translate fetchspec if necessary */
1097   {
1098     NSDictionary *map;
1099     
1100     if ((map = [self->object davAttributeMapInContext:_ctx])) {
1101       [_ctx setObject:map forKey:@"DAVPropertyMap"];
1102       fs = [fs fetchSpecificationByApplyingKeyMap:map];
1103       [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"];
1104     }
1105   }
1106   
1107   /* perform call */
1108   
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)"];
1112   }
1113   
1114   return result;
1115 }
1116
1117 /* Exchange WebDAV methods */
1118
1119 - (id)doNOTIFY:(WOContext *)_ctx {
1120   return [self httpException:403 reason:@"NOTIFY not yet implemented"];
1121 }
1122
1123 - (id)doPOLL:(WOContext *)_ctx {
1124   SoSubscriptionManager *sm;
1125   WORequest  *rq;
1126   NSString   *subscriptionID;
1127   NSArray    *ids;
1128   NSURL      *url;
1129   
1130   rq  = [_ctx request];
1131   sm  = [SoSubscriptionManager sharedSubscriptionManager];
1132   url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1133   
1134   if (url == nil) {
1135     return [self httpException:500
1136                  reason:@"could not calculate URL of WebDAV object !"];
1137   }
1138   
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"];
1143   }
1144   
1145   ids = [subscriptionID componentsSeparatedByString:@","];
1146   
1147   return [sm pollSubscriptions:ids onURL:url];
1148 }
1149
1150 - (id)doSUBSCRIBE:(WOContext *)_ctx {
1151   SoSubscriptionManager *sm;
1152   WORequest  *rq;
1153   WOResponse *r;
1154   NSURL    *url;
1155   id       callback;
1156   NSString *notificationType;
1157   NSString *notificationDelay;
1158   NSString *lifetime;
1159   NSString *subscriptionID;
1160   
1161   rq  = [_ctx request];
1162   r   = [_ctx response];
1163   sm  = [SoSubscriptionManager sharedSubscriptionManager];
1164   url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1165   
1166   if (url == nil) {
1167     return [self httpException:500
1168                  reason:@"could not calculate URL of WebDAV object !"];
1169   }
1170   
1171   subscriptionID = [rq headerForKey:@"subscription-id"];
1172   
1173   /* first check, whether it's an existing subscription to be renewed */
1174   
1175   if ([subscriptionID isNotEmpty]) {
1176     NSString *newId;
1177     
1178     if ((newId = [sm renewSubscription:subscriptionID onURL:url]) == nil) {
1179       return [self httpException:412 /* precondition failed */
1180                    reason:@"did not find provided subscription ID !"];
1181     }
1182     return newId;
1183   }
1184   
1185   if ((callback = [rq headerForKey:@"call-back"])) {
1186     NSURL *url;
1187     
1188     if ((url = [NSURL URLWithString:[callback stringValue]]) == nil) {
1189       [self errorWithFormat:@"could not parse callback URL '%@'", 
1190               callback];
1191       return [self httpException:400 /* Bad Request */
1192                    reason:@"missing valid callback URL !"];
1193     }
1194     else
1195       callback = url;
1196   }
1197   
1198   /* TODO: add sanity checking of notification-type as described in docs */
1199   /* TODO: check depth */
1200   
1201   notificationDelay = [rq headerForKey:@"notification-delay"];
1202   notificationType  = [rq headerForKey:@"notification-type"];
1203   lifetime          = [rq headerForKey:@"subscription-lifetime"];
1204   
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;
1211 }
1212 - (id)doUNSUBSCRIBE:(WOContext *)_ctx {
1213   SoSubscriptionManager *sm;
1214   WORequest  *rq;
1215   WOResponse *r;
1216   NSString *subscriptionID;
1217   NSURL    *url;
1218   
1219   rq  = [_ctx request];
1220   r   = [_ctx response];
1221   sm  = [SoSubscriptionManager sharedSubscriptionManager];
1222   url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1223   
1224   if (url == nil) {
1225     return [self httpException:500
1226                  reason:@"could not calculate URL of WebDAV object !"];
1227   }
1228   
1229   subscriptionID = [rq headerForKey:@"subscription-id"];
1230   if (![subscriptionID isNotEmpty]) {
1231     return [self httpException:400 /* Bad Request */
1232                  reason:@"missing subscription id !"];
1233   }
1234   
1235   if ([sm unsubscribeID:subscriptionID onURL:url]) {
1236     [r setStatus:200];
1237     return r;
1238   }
1239
1240   return [self httpException:400 /* Bad Request */
1241                reason:@"unsubscribe failed (invalid or old id ?)"];
1242 }
1243
1244 /* Exchange bulk methods */
1245
1246 - (NSArray *)urlPartsForTargets:(NSArray *)_targets basePath:(NSString *)_base{
1247   /*
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).
1251   */
1252   NSMutableArray *ma;
1253   unsigned i, count;
1254   
1255   if ((count = [_targets count]) == 0)
1256     return [NSArray array];
1257   
1258   ma = [NSMutableArray arrayWithCapacity:count];
1259   for (i = 0; i < count; i++) {
1260     NSString *target;
1261     
1262     target = [_targets objectAtIndex:i];
1263     if (debugBulkTarget)
1264       [self logWithFormat:@"  MORPH target '%@'", target];
1265     
1266     /* extract the path from full URLs */
1267     if ([target isAbsoluteURL]) {
1268       NSURL *url;
1269       
1270       /* fix an Evolution bug, uses the 'unsafe' "@" in the URL ! */
1271       if ([target rangeOfString:@"@"].length > 0) {
1272         target = [target stringByReplacingString:@"@"
1273                          withString:@"%40"];
1274       }
1275       
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];
1280       }
1281       else {
1282         [self errorWithFormat:@"could not parse BPROPFIND target '%@' !",
1283                 target];
1284       }
1285     }
1286     
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];
1292     }
1293     
1294     /* add the target */
1295     target = [target stringByUnescapingURL];
1296     if (debugBulkTarget) [self logWithFormat:@"  ADD target '%@'", target];
1297     [ma addObject:target];
1298   }
1299   return ma;
1300 }
1301
1302 - (id)doBPROPFIND:(WOContext *)_ctx {
1303   /*
1304     TODO: could optimize a BPROPFIND on a single target to use PROPFIND
1305     
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.
1312     
1313     Important: the URI object *must* support the "bulkTargetKeys" fetch hint,
1314     otherwise the operation will run on the object itself.
1315     
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 ...)
1319   */
1320   SoSecurityManager    *sm;
1321   NSException          *e;
1322   EOFetchSpecification *fs;
1323   WORequest *rq;
1324   NSString  *depth; /* 0, 1, 1,noroot or infinity */
1325   NSArray   *propNames;
1326   NSArray   *targets, *rtargets;
1327   BOOL      findAll;
1328   BOOL      findNames;
1329   id        result;
1330   NSDictionary *map;
1331   
1332   /* check permissions */
1333   
1334   sm = [_ctx soSecurityManager];
1335   e  = [sm validatePermission:SoPerm_AccessContentsInformation 
1336            onObject:self->object
1337            inContext:_ctx];
1338   if (e != nil) return e;
1339
1340   /* perform search */
1341   
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"];
1345   }
1346   
1347   rq = [_ctx request];
1348   depth = [rq headerForKey:@"depth"];
1349   if (![depth isNotEmpty]) depth = @"infinity";
1350   
1351   [self lockParser:davsax];
1352   {
1353     [xmlParser parseFromSource:[rq content]];
1354     propNames = [[davsax propFindQueriedNames] copy];
1355     findAll   = [davsax  propFindAllProperties];
1356     findNames = [davsax  propFindPropertyNames];
1357     targets   = [[davsax  bpropFindTargets] copy];
1358   }
1359   [self unlockParser:davsax];
1360   propNames = [propNames autorelease];
1361   targets   = [targets   autorelease];
1362   
1363   if (![targets isNotEmpty])
1364     return [NSArray array];
1365   
1366   /* check query all properties */
1367   
1368   if (propNames == nil)
1369     propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx];
1370   
1371   /* morph targets */
1372   
1373   rtargets = [self urlPartsForTargets:targets
1374                    basePath:[[rq uri] stringByUnescapingURL]];
1375   
1376   [self debugWithFormat:@"BPROPFIND targets: %@", rtargets];
1377   
1378   /* build the fetch-spec */
1379   {
1380     NSMutableDictionary *hints;
1381     
1382     hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx]
1383                   propNames:propNames findAll:findAll namesOnly:findNames];
1384     [hints setObject:rtargets forKey:@"bulkTargetKeys"];
1385     
1386     fs = [EOFetchSpecification alloc];
1387     fs = [fs initWithEntityName:[self baseURLForContext:_ctx]
1388              qualifier:nil
1389              sortOrderings:nil
1390              usesDistinct:NO isDeep:NO hints:hints];
1391     fs = [fs autorelease];
1392   }
1393   
1394   [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
1395   
1396   /* 
1397      translate fetchspec if necessary - we currently cannot allow a map
1398      for each target, so we use the map of the queried target.
1399   */
1400   if ((map = [self->object davAttributeMapInContext:_ctx])) {
1401     [_ctx setObject:map forKey:@"DAVPropertyMap"];
1402     fs = [fs fetchSpecificationByApplyingKeyMap:map];
1403     [_ctx setObject:fs  forKey:@"DAVMappedFetchSpecification"];
1404   }
1405
1406   /* perform */
1407   
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)"];
1411   }
1412   
1413   return result;
1414 #if 0  
1415   /* now, for each BPROPFIND target ... */
1416   {
1417     NSEnumerator *e;
1418     NSString *targetURL;
1419     
1420     result = [NSMutableArray arrayWithCapacity:32];
1421     
1422     e = [targets objectEnumerator];
1423     while ((targetURL = [e nextObject])) {
1424       NSAutoreleasePool *pool;
1425       WOContext   *localContext;
1426       WORequest   *localRequest;
1427       NSException *e;
1428       id targetObject;
1429       id targetResult;
1430       
1431       pool = [[NSAutoreleasePool alloc] init];
1432       
1433       /* setup the "subrequest" */
1434       
1435       if ([targetURL isAbsoluteURL]) {
1436         NSURL *url;
1437         
1438         if ((url = [NSURL URLWithString:targetURL]))
1439           targetURL = [url path];
1440         else {
1441           [self errorWithFormat:@"could not parse target-url '%@'", targetURL];
1442         }
1443       }
1444       
1445       localRequest = [[WORequest alloc] initWithMethod:@"PROPFIND"
1446                                         uri:targetURL
1447                                         httpVersion:[rq httpVersion]
1448                                         headers:[rq headers]
1449                                         content:nil
1450                                         userInfo:nil];
1451       localContext = 
1452         [[[WOContext alloc] initWithRequest:localRequest] autorelease];
1453       [localRequest autorelease];
1454       
1455       /* resetup fetchspec */
1456       [fs setEntityName:targetURL];
1457       
1458       /* traverse URL */
1459       
1460       targetObject = [_ctx traversalRoot];
1461       targetObject = [targetObject traversePathArray:
1462                                      [localRequest requestHandlerPathArray]
1463                                    inContext:localContext
1464                                    error:&e
1465                                    acquire:NO];
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];
1473         targetResult = e;
1474       }
1475       else {
1476         /* perform query */
1477         
1478         targetResult = [targetObject performWebDAVQuery:fs 
1479                                               inContext:localContext];
1480         if (targetResult == nil) {
1481           targetResult = 
1482           [self httpException:500 /* Server Error */
1483                 reason:@"could not perform query (object returned nil)"];
1484         }
1485       }
1486       
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];
1492
1493       [pool release];
1494     }
1495   }
1496   
1497   /* perform */
1498   
1499   if (result) return result;
1500 #endif
1501 }
1502
1503 - (id)doBCOPY:(WOContext *)_ctx {
1504   return [self httpException:403 /* forbidden */
1505                reason:@"BCOPY not yet implemented."];
1506 }
1507 - (id)doBDELETE:(WOContext *)_ctx {
1508   return [self httpException:403 /* forbidden */
1509                reason:@"BDELETE not yet implemented."];
1510 }
1511 - (id)doBMOVE:(WOContext *)_ctx {
1512   return [self httpException:403 /* forbidden */
1513                reason:@"WebDAV operation not yet implemented."];
1514 }
1515
1516 - (id)doBPROPPATCH:(WOContext *)_ctx {
1517   return [self httpException:403 /* forbidden */
1518                reason:@"WebDAV operation not yet implemented."];
1519 }
1520
1521 /* DAV reports */
1522
1523 - (id)doREPORT:(WOContext *)_ctx {
1524   id<DOMDocument> domDocument;
1525   WORequest *rq;
1526   NSString  *mname;
1527   id method, resultObject;
1528   
1529   rq = [_ctx request];
1530   
1531   /* ensure XML */
1532
1533   if (![[rq headerForKey:@"content-type"] hasPrefix:@"text/xml"]) {
1534     return [self httpException:400 /* invalid request */
1535                  reason:@"XML entity expected for WebDAV REPORT."];
1536   }
1537
1538   /* retrieve XML */
1539
1540   if ((domDocument = [rq contentAsDOMDocument]) == nil) {
1541     return [self httpException:400 /* invalid request */
1542                  reason:@"Could not parse XML of WebDAV REPORT."];
1543   }
1544   
1545   /* first try to lookup method with fully qualified name */
1546   
1547   mname  = [NSString stringWithFormat:@"{%@}%@",
1548                        [[domDocument documentElement] namespaceURI],
1549                        [[domDocument documentElement] localName]];
1550   method = [self->object lookupName:mname inContext:_ctx acquire:NO];
1551   
1552   if (method == nil || [method isKindOfClass:[NSException class]]) {
1553     /* then try to lookup by simplified name */
1554     id m2;
1555     
1556     m2 = [self->object lookupName:[[domDocument documentElement] localName]
1557                        inContext:_ctx acquire:NO];
1558     if (m2 == nil)
1559       ; /* failed */
1560     else if ([m2 isKindOfClass:[NSException class]]) {
1561       if (method == nil)
1562         method = m2; /* use the second exceptions */
1563     }
1564     else {
1565       method = m2;
1566       mname  = [[domDocument documentElement] localName];
1567     }
1568   }
1569   
1570   // TODO: what I would really like to have here is a pluggable dispatcher
1571   //       mechanism which translates the report payload into a customized
1572   //       method call.
1573   
1574   /* check for lookup errors */
1575   
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"];
1580   }
1581   else if ([method isKindOfClass:[NSException class]]) {
1582     [self logWithFormat:@"failed to lookup the REPORT: %@", method];
1583     return method;
1584   }
1585   else if (![method isCallable]) {
1586     [self warnWithFormat:
1587             @"object found for REPORT '%@' is not callable: %@",
1588             mname, method];
1589   }
1590   [self debugWithFormat:@"REPORT method: %@", method];
1591
1592   /* perform call */
1593   
1594   resultObject = [method callOnObject:[_ctx clientObject] inContext:_ctx];
1595   if (debugOn) [self debugWithFormat:@"got REPORT result: %@", resultObject];
1596   return resultObject;
1597 }
1598
1599 /* CalDAV */
1600
1601 - (id)doMKCALENDAR:(WOContext *)_ctx {
1602   return [self httpException:405 /* method not allowed */
1603                reason:@"CalDAV calendar creation not yet implemented."];
1604 }
1605
1606 /* DAV access control lists */
1607
1608 - (id)doACL:(WOContext *)_ctx {
1609   return [self httpException:405 /* method not allowed */
1610                reason:@"WebDAV operation not yet implemented."];
1611 }
1612
1613 /* DAV binding */
1614
1615 - (id)doBIND:(WOContext *)_ctx {
1616   return [self httpException:405 /* method not allowed */
1617                reason:@"WebDAV operation not yet implemented."];
1618 }
1619
1620 /* DAV ordering */
1621
1622 - (id)doORDERPATCH:(WOContext *)_ctx {
1623   return [self httpException:405 /* method not allowed */
1624                reason:@"WebDAV operation not yet implemented."];
1625 }
1626
1627 /* DAV deltav */
1628
1629 - (id)doCHECKOUT:(WOContext *)_ctx {
1630   return [self httpException:405 /* method not allowed */
1631                reason:@"WebDAV operation not yet implemented."];
1632 }
1633 - (id)doUNCHECKOUT:(WOContext *)_ctx {
1634   return [self httpException:405 /* method not allowed */
1635                reason:@"WebDAV operation not yet implemented."];
1636 }
1637 - (id)doCHECKIN:(WOContext *)_ctx {
1638   return [self httpException:405 /* method not allowed */
1639                reason:@"WebDAV operation not yet implemented."];
1640 }
1641 - (id)doMKWORKSPACE:(WOContext *)_ctx {
1642   return [self httpException:405 /* method not allowed */
1643                reason:@"WebDAV operation not yet implemented."];
1644 }
1645 - (id)doUPDATE:(WOContext *)_ctx {
1646   return [self httpException:405 /* method not allowed */
1647                reason:@"WebDAV operation not yet implemented."];
1648 }
1649 - (id)doMERGE:(WOContext *)_ctx {
1650   return [self httpException:405 /* method not allowed */
1651                reason:@"WebDAV operation not yet implemented."];
1652 }
1653 - (id)doVERSIONCONTROL:(WOContext *)_ctx {
1654   return [self httpException:405 /* method not allowed */
1655                reason:@"WebDAV operation not yet implemented."];
1656 }
1657
1658 /* perform dispatch */
1659
1660 - (id)performMethod:(NSString *)_method inContext:(WOContext *)_ctx {
1661   SoSecurityManager *sm;
1662   NSException       *e;
1663   NSString *s;
1664   SEL      sel;
1665   
1666   /* check basic WebDAV permission */
1667   
1668   sm = [_ctx soSecurityManager];
1669   e  = [sm validatePermission:SoPerm_WebDAVAccess
1670            onObject:self->object
1671            inContext:_ctx];
1672   if (e != nil) return e;
1673   
1674   /* perform search */
1675   
1676   _method = [_method uppercaseString];
1677   _method = [_method stringByReplacingString:@"-" withString:@""];
1678   s = [NSString stringWithFormat:@"do%@:", _method];
1679   sel = NSSelectorFromString(s);
1680   
1681   if (![self respondsToSelector:sel]) {
1682     [self logWithFormat:@"unknown WebDAV method: '%@'", _method];
1683     [[_ctx response] setStatus:405 /* invalid method */];
1684     return [_ctx response];
1685   }
1686   
1687   return [self performSelector:sel withObject:_ctx];
1688 }
1689
1690 - (BOOL)setupXmlParser {
1691   if (xmlParser == nil) {
1692     xmlParser =
1693       [[[SaxXMLReaderFactory standardXMLReaderFactory] 
1694                              createXMLReaderForMimeType:@"text/xml"]
1695                              retain];
1696     if (xmlParser == nil)
1697       return NO;
1698   }
1699   if (davsax == nil) {
1700     if ((davsax = [[SaxDAVHandler alloc] init]) == nil)
1701       return NO;
1702   }
1703   return YES;
1704 }
1705
1706 - (id)dispatchInContext:(WOContext *)_ctx {
1707   NSAutoreleasePool *pool;
1708   WOResponse *r;
1709   id result;
1710   
1711   if (gmt == nil) gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
1712   
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."];
1718     return r;
1719   }
1720   
1721   pool = [[NSAutoreleasePool alloc] init];
1722   result = [[self performMethod:[[_ctx request] method] inContext:_ctx] retain];
1723   [pool release];
1724   return [result autorelease];
1725 }
1726
1727 /* logging */
1728
1729 - (NSString *)loggingPrefix {
1730   return @"[obj-dav-dispatch]";
1731 }
1732 - (BOOL)isDebuggingEnabled {
1733   return debugOn ? YES : NO;
1734 }
1735
1736 /* description */
1737
1738 - (NSString *)description {
1739   NSMutableString *ms;
1740   
1741   ms = [NSMutableString stringWithCapacity:64];
1742   [ms appendFormat:@"<0x%p[%@]:", self,
1743         NSStringFromClass((Class)*(void**)self)];
1744   
1745   if (self->object)
1746     [ms appendFormat:@" object=%@", self->object];
1747   else
1748     [ms appendString:@" <no object>"];
1749   
1750   [ms appendString:@">"];
1751   return ms;
1752 }
1753
1754 @end /* SoObjectWebDAVDispatcher */