]> err.no Git - sope/blob - sope-appserver/NGObjWeb/WebDAV/SoWebDAVRenderer.m
minor url optimization
[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   NSString *href;
379   
380   href = [_href stringValue];
381   
382   if (debugOn) {
383     // TODO: this happens if we access using Goliath
384     if ([href hasPrefix:@"http:/"] && ![href hasPrefix:@"http://"]) {
385       [self logWithFormat:@"BROKEN URL: %@", _href];
386       return nil;
387     }
388   }
389   
390   if (href == nil) {
391     if (debugOn) {
392       [self logWithFormat:
393               @"WARNING: using baseURL for href, "
394               @"entry did not provide a URL: %@", baseURL];
395     }
396     href = [baseURL stringValue];
397   }
398   else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
399     // TODO: use "real" URL processing
400     href = [baseURL stringByAppendingPathComponent:href];
401   }
402   return href;
403 }
404 - (id)tidyStatus:(id)stat {
405   if (stat == nil)
406     stat = @"HTTP/1.1 200 OK";
407   else if ([stat isKindOfClass:[NSException class]]) {
408     int i;
409     
410     if ((i = [stat httpStatus]) > 0)
411       stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
412     else {
413       stat = [(NSException *)stat name];
414       stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
415     }
416   }
417   return stat;
418 }
419
420 - (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx 
421   namesOnly:(BOOL)_namesOnly
422   attributes:(NSArray *)_attrs
423   propertyMap:(NSDictionary *)_propMap
424   baseURL:(NSString *)baseURL
425   tagToPrefix:(NSDictionary *)extNameCache
426   nsToPrefix:(NSDictionary *)nsToPrefix
427 {
428   /* Note: the entry is an NSArray in case _namesOnly is requested! */
429   // TODO: use -valueForKey: to improve NSNull handling ?
430   WOResponse   *r;
431   NSEnumerator *keys;
432   NSString     *key;
433   id   href = nil;
434   id   stat = nil;
435   BOOL isBrief;
436   
437   r = [_ctx response];
438   isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
439   
440   if (debugOn) {
441     [self debugWithFormat:@"    render entry: 0x%08X<%@>%s%s",
442           entry, NSStringFromClass([entry class]),
443           isBrief    ? " brief"      : "",
444           _namesOnly ? " names-only" : ""];
445   }
446
447   /* we do not map these DAV properties because they are very special */
448   if (!_namesOnly) {
449     if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
450       if ((key = [_propMap objectForKey:@"{DAV:}href"]) != nil) {
451         if ((href = [entry valueForKey:key]) == nil) {
452           if (debugOn) {
453             [self debugWithFormat:
454                   @"WARNING: no value for {DAV:}href key '%@': %@",
455                     key, entry];
456         }
457         }
458       }
459       else if (debugOn) {
460         [self debugWithFormat:
461               @"WARNING: no key for {DAV:}href in property map !"];
462       }
463     }
464     if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
465       if ((key = [_propMap objectForKey:@"{DAV:}status"]))
466         stat = [entry valueForKey:key];
467     }
468
469     /* tidy href */
470     href = [self tidyHref:href baseURL:baseURL];
471     
472     /* tidy status */
473     stat = [self tidyStatus:stat];
474   }
475   else { /* propnames only */
476     href = [baseURL stringValue];
477     stat = @"HTTP/1.1 200 OK";
478   }
479   
480   if (debugOn) {
481     [self debugWithFormat:@"    status: %@", stat];
482     [self debugWithFormat:@"    href:   %@", href];
483   }
484   
485   /* generate */
486   [r appendContentString:@"<D:response>"];
487   if (formatOutput) [r appendContentCharacter:'\n'];
488   
489   if ([href isNotNull]) {
490     [r appendContentString:@"<D:href>"];
491     /*
492       TODO: need to find out what is appropriate! While Cadaver and ZideLook
493             (both Neon+Expat) seem to be fine with this, OSX reports invalid
494             characters (displayed as '#') for umlauts.
495             It might be that we are supposed to use *URL* escaping in any 
496             case! (notably entering a directory with an umlaut doesn't seem
497             to work in Cadaver either because of a URL mismatch!)
498       Note: we cannot apply URL encoding in this place, because it will encode
499             all URL special chars ... where are URLs escaped?
500       Note: we always need to apply XML escaping (even though higher-level
501             characters might be already encoded)!
502     */
503     [r appendContentXMLString:[href stringValue]];
504     [r appendContentString:@"</D:href>"];
505     if (formatOutput) [r appendContentCharacter:'\n'];
506   }
507   else {
508     [self logWithFormat:
509             @"WARNING: WebDAV result entry has no valid href: %@", entry];
510   }
511   
512   [r appendContentString:@"<D:propstat>"];
513   if (stat) {
514     [r appendContentString:@"<D:status>"];
515     [r appendContentXMLString:[stat stringValue]];
516     [r appendContentString:@"</D:status>"];
517   }
518   
519   [r appendContentString:@"<D:prop>"];
520   if (formatOutput) [r appendContentCharacter:'\n'];
521   
522   /* now the properties */
523   
524   keys = [_attrs objectEnumerator] ;
525   while ((key = [keys nextObject])) {
526     NSString *extName;
527     NSString *okey;
528     id value;
529     
530 #if 0 /* this filter probably doesn't make sense ? */
531     /* filter out predefined props */
532     if ([key isEqualToString:@"{DAV:}href"])   continue;
533     if ([key isEqualToString:@"{DAV:}status"]) continue;
534 #endif
535     
536     extName = [extNameCache objectForKey:key];
537     
538     if (_namesOnly) {
539       [r appendContentCharacter:'<'];
540       [r appendContentString:extName];
541       [r appendContentString:@"/>"];
542       if (formatOutput) [r appendContentCharacter:'\n'];
543       continue;
544     }
545     
546     // TODO: we should support property status (eg encode 404 on NSNull)
547       
548     if ((okey = [_propMap objectForKey:key]) == nil)
549       okey = key;
550       
551     if ([key isEqualToString:@"{DAV:}href"])
552       value = href;
553     else
554       value = [entry valueForKey:okey];
555     
556     if ([value isNotNull]) {
557       NSString *s;
558         
559       if ([value isKindOfClass:[SoWebDAVValue class]]) {
560           s = [value stringForTag:key rawName:extName
561                      inContext:_ctx prefixes:nsToPrefix];
562           [r appendContentString:s];
563       }
564       else {
565           [r appendContentCharacter:'<'];
566           [r appendContentString:extName];
567           [r appendContentCharacter:'>'];
568           
569           s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
570           [r appendContentString:s];
571           
572           [r appendContentString:@"</"];
573           [r appendContentString:extName];
574           [r appendContentString:@">"];
575           if (formatOutput) [r appendContentCharacter:'\n'];
576       }
577       continue;
578     }
579     
580     if (!isBrief) { 
581       /* 
582          Not sure whether this is correct, do we need to encode null attrs?
583          Seems like Evo gets confused on that.
584          TODO: probably add a 404 property status for that!
585       */
586       [r appendContentCharacter:'<'];
587       [r appendContentString:extName];
588       [r appendContentString:@"/>"];
589       if (formatOutput) [r appendContentCharacter:'\n'];
590     }
591   }
592       
593   [r appendContentString:@"</D:prop></D:propstat></D:response>"];
594   if (formatOutput) [r appendContentCharacter:'\n'];
595 }
596
597 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
598   tagToExtName:(NSMutableDictionary *)_tagToExtName
599   nsToPrefix:(NSMutableDictionary *)_nsToPrefix
600 {
601   unichar autoPrefix[2] = { ('a' - 1), 0 };
602   NSEnumerator *e;
603   NSString     *fqn;
604   
605   e = [_attrs objectEnumerator];
606   while ((fqn = [e nextObject])) {
607     NSString *ns, *localName, *prefix, *extName;
608     
609     if ([_tagToExtName objectForKey:fqn]) continue;
610     
611     if (![fqn xmlIsFQN]) {
612       /* hm, no namespace given :-(, using DAV */
613       ns        = @"DAV:";
614       localName = fqn;
615     }
616     else {
617       ns        = [fqn xmlNamespaceURI];
618       localName = [fqn xmlLocalName];
619     }
620     
621     if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
622       if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
623         (autoPrefix[0])++;
624         prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
625       }
626       [_nsToPrefix setObject:prefix forKey:ns];
627     }
628     
629     extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
630     [_tagToExtName setObject:extName forKey:fqn];
631   }
632 }
633
634 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
635   NSMutableString *ms;
636   NSEnumerator *nse;
637   NSString *ns;
638   
639   ms = [NSMutableString stringWithCapacity:256];
640   nse = [_nsToPrefix keyEnumerator];
641   while ((ns = [nse nextObject])) {
642     [ms appendString:@" xmlns:"];
643     [ms appendString:[_nsToPrefix objectForKey:ns]];
644     [ms appendString:@"=\""];
645     [ms appendString:ns];
646     [ms appendString:@"\""];
647   }
648   return ms;
649 }
650
651 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx 
652   namesOnly:(BOOL)_namesOnly
653   attributes:(NSArray *)_attrs
654   propertyMap:(NSDictionary *)_propMap
655 {
656   NSMutableDictionary *extNameCache = nil;
657   NSMutableDictionary *nsToPrefix   = nil;
658   NSAutoreleasePool   *pool;
659   WOResponse *r;
660   unsigned   entryCount;
661   
662   pool = [[NSAutoreleasePool alloc] init];
663   r = [_ctx response];
664   
665   if (![_entries isKindOfClass:[NSEnumerator class]]) {
666     if ([_entries isKindOfClass:[NSArray class]]) {
667       [self debugWithFormat:@"  render %i entries", [_entries count]];
668       _entries = [_entries objectEnumerator];
669     }
670     else {
671       [self debugWithFormat:@"  render a single object ..."];
672       _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
673     }
674   }
675   
676   /* collect used namespaces */
677   
678   nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
679   [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
680   
681   /* 
682      the extNameCache is used to map fully qualified tag names to their
683      prefixed external representation 
684   */
685   extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
686   
687   // TODO: only walk attrs, if available
688   /*
689     Walk all attributes of all entries to collect names. We might be able
690     to take a look at just the first record if it is guaranteed, that all
691     records have all properties (even if the value is NSNull) ?
692   */
693   [self buildPrefixMapForAttributes:_attrs
694         tagToExtName:extNameCache
695         nsToPrefix:nsToPrefix];
696   
697   /* generate multistatus */
698    
699   [r setStatus:207 /* multistatus */];
700   [r setHeader:@"no-cache" forKey:@"pragma"];
701   [r setHeader:@"no-cache" forKey:@"cache-control"];
702   
703   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
704   [r appendContentString:@"<D:multistatus"];
705   [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
706   [r appendContentString:@">"];
707   if (formatOutput) [r appendContentCharacter:'\n'];
708   {
709     NSString *baseURL;
710     NSString *range;
711     id entry;
712     
713     baseURL = [self baseURLForContext:_ctx];
714     [self debugWithFormat:@"  baseURL: %@", baseURL];
715     
716     entryCount = 0; /* Note: this will clash with streamed output later */
717     while ((entry = [_entries nextObject])) {
718       [self renderSearchResultEntry:entry inContext:_ctx
719             namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
720             baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
721       entryCount++;
722     }
723     [self debugWithFormat:@"  rendered %i entries", entryCount];
724     
725     /*
726       If we got a "rows" range header, we report back the actual rows
727       delivered. Since we do not really support ranges in the moment,
728       we just report all rows ... 
729       TODO: support for row ranges.
730     */
731     if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
732       /* sample: "Content-Range: rows 0-143; total=144" */
733       NSString *v;
734       
735       v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i", 
736                               entryCount>0?(entryCount - 1):0, entryCount];
737       [r setHeader:v forKey:@"content-range"];
738       [v release];
739     }
740   }
741   [r appendContentString:@"</D:multistatus>"];
742   if (formatOutput) [r appendContentCharacter:'\n'];
743   
744   [pool release];
745 }
746
747 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
748   EOFetchSpecification *fs;
749   NSDictionary *propMap;
750   
751   if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
752     return NO;
753   
754   if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
755     propMap = [_object davAttributeMapInContext:_ctx];
756
757   if (debugOn) {
758     [self debugWithFormat:@"render search result 0x%08X<%@>",
759             _object, NSStringFromClass([_object class])];
760   }
761   
762   [self renderSearchResult:_object inContext:_ctx
763         namesOnly:[fs queryWebDAVPropertyNamesOnly]
764         attributes:[fs selectedWebDAVPropertyNames]
765         propertyMap:propMap];
766   
767   if (debugOn) 
768     [self debugWithFormat:@"finished rendering."];
769   return YES;
770 }
771
772 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
773   /* TODO: this is a fake ! */
774   WOResponse *r;
775   
776   if (_object == nil) return NO;
777   
778   r = [_ctx response];
779   
780   [r setStatus:200];
781   [r setContentEncoding:NSUTF8StringEncoding];
782   [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
783   [r setHeader:[_object stringValue]          forKey:@"lock-token"];
784   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
785   [r appendContentString:@"<D:prop xmlns:D=\"DAV:\">"];
786   [r appendContentString:@"<D:lockdiscovery>"];
787   [r appendContentString:@"<D:activelock>"];
788   if (formatOutput) [r appendContentCharacter:'\n'];
789   
790   /* this is the href of the lock, not of the locked resource */
791   [r appendContentString:@"<D:locktoken><D:href>"];
792   [r appendContentString:[_object stringValue]];
793   [r appendContentString:@"</D:href></D:locktoken>"];
794   if (formatOutput) [r appendContentCharacter:'\n'];
795   
796   // TODO: locktype,  eg <D:locktype><D:write/></D:locktype>
797   // TODO: lockscope, eg <D:lockscope><D:exclusive/></D:lockscope>
798   // TODO: depth,     eg <D:depth>Infinitiy</D:depth>
799   // TODO: owner,     eg <D:owner><D:href>...</D:href></D:owner>
800   // TODO: timeout,   eg <D:timeout>Second-604800</D:timeout>
801   
802   [r appendContentString:@"</D:activelock>"];
803   [r appendContentString:@"</D:lockdiscovery>"];
804   [r appendContentString:@"</D:prop>"];
805   if (formatOutput) [r appendContentCharacter:'\n'];
806   return YES;
807 }
808
809 - (BOOL)renderOptions:(id)_object inContext:(WOContext *)_ctx {
810   WOResponse *r = [_ctx response];
811   
812   [r setStatus:200];
813   [r setHeader:@"1,2" forKey:@"DAV"]; // TODO: select protocol level
814   //[r setHeader:@"" forKey:@"Etag"]; 
815   [r setHeader:[_object componentsJoinedByString:@", "] forKey:@"allow"];
816   return YES;
817 }
818
819 - (BOOL)renderSubscription:(id)_object inContext:(WOContext *)_ctx {
820   // TODO: this is fake, mirrors request
821   WOResponse *r = [_ctx response];
822   WORequest  *rq;
823   NSString   *callback;
824   NSString   *notificationType;
825   NSString   *lifetime;
826   
827   rq                = [_ctx request];
828   callback          = [rq headerForKey:@"call-back"];
829   notificationType  = [rq headerForKey:@"notification-type"];
830   lifetime          = [rq headerForKey:@"subscription-lifetime"];
831   
832   [r setStatus:200];
833   if (notificationType)
834     [r setHeader:notificationType forKey:@"notification-type"];
835   if (lifetime)
836     [r setHeader:lifetime         forKey:@"subscription-lifetime"];
837   if (callback)
838     [r setHeader:callback         forKey:@"callback"];
839   [r setHeader:[self baseURLForContext:_ctx] forKey:@"content-location"];
840   [r setHeader:_object forKey:@"subscription-id"];
841   return YES;
842 }
843
844 - (BOOL)renderPropPatchResult:(id)_object inContext:(WOContext *)_ctx {
845   NSMutableDictionary *extNameCache = nil;
846   NSMutableDictionary *nsToPrefix   = nil;
847   WOResponse *r = [_ctx response];
848   
849   if (_object == nil) return NO;
850   
851   nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
852   [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
853   extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
854   [self buildPrefixMapForAttributes:_object
855         tagToExtName:extNameCache
856         nsToPrefix:nsToPrefix];
857   
858   [r setStatus:207 /* multistatus */];
859   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
860   [r appendContentString:@"<D:multistatus"];
861   [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
862   [r appendContentString:@">"];
863   if (formatOutput) [r appendContentCharacter:'\n'];
864   
865   [r appendContentString:@"<D:response>"];
866   if (formatOutput) [r appendContentCharacter:'\n'];
867   [r appendContentString:@"<D:href>"];
868   [r appendContentString:[[_ctx request] uri]];
869   [r appendContentString:@"</D:href>"];
870   if (formatOutput) [r appendContentCharacter:'\n'];
871   [r appendContentString:@"<D:propstat><D:status>HTTP/1.1 200 OK</D:status>"];
872   if (formatOutput) [r appendContentCharacter:'\n'];
873   [r appendContentString:@"<D:prop>"];
874   if (formatOutput) [r appendContentCharacter:'\n'];
875   
876   /* encode properties */
877   {
878     NSEnumerator *e;
879     NSString *tag;
880     
881     e = [_object objectEnumerator];
882     while ((tag = [e nextObject])) {
883       NSString *extName;
884       
885       extName = [extNameCache objectForKey:tag];
886       [r appendContentCharacter:'<'];
887       [r appendContentString:extName];
888       [r appendContentString:@"/>"];
889       if (formatOutput) [r appendContentCharacter:'\n'];
890     }
891   }
892   
893   [r appendContentString:@"</D:prop></D:propstat></D:response>"];
894   if (formatOutput) [r appendContentCharacter:'\n'];
895   [r appendContentString:@"</D:multistatus>"];
896   if (formatOutput) [r appendContentCharacter:'\n'];
897   
898   return YES;
899 }
900
901 - (BOOL)renderDeleteResult:(id)_object inContext:(WOContext *)_ctx {
902   WOResponse *r = [_ctx response];
903   
904   if (_object == nil || [_object boolValue]) {
905     [r setStatus:204 /* no content */];
906     //[r appendContentString:@"object was deleted."];
907     return YES;
908   }
909   
910   if ([_object isKindOfClass:[NSNumber class]]) {
911     [r setStatus:[_object intValue]];
912     if ([r status] != 204 /* No Content */)
913       [r appendContentString:@"object could not be deleted."];
914   }
915   else {
916     [r setStatus:500 /* server error */];
917     [r appendContentString:@"object could not be deleted. reason: "];
918     [r appendContentHTMLString:[_object stringValue]];
919   }
920   return YES;
921 }
922
923 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
924   inContext:(WOContext *)_ctx 
925 {
926   WOResponse *r = [_ctx response];
927   
928   if (_object == nil) {
929     [r setStatus:_defStatus /* no content */];
930     return YES;
931   }
932   
933   if ([_object isKindOfClass:[NSNumber class]]) {
934     if ([_object intValue] < 100) {
935       [r setStatus:_defStatus /* no content */];
936       return YES;
937     }
938     else {
939       [r setStatus:[_object intValue]];
940     }
941   }
942   else {
943     [r setStatus:_defStatus /* no content */];
944   }
945   return YES;
946 }
947 - (BOOL)renderUploadResult:(id)_object inContext:(WOContext *)_ctx {
948   WOResponse *r = [_ctx response];
949   
950   if (_object == nil) {
951     [r setStatus:204 /* no content */];
952     return YES;
953   }
954   
955   if ([_object isKindOfClass:[NSNumber class]]) {
956     if ([_object intValue] < 100) {
957       [r setStatus:204 /* no content */];
958       return YES;
959     }
960     
961     [r setStatus:[_object intValue]];
962     if ([_object intValue] >= 300) {
963       [r setHeader:@"text/html" forKey:@"content-type"];
964       [r appendContentString:@"object could not be stored."];
965     }
966     
967     return YES;
968   }
969   
970   [r setStatus:204 /* no content */];
971   return YES;
972 }
973
974 - (void)renderPollList:(NSArray *)_sids code:(int)_code
975   inContext:(WOContext *)_ctx 
976 {
977   WOResponse   *r = [_ctx response];
978   NSEnumerator *e;
979   NSString *sid;
980   NSString *href;
981   
982   if ([_sids count] == 0) return;
983   href = [self baseURLForContext:_ctx];
984
985   [r appendContentString:@"<D:response>"];
986   if (formatOutput) [r appendContentCharacter:'\n'];
987   [r appendContentString:@"<D:href>"];
988   [r appendContentString:href];
989   [r appendContentString:@"</D:href>"];
990   if (formatOutput) [r appendContentCharacter:'\n'];
991   
992   [r appendContentString:@"<D:status>HTTP/1.1 "];
993   if (_code == 200)
994     [r appendContentString:@"200 OK"];
995   else if (_code == 204)
996     [r appendContentString:@"204 No Content"];
997   else {
998     NSString *s;
999     s = [NSString stringWithFormat:@"%i code%i"];
1000     [r appendContentString:s];
1001   }
1002   [r appendContentString:@"</D:status>"];
1003   if (formatOutput) [r appendContentCharacter:'\n'];
1004   
1005   [r appendContentString:@"<E:subscriptionID>"];
1006   if (formatOutput) [r appendContentCharacter:'\n'];
1007   e = [_sids objectEnumerator];
1008   while ((sid = [e nextObject])) {
1009     if (formatOutput) [r appendContentString:@"  "];
1010     [r appendContentString:@"<li>"];
1011     [r appendContentString:sid];
1012     [r appendContentString:@"</li>"];
1013     if (formatOutput) [r appendContentCharacter:'\n'];
1014   }
1015   [r appendContentString:@"</E:subscriptionID>"];
1016   if (formatOutput) [r appendContentCharacter:'\n'];
1017   [r appendContentString:@"</D:response>"];
1018   if (formatOutput) [r appendContentCharacter:'\n'];
1019 }
1020
1021 - (BOOL)renderPollResult:(id)_object inContext:(WOContext *)_ctx {
1022   WOResponse *r = [_ctx response];
1023   
1024   if (_object == nil) {
1025     [r setStatus:204 /* no content */];
1026     return YES;
1027   }
1028
1029   if ([_object isKindOfClass:[NSDictionary class]]) {
1030     NSArray  *pending, *inactive;
1031     
1032     pending  = [_object objectForKey:@"pending"];
1033     inactive = [_object objectForKey:@"inactive"];
1034     
1035     [r setStatus:207 /* Multi-Status */];
1036     [r setHeader:@"text/xml" forKey:@"content-type"];
1037
1038     [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1039     [r appendContentString:@"<D:multistatus "];
1040     [r appendContentString:@" xmlns:D=\"DAV:\""];
1041     [r appendContentString:
1042          @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1043     [r appendContentString:@">"];
1044     if (formatOutput) [r appendContentCharacter:'\n'];
1045     
1046     [self renderPollList:pending  code:200 inContext:_ctx];
1047     [self renderPollList:inactive code:204 inContext:_ctx];
1048     
1049     [r appendContentString:@"</D:multistatus>"];
1050     if (formatOutput) [r appendContentCharacter:'\n'];
1051   }
1052   else if ([_object isKindOfClass:[NSArray class]]) {
1053     [r setStatus:207 /* Multi-Status */];
1054     [r setHeader:@"text/xml" forKey:@"content-type"];
1055
1056     [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1057     [r appendContentString:@"<D:multistatus "];
1058     [r appendContentString:@" xmlns:D=\"DAV:\""];
1059     [r appendContentString:
1060          @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1061     [r appendContentString:@">"];
1062     if (formatOutput) [r appendContentCharacter:'\n'];
1063     
1064     [self renderPollList:_object code:200 inContext:_ctx];
1065     
1066     [r appendContentString:@"</D:multistatus>"];
1067     if (formatOutput) [r appendContentCharacter:'\n'];
1068   }
1069   else {
1070     [r setStatus:204 /* no content */];
1071     //[r appendContentString:@"object was stored."];
1072   }
1073   return YES;
1074 }
1075
1076 - (BOOL)renderMkColResult:(id)_object inContext:(WOContext *)_ctx {
1077   WOResponse *r = [_ctx response];
1078   
1079   if (_object == nil || [_object boolValue]) {
1080     [r setStatus:201 /* Created */];
1081     return YES;
1082   }
1083   
1084   if ([_object isKindOfClass:[NSNumber class]]) {
1085     [r setStatus:[_object intValue]];
1086     [r appendContentString:@"object could not be created."];
1087   }
1088   else {
1089     [r setStatus:500 /* server error */];
1090     [r appendContentString:@"object could not be deleted. reason: "];
1091     [r appendContentHTMLString:[_object stringValue]];
1092   }
1093   return YES;
1094 }
1095
1096 /* debugging */
1097
1098 - (BOOL)isDebuggingEnabled {
1099   return debugOn;
1100 }
1101
1102 @end /* SoWebDAVRenderer */