]> err.no Git - sope/blob - sope-appserver/NGObjWeb/WebDAV/SoObjectWebDAVDispatcher.m
renamed packages as discussed in the developer list
[sope] / sope-appserver / NGObjWeb / WebDAV / SoObjectWebDAVDispatcher.m
1 /*
2   Copyright (C) 2002-2004 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21 // $Id$
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 <NGObjWeb/WORequest.h>
35 #include <NGObjWeb/WOResponse.h>
36 #include <NGObjWeb/WOContext.h>
37 #include <NGObjWeb/WEClientCapabilities.h>
38 #include <SaxObjC/SaxObjC.h>
39 #include <SaxObjC/XMLNamespaces.h>
40 #include <NGExtensions/NSString+Ext.h>
41 #include "common.h"
42
43 @interface WORequest(HackURI)
44 - (void)_hackSetURI:(NSString *)_vuri;
45 @end
46
47 @implementation SoObjectWebDAVDispatcher
48
49 static int      debugOn = -1;
50 static BOOL     debugBulkTarget = NO;
51 static NSNumber *yesNum = nil;
52
53 + (void)initialize {
54   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
55   static BOOL didInit = NO;
56   if (didInit) return;
57   didInit = YES;
58   
59   debugOn = [ud boolForKey:@"SoObjectDAVDispatcherDebugEnabled"] ? 1 : 0;
60   if (debugOn) NSLog(@"Note: WebDAV dispatcher debugging is enabled.");
61   if (yesNum == nil) yesNum = [[NSNumber numberWithBool:YES] retain];
62 }
63
64 // THREAD
65 static id<NSObject,SaxXMLReader> xmlParser = nil;
66 static SaxDAVHandler             *davsax   = nil;
67 static NSTimeZone                *gmt      = nil;
68
69 - (id)initWithObject:(id)_object {
70   if ((self = [super init])) {
71     self->object = [_object retain];
72   }
73   return self;
74 }
75 - (void)dealloc {
76   [self->object release];
77   [super dealloc];
78 }
79
80 /* parser */
81
82 - (void)lockParser:(id)_sax {
83   [_sax reset];
84   [xmlParser setContentHandler:_sax];
85   [xmlParser setErrorHandler:_sax];
86 }
87 - (void)unlockParser:(id)_sax {
88   [xmlParser setContentHandler:nil];
89   [xmlParser setErrorHandler:nil];
90   [_sax reset];
91 }
92
93 /* common stuff */
94
95 - (NSException *)httpException:(int)_status reason:(NSString *)_reason {
96   NSDictionary *ui;
97
98   ui = [NSDictionary dictionaryWithObjectsAndKeys:
99                        self, @"dispatcher",
100                        [NSNumber numberWithInt:_status], @"http-status",
101                        nil];
102   return [NSException exceptionWithName:
103                         [NSString stringWithFormat:@"HTTP%i", _status]
104                       reason:_reason
105                       userInfo:ui];
106 }
107
108 - (NSArray *)allowedMethods {
109   static NSArray *defMethods = nil;
110   NSMutableArray *allow;
111
112   if (defMethods == nil) {
113     defMethods = [[[NSUserDefaults standardUserDefaults] 
114                                    arrayForKey:@"SoWebDAVDefaultAllowMethods"] 
115                                    copy];
116   }
117   
118   allow = [NSMutableArray arrayWithCapacity:16];
119   if (defMethods) [allow addObjectsFromArray:defMethods];
120   
121   if ([self->object 
122            respondsToSelector:@selector(performWebDAVQuery:inContext:)]) {
123     [allow addObject:@"PROPFIND"];
124     [allow addObject:@"SEARCH"];
125   }
126   if ([self->object respondsToSelector:
127              @selector(davSetProperties:removePropertiesNamed:)])
128     [allow addObject:@"PROPPATCH"];
129   
130   return allow;
131 }
132
133 - (NSString *)baseURLForContext:(WOContext *)_ctx {
134   /*
135     Note: Evolution doesn't correctly transfer the "Host:" header, it
136     misses the port argument :-(
137   */
138   NSString  *baseURL;
139   WORequest *rq;
140   NSString  *hostport;
141   id tmp;
142   
143   rq = [_ctx request];
144   
145   if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
146     hostport = tmp;
147     if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
148       hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
149   }
150   else if ((tmp = [rq headerForKey:@"host"]))
151     hostport = tmp;
152   else
153     hostport = [[NSHost currentHost] name];
154   
155   baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
156   return baseURL;
157 }
158
159 - (id)primaryCallWebDAVMethod:(NSString *)_name inContext:(WOContext *)_ctx {
160   id method;
161   
162   method = [self->object lookupName:_name inContext:_ctx acquire:NO];
163   if (method == nil) {
164     return [self httpException:501 /* Not Implemented */
165                  reason:@"target object does not support requested operation"];
166   }
167   if ([method isKindOfClass:[NSException class]]) {
168     [self logWithFormat:@"could not lookup method, got exception: %@", method];
169     return method;
170   }
171   
172   [self debugWithFormat:@"  %@ method: %@", _name, method];
173   return [method callOnObject:self->object inContext:_ctx];
174 }
175
176 /* core HTTP methods */
177
178 - (id)doGET:(WOContext *)_ctx {
179   NSException *e;
180   id methodObject;
181   
182   
183   if ((methodObject = 
184        [self->object lookupName:@"GET" inContext:_ctx acquire:NO]) == nil)
185     methodObject = [self->object lookupDefaultMethod];
186   else {
187     if ((e = [self->object validateName:@"GET" inContext:_ctx]))
188       return e;
189   }
190   
191   if (methodObject == nil)
192     return self->object;
193   if ([methodObject isKindOfClass:[NSException class]])
194     return methodObject;
195   
196   if ([methodObject respondsToSelector:
197                       @selector(takeValuesFromRequest:inContext:)])
198     [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx];
199   
200   return [methodObject callOnObject:self->object inContext:_ctx];
201 }
202
203 - (id)doPUT:(WOContext *)_ctx {
204   SoSecurityManager *sm;
205   NSException       *e;
206   NSString          *pathInfo;
207   
208   pathInfo = [_ctx pathInfo];
209   [self debugWithFormat:@"doPUT (pathinfo='%@')", pathInfo];
210   
211   /* check permissions */
212   
213   sm = [_ctx soSecurityManager];
214   e  = [sm validatePermission:
215              ([pathInfo length] > 0)
216              ? SoPerm_AddDocumentsImagesAndFiles
217              : SoPerm_ChangeImagesAndFiles
218            onObject:self->object
219            inContext:_ctx];
220   if (e) return e;
221   
222   if ((e = [self->object validateName:@"PUT" inContext:_ctx]))
223     return e;
224   
225   /* perform */
226   
227   if ([pathInfo length] > 0) {
228     /* check whether all the parent collections are available */
229     if ([pathInfo rangeOfString:@"/"].length > 0) {
230       return [self httpException:409 /* Conflict */
231                    reason:
232                      @"invalid WebDAV PUT request, first create all "
233                      @"parent collections !"];
234     }
235   }
236
237   return [self primaryCallWebDAVMethod:@"PUT" inContext:_ctx];
238 }
239
240 - (id)doPOST:(WOContext *)_ctx {
241   NSException *e;
242   
243   if ((e = [self->object validateName:@"POST" inContext:_ctx]))
244     return e;
245   
246   return [self primaryCallWebDAVMethod:@"POST" inContext:_ctx];
247 }
248
249 - (id)doDELETE:(WOContext *)_ctx {
250   SoSecurityManager *sm;
251   NSException       *e;
252   
253   /* check permissions */
254   
255   sm = [_ctx soSecurityManager];
256   e  = [sm validatePermission:SoPerm_DeleteObjects
257            onObject:self->object
258            inContext:_ctx];
259   if (e) return e;
260   if ((e = [self->object validateName:@"DELETE" inContext:_ctx]))
261     return e;
262
263   // TODO: IE WebFolders sent a "Destroy" header together with the
264   //       DELETE request, eg:
265   //       "Destroy: NoUndelete"
266   
267   return [self primaryCallWebDAVMethod:@"DELETE" inContext:_ctx];
268 }
269
270 - (id)doOPTIONS:(WOContext *)_ctx {
271   return [self allowedMethods];
272 }
273
274 - (id)doHEAD:(WOContext *)_ctx {
275   return [self doGET:_ctx];
276 }
277
278 /* core WebDAV methods */
279
280 - (id)doMKCOL:(WOContext *)_ctx {
281   SoSecurityManager *sm;
282   NSException       *e;
283   NSString          *pathInfo;
284   
285   pathInfo = [_ctx pathInfo];
286   if ([pathInfo length] == 0) {
287     /* MKCOL target already exists ... */
288     WOResponse *r;
289
290     [self logWithFormat:@"MKCOL target exists !"];
291     
292     r = [_ctx response];
293     [r setStatus:405 /* method not allowed */];
294     [r appendContentString:@"collection already exists !"];
295     return r;
296   }
297   
298   /* check permissions */
299   
300   sm = [_ctx soSecurityManager];
301   e  = [sm validatePermission:SoPerm_AddFolders 
302            onObject:self->object
303            inContext:_ctx];
304   if (e) return e;
305
306   /* check whether all the parent collections are available */
307   if ([pathInfo rangeOfString:@"/"].length > 0) {
308     return [self httpException:409 /* Conflict */
309                  reason:
310                    @"invalid WebDAV MKCOL request, first create all "
311                    @"parent collections !"];
312   }
313   
314   /* check whether the object supports creating collections */
315
316   if (![self->object respondsToSelector:
317               @selector(davCreateCollection:inContext:)]) {
318     /* Note: this should never happen, as this is implemented on NSObject */
319     
320     [self logWithFormat:@"MKCOL: object '%@' path-info '%@'", 
321             self->object, pathInfo];
322     return [self httpException:405 /* not allowed */
323                  reason:
324                    @"this object cannot create a new collection with MKCOL"];
325   }
326   
327   if ((e = [self->object davCreateCollection:pathInfo inContext:_ctx])) {
328     [self debugWithFormat:@"creation of collection '%@' failed: %@",
329             pathInfo, e];
330     return e;
331   }
332   
333   [self debugWithFormat:@"created collection."];
334   return [NSNumber numberWithBool:YES];
335 }
336
337 - (NSString *)scopeForDepth:(NSString *)_depth inContext:(WOContext *)_ctx {
338   NSString *scope;
339   
340   if ([_depth hasPrefix:@"0"])
341     scope = @"self";
342   else if ([_depth hasPrefix:@"1,noroot"])
343     scope = @"flat";
344   else if ([_depth hasPrefix:@"1"]) {
345     NSString *ua;
346     
347     scope = @"flat+self";
348     
349     /* some special handling for IE ... */
350     if ((ua = [[[_ctx request] clientCapabilities] userAgentType])) {
351       if ([ua isEqualToString:@"Evolution"])
352         scope = @"flat";
353       else if ( [ua isEqualToString:@"WebFolder"])
354         scope = @"flat";
355     }
356   }
357   else if ([_depth hasPrefix:@"infinity"])
358     scope = @"deep";
359   else
360     scope = @"deep";
361
362   return scope;
363 }
364
365 - (NSMutableDictionary *)hintsWithScope:(NSString *)_scope
366   propNames:(NSArray *)_propNames
367   findAll:(BOOL)_findAll
368   namesOnly:(BOOL)_namesOnly
369 {
370   NSMutableDictionary *hints;
371   
372   hints = [NSMutableDictionary dictionaryWithCapacity:4];
373
374   if (_scope)
375     [hints setObject:_scope forKey:@"scope"];
376   if (_propNames)
377     [hints setObject:_propNames forKey:@"attributes"];
378   // else if (_findAll) ; /* empty attributes */
379   
380   if (_namesOnly)
381     [hints setObject:[NSNumber numberWithBool:YES] forKey:@"namesOnly"];
382   return hints;
383 }
384
385 - (id)doPROPFIND:(WOContext *)_ctx {
386   SoSecurityManager    *sm;
387   NSException          *e;
388   EOFetchSpecification *fs;
389   WORequest *rq;
390   NSString  *uri;
391   NSString  *depth; /* 0, 1, 1,noroot or infinity */
392   NSArray   *propNames, *rtargets;
393   BOOL      findAll;
394   BOOL      findNames;
395   id        result;
396   NSRange   r;
397   
398   /* check permissions */
399   
400   sm = [_ctx soSecurityManager];
401   e  = [sm validatePermission:SoPerm_AccessContentsInformation 
402            onObject:self->object
403            inContext:_ctx];
404   if (e) return e;
405   
406   /* perform search */
407   
408   if (![self->object respondsToSelector:
409               @selector(performWebDAVQuery:inContext:)]) {
410     return [self httpException:405 /* not allowed */
411                  reason:@"this object cannot not execute a PROPFIND query"];
412   }
413   
414   rq    = [_ctx request];
415   depth = [rq headerForKey:@"depth"];
416   uri   = [rq uri];
417   
418   if ([depth length] == 0) depth = @"infinity";
419   
420   [self lockParser:davsax];
421   {
422     [xmlParser parseFromSource:[rq content]];
423     propNames = [[davsax propFindQueriedNames] copy];
424     findAll   = [davsax  propFindAllProperties];
425     findNames = [davsax  propFindPropertyNames];
426   }
427   [self unlockParser:davsax];
428   propNames = [propNames autorelease];
429   
430   /* check query all properties */
431   
432   if (propNames == nil)
433     propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx];
434   
435   /* check for a ZideStore ranges query (a BPROPFIND "emulation") */
436   
437   if (debugOn) [self logWithFormat:@"request uri: %@", uri];
438   r = [uri rangeOfString:@"_range"];
439   if (r.length > 0) { /* ZideStore range query */
440     NSString *s;
441     NSArray  *ids;
442     
443     if (debugOn)
444       [self logWithFormat:@"  detected a ZideStore range query: '%@'", uri];
445     
446     s = [uri substringFromIndex:(r.location + r.length)];
447     if ([s hasSuffix:@"/"]) s = [s substringToIndex:([s length] - 1)];
448     if ([s hasPrefix:@"_"]) s = [s substringFromIndex:1];
449     
450     ids = ([s length] == 0)
451       ? [NSArray array]
452       : [s componentsSeparatedByString:@"_"];
453     
454     // TODO: should use -stringByUnescapingURL on IDs (not required for ints)
455     
456     rtargets = ids;
457     if (debugOn) 
458       [self logWithFormat:@"  IDs: %@", [ids componentsJoinedByString:@","]];
459     
460     /* patch URI, could have side-effects ? */
461     [self logWithFormat:
462             @"NOTE: hacked URI, _range_ part won't be visible in the HTTP "
463             @"access log:\n%@", uri];
464     [rq _hackSetURI:[uri substringToIndex:r.location]];
465   }
466   else
467     rtargets = nil;
468   
469   /* build the fetch-spec */
470   {
471     NSMutableDictionary *hints;
472     
473     hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx]
474                   propNames:propNames findAll:findAll namesOnly:findNames];
475     if (rtargets) /* range-query keys */
476       [hints setObject:rtargets forKey:@"bulkTargetKeys"];
477     
478     fs = [EOFetchSpecification alloc];
479     fs = [fs initWithEntityName:[self baseURLForContext:_ctx]
480              qualifier:nil
481              sortOrderings:nil
482              usesDistinct:NO isDeep:NO hints:hints];
483     fs = [fs autorelease];
484
485     if (debugOn) [self logWithFormat:@"  propfind fetchspec: %@", fs];
486   }
487   
488   [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
489   
490   /* translate fetchspec if necessary */
491   {
492     NSDictionary *map;
493     
494     if ((map = [self->object davAttributeMapInContext:_ctx])) {
495       [_ctx setObject:map forKey:@"DAVPropertyMap"];
496       fs = [fs fetchSpecificationByApplyingKeyMap:map];
497       [_ctx setObject:fs  forKey:@"DAVMappedFetchSpecification"];
498     }
499   }
500   
501   /* perform */
502   
503   if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
504     return [self httpException:500 /* Server Error */
505                  reason:@"could not perform query (object returned nil)"];
506   }
507   
508   if (debugOn) [self logWithFormat:@"  propfind result: %@", result];
509   
510   return result;
511 }
512
513 - (BOOL)allowDeletePropertiesOnNewObjectInContext:(WOContext *)_ctx {
514   NSString *ua;
515   
516   ua = [[_ctx request] headerForKey:@"user-agent"];
517   if ([ua hasPrefix:@"Evolution"]) {
518     /* if Evo creates tasks, it tries to delete some props at the same time */
519     return YES;
520   }
521   if ([ua hasPrefix:@"CFNetwork"]) {
522     /* iSync trying to create a record ... */
523     return YES;
524   }
525   
526   [self logWithFormat:@"do not allow delete properties on new object for: %@",
527           ua];
528   return NO;
529 }
530
531 - (id)doPROPPATCH:(WOContext *)_ctx {
532   SoSecurityManager *sm;
533   NSException       *e;
534   NSMutableArray    *resProps;
535   NSArray           *delProps;
536   NSDictionary      *setProps;
537   NSString          *pathInfo;
538   
539   pathInfo = [_ctx pathInfo];
540   
541   /* check permissions */
542   
543   sm = [_ctx soSecurityManager];
544   e  = [sm validatePermission:([pathInfo length] > 0)
545              ? SoPerm_AddDocumentsImagesAndFiles
546              : SoPerm_ChangeImagesAndFiles
547            onObject:self->object
548            inContext:_ctx];
549   if (e) return e;
550   
551   /* check for conflicts */
552
553   if ([pathInfo length] > 0) {
554     /* check whether all the parent collections are available */
555     if ([pathInfo rangeOfString:@"/"].length > 0) {
556       return [self httpException:409 /* Conflict */
557                    reason:
558                      @"invalid WebDAV PROPPATCH request, first create all "
559                      @"parent collections !"];
560     }
561   }
562   
563   /* check whether the object supports patching */
564
565   if ([pathInfo length] > 0) {
566     if (![self->object respondsToSelector:
567                 @selector(davCreateObject:properties:inContext:)]) {
568       [self debugWithFormat:@"cannot create new object via DAV on %@",
569               self->object];
570       return [self httpException:405 /* not allowed */
571                    reason:
572                      @"this object cannot create a new object with PROPPATCH"];
573     }
574   }
575   else {
576     if (![self->object respondsToSelector:
577             @selector(davSetProperties:removePropertiesNamed:inContext:)]) {
578       [self debugWithFormat:@"cannot change object props via DAV on %@",
579               self->object];
580       return [self httpException:405 /* not allowed */
581                    reason:@"this object cannot PROPPATCH the attributes"];
582     }
583   }
584   
585   /* parse request */
586   
587   [self lockParser:davsax];
588   {
589     [xmlParser parseFromSource:[[_ctx request] content]];
590     delProps = [[davsax propPatchPropertyNamesToRemove] copy];
591     setProps = [[davsax propPatchValues] copy];
592   }
593   [self unlockParser:davsax];
594   delProps = [delProps autorelease];
595   setProps = [setProps autorelease];
596   
597   if (delProps == nil && setProps == nil) {
598     [self logWithFormat:@"WARNING: got no properties in PROPPATCH !"];
599     return [self httpException:400 /* bad request */
600                  reason:@"got no properties in PROPPATCH !"];
601   }
602   
603   if ([pathInfo length] > 0) {
604     /* a create object cannot delete props ... */
605     if ([delProps count] > 0) {
606       if (![self allowDeletePropertiesOnNewObjectInContext:_ctx]) {
607         [self logWithFormat:@"shall delete props in new object '%@': %@",
608                 pathInfo, delProps];
609         return [self httpException:400 /* bad request */
610                      reason:@"cannot delete properties of a new object"];
611       }
612       [self debugWithFormat:@"deleting properties on a new object: %@ ...",
613               delProps];
614     }
615   }
616   
617   resProps = [NSMutableArray arrayWithCapacity:16];
618   if (delProps) [resProps addObjectsFromArray:delProps];
619   if (setProps) [resProps addObjectsFromArray:[setProps allKeys]];
620   
621   /* map attributes */
622   {
623     NSDictionary *map;
624     
625     if ((map = [self->object davAttributeMapInContext:_ctx])) {
626       unsigned count;
627       
628       [_ctx setObject:map forKey:@"DAVPropertyMap"];
629       
630       if ((count = [delProps count]) > 0) {
631         NSMutableArray *mappedDelProps;
632         unsigned i;
633         
634         mappedDelProps = [NSMutableArray arrayWithCapacity:(count + 1)];
635         for (i = 0; i < count; i++) {
636           NSString *k, *tk;
637           
638           k  = [delProps objectAtIndex:i];
639           tk = [map valueForKey:k];
640           
641           [mappedDelProps addObject:(tk ? tk : k)];
642         }
643         delProps = mappedDelProps;
644       }
645       if ((count = [setProps count]) > 0) {
646         NSMutableDictionary *mappedSetProps;
647         NSEnumerator *keys;
648         NSString *k;
649         
650         mappedSetProps = [NSMutableDictionary dictionaryWithCapacity:count];
651         keys = [setProps keyEnumerator];
652         while ((k = [keys nextObject])) {
653           NSString *tk;
654           
655           tk = [map valueForKey:k];
656           [mappedSetProps setObject:[setProps objectForKey:k]
657                           forKey:(tk ? tk : k)];
658         }
659         setProps = mappedSetProps;
660       }
661     }
662   }
663   
664   if (debugOn) {
665     [self debugWithFormat:@"PROPPATCH '%@': delete=%@, set=%@",
666             pathInfo, delProps, setProps];
667   }
668   
669   if ([pathInfo length] == 0) {
670     /* edit an object */
671     NSException *e;
672     
673     e = [self->object 
674              davSetProperties:setProps
675              removePropertiesNamed:delProps
676              inContext:_ctx];
677     if (e) return e;
678   }
679   else {
680     /* create an object */
681     id newChild;
682     
683     newChild = [self->object 
684                     davCreateObject:pathInfo
685                     properties:setProps
686                     inContext:_ctx];
687     if ([newChild isKindOfClass:[NSException class]]) 
688       return newChild;
689     
690     [self debugWithFormat:@"created: %@", newChild];
691   }
692   
693   /* generate response */
694   return resProps;
695 }
696
697 - (id)doLOCK:(WOContext *)_ctx {
698   SoSecurityManager *sm;
699   NSException       *e;
700   SoDAVLockManager *lockManager;
701   WORequest  *rq;
702   WOResponse *r;
703   NSString   *ifValue, *lockDepth;
704   id token;
705   
706   /* check permissions */
707   
708   sm = [_ctx soSecurityManager];
709   e  = [sm validatePermission:SoPerm_WebDAVLockItems
710            onObject:self->object
711            inContext:_ctx];
712   if (e) return e;
713   
714   /* check lock manager */
715   
716   if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) {
717     return [self httpException:405 /* method not allowed */
718                  reason:@"target object does not support locking !"];
719   }
720   
721   rq = [_ctx request];
722   r  = [_ctx response];
723   
724   lockDepth = [rq headerForKey:@"depth"];
725   ifValue   = [rq headerForKey:@"if"];
726   
727   if (lockDepth != nil && ![lockDepth isEqualToString:@"0"]) {
728     [self logWithFormat:
729             @"WARNING: 'depth' locking not supported yet (depth=%@)!", 
730             lockDepth];
731   }
732   if (ifValue) {
733     [self logWithFormat:
734             @"WARNING: 'if' locking not supported yet, if: '%@'", ifValue];
735   }
736   
737   // need to parse lockinfo
738   
739   token = [lockManager lockURI:[rq uri]
740                        timeout:[rq headerForKey:@"timeout"]
741                        scope:@"exclusive"
742                        type:@"write"
743                        owner:nil];
744   if (token == nil) {
745     /* already locked */
746     return [self httpException:423 /* locked */
747                  reason:@"object locked, lock manager did not provide token."];
748   }
749   
750   [self debugWithFormat:@"locked: %@ (token %@)", [[_ctx request] uri], token];
751   return token;
752 }
753
754 - (id)doUNLOCK:(WOContext *)_ctx {
755   SoSecurityManager *sm;
756   NSException       *e;
757   SoDAVLockManager  *lockManager;
758   NSString *token;
759   
760   /* check permissions */
761   
762   sm = [_ctx soSecurityManager];
763   e  = [sm validatePermission:SoPerm_WebDAVUnlockItems
764            onObject:self->object
765            inContext:_ctx];
766   if (e) return e;
767   
768   /* check lock manager */
769   
770   if ((lockManager = [self->object davLockManagerInContext:_ctx]) == nil) {
771     return [self httpException:405 /* method not allowed */
772                  reason:@"target object does not support locking."];
773   }
774   
775   token = [[_ctx request] headerForKey:@"lock-token"];
776   
777   [lockManager unlockURI:[[_ctx request] uri] token:token];
778   
779   [self debugWithFormat:
780           @"unlocked: %@ (token %@)", [[_ctx request] uri], token];
781   
782   [[_ctx response] setStatus:204 /* fake ok */];
783   return [_ctx response];
784 }
785
786 - (NSException *)extractDestinationPath:(NSArray **)path_
787   fromContext:(WOContext *)_ctx
788 {
789   NSString *absDestURL;
790   NSURL    *destURL, *srvURL;
791   
792   if (path_) *path_ = nil;
793   
794   /* TODO: check proper permission prior attempting a move */
795   
796   absDestURL = [[_ctx request] headerForKey:@"destination"];
797   if ([absDestURL length] == 0) {
798     return [self httpException:400 /* Bad Request */
799                  reason:
800                    @"the destination WebDAV header was missing "
801                    @"for the MOVE/COPY operation"];
802   }
803   if ((destURL = [NSURL URLWithString:absDestURL]) == nil) {
804     [self logWithFormat:@"MOVE: got invalid destination URL: '%@'", 
805             absDestURL];
806     return [self httpException:400 /* Bad Request */
807                  reason:@"the MOVE/COPY destination is not a valid URL!"];
808   }
809   
810   srvURL = [_ctx serverURL];
811   
812   [self debugWithFormat:@"move/copy:\n  to:    %@\n  server: %@)", 
813           [destURL absoluteString], [srvURL absoluteString]];
814   
815   /* check whether URL is on the same server ... */
816   if (![[srvURL host] isEqualToString:[destURL host]] ||
817       ![[srvURL port] isEqual:[destURL port]]) {
818     /* 
819        The WebDAV spec is not really clear on what we should return in this
820        case? Let me know if anybody has a suggestion ...
821     */
822     [self logWithFormat:@"tried to do a cross server move (%@ vs %@)",
823             [srvURL absoluteString], [destURL absoluteString]];
824     return [self httpException:403 /* Forbidden */
825                  reason:@"MOVE destination is on a different host."];
826   }
827   
828   if (path_) {
829     NSMutableArray *ma;
830     unsigned i;
831     
832     /* TODO: hack hack hack */
833     ma = [[[destURL path] componentsSeparatedByString:@"/"] mutableCopy];
834     if ([ma count] > 0) // leading slash ("")
835       [ma removeObjectAtIndex:0];
836     if ([ma count] > 0) // the appname (eg zidestore)
837       [ma removeObjectAtIndex:0];
838     if ([ma count] > 0) // the request handler key (eg so)
839       [ma removeObjectAtIndex:0];
840     
841     /* unescape path components */
842     for (i = 0; i < [ma count]; i++) {
843       NSString *s = [ma objectAtIndex:i], *ns;
844       
845       ns = [s stringByUnescapingURL];
846       if (ns != s)
847         [ma replaceObjectAtIndex:i withObject:ns];
848     }
849     
850     *path_ = [ma copy];
851     [ma release];
852   }
853   return nil;
854 }
855 - (NSException *)lookupDestinationObject:(id *)target_ 
856   andNewName:(NSString **)name_
857   inContext:(WOContext *)_ctx
858 {
859   NSException *error;
860   NSArray     *targetPath;
861   id          root;
862   
863   if ((error = [self extractDestinationPath:&targetPath fromContext:_ctx]))
864     return error;
865
866   if ((root = [_ctx application]) == nil)
867     root = [WOApplication application];
868   if (root == nil) {
869     return [self httpException:500 /* internal server error */
870                  reason:@"did not find SOPE root object"];
871   }
872   
873   /* TODO: we should probably use a subcontext?! */
874   [_ctx setObject:yesNum forKey:@"isDestinationPathLookup"];
875   *target_ = [root traversePathArray:targetPath
876                    inContext:_ctx
877                    error:&error
878                    acquire:NO];
879   if (error) {
880     [self logWithFormat:@"could not resolve destination object (%@): %@",
881             [targetPath componentsJoinedByString:@" => "],
882             error];
883     return error;
884   }
885
886   if (name_) *name_ = [[[_ctx pathInfo] copy] autorelease];
887   
888   if (*target_ == nil) {
889     [self debugWithFormat:@"MOVE/COPY destination could not be found."];
890     return [self httpException:404 /* Not Found */
891                  reason:@"did not find target object"];
892   }
893   
894   [self debugWithFormat:@"SOURCE: %@", self->object];
895   [self debugWithFormat:@"TARGET: %@ (PI %@)", *target_, [_ctx pathInfo]];
896   return nil;
897 }
898
899 - (id)doCOPY:(WOContext *)_ctx {
900   NSException *error;
901   NSString    *newName;
902   id          targetObject;
903   
904   /* TODO: check proper permission prior attempting a copy */
905   
906   error = [self lookupDestinationObject:&targetObject andNewName:&newName
907                 inContext:_ctx];
908   if (error) return error;
909   
910   error = [self->object 
911                davCopyToTargetObject:targetObject newName:newName
912                inContext:_ctx];
913   if (error) {
914     [self debugWithFormat:@"WebDAV COPY operation failed: %@", error];
915     return error;
916   }
917   
918   return ([newName length] > 0)
919     ? [NSNumber numberWithBool:201 /* Created */]
920     : [NSNumber numberWithBool:204 /* No Content */];
921 }
922
923 - (id)doMOVE:(WOContext *)_ctx {
924   NSException *error;
925   NSString    *newName;
926   id          targetObject;
927   
928   /* TODO: check proper permission prior attempting a move */
929   
930   error = [self lookupDestinationObject:&targetObject andNewName:&newName
931                 inContext:_ctx];
932   if (error) return error;
933   
934   /*
935     Note: more relevant headers:
936       overwrite: T|F      (overwrite target) [rc: 201 vs 204!]
937       depth:     infinity
938       and locking tokens of course ...
939   */
940   
941   // TODO: should we check in this place for some constraints,
942   //       eg moving a collection to a non-collection or something
943   //       like that?
944   
945   error = [self->object 
946                davMoveToTargetObject:targetObject newName:newName
947                inContext:_ctx];
948   if (error) {
949     [self debugWithFormat:@"WebDAV MOVE operation failed: %@", error];
950     return error;
951   }
952   
953   return ([newName length] > 0)
954     ? [NSNumber numberWithBool:201 /* Created */]
955     : [NSNumber numberWithBool:204 /* No Content */];
956 }
957
958 /* WebDAV search methods */
959
960 - (id)doSEARCH:(WOContext *)_ctx {
961   SoSecurityManager    *sm;
962   NSException          *e;
963   EOFetchSpecification *fs;
964   NSString *baseURL;
965   id       result;
966   NSString *range;
967   
968   /* check permissions */
969   
970   sm = [_ctx soSecurityManager];
971   e  = [sm validatePermission:SoPerm_AccessContentsInformation 
972            onObject:self->object
973            inContext:_ctx];
974   if (e) return e;
975
976   /* perform search */
977   
978   if (![self->object 
979             respondsToSelector:@selector(performWebDAVQuery:inContext:)]) {
980     [[_ctx response] setStatus:405 /* not allowed */];
981     [[_ctx response] appendContentString:
982                        @"this object cannot not execute a SEARCH query"];
983     return [_ctx response];
984   }
985   
986   baseURL = [NSString stringWithFormat:@"http://%@%@",
987                         [[_ctx request] headerForKey:@"host"],
988                         [[_ctx request] uri]];
989   
990   [self lockParser:davsax];
991   {
992     [xmlParser parseFromSource:[[_ctx request] content]];
993     fs = [[davsax searchFetchSpecification] retain];
994   }
995   [self unlockParser:davsax];
996   
997   fs = [fs autorelease];
998   if (fs == nil) {
999     [[_ctx response] setStatus:400 /* Bad Request */];
1000     [[_ctx response] appendContentString:
1001                        @"could not process SEARCH query specification"];
1002     return [_ctx response];
1003   }
1004
1005   /* range */
1006   if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
1007     /* TODO: parse range header and add to fetch-specification */
1008     NSRange r;
1009     
1010     r = [range rangeOfString:@"rows="];
1011     if (r.length > 0) {
1012       range = [range substringFromIndex:(r.location + r.length)];
1013       [self debugWithFormat:
1014               @"Note: got a row range header (ignored): '%@'", range];
1015     }
1016     else
1017       [self logWithFormat:@"Note: got a range header (ignored): '%@'", range];
1018   }
1019   
1020   /* override entity name ... (FROM xxx isn't yet parsed correctly) */
1021   [fs setEntityName:baseURL];
1022   
1023   [self debugWithFormat:@"SEARCH: %@", fs];
1024   
1025   [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
1026
1027   /* translate fetchspec if necessary */
1028   {
1029     NSDictionary *map;
1030     
1031     if ((map = [self->object davAttributeMapInContext:_ctx])) {
1032       [_ctx setObject:map forKey:@"DAVPropertyMap"];
1033       fs = [fs fetchSpecificationByApplyingKeyMap:map];
1034       [_ctx setObject:fs forKey:@"DAVMappedFetchSpecification"];
1035     }
1036   }
1037   
1038   /* perform call */
1039   
1040   if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
1041     return [self httpException:500 /* Server Error */
1042                  reason:@"could not execute SEARCH query (returned nil)"];
1043   }
1044   
1045   return result;
1046 }
1047
1048 /* Exchange WebDAV methods */
1049
1050 - (id)doNOTIFY:(WOContext *)_ctx {
1051   return [self httpException:403 reason:@"NOTIFY not yet implemented"];
1052 }
1053
1054 - (id)doPOLL:(WOContext *)_ctx {
1055   SoSubscriptionManager *sm;
1056   WORequest  *rq;
1057   NSString   *subscriptionID;
1058   NSArray    *ids;
1059   NSURL      *url;
1060   
1061   rq  = [_ctx request];
1062   sm  = [SoSubscriptionManager sharedSubscriptionManager];
1063   url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1064   
1065   if (url == nil) {
1066     return [self httpException:500
1067                  reason:@"could not calculate URL of WebDAV object !"];
1068   }
1069   
1070   subscriptionID = [rq headerForKey:@"subscription-id"];
1071   if ([subscriptionID length] == 0) {
1072     return [self httpException:400 /* Bad Request */
1073                  reason:@"did not find subscription-id header in POLL"];
1074   }
1075   
1076   ids = [subscriptionID componentsSeparatedByString:@","];
1077   
1078   return [sm pollSubscriptions:ids onURL:url];
1079 }
1080
1081 - (id)doSUBSCRIBE:(WOContext *)_ctx {
1082   SoSubscriptionManager *sm;
1083   WORequest  *rq;
1084   WOResponse *r;
1085   NSURL    *url;
1086   id       callback;
1087   NSString *notificationType;
1088   NSString *notificationDelay;
1089   NSString *lifetime;
1090   NSString *subscriptionID;
1091   
1092   rq  = [_ctx request];
1093   r   = [_ctx response];
1094   sm  = [SoSubscriptionManager sharedSubscriptionManager];
1095   url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1096   
1097   if (url == nil) {
1098     return [self httpException:500
1099                  reason:@"could not calculate URL of WebDAV object !"];
1100   }
1101   
1102   subscriptionID = [rq headerForKey:@"subscription-id"];
1103   
1104   /* first check, whether it's an existing subscription to be renewed */
1105   
1106   if ([subscriptionID length] > 0) {
1107     NSString *newId;
1108     
1109     if ((newId = [sm renewSubscription:subscriptionID onURL:url]) == nil) {
1110       return [self httpException:412 /* precondition failed */
1111                    reason:@"did not find provided subscription ID !"];
1112     }
1113     return newId;
1114   }
1115   
1116   if ((callback = [rq headerForKey:@"call-back"])) {
1117     NSURL *url;
1118     
1119     if ((url = [NSURL URLWithString:[callback stringValue]]) == nil) {
1120       [self debugWithFormat:@"ERROR: could not parse callback URL '%@'", 
1121               callback];
1122       return [self httpException:400 /* Bad Request */
1123                    reason:@"missing valid callback URL !"];
1124     }
1125     else
1126       callback = url;
1127   }
1128   
1129   /* TODO: add sanity checking of notification-type as described in docs */
1130   /* TODO: check depth */
1131   
1132   notificationDelay = [rq headerForKey:@"notification-delay"];
1133   notificationType  = [rq headerForKey:@"notification-type"];
1134   lifetime          = [rq headerForKey:@"subscription-lifetime"];
1135   
1136   subscriptionID = [sm subscribeURL:url forObserver:callback
1137                        type:notificationType 
1138                        delay:notificationDelay
1139                          ? [notificationDelay doubleValue] : 0.0
1140                        lifetime:lifetime ? [lifetime doubleValue] : 0.0];
1141   return subscriptionID;
1142 }
1143 - (id)doUNSUBSCRIBE:(WOContext *)_ctx {
1144   SoSubscriptionManager *sm;
1145   WORequest  *rq;
1146   WOResponse *r;
1147   NSString *subscriptionID;
1148   NSURL    *url;
1149   
1150   rq  = [_ctx request];
1151   r   = [_ctx response];
1152   sm  = [SoSubscriptionManager sharedSubscriptionManager];
1153   url = [NSURL URLWithString:[self->object baseURLInContext:_ctx]];
1154   
1155   if (url == nil) {
1156     return [self httpException:500
1157                  reason:@"could not calculate URL of WebDAV object !"];
1158   }
1159   
1160   subscriptionID = [rq headerForKey:@"subscription-id"];
1161   if ([subscriptionID length] == 0) {
1162     return [self httpException:400 /* Bad Request */
1163                  reason:@"missing subscription id !"];
1164   }
1165   
1166   if ([sm unsubscribeID:subscriptionID onURL:url]) {
1167     [r setStatus:200];
1168     return r;
1169   }
1170   else {
1171     return [self httpException:400 /* Bad Request */
1172                  reason:@"unsubscribe failed (invalid or old id ?)"];
1173   }
1174 }
1175
1176 /* Exchange bulk methods */
1177
1178 - (NSArray *)urlPartsForTargets:(NSArray *)_targets basePath:(NSString *)_base{
1179   /*
1180     Transform the target URLs given to the BPROPFIND operation. This is a
1181     simplified implementation, for example we expect that the URLs are all
1182     located in the same URL space (on same host and port).
1183   */
1184   NSMutableArray *ma;
1185   unsigned i, count;
1186   
1187   if ((count = [_targets count]) == 0)
1188     return [NSArray array];
1189   
1190   ma = [NSMutableArray arrayWithCapacity:count];
1191   for (i = 0; i < count; i++) {
1192     NSString *target;
1193     
1194     target = [_targets objectAtIndex:i];
1195     if (debugBulkTarget)
1196       [self logWithFormat:@"  MORPH target '%@'", target];
1197     
1198     /* extract the path from full URLs */
1199     if ([target isAbsoluteURL]) {
1200       NSURL *url;
1201       
1202       /* fix an Evolution bug, uses the 'unsafe' "@" in the URL ! */
1203       if ([target rangeOfString:@"@"].length > 0) {
1204         target = [target stringByReplacingString:@"@"
1205                          withString:@"%40"];
1206       }
1207       
1208       if ((url = [NSURL URLWithString:target])) {
1209         if (debugBulkTarget) [self logWithFormat:@"got URL: %@", url];
1210         target = [url path];
1211         if (debugBulkTarget) [self logWithFormat:@"path: %@", target];
1212       }
1213       else {
1214         [self logWithFormat:@"ERROR: could not parse BPROPFIND target '%@' !",
1215                 target];
1216       }
1217     }
1218     
1219     /* make the target name relative to the request URI */
1220     if ([target hasPrefix:_base]) {
1221       target = [target substringFromIndex:[_base length]];
1222       if ([target hasPrefix:@"/"])
1223         target = [target substringFromIndex:1];
1224     }
1225     
1226     /* add the target */
1227     target = [target stringByUnescapingURL];
1228     if (debugBulkTarget) [self logWithFormat:@"  ADD target '%@'", target];
1229     [ma addObject:target];
1230   }
1231   return ma;
1232 }
1233
1234 - (id)doBPROPFIND:(WOContext *)_ctx {
1235   /*
1236     TODO: could optimize a BPROPFIND on a single target to use PROPFIND
1237     
1238     How are BPROPFINDs mapped ? BPROPFIND corresponds to SKYRiX 4.1
1239     "fetch-by-globalids" commands, that is, a search gets passed a list
1240     of primary keys to fetch.
1241     BPROPFIND is implemented in a similiar way, the target URLs are converted
1242     to be relative to the URI object and are passed to the query datasource
1243     using the "bulkTargetKeys" fetch hint.
1244     
1245     Important: the URI object *must* support the "bulkTargetKeys" fetch hint,
1246     otherwise the operation will run on the object itself.
1247     
1248     Note: Previously BPROPFIND was mapped to a set of individual requests,
1249     but obviously this doesn't match SQL very well (resulting in an individual
1250     SQL query for each entity ...)
1251   */
1252   SoSecurityManager    *sm;
1253   NSException          *e;
1254   EOFetchSpecification *fs;
1255   WORequest *rq;
1256   NSString  *depth; /* 0, 1, 1,noroot or infinity */
1257   NSArray   *propNames;
1258   NSArray   *targets, *rtargets;
1259   BOOL      findAll;
1260   BOOL      findNames;
1261   id        result;
1262   NSDictionary *map;
1263   
1264   /* check permissions */
1265   
1266   sm = [_ctx soSecurityManager];
1267   e  = [sm validatePermission:SoPerm_AccessContentsInformation 
1268            onObject:self->object
1269            inContext:_ctx];
1270   if (e) return e;
1271
1272   /* perform search */
1273   
1274   if (![self->object respondsToSelector:@selector(performWebDAVQuery:inContext:)]) {
1275     return [self httpException:405 /* not allowed */
1276                  reason:@"this object cannot not execute a PROPFIND query"];
1277   }
1278   
1279   rq = [_ctx request];
1280   depth = [rq headerForKey:@"depth"];
1281   if ([depth length] == 0) depth = @"infinity";
1282   
1283   [self lockParser:davsax];
1284   {
1285     [xmlParser parseFromSource:[rq content]];
1286     propNames = [[davsax propFindQueriedNames] copy];
1287     findAll   = [davsax  propFindAllProperties];
1288     findNames = [davsax  propFindPropertyNames];
1289     targets   = [[davsax  bpropFindTargets] copy];
1290   }
1291   [self unlockParser:davsax];
1292   propNames = [propNames autorelease];
1293   targets   = [targets   autorelease];
1294   
1295   if ([targets count] == 0)
1296     return [NSArray array];
1297   
1298   /* check query all properties */
1299   
1300   if (propNames == nil)
1301     propNames = [self->object defaultWebDAVPropertyNamesInContext:_ctx];
1302   
1303   /* morph targets */
1304   
1305   rtargets = [self urlPartsForTargets:targets
1306                    basePath:[[rq uri] stringByUnescapingURL]];
1307   
1308   [self debugWithFormat:@"BPROPFIND targets: %@", rtargets];
1309   
1310   /* build the fetch-spec */
1311   {
1312     NSMutableDictionary *hints;
1313     
1314     hints = [self hintsWithScope:[self scopeForDepth:depth inContext:_ctx]
1315                   propNames:propNames findAll:findAll namesOnly:findNames];
1316     [hints setObject:rtargets forKey:@"bulkTargetKeys"];
1317     
1318     fs = [EOFetchSpecification alloc];
1319     fs = [fs initWithEntityName:[self baseURLForContext:_ctx]
1320              qualifier:nil
1321              sortOrderings:nil
1322              usesDistinct:NO isDeep:NO hints:hints];
1323     fs = [fs autorelease];
1324   }
1325   
1326   [_ctx setObject:fs forKey:@"DAVFetchSpecification"];
1327   
1328   /* 
1329      translate fetchspec if necessary - we currently cannot allow a map
1330      for each target, so we use the map of the queried target.
1331   */
1332   if ((map = [self->object davAttributeMapInContext:_ctx])) {
1333     [_ctx setObject:map forKey:@"DAVPropertyMap"];
1334     fs = [fs fetchSpecificationByApplyingKeyMap:map];
1335     [_ctx setObject:fs  forKey:@"DAVMappedFetchSpecification"];
1336   }
1337
1338   /* perform */
1339   
1340   if ((result = [self->object performWebDAVQuery:fs inContext:_ctx]) == nil) {
1341     return [self httpException:500 /* Server Error */
1342                  reason:@"could not perform query (object returned nil)"];
1343   }
1344   
1345   return result;
1346 #if 0  
1347   /* now, for each BPROPFIND target ... */
1348   {
1349     NSEnumerator *e;
1350     NSString *targetURL;
1351     
1352     result = [NSMutableArray arrayWithCapacity:32];
1353     
1354     e = [targets objectEnumerator];
1355     while ((targetURL = [e nextObject])) {
1356       NSAutoreleasePool *pool;
1357       WOContext   *localContext;
1358       WORequest   *localRequest;
1359       NSException *e;
1360       id targetObject;
1361       id targetResult;
1362       
1363       pool = [[NSAutoreleasePool alloc] init];
1364       
1365       /* setup the "subrequest" */
1366       
1367       if ([targetURL isAbsoluteURL]) {
1368         NSURL *url;
1369         
1370         if ((url = [NSURL URLWithString:targetURL]))
1371           targetURL = [url path];
1372         else {
1373           [self logWithFormat:@"ERROR: could not parse target-url '%@'",
1374                   targetURL];
1375         }
1376       }
1377       
1378       localRequest = [[WORequest alloc] initWithMethod:@"PROPFIND"
1379                                         uri:targetURL
1380                                         httpVersion:[rq httpVersion]
1381                                         headers:[rq headers]
1382                                         content:nil
1383                                         userInfo:nil];
1384       localContext = 
1385         [[[WOContext alloc] initWithRequest:localRequest] autorelease];
1386       [localRequest autorelease];
1387       
1388       /* resetup fetchspec */
1389       [fs setEntityName:targetURL];
1390       
1391       /* traverse URL */
1392       
1393       targetObject = [_ctx traversalRoot];
1394       targetObject = [targetObject traversePathArray:
1395                                      [localRequest requestHandlerPathArray]
1396                                    inContext:localContext
1397                                    error:&e
1398                                    acquire:NO];
1399       if (targetObject == nil) {
1400         [self logWithFormat:@"did not find BPROPFIND target: %@", targetURL];
1401         [self logWithFormat:@"  root:   %@", [_ctx traversalRoot]];
1402         [self logWithFormat:@"  path:   %@", 
1403                 [[localRequest requestHandlerPathArray] 
1404                                componentsJoinedByString:@"/"]];
1405         [self logWithFormat:@"  error:  %@", e];
1406         targetResult = e;
1407       }
1408       else {
1409         /* perform query */
1410         
1411         targetResult = [targetObject performWebDAVQuery:fs 
1412                                      inContext:localContext];
1413         if (targetResult == nil) {
1414           targetResult = 
1415             [self httpException:500 /* Server Error */
1416                   reason:@"could not perform query (object returned nil)"];
1417         }
1418       }
1419       
1420       // do we need to distinguish the queries somehow ? (href generation)
1421       if ([targetResult isKindOfClass:[NSArray class]])
1422         [result addObjectsFromArray:targetResult];
1423       else if (targetResult)
1424         [result addObject:targetResult];
1425       
1426       [pool release];
1427     }
1428   }
1429   
1430   /* perform */
1431   
1432   if (result) return result;
1433 #endif
1434 }
1435
1436 - (id)doBCOPY:(WOContext *)_ctx {
1437   return [self httpException:403 /* forbidden */
1438                reason:@"BCOPY not yet implemented."];
1439 }
1440 - (id)doBDELETE:(WOContext *)_ctx {
1441   return [self httpException:403 /* forbidden */
1442                reason:@"BDELETE not yet implemented."];
1443 }
1444 - (id)doBMOVE:(WOContext *)_ctx {
1445   return [self httpException:403 /* forbidden */
1446                reason:@"WebDAV operation not yet implemented."];
1447 }
1448
1449 - (id)doBPROPPATCH:(WOContext *)_ctx {
1450   return [self httpException:403 /* forbidden */
1451                reason:@"WebDAV operation not yet implemented."];
1452 }
1453
1454 /* DAV access control lists */
1455
1456 - (id)doACL:(WOContext *)_ctx {
1457   return [self httpException:405 /* method not allowed */
1458                reason:@"WebDAV operation not yet implemented."];
1459 }
1460
1461 /* DAV binding */
1462
1463 - (id)doBIND:(WOContext *)_ctx {
1464   return [self httpException:405 /* method not allowed */
1465                reason:@"WebDAV operation not yet implemented."];
1466 }
1467
1468 /* DAV ordering */
1469
1470 - (id)doORDERPATCH:(WOContext *)_ctx {
1471   return [self httpException:405 /* method not allowed */
1472                reason:@"WebDAV operation not yet implemented."];
1473 }
1474
1475 /* DAV deltav */
1476
1477 - (id)doCHECKOUT:(WOContext *)_ctx {
1478   return [self httpException:405 /* method not allowed */
1479                reason:@"WebDAV operation not yet implemented."];
1480 }
1481 - (id)doUNCHECKOUT:(WOContext *)_ctx {
1482   return [self httpException:405 /* method not allowed */
1483                reason:@"WebDAV operation not yet implemented."];
1484 }
1485 - (id)doCHECKIN:(WOContext *)_ctx {
1486   return [self httpException:405 /* method not allowed */
1487                reason:@"WebDAV operation not yet implemented."];
1488 }
1489 - (id)doMKWORKSPACE:(WOContext *)_ctx {
1490   return [self httpException:405 /* method not allowed */
1491                reason:@"WebDAV operation not yet implemented."];
1492 }
1493 - (id)doUPDATE:(WOContext *)_ctx {
1494   return [self httpException:405 /* method not allowed */
1495                reason:@"WebDAV operation not yet implemented."];
1496 }
1497 - (id)doMERGE:(WOContext *)_ctx {
1498   return [self httpException:405 /* method not allowed */
1499                reason:@"WebDAV operation not yet implemented."];
1500 }
1501 - (id)doVERSIONCONTROL:(WOContext *)_ctx {
1502   return [self httpException:405 /* method not allowed */
1503                reason:@"WebDAV operation not yet implemented."];
1504 }
1505
1506 /* perform dispatch */
1507
1508 - (id)performMethod:(NSString *)_method inContext:(WOContext *)_ctx {
1509   SoSecurityManager *sm;
1510   NSException       *e;
1511   NSString *s;
1512   SEL      sel;
1513   
1514   /* check basic WebDAV permission */
1515   
1516   sm = [_ctx soSecurityManager];
1517   e  = [sm validatePermission:SoPerm_WebDAVAccess
1518            onObject:self->object
1519            inContext:_ctx];
1520   if (e) return e;
1521   
1522   /* perform search */
1523   
1524   _method = [_method uppercaseString];
1525   _method = [_method stringByReplacingString:@"-" withString:@""];
1526   s = [NSString stringWithFormat:@"do%@:", _method];
1527   sel = NSSelectorFromString(s);
1528   
1529   if (![self respondsToSelector:sel]) {
1530     [self logWithFormat:@"unknown WebDAV method: '%@'", _method];
1531     [[_ctx response] setStatus:405 /* invalid method */];
1532     return [_ctx response];
1533   }
1534   
1535   return [self performSelector:sel withObject:_ctx];
1536 }
1537
1538 - (BOOL)setupXmlParser {
1539   if (xmlParser == nil) {
1540     xmlParser =
1541       [[[SaxXMLReaderFactory standardXMLReaderFactory] 
1542                              createXMLReaderForMimeType:@"text/xml"]
1543                              retain];
1544     if (xmlParser == nil)
1545       return NO;
1546   }
1547   if (davsax == nil) {
1548     if ((davsax = [[SaxDAVHandler alloc] init]) == nil)
1549       return NO;
1550   }
1551   return YES;
1552 }
1553
1554 - (id)dispatchInContext:(WOContext *)_ctx {
1555   NSAutoreleasePool *pool;
1556   WOResponse *r;
1557   id result;
1558   
1559   if (gmt == nil) gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
1560   
1561   /* setup XML parser */
1562   if (![self setupXmlParser]) {
1563     r = [_ctx response];
1564     [r setStatus:500 /* internal server error */];
1565     [r appendContentString:@"did not find an XML parser, cannot process DAV."];
1566     return r;
1567   }
1568   
1569   pool = [[NSAutoreleasePool alloc] init];
1570   result = [[self performMethod:[[_ctx request] method] inContext:_ctx] retain];
1571   [pool release];
1572   return [result autorelease];
1573 }
1574
1575 /* logging */
1576
1577 - (NSString *)loggingPrefix {
1578   return @"[obj-dav-dispatch]";
1579 }
1580 - (BOOL)isDebuggingEnabled {
1581   return debugOn ? YES : NO;
1582 }
1583
1584 /* description */
1585
1586 - (NSString *)description {
1587   NSMutableString *ms;
1588   
1589   ms = [NSMutableString stringWithCapacity:64];
1590   [ms appendFormat:@"<0x%08X[%@]:", self,
1591         NSStringFromClass((Class)*(void**)self)];
1592   
1593   if (self->object)
1594     [ms appendFormat:@" object=%@", self->object];
1595   else
1596     [ms appendString:@" <no object>"];
1597   
1598   [ms appendString:@">"];
1599   return ms;
1600 }
1601
1602 @end /* SoObjectWebDAVDispatcher */