]> err.no Git - sope/blob - sope-appserver/NGObjWeb/WebDAV/SoWebDAVRenderer.m
improved WebDAV locking (send lock-token header)
[sope] / sope-appserver / NGObjWeb / WebDAV / SoWebDAVRenderer.m
1 /*
2   Copyright (C) 2000-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
22 #include "SoWebDAVRenderer.h"
23 #include "SoWebDAVValue.h"
24 #include "SoObject+SoDAV.h"
25 #include "EOFetchSpecification+SoDAV.h"
26 #include "NSException+HTTP.h"
27 #include <NGObjWeb/WOContext.h>
28 #include <NGObjWeb/WOResponse.h>
29 #include <NGObjWeb/WORequest.h>
30 #include <NGObjWeb/WOElement.h>
31 #include <SaxObjC/XMLNamespaces.h>
32 #include <NGExtensions/NSString+Ext.h>
33 #include "common.h"
34
35 /*
36   What HotMail uses for responses:
37     <?xml version="1.0" encoding="Windows-1252"?>
38     Headers:
39       Server:              Microsoft-IIS/5.0
40       X-Timestamp:         folders=1035823428, ACTIVE=1035813212
41       Client-Response-Num: 1
42       Client-Date:         <date>
43       Expires:             ...
44       P3P:                 BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo
45 */
46
47 #define XMLNS_INTTASK \
48 @"{http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/}"
49
50 @interface SoWebDAVRenderer(Privates)
51 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
52   inContext:(WOContext *)_ctx; 
53 @end
54
55 @implementation SoWebDAVRenderer
56
57 static NSDictionary *predefinedNamespacePrefixes = nil;
58 static NSTimeZone   *gmt         = nil;
59 static BOOL         debugOn      = NO;
60 static BOOL         formatOutput = NO;
61
62 + (void)initialize {
63   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
64   static BOOL didInit = NO;
65   if (didInit) return;
66   didInit = YES;
67   if (gmt == nil) 
68     gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
69
70   if (predefinedNamespacePrefixes == nil) {
71     predefinedNamespacePrefixes = 
72       [[ud objectForKey:@"SoPreferredNamespacePrefixes"] copy];
73   }
74   formatOutput = [ud boolForKey:@"SoWebDAVFormatOutput"];
75   
76   if ((debugOn = [ud boolForKey:@"SoRendererDebugEnabled"]))
77     NSLog(@"enabled debugging in SoWebDAVRenderer (SoRendererDebugEnabled)");
78 }
79
80 + (id)sharedRenderer {
81   static SoWebDAVRenderer *r = nil; // THREAD
82   if (r == nil) r = [[SoWebDAVRenderer alloc] init];
83   return r;
84 }
85
86 - (NSString *)preferredPrefixForNamespace:(NSString *)_uri {
87   return [predefinedNamespacePrefixes objectForKey:_uri];
88 }
89
90 /* key render entry-point */
91
92 - (void)_fixupResponse:(WOResponse *)_r inContext:(WOContext *)_ctx {
93   NSDate   *now;
94   NSString *nowHttpString;
95   id tmp;
96   
97   if ((tmp = [_r headerForKey:@"server"]) == nil) {
98     // TODO: add application name as primary name
99     [_r setHeader:@"SOPE 4.2/WebDAV" forKey:@"server"];
100   }
101   
102   [_r setHeader:@"close" forKey:@"connection"];
103   [_r setHeader:@"DAV"   forKey:@"Ms-Author-Via"];
104   
105   // what program uses that header ?
106   [_r setHeader:@"200 No error" forKey:@"X-Dav-Error"];
107
108   if ((tmp = [_r headerForKey:@"content-type"]) == nil)
109     [_r setHeader:@"text/xml" forKey:@"content-type"];
110     
111   now = [NSDate date];
112   nowHttpString = [now descriptionWithCalendarFormat:
113                          @"%a, %d %b %Y %H:%M:%S GMT"
114                        timeZone:gmt
115                        locale:nil];
116     
117   if ((tmp = [_r headerForKey:@"date"]) == nil)
118     [_r setHeader:nowHttpString forKey:@"date"];
119
120 #if 0 /* currently none of the clients allows zipping, retry later ... */
121   /* try zipping */
122   if ([_r shouldZipResponseToRequest:nil]) {
123     [self logWithFormat:@"zipping DAV result ..."];
124     [_r zipResponse];
125   }
126 #endif
127 }
128
129 - (NSString *)mimeTypeForData:(NSData *)_data inContext:(WOContext *)_ctx {
130   /* should check extension for MIME type */
131   return @"application/octet-stream";
132 }
133 - (NSString *)mimeTypeForString:(NSString *)_str inContext:(WOContext *)_ctx {
134   /* should check extension for MIME type */
135
136   if ([_str hasPrefix:@"<?xml"])
137     return @"text/xml; charset=\"utf-8\"";
138   if ([_str hasPrefix:@"<html"])
139     return @"text/html; charset=\"utf-8\"";
140   
141   return @"text/plain; charset=\"utf-8\"";
142 }
143
144 - (BOOL)renderObjectBodyResult:(id)_object inContext:(WOContext *)_ctx 
145   onlyHead:(BOOL)_onlyHead
146 {
147   WOResponse *r = [_ctx response];
148   NSString *tmp;
149   
150   /*
151     TODO: implement proper etag support. This probably implies that we need
152           to pass in some structure or store the etag in the context?
153           We cannot use davEntityTag on the input parameter, since this is
154           usually the plain object.
155   */
156   tmp = @"0"; // fallback, cannot use the thing above
157   [r setHeader:tmp forKey:@"ETag"]; // required for WebFolder PUTs
158   
159   if ([_object isKindOfClass:[NSData class]]) {
160     [r setHeader:[self mimeTypeForData:_object inContext:_ctx]
161        forKey:@"content-type"];
162     
163     [r setHeader:[NSString stringWithFormat:@"%d", [_object length]]
164        forKey:@"content-length"];
165     if (!_onlyHead) [r setContent:_object];
166     return YES;
167   }
168
169   if ([_object isKindOfClass:[NSString class]]) {
170     NSData *data;
171     
172     [r setHeader:[self mimeTypeForString:_object inContext:_ctx]
173        forKey:@"content-type"];
174     
175     data = [_object dataUsingEncoding:NSUTF8StringEncoding];
176     [r setHeader:[NSString stringWithFormat:@"%d", [data length]]
177        forKey:@"content-length"];
178     [r setContent:data];
179     return YES;
180   }
181   
182   if ([_object respondsToSelector:@selector(appendToResponse:inContext:)]) {
183     unsigned len;
184     NSData   *data;
185     
186     [_object appendToResponse:r inContext:_ctx];
187     
188     data = [r content];
189     if ([[r headerForKey:@"content-type"] length] == 0) {
190       [r setHeader:[self mimeTypeForData:data inContext:_ctx]
191          forKey:@"content-type"];
192     }
193     len = [data length];
194     if (_onlyHead) [r setContent:nil];
195     data = nil;
196     [r setHeader:[NSString stringWithFormat:@"%d", len]
197        forKey:@"content-length"];
198     return YES;
199   }
200   
201   [self logWithFormat:@"ERROR: don't know how to render: %@", _object];
202   return NO;
203 }
204
205 - (NSException *)renderObject:(id)_object inContext:(WOContext *)_ctx {
206   NSString *m;
207   unichar  c1;
208   BOOL ok;
209   
210   if ([_object isKindOfClass:[WOResponse class]]) {
211     if (_object != [_ctx response]) {
212       [self logWithFormat:@"response mismatch"];
213       return [NSException exceptionWithHTTPStatus:500 /* internal error */];
214     }
215     [self _fixupResponse:_object inContext:_ctx];
216     return nil;
217   }
218   
219   m = [[_ctx request] method];
220   if ([m length] == 0) {
221     return [NSException exceptionWithHTTPStatus:400 /* bad request */
222                         reason:@"missing method name!"];
223   }
224   c1 = [m characterAtIndex:0];
225
226   ok = NO;
227   switch (c1) {
228   case 'B':
229     if ([m isEqualToString:@"BPROPFIND"])
230       ok = [self renderSearchResult:_object inContext:_ctx];
231     break;
232   case 'C':
233     if ([m isEqualToString:@"COPY"]) {
234       ok = [self renderStatusResult:_object 
235                  withDefaultStatus:201 /* Created */
236                  inContext:_ctx];
237     }
238     break;
239   case 'D':
240     if ([m isEqualToString:@"DELETE"])
241       ok = [self renderDeleteResult:_object inContext:_ctx];
242     break;
243   case 'G':
244     if ([m isEqualToString:@"GET"])
245       ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:NO];
246     break;
247   case 'H':
248     if ([m isEqualToString:@"HEAD"])
249       ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:YES];
250     break;
251   case 'L':
252     if ([m isEqualToString:@"LOCK"])
253       ok = [self renderLockToken:_object inContext:_ctx];
254     break;
255   case 'M':
256     if ([m isEqualToString:@"MKCOL"])
257       ok = [self renderMkColResult:_object inContext:_ctx];
258     else if ([m isEqualToString:@"MOVE"]) {
259       ok = [self renderStatusResult:_object 
260                  withDefaultStatus:201 /* Created */
261                  inContext:_ctx];
262     }
263     break;
264   case 'O':
265     if ([m isEqualToString:@"OPTIONS"])
266       ok = [self renderOptions:_object inContext:_ctx];
267     break;
268   case 'P':
269     if ([m isEqualToString:@"PUT"])
270       ok = [self renderUploadResult:_object inContext:_ctx];
271     else if ([m isEqualToString:@"PROPFIND"])
272       ok = [self renderSearchResult:_object inContext:_ctx];
273     else if ([m isEqualToString:@"PROPPATCH"])
274       ok = [self renderPropPatchResult:_object inContext:_ctx];
275     else if ([m isEqualToString:@"POLL"])
276       ok = [self renderPollResult:_object inContext:_ctx];
277     break;
278   case 'S':
279     if ([m isEqualToString:@"SEARCH"])
280       ok = [self renderSearchResult:_object inContext:_ctx];
281     else if ([m isEqualToString:@"SUBSCRIBE"])
282       ok = [self renderSubscription:_object inContext:_ctx];
283     break;
284     
285   default:
286     ok = NO;
287     break;
288   }
289   
290   if (ok) [self _fixupResponse:[_ctx response] inContext:_ctx];
291   return ok
292     ? nil
293     : [NSException exceptionWithHTTPStatus:500 /* server error */];
294 }
295
296 - (BOOL)canRenderObject:(id)_object inContext:(WOContext *)_ctx {
297   if ([_object isKindOfClass:[NSException class]])
298     return NO;
299   return YES;
300 }
301
302 - (NSString *)stringForValue:(id)_value ofProperty:(NSString *)_prop 
303   prefixes:(NSDictionary *)_prefixes
304 {
305   /* seems like this is the default date value */
306   NSString *datefmt = @"%a, %d %b %Y %H:%M:%S GMT";
307   
308   if (_value == nil)
309     return nil;
310   if (![_value isNotNull])
311     return nil;
312   
313   /* special processing for some properties */
314   
315   if ([_prop isEqualToString:@"{DAV:}resourcetype"]) {
316     _value = [_value stringValue];
317     if ([_value length] == 0) return nil;
318     
319     return [NSString stringWithFormat:@"<%@:%@/>",
320                        [_prefixes objectForKey:@"DAV:"], _value];
321   }
322   else if ([_prop isEqualToString:@"{DAV:}creationdate"])
323     datefmt = @"%Y-%m-%dT%H:%M:%S%zZ";
324
325   /* special processing for some properties  */
326   
327   // TODO: move this to user-level code ! 
328   //   HH: what is that ? it does not do anything anyway ?
329   if ([_prop hasPrefix:XMLNS_INTTASK]) {
330     if ([_prop hasSuffix:@"}0x00008102"]) {
331     }
332   }
333   
334   /* special processing for some classes */
335   
336   if ([_value isKindOfClass:[NSString class]])
337     return [_value stringByEscapingXMLString];
338   
339   if ([_value isKindOfClass:[NSNumber class]])
340     return [_value stringValue];
341   
342   if ([_value isKindOfClass:[NSDate class]]) {
343     return [_value descriptionWithCalendarFormat:datefmt
344                    timeZone:gmt
345                    locale:nil];
346   }
347   
348   return [[_value stringValue] stringByEscapingXMLString];
349 }
350
351 - (NSString *)baseURLForContext:(WOContext *)_ctx {
352   /*
353     Note: Evolution doesn't correctly transfer the "Host:" header, it
354     misses the port argument :-(
355   */
356   NSString  *baseURL;
357   WORequest *rq;
358   NSString  *hostport;
359   id tmp;
360   
361   rq = [_ctx request];
362   
363   if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
364     hostport = tmp;
365     if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
366       hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
367   }
368   else if ((tmp = [rq headerForKey:@"host"]))
369     hostport = tmp;
370   else
371     hostport = [[NSHost currentHost] name];
372   
373   baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
374   return baseURL;
375 }
376
377 - (NSString *)tidyHref:(id)href baseURL:(id)baseURL {
378   href = [href stringValue];
379   if (href == nil) {
380     if (debugOn) {
381       [self logWithFormat:
382               @"WARNING: using baseURL for href, "
383               @"entry did not provide a URL: %@", baseURL];
384     }
385     href = [baseURL stringValue];
386   }
387   else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
388     // TODO: use "real" URL processing
389     href = [baseURL stringByAppendingPathComponent:href];
390   }
391   return href;
392 }
393 - (id)tidyStatus:(id)stat {
394   if (stat == nil)
395     stat = @"HTTP/1.1 200 OK";
396   else if ([stat isKindOfClass:[NSException class]]) {
397     int i;
398     
399     if ((i = [stat httpStatus]) > 0)
400       stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
401     else {
402       stat = [(NSException *)stat name];
403       stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
404     }
405   }
406   return stat;
407 }
408
409 - (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx 
410   namesOnly:(BOOL)_namesOnly
411   attributes:(NSArray *)_attrs
412   propertyMap:(NSDictionary *)_propMap
413   baseURL:(NSString *)baseURL
414   tagToPrefix:(NSDictionary *)extNameCache
415   nsToPrefix:(NSDictionary *)nsToPrefix
416 {
417   /* Note: the entry is an NSArray in case _namesOnly is requested! */
418   // TODO: use -valueForKey: to improve NSNull handling ?
419   WOResponse   *r;
420   NSEnumerator *keys;
421   NSString     *key;
422   id   href = nil;
423   id   stat = nil;
424   BOOL isBrief;
425   
426   r = [_ctx response];
427   isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
428   
429   if (debugOn) {
430     [self debugWithFormat:@"    render entry: 0x%08X<%@>%s%s",
431           entry, NSStringFromClass([entry class]),
432           isBrief    ? " brief"      : "",
433           _namesOnly ? " names-only" : ""];
434   }
435
436   /* we do not map these DAV properties because they are very special */
437   if (!_namesOnly) {
438     if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
439       if ((key = [_propMap objectForKey:@"{DAV:}href"])) {
440         if ((href = [entry valueForKey:key]) == nil) {
441           if (debugOn) {
442             [self debugWithFormat:
443                   @"WARNING: no value for {DAV:}href key '%@': %@",
444                     key, entry];
445         }
446         }
447       }
448       else if (debugOn) {
449         [self debugWithFormat:
450               @"WARNING: no key for {DAV:}href in property map !"];
451       }
452     }
453     if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
454       if ((key = [_propMap objectForKey:@"{DAV:}status"]))
455         stat = [entry valueForKey:key];
456     }
457
458     /* tidy href */
459     href = [self tidyHref:href baseURL:baseURL];
460     
461     /* tidy status */
462     stat = [self tidyStatus:stat];
463   }
464   else { /* propnames only */
465     href = [baseURL stringValue];
466     stat = @"HTTP/1.1 200 OK";
467   }
468   
469   if (debugOn) {
470     [self debugWithFormat:@"    status: %@", stat];
471     [self debugWithFormat:@"    href:   %@", href];
472   }
473   
474   /* generate */
475   [r appendContentString:@"<D:response>"];
476   if (formatOutput) [r appendContentCharacter:'\n'];
477   
478   if (href) {
479     [r appendContentString:@"<D:href>"];
480     /*
481       TODO: need to find out what is appropriate! While Cadaver and ZideLook
482             (both Neon+Expat) seem to be fine with this, OSX reports invalid
483             characters (displayed as '#') for umlauts.
484             It might be that we are supposed to use *URL* escaping in any 
485             case! (notably entering a directory with an umlaut doesn't seem
486             to work in Cadaver either because of a URL mismatch!)
487       Note: we cannot apply URL encoding in this place, because it will encode
488             all URL special chars ... where are URLs escaped?
489       Note: we always need to apply XML escaping (even though higher-level
490             characters might be already encoded)!
491     */
492     [r appendContentXMLString:[href stringValue]];
493     [r appendContentString:@"</D:href>"];
494     if (formatOutput) [r appendContentCharacter:'\n'];
495   }
496       
497   [r appendContentString:@"<D:propstat>"];
498   if (stat) {
499     [r appendContentString:@"<D:status>"];
500     [r appendContentXMLString:[stat stringValue]];
501     [r appendContentString:@"</D:status>"];
502   }
503   
504   [r appendContentString:@"<D:prop>"];
505   if (formatOutput) [r appendContentCharacter:'\n'];
506   
507   /* now the properties */
508   
509   keys = [_attrs objectEnumerator] ;
510   while ((key = [keys nextObject])) {
511     NSString *extName;
512     NSString *okey;
513     id value;
514     
515 #if 0 /* this filter probably doesn't make sense ? */
516     /* filter out predefined props */
517     if ([key isEqualToString:@"{DAV:}href"])   continue;
518     if ([key isEqualToString:@"{DAV:}status"]) continue;
519 #endif
520     
521     extName = [extNameCache objectForKey:key];
522     
523     if (_namesOnly) {
524       [r appendContentCharacter:'<'];
525       [r appendContentString:extName];
526       [r appendContentString:@"/>"];
527       if (formatOutput) [r appendContentCharacter:'\n'];
528       continue;
529     }
530     
531     // TODO: we should support property status (eg encode 404 on NSNull)
532       
533     if ((okey = [_propMap objectForKey:key]) == nil)
534       okey = key;
535       
536     if ([key isEqualToString:@"{DAV:}href"])
537       value = href;
538     else
539       value = [entry valueForKey:okey];
540     
541     if ([value isNotNull]) {
542       NSString *s;
543         
544       if ([value isKindOfClass:[SoWebDAVValue class]]) {
545           s = [value stringForTag:key rawName:extName
546                      inContext:_ctx prefixes:nsToPrefix];
547           [r appendContentString:s];
548       }
549       else {
550           [r appendContentCharacter:'<'];
551           [r appendContentString:extName];
552           [r appendContentCharacter:'>'];
553           
554           s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
555           [r appendContentString:s];
556           
557           [r appendContentString:@"</"];
558           [r appendContentString:extName];
559           [r appendContentString:@">"];
560           if (formatOutput) [r appendContentCharacter:'\n'];
561       }
562       continue;
563     }
564     
565     if (!isBrief) { 
566       /* 
567          Not sure whether this is correct, do we need to encode null attrs?
568          Seems like Evo gets confused on that.
569          TODO: probably add a 404 property status for that!
570       */
571       [r appendContentCharacter:'<'];
572       [r appendContentString:extName];
573       [r appendContentString:@"/>"];
574       if (formatOutput) [r appendContentCharacter:'\n'];
575     }
576   }
577       
578   [r appendContentString:@"</D:prop></D:propstat></D:response>"];
579   if (formatOutput) [r appendContentCharacter:'\n'];
580 }
581
582 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
583   tagToExtName:(NSMutableDictionary *)_tagToExtName
584   nsToPrefix:(NSMutableDictionary *)_nsToPrefix
585 {
586   unichar autoPrefix[2] = { ('a' - 1), 0 };
587   NSEnumerator *e;
588   NSString     *fqn;
589   
590   e = [_attrs objectEnumerator];
591   while ((fqn = [e nextObject])) {
592     NSString *ns, *localName, *prefix, *extName;
593     
594     if ([_tagToExtName objectForKey:fqn]) continue;
595     
596     if (![fqn xmlIsFQN]) {
597       /* hm, no namespace given :-(, using DAV */
598       ns        = @"DAV:";
599       localName = fqn;
600     }
601     else {
602       ns        = [fqn xmlNamespaceURI];
603       localName = [fqn xmlLocalName];
604     }
605     
606     if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
607       if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
608         (autoPrefix[0])++;
609         prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
610       }
611       [_nsToPrefix setObject:prefix forKey:ns];
612     }
613     
614     extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
615     [_tagToExtName setObject:extName forKey:fqn];
616   }
617 }
618
619 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
620   NSMutableString *ms;
621   NSEnumerator *nse;
622   NSString *ns;
623   
624   ms = [NSMutableString stringWithCapacity:256];
625   nse = [_nsToPrefix keyEnumerator];
626   while ((ns = [nse nextObject])) {
627     [ms appendString:@" xmlns:"];
628     [ms appendString:[_nsToPrefix objectForKey:ns]];
629     [ms appendString:@"=\""];
630     [ms appendString:ns];
631     [ms appendString:@"\""];
632   }
633   return ms;
634 }
635
636 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx 
637   namesOnly:(BOOL)_namesOnly
638   attributes:(NSArray *)_attrs
639   propertyMap:(NSDictionary *)_propMap
640 {
641   NSMutableDictionary *extNameCache = nil;
642   NSMutableDictionary *nsToPrefix   = nil;
643   NSAutoreleasePool   *pool;
644   WOResponse *r;
645   unsigned   entryCount;
646   
647   pool = [[NSAutoreleasePool alloc] init];
648   r = [_ctx response];
649   
650   if (![_entries isKindOfClass:[NSEnumerator class]]) {
651     if ([_entries isKindOfClass:[NSArray class]]) {
652       [self debugWithFormat:@"  render %i entries", [_entries count]];
653       _entries = [_entries objectEnumerator];
654     }
655     else {
656       [self debugWithFormat:@"  render a single object ..."];
657       _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
658     }
659   }
660   
661   /* collect used namespaces */
662   
663   nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
664   [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
665   
666   /* 
667      the extNameCache is used to map fully qualified tag names to their
668      prefixed external representation 
669   */
670   extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
671   
672   // TODO: only walk attrs, if available
673   /*
674     Walk all attributes of all entries to collect names. We might be able
675     to take a look at just the first record if it is guaranteed, that all
676     records have all properties (even if the value is NSNull) ?
677   */
678   [self buildPrefixMapForAttributes:_attrs
679         tagToExtName:extNameCache
680         nsToPrefix:nsToPrefix];
681   
682   /* generate multistatus */
683    
684   [r setStatus:207 /* multistatus */];
685   [r setHeader:@"no-cache" forKey:@"pragma"];
686   [r setHeader:@"no-cache" forKey:@"cache-control"];
687   
688   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
689   [r appendContentString:@"<D:multistatus"];
690   [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
691   [r appendContentString:@">"];
692   if (formatOutput) [r appendContentCharacter:'\n'];
693   {
694     NSString *baseURL;
695     NSString *range;
696     id entry;
697     
698     baseURL = [self baseURLForContext:_ctx];
699     [self debugWithFormat:@"  baseURL: %@", baseURL];
700     
701     entryCount = 0; /* Note: this will clash with streamed output later */
702     while ((entry = [_entries nextObject])) {
703       [self renderSearchResultEntry:entry inContext:_ctx
704             namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
705             baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
706       entryCount++;
707     }
708     [self debugWithFormat:@"  rendered %i entries", entryCount];
709     
710     /*
711       If we got a "rows" range header, we report back the actual rows
712       delivered. Since we do not really support ranges in the moment,
713       we just report all rows ... 
714       TODO: support for row ranges.
715     */
716     if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
717       /* sample: "Content-Range: rows 0-143; total=144" */
718       NSString *v;
719       
720       v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i", 
721                               entryCount>0?(entryCount - 1):0, entryCount];
722       [r setHeader:v forKey:@"content-range"];
723       [v release];
724     }
725   }
726   [r appendContentString:@"</D:multistatus>"];
727   if (formatOutput) [r appendContentCharacter:'\n'];
728   
729   [pool release];
730 }
731
732 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
733   EOFetchSpecification *fs;
734   NSDictionary *propMap;
735   
736   if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
737     return NO;
738   
739   if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
740     propMap = [_object davAttributeMapInContext:_ctx];
741
742   if (debugOn) {
743     [self debugWithFormat:@"render search result 0x%08X<%@>",
744             _object, NSStringFromClass([_object class])];
745   }
746   
747   [self renderSearchResult:_object inContext:_ctx
748         namesOnly:[fs queryWebDAVPropertyNamesOnly]
749         attributes:[fs selectedWebDAVPropertyNames]
750         propertyMap:propMap];
751   
752   if (debugOn) 
753     [self debugWithFormat:@"finished rendering."];
754   return YES;
755 }
756
757 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
758   /* TODO: this is a fake ! */
759   WOResponse *r;
760   
761   if (_object == nil) return NO;
762   
763   r = [_ctx response];
764   
765   [r setStatus:200];
766   [r setContentEncoding:NSUTF8StringEncoding];
767   [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
768   [r setHeader:[_object stringValue]          forKey:@"lock-token"];
769   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
770   [r appendContentString:@"<D:prop xmlns:D=\"DAV:\">"];
771   [r appendContentString:@"<D:lockdiscovery>"];
772   [r appendContentString:@"<D:activelock>"];
773   if (formatOutput) [r appendContentCharacter:'\n'];
774   
775   /* this is the href of the lock, not of the locked resource */
776   [r appendContentString:@"<D:locktoken><D:href>"];
777   [r appendContentString:[_object stringValue]];
778   [r appendContentString:@"</D:href></D:locktoken>"];
779   if (formatOutput) [r appendContentCharacter:'\n'];
780   
781   // TODO: locktype,  eg <D:locktype><D:write/></D:locktype>
782   // TODO: lockscope, eg <D:lockscope><D:exclusive/></D:lockscope>
783   // TODO: depth,     eg <D:depth>Infinitiy</D:depth>
784   // TODO: owner,     eg <D:owner><D:href>...</D:href></D:owner>
785   // TODO: timeout,   eg <D:timeout>Second-604800</D:timeout>
786   
787   [r appendContentString:@"</D:activelock>"];
788   [r appendContentString:@"</D:lockdiscovery>"];
789   [r appendContentString:@"</D:prop>"];
790   if (formatOutput) [r appendContentCharacter:'\n'];
791   return YES;
792 }
793
794 - (BOOL)renderOptions:(id)_object inContext:(WOContext *)_ctx {
795   WOResponse *r = [_ctx response];
796   
797   [r setStatus:200];
798   [r setHeader:@"1,2" forKey:@"DAV"]; // TODO: select protocol level
799   //[r setHeader:@"" forKey:@"Etag"]; 
800   [r setHeader:[_object componentsJoinedByString:@", "] forKey:@"allow"];
801   return YES;
802 }
803
804 - (BOOL)renderSubscription:(id)_object inContext:(WOContext *)_ctx {
805   // TODO: this is fake, mirrors request
806   WOResponse *r = [_ctx response];
807   WORequest  *rq;
808   NSString   *callback;
809   NSString   *notificationType;
810   NSString   *lifetime;
811   
812   rq                = [_ctx request];
813   callback          = [rq headerForKey:@"call-back"];
814   notificationType  = [rq headerForKey:@"notification-type"];
815   lifetime          = [rq headerForKey:@"subscription-lifetime"];
816   
817   [r setStatus:200];
818   if (notificationType)
819     [r setHeader:notificationType forKey:@"notification-type"];
820   if (lifetime)
821     [r setHeader:lifetime         forKey:@"subscription-lifetime"];
822   if (callback)
823     [r setHeader:callback         forKey:@"callback"];
824   [r setHeader:[self baseURLForContext:_ctx] forKey:@"content-location"];
825   [r setHeader:_object forKey:@"subscription-id"];
826   return YES;
827 }
828
829 - (BOOL)renderPropPatchResult:(id)_object inContext:(WOContext *)_ctx {
830   NSMutableDictionary *extNameCache = nil;
831   NSMutableDictionary *nsToPrefix   = nil;
832   WOResponse *r = [_ctx response];
833   
834   if (_object == nil) return NO;
835   
836   nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
837   [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
838   extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
839   [self buildPrefixMapForAttributes:_object
840         tagToExtName:extNameCache
841         nsToPrefix:nsToPrefix];
842   
843   [r setStatus:207 /* multistatus */];
844   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
845   [r appendContentString:@"<D:multistatus"];
846   [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
847   [r appendContentString:@">"];
848   if (formatOutput) [r appendContentCharacter:'\n'];
849   
850   [r appendContentString:@"<D:response>"];
851   if (formatOutput) [r appendContentCharacter:'\n'];
852   [r appendContentString:@"<D:href>"];
853   [r appendContentString:[[_ctx request] uri]];
854   [r appendContentString:@"</D:href>"];
855   if (formatOutput) [r appendContentCharacter:'\n'];
856   [r appendContentString:@"<D:propstat><D:status>HTTP/1.1 200 OK</D:status>"];
857   if (formatOutput) [r appendContentCharacter:'\n'];
858   [r appendContentString:@"<D:prop>"];
859   if (formatOutput) [r appendContentCharacter:'\n'];
860   
861   /* encode properties */
862   {
863     NSEnumerator *e;
864     NSString *tag;
865     
866     e = [_object objectEnumerator];
867     while ((tag = [e nextObject])) {
868       NSString *extName;
869       
870       extName = [extNameCache objectForKey:tag];
871       [r appendContentCharacter:'<'];
872       [r appendContentString:extName];
873       [r appendContentString:@"/>"];
874       if (formatOutput) [r appendContentCharacter:'\n'];
875     }
876   }
877   
878   [r appendContentString:@"</D:prop></D:propstat></D:response>"];
879   if (formatOutput) [r appendContentCharacter:'\n'];
880   [r appendContentString:@"</D:multistatus>"];
881   if (formatOutput) [r appendContentCharacter:'\n'];
882   
883   return YES;
884 }
885
886 - (BOOL)renderDeleteResult:(id)_object inContext:(WOContext *)_ctx {
887   WOResponse *r = [_ctx response];
888   
889   if (_object == nil || [_object boolValue]) {
890     [r setStatus:204 /* no content */];
891     //[r appendContentString:@"object was deleted."];
892     return YES;
893   }
894   
895   if ([_object isKindOfClass:[NSNumber class]]) {
896     [r setStatus:[_object intValue]];
897     if ([r status] != 204 /* No Content */)
898       [r appendContentString:@"object could not be deleted."];
899   }
900   else {
901     [r setStatus:500 /* server error */];
902     [r appendContentString:@"object could not be deleted. reason: "];
903     [r appendContentHTMLString:[_object stringValue]];
904   }
905   return YES;
906 }
907
908 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
909   inContext:(WOContext *)_ctx 
910 {
911   WOResponse *r = [_ctx response];
912   
913   if (_object == nil) {
914     [r setStatus:_defStatus /* no content */];
915     return YES;
916   }
917   
918   if ([_object isKindOfClass:[NSNumber class]]) {
919     if ([_object intValue] < 100) {
920       [r setStatus:_defStatus /* no content */];
921       return YES;
922     }
923     else {
924       [r setStatus:[_object intValue]];
925     }
926   }
927   else {
928     [r setStatus:_defStatus /* no content */];
929   }
930   return YES;
931 }
932 - (BOOL)renderUploadResult:(id)_object inContext:(WOContext *)_ctx {
933   WOResponse *r = [_ctx response];
934   
935   if (_object == nil) {
936     [r setStatus:204 /* no content */];
937     return YES;
938   }
939   
940   if ([_object isKindOfClass:[NSNumber class]]) {
941     if ([_object intValue] < 100) {
942       [r setStatus:204 /* no content */];
943       return YES;
944     }
945     
946     [r setStatus:[_object intValue]];
947     if ([_object intValue] >= 300) {
948       [r setHeader:@"text/html" forKey:@"content-type"];
949       [r appendContentString:@"object could not be stored."];
950     }
951     
952     return YES;
953   }
954   
955   [r setStatus:204 /* no content */];
956   return YES;
957 }
958
959 - (void)renderPollList:(NSArray *)_sids code:(int)_code
960   inContext:(WOContext *)_ctx 
961 {
962   WOResponse   *r = [_ctx response];
963   NSEnumerator *e;
964   NSString *sid;
965   NSString *href;
966   
967   if ([_sids count] == 0) return;
968   href = [self baseURLForContext:_ctx];
969
970   [r appendContentString:@"<D:response>"];
971   if (formatOutput) [r appendContentCharacter:'\n'];
972   [r appendContentString:@"<D:href>"];
973   [r appendContentString:href];
974   [r appendContentString:@"</D:href>"];
975   if (formatOutput) [r appendContentCharacter:'\n'];
976   
977   [r appendContentString:@"<D:status>HTTP/1.1 "];
978   if (_code == 200)
979     [r appendContentString:@"200 OK"];
980   else if (_code == 204)
981     [r appendContentString:@"204 No Content"];
982   else {
983     NSString *s;
984     s = [NSString stringWithFormat:@"%i code%i"];
985     [r appendContentString:s];
986   }
987   [r appendContentString:@"</D:status>"];
988   if (formatOutput) [r appendContentCharacter:'\n'];
989   
990   [r appendContentString:@"<E:subscriptionID>"];
991   if (formatOutput) [r appendContentCharacter:'\n'];
992   e = [_sids objectEnumerator];
993   while ((sid = [e nextObject])) {
994     if (formatOutput) [r appendContentString:@"  "];
995     [r appendContentString:@"<li>"];
996     [r appendContentString:sid];
997     [r appendContentString:@"</li>"];
998     if (formatOutput) [r appendContentCharacter:'\n'];
999   }
1000   [r appendContentString:@"</E:subscriptionID>"];
1001   if (formatOutput) [r appendContentCharacter:'\n'];
1002   [r appendContentString:@"</D:response>"];
1003   if (formatOutput) [r appendContentCharacter:'\n'];
1004 }
1005
1006 - (BOOL)renderPollResult:(id)_object inContext:(WOContext *)_ctx {
1007   WOResponse *r = [_ctx response];
1008   
1009   if (_object == nil) {
1010     [r setStatus:204 /* no content */];
1011     return YES;
1012   }
1013
1014   if ([_object isKindOfClass:[NSDictionary class]]) {
1015     NSArray  *pending, *inactive;
1016     
1017     pending  = [_object objectForKey:@"pending"];
1018     inactive = [_object objectForKey:@"inactive"];
1019     
1020     [r setStatus:207 /* Multi-Status */];
1021     [r setHeader:@"text/xml" forKey:@"content-type"];
1022
1023     [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1024     [r appendContentString:@"<D:multistatus "];
1025     [r appendContentString:@" xmlns:D=\"DAV:\""];
1026     [r appendContentString:
1027          @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1028     [r appendContentString:@">"];
1029     if (formatOutput) [r appendContentCharacter:'\n'];
1030     
1031     [self renderPollList:pending  code:200 inContext:_ctx];
1032     [self renderPollList:inactive code:204 inContext:_ctx];
1033     
1034     [r appendContentString:@"</D:multistatus>"];
1035     if (formatOutput) [r appendContentCharacter:'\n'];
1036   }
1037   else if ([_object isKindOfClass:[NSArray class]]) {
1038     [r setStatus:207 /* Multi-Status */];
1039     [r setHeader:@"text/xml" forKey:@"content-type"];
1040
1041     [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1042     [r appendContentString:@"<D:multistatus "];
1043     [r appendContentString:@" xmlns:D=\"DAV:\""];
1044     [r appendContentString:
1045          @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1046     [r appendContentString:@">"];
1047     if (formatOutput) [r appendContentCharacter:'\n'];
1048     
1049     [self renderPollList:_object code:200 inContext:_ctx];
1050     
1051     [r appendContentString:@"</D:multistatus>"];
1052     if (formatOutput) [r appendContentCharacter:'\n'];
1053   }
1054   else {
1055     [r setStatus:204 /* no content */];
1056     //[r appendContentString:@"object was stored."];
1057   }
1058   return YES;
1059 }
1060
1061 - (BOOL)renderMkColResult:(id)_object inContext:(WOContext *)_ctx {
1062   WOResponse *r = [_ctx response];
1063   
1064   if (_object == nil || [_object boolValue]) {
1065     [r setStatus:201 /* Created */];
1066     return YES;
1067   }
1068   
1069   if ([_object isKindOfClass:[NSNumber class]]) {
1070     [r setStatus:[_object intValue]];
1071     [r appendContentString:@"object could not be created."];
1072   }
1073   else {
1074     [r setStatus:500 /* server error */];
1075     [r appendContentString:@"object could not be deleted. reason: "];
1076     [r appendContentHTMLString:[_object stringValue]];
1077   }
1078   return YES;
1079 }
1080
1081 /* debugging */
1082
1083 - (BOOL)isDebuggingEnabled {
1084   return debugOn;
1085 }
1086
1087 @end /* SoWebDAVRenderer */