]> err.no Git - sope/blob - sope-appserver/NGObjWeb/WebDAV/SoWebDAVRenderer.m
renamed packages as discussed in the developer list
[sope] / sope-appserver / NGObjWeb / WebDAV / SoWebDAVRenderer.m
1 /*
2   Copyright (C) 2000-2003 SKYRIX Software AG
3
4   This file is part of OGo
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 "SoWebDAVRenderer.h"
24 #include "SoWebDAVValue.h"
25 #include "SoObject+SoDAV.h"
26 #include "EOFetchSpecification+SoDAV.h"
27 #include "NSException+HTTP.h"
28 #include <NGObjWeb/WOContext.h>
29 #include <NGObjWeb/WOResponse.h>
30 #include <NGObjWeb/WORequest.h>
31 #include <NGObjWeb/WOElement.h>
32 #include <SaxObjC/XMLNamespaces.h>
33 #include <NGExtensions/NSString+Ext.h>
34 #include "common.h"
35
36 /*
37   What HotMail uses for responses:
38     <?xml version="1.0" encoding="Windows-1252"?>
39     Headers:
40       Server:              Microsoft-IIS/5.0
41       X-Timestamp:         folders=1035823428, ACTIVE=1035813212
42       Client-Response-Num: 1
43       Client-Date:         <date>
44       Expires:             ...
45       P3P:                 BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo
46 */
47
48 #define XMLNS_INTTASK \
49 @"{http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/}"
50
51 @interface SoWebDAVRenderer(Privates)
52 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
53   inContext:(WOContext *)_ctx; 
54 @end
55
56 @implementation SoWebDAVRenderer
57
58 static NSDictionary *predefinedNamespacePrefixes = nil;
59 static NSTimeZone   *gmt         = nil;
60 static BOOL         debugOn      = NO;
61 static BOOL         formatOutput = NO;
62
63 + (void)initialize {
64   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
65   static BOOL didInit = NO;
66   if (didInit) return;
67   didInit = YES;
68   if (gmt == nil) 
69     gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
70
71   if (predefinedNamespacePrefixes == nil) {
72     predefinedNamespacePrefixes = 
73       [[ud objectForKey:@"SoPreferredNamespacePrefixes"] copy];
74   }
75   formatOutput = [ud boolForKey:@"SoWebDAVFormatOutput"];
76   
77   if ((debugOn = [ud boolForKey:@"SoRendererDebugEnabled"]))
78     NSLog(@"enabled debugging in SoWebDAVRenderer (SoRendererDebugEnabled)");
79 }
80
81 + (id)sharedRenderer {
82   static SoWebDAVRenderer *r = nil; // THREAD
83   if (r == nil) r = [[SoWebDAVRenderer alloc] init];
84   return r;
85 }
86
87 - (NSString *)preferredPrefixForNamespace:(NSString *)_uri {
88   return [predefinedNamespacePrefixes objectForKey:_uri];
89 }
90
91 /* key render entry-point */
92
93 - (void)_fixupResponse:(WOResponse *)_r inContext:(WOContext *)_ctx {
94   NSDate   *now;
95   NSString *nowHttpString;
96   id tmp;
97   
98   if ((tmp = [_r headerForKey:@"server"]) == nil) {
99     // TODO: add application name as primary name
100     [_r setHeader:@"SOPE 4.2/WebDAV" forKey:@"server"];
101   }
102   
103   [_r setHeader:@"close" forKey:@"connection"];
104   [_r setHeader:@"DAV"   forKey:@"Ms-Author-Via"];
105   
106   // what program uses that header ?
107   [_r setHeader:@"200 No error" forKey:@"X-Dav-Error"];
108
109   if ((tmp = [_r headerForKey:@"content-type"]) == nil)
110     [_r setHeader:@"text/xml" forKey:@"content-type"];
111     
112   now = [NSDate date];
113   nowHttpString = [now descriptionWithCalendarFormat:
114                          @"%a, %d %b %Y %H:%M:%S GMT"
115                        timeZone:gmt
116                        locale:nil];
117     
118   if ((tmp = [_r headerForKey:@"date"]) == nil)
119     [_r setHeader:nowHttpString forKey:@"date"];
120
121 #if 0 /* currently none of the clients allows zipping, retry later ... */
122   /* try zipping */
123   if ([_r shouldZipResponseToRequest:nil]) {
124     [self logWithFormat:@"zipping DAV result ..."];
125     [_r zipResponse];
126   }
127 #endif
128 }
129
130 - (NSString *)mimeTypeForData:(NSData *)_data inContext:(WOContext *)_ctx {
131   /* should check extension for MIME type */
132   return @"application/octet-stream";
133 }
134 - (NSString *)mimeTypeForString:(NSString *)_str inContext:(WOContext *)_ctx {
135   /* should check extension for MIME type */
136
137   if ([_str hasPrefix:@"<?xml"])
138     return @"text/xml; charset=\"utf-8\"";
139   if ([_str hasPrefix:@"<html"])
140     return @"text/html; charset=\"utf-8\"";
141   
142   return @"text/plain; charset=\"utf-8\"";
143 }
144
145 - (BOOL)renderObjectBodyResult:(id)_object inContext:(WOContext *)_ctx 
146   onlyHead:(BOOL)_onlyHead
147 {
148   WOResponse *r = [_ctx response];
149   NSString *tmp;
150   
151   /*
152     TODO: implement proper etag support. This probably implies that we need
153           to pass in some structure or store the etag in the context?
154           We cannot use davEntityTag on the input parameter, since this is
155           usually the plain object.
156   */
157   tmp = @"0"; // fallback, cannot use the thing above
158   [r setHeader:tmp forKey:@"ETag"]; // required for WebFolder PUTs
159   
160   if ([_object isKindOfClass:[NSData class]]) {
161     [r setHeader:[self mimeTypeForData:_object inContext:_ctx]
162        forKey:@"content-type"];
163     
164     [r setHeader:[NSString stringWithFormat:@"%d", [_object length]]
165        forKey:@"content-length"];
166     if (!_onlyHead) [r setContent:_object];
167     return YES;
168   }
169
170   if ([_object isKindOfClass:[NSString class]]) {
171     NSData *data;
172     
173     [r setHeader:[self mimeTypeForString:_object inContext:_ctx]
174        forKey:@"content-type"];
175     
176     data = [_object dataUsingEncoding:NSUTF8StringEncoding];
177     [r setHeader:[NSString stringWithFormat:@"%d", [data length]]
178        forKey:@"content-length"];
179     [r setContent:data];
180     return YES;
181   }
182   
183   if ([_object respondsToSelector:@selector(appendToResponse:inContext:)]) {
184     unsigned len;
185     NSData   *data;
186     
187     [_object appendToResponse:r inContext:_ctx];
188     
189     data = [r content];
190     if ([[r headerForKey:@"content-type"] length] == 0) {
191       [r setHeader:[self mimeTypeForData:data inContext:_ctx]
192          forKey:@"content-type"];
193     }
194     len = [data length];
195     if (_onlyHead) [r setContent:nil];
196     data = nil;
197     [r setHeader:[NSString stringWithFormat:@"%d", len]
198        forKey:@"content-length"];
199     return YES;
200   }
201   
202   [self logWithFormat:@"ERROR: don't know how to render: %@", _object];
203   return NO;
204 }
205
206 - (NSException *)renderObject:(id)_object inContext:(WOContext *)_ctx {
207   NSString *m;
208   unichar  c1;
209   BOOL ok;
210   
211   if ([_object isKindOfClass:[WOResponse class]]) {
212     if (_object != [_ctx response]) {
213       [self logWithFormat:@"response mismatch"];
214       return [NSException exceptionWithHTTPStatus:500 /* internal error */];
215     }
216     [self _fixupResponse:_object inContext:_ctx];
217     return nil;
218   }
219   
220   m = [[_ctx request] method];
221   if ([m length] == 0) {
222     return [NSException exceptionWithHTTPStatus:400 /* bad request */
223                         reason:@"missing method name!"];
224   }
225   c1 = [m characterAtIndex:0];
226
227   ok = NO;
228   switch (c1) {
229   case 'B':
230     if ([m isEqualToString:@"BPROPFIND"])
231       ok = [self renderSearchResult:_object inContext:_ctx];
232     break;
233   case 'C':
234     if ([m isEqualToString:@"COPY"]) {
235       ok = [self renderStatusResult:_object 
236                  withDefaultStatus:201 /* Created */
237                  inContext:_ctx];
238     }
239     break;
240   case 'D':
241     if ([m isEqualToString:@"DELETE"])
242       ok = [self renderDeleteResult:_object inContext:_ctx];
243     break;
244   case 'G':
245     if ([m isEqualToString:@"GET"])
246       ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:NO];
247     break;
248   case 'H':
249     if ([m isEqualToString:@"HEAD"])
250       ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:YES];
251     break;
252   case 'L':
253     if ([m isEqualToString:@"LOCK"])
254       ok = [self renderLockToken:_object inContext:_ctx];
255     break;
256   case 'M':
257     if ([m isEqualToString:@"MKCOL"])
258       ok = [self renderMkColResult:_object inContext:_ctx];
259     else if ([m isEqualToString:@"MOVE"]) {
260       ok = [self renderStatusResult:_object 
261                  withDefaultStatus:201 /* Created */
262                  inContext:_ctx];
263     }
264     break;
265   case 'O':
266     if ([m isEqualToString:@"OPTIONS"])
267       ok = [self renderOptions:_object inContext:_ctx];
268     break;
269   case 'P':
270     if ([m isEqualToString:@"PUT"])
271       ok = [self renderUploadResult:_object inContext:_ctx];
272     else if ([m isEqualToString:@"PROPFIND"])
273       ok = [self renderSearchResult:_object inContext:_ctx];
274     else if ([m isEqualToString:@"PROPPATCH"])
275       ok = [self renderPropPatchResult:_object inContext:_ctx];
276     else if ([m isEqualToString:@"POLL"])
277       ok = [self renderPollResult:_object inContext:_ctx];
278     break;
279   case 'S':
280     if ([m isEqualToString:@"SEARCH"])
281       ok = [self renderSearchResult:_object inContext:_ctx];
282     else if ([m isEqualToString:@"SUBSCRIBE"])
283       ok = [self renderSubscription:_object inContext:_ctx];
284     break;
285     
286   default:
287     ok = NO;
288     break;
289   }
290   
291   if (ok) [self _fixupResponse:[_ctx response] inContext:_ctx];
292   return ok
293     ? nil
294     : [NSException exceptionWithHTTPStatus:500 /* server error */];
295 }
296
297 - (BOOL)canRenderObject:(id)_object inContext:(WOContext *)_ctx {
298   if ([_object isKindOfClass:[NSException class]])
299     return NO;
300   return YES;
301 }
302
303 - (NSString *)stringForValue:(id)_value ofProperty:(NSString *)_prop 
304   prefixes:(NSDictionary *)_prefixes
305 {
306   /* seems like this is the default date value */
307   NSString *datefmt = @"%a, %d %b %Y %H:%M:%S GMT";
308   
309   if (_value == nil)
310     return nil;
311   if (![_value isNotNull])
312     return nil;
313   
314   /* special processing for some properties */
315   
316   if ([_prop isEqualToString:@"{DAV:}resourcetype"]) {
317     _value = [_value stringValue];
318     if ([_value length] == 0) return nil;
319     
320     return [NSString stringWithFormat:@"<%@:%@/>",
321                        [_prefixes objectForKey:@"DAV:"], _value];
322   }
323   else if ([_prop isEqualToString:@"{DAV:}creationdate"])
324     datefmt = @"%Y-%m-%dT%H:%M:%S%zZ";
325
326   /* special processing for some properties  */
327   
328   // TODO: move this to user-level code ! 
329   //   HH: what is that ? it does not do anything anyway ?
330   if ([_prop hasPrefix:XMLNS_INTTASK]) {
331     if ([_prop hasSuffix:@"}0x00008102"]) {
332     }
333   }
334   
335   /* special processing for some classes */
336   
337   if ([_value isKindOfClass:[NSString class]])
338     return [_value stringByEscapingXMLString];
339   
340   if ([_value isKindOfClass:[NSNumber class]])
341     return [_value stringValue];
342   
343   if ([_value isKindOfClass:[NSDate class]]) {
344     return [_value descriptionWithCalendarFormat:datefmt
345                    timeZone:gmt
346                    locale:nil];
347   }
348   
349   return [[_value stringValue] stringByEscapingXMLString];
350 }
351
352 - (NSString *)baseURLForContext:(WOContext *)_ctx {
353   /*
354     Note: Evolution doesn't correctly transfer the "Host:" header, it
355     misses the port argument :-(
356   */
357   NSString  *baseURL;
358   WORequest *rq;
359   NSString  *hostport;
360   id tmp;
361   
362   rq = [_ctx request];
363   
364   if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
365     hostport = tmp;
366     if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
367       hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
368   }
369   else if ((tmp = [rq headerForKey:@"host"]))
370     hostport = tmp;
371   else
372     hostport = [[NSHost currentHost] name];
373   
374   baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
375   return baseURL;
376 }
377
378 - (NSString *)tidyHref:(id)href baseURL:(id)baseURL {
379   href = [href stringValue];
380   if (href == nil) {
381     if (debugOn) {
382       [self logWithFormat:
383               @"WARNING: using baseURL for href, "
384               @"entry did not provide a URL: %@", baseURL];
385     }
386     href = [baseURL stringValue];
387   }
388   else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
389     // TODO: use "real" URL processing
390     href = [baseURL stringByAppendingPathComponent:href];
391   }
392   return href;
393 }
394 - (id)tidyStatus:(id)stat {
395   if (stat == nil)
396     stat = @"HTTP/1.1 200 OK";
397   else if ([stat isKindOfClass:[NSException class]]) {
398     int i;
399     
400     if ((i = [stat httpStatus]) > 0)
401       stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
402     else {
403       stat = [(NSException *)stat name];
404       stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
405     }
406   }
407   return stat;
408 }
409
410 - (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx 
411   namesOnly:(BOOL)_namesOnly
412   attributes:(NSArray *)_attrs
413   propertyMap:(NSDictionary *)_propMap
414   baseURL:(NSString *)baseURL
415   tagToPrefix:(NSDictionary *)extNameCache
416   nsToPrefix:(NSDictionary *)nsToPrefix
417 {
418   /* Note: the entry is an NSArray in case _namesOnly is requested! */
419   // TODO: use -valueForKey: to improve NSNull handling ?
420   WOResponse   *r;
421   NSEnumerator *keys;
422   NSString     *key;
423   id   href = nil;
424   id   stat = nil;
425   BOOL isBrief;
426   
427   r = [_ctx response];
428   isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
429   
430   if (debugOn) {
431     [self debugWithFormat:@"    render entry: 0x%08X<%@>%s%s",
432           entry, NSStringFromClass([entry class]),
433           isBrief    ? " brief"      : "",
434           _namesOnly ? " names-only" : ""];
435   }
436
437   /* we do not map these DAV properties because they are very special */
438   if (!_namesOnly) {
439     if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
440       if ((key = [_propMap objectForKey:@"{DAV:}href"])) {
441         if ((href = [entry valueForKey:key]) == nil) {
442           if (debugOn) {
443             [self debugWithFormat:
444                   @"WARNING: no value for {DAV:}href key '%@': %@",
445                     key, entry];
446         }
447         }
448       }
449       else if (debugOn) {
450         [self debugWithFormat:
451               @"WARNING: no key for {DAV:}href in property map !"];
452       }
453     }
454     if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
455       if ((key = [_propMap objectForKey:@"{DAV:}status"]))
456         stat = [entry valueForKey:key];
457     }
458
459     /* tidy href */
460     href = [self tidyHref:href baseURL:baseURL];
461     
462     /* tidy status */
463     stat = [self tidyStatus:stat];
464   }
465   else { /* propnames only */
466     href = [baseURL stringValue];
467     stat = @"HTTP/1.1 200 OK";
468   }
469   
470   if (debugOn) {
471     [self debugWithFormat:@"    status: %@", stat];
472     [self debugWithFormat:@"    href:   %@", href];
473   }
474   
475   /* generate */
476   [r appendContentString:@"<D:response>"];
477   if (formatOutput) [r appendContentCharacter:'\n'];
478   
479   if (href) {
480     [r appendContentString:@"<D:href>"];
481     /*
482       TODO: need to find out what is appropriate! While Cadaver and ZideLook
483             (both Neon+Expat) seem to be fine with this, OSX reports invalid
484             characters (displayed as '#') for umlauts.
485             It might be that we are supposed to use *URL* escaping in any 
486             case! (notably entering a directory with an umlaut doesn't seem
487             to work in Cadaver either because of a URL mismatch!)
488       Note: we cannot apply URL encoding in this place, because it will encode
489             all URL special chars ... where are URLs escaped?
490       Note: we always need to apply XML escaping (even though higher-level
491             characters might be already encoded)!
492     */
493     [r appendContentXMLString:[href stringValue]];
494     [r appendContentString:@"</D:href>"];
495     if (formatOutput) [r appendContentCharacter:'\n'];
496   }
497       
498   [r appendContentString:@"<D:propstat>"];
499   if (stat) {
500     [r appendContentString:@"<D:status>"];
501     [r appendContentXMLString:[stat stringValue]];
502     [r appendContentString:@"</D:status>"];
503   }
504   
505   [r appendContentString:@"<D:prop>"];
506   if (formatOutput) [r appendContentCharacter:'\n'];
507   
508   /* now the properties */
509   
510   keys = [_attrs objectEnumerator] ;
511   while ((key = [keys nextObject])) {
512     NSString *extName;
513     NSString *okey;
514     id value;
515     
516 #if 0 /* this filter probably doesn't make sense ? */
517     /* filter out predefined props */
518     if ([key isEqualToString:@"{DAV:}href"])   continue;
519     if ([key isEqualToString:@"{DAV:}status"]) continue;
520 #endif
521     
522     extName = [extNameCache objectForKey:key];
523     
524     if (_namesOnly) {
525       [r appendContentCharacter:'<'];
526       [r appendContentString:extName];
527       [r appendContentString:@"/>"];
528       if (formatOutput) [r appendContentCharacter:'\n'];
529       continue;
530     }
531     
532     // TODO: we should support property status (eg encode 404 on NSNull)
533       
534     if ((okey = [_propMap objectForKey:key]) == nil)
535       okey = key;
536       
537     if ([key isEqualToString:@"{DAV:}href"])
538       value = href;
539     else
540       value = [entry valueForKey:okey];
541     
542     if ([value isNotNull]) {
543       NSString *s;
544         
545       if ([value isKindOfClass:[SoWebDAVValue class]]) {
546           s = [value stringForTag:key rawName:extName
547                      inContext:_ctx prefixes:nsToPrefix];
548           [r appendContentString:s];
549       }
550       else {
551           [r appendContentCharacter:'<'];
552           [r appendContentString:extName];
553           [r appendContentCharacter:'>'];
554           
555           s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
556           [r appendContentString:s];
557           
558           [r appendContentString:@"</"];
559           [r appendContentString:extName];
560           [r appendContentString:@">"];
561           if (formatOutput) [r appendContentCharacter:'\n'];
562       }
563       continue;
564     }
565     
566     if (!isBrief) { 
567       /* 
568          Not sure whether this is correct, do we need to encode null attrs?
569          Seems like Evo gets confused on that.
570          TODO: probably add a 404 property status for that!
571       */
572       [r appendContentCharacter:'<'];
573       [r appendContentString:extName];
574       [r appendContentString:@"/>"];
575       if (formatOutput) [r appendContentCharacter:'\n'];
576     }
577   }
578       
579   [r appendContentString:@"</D:prop></D:propstat></D:response>"];
580   if (formatOutput) [r appendContentCharacter:'\n'];
581 }
582
583 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
584   tagToExtName:(NSMutableDictionary *)_tagToExtName
585   nsToPrefix:(NSMutableDictionary *)_nsToPrefix
586 {
587   unichar autoPrefix[2] = { ('a' - 1), 0 };
588   NSEnumerator *e;
589   NSString     *fqn;
590   
591   e = [_attrs objectEnumerator];
592   while ((fqn = [e nextObject])) {
593     NSString *ns, *localName, *prefix, *extName;
594     
595     if ([_tagToExtName objectForKey:fqn]) continue;
596     
597     if (![fqn xmlIsFQN]) {
598       /* hm, no namespace given :-(, using DAV */
599       ns        = @"DAV:";
600       localName = fqn;
601     }
602     else {
603       ns        = [fqn xmlNamespaceURI];
604       localName = [fqn xmlLocalName];
605     }
606     
607     if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
608       if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
609         (autoPrefix[0])++;
610         prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
611       }
612       [_nsToPrefix setObject:prefix forKey:ns];
613     }
614     
615     extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
616     [_tagToExtName setObject:extName forKey:fqn];
617   }
618 }
619
620 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
621   NSMutableString *ms;
622   NSEnumerator *nse;
623   NSString *ns;
624   
625   ms = [NSMutableString stringWithCapacity:256];
626   nse = [_nsToPrefix keyEnumerator];
627   while ((ns = [nse nextObject])) {
628     [ms appendString:@" xmlns:"];
629     [ms appendString:[_nsToPrefix objectForKey:ns]];
630     [ms appendString:@"=\""];
631     [ms appendString:ns];
632     [ms appendString:@"\""];
633   }
634   return ms;
635 }
636
637 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx 
638   namesOnly:(BOOL)_namesOnly
639   attributes:(NSArray *)_attrs
640   propertyMap:(NSDictionary *)_propMap
641 {
642   NSMutableDictionary *extNameCache = nil;
643   NSMutableDictionary *nsToPrefix   = nil;
644   NSAutoreleasePool   *pool;
645   WOResponse *r;
646   unsigned   entryCount;
647   
648   pool = [[NSAutoreleasePool alloc] init];
649   r = [_ctx response];
650   
651   if (![_entries isKindOfClass:[NSEnumerator class]]) {
652     if ([_entries isKindOfClass:[NSArray class]]) {
653       [self debugWithFormat:@"  render %i entries", [_entries count]];
654       _entries = [_entries objectEnumerator];
655     }
656     else {
657       [self debugWithFormat:@"  render a single object ..."];
658       _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
659     }
660   }
661   
662   /* collect used namespaces */
663   
664   nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
665   [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
666   
667   /* 
668      the extNameCache is used to map fully qualified tag names to their
669      prefixed external representation 
670   */
671   extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
672   
673   // TODO: only walk attrs, if available
674   /*
675     Walk all attributes of all entries to collect names. We might be able
676     to take a look at just the first record if it is guaranteed, that all
677     records have all properties (even if the value is NSNull) ?
678   */
679   [self buildPrefixMapForAttributes:_attrs
680         tagToExtName:extNameCache
681         nsToPrefix:nsToPrefix];
682   
683   /* generate multistatus */
684    
685   [r setStatus:207 /* multistatus */];
686   [r setHeader:@"no-cache" forKey:@"pragma"];
687   [r setHeader:@"no-cache" forKey:@"cache-control"];
688   
689   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
690   [r appendContentString:@"<D:multistatus"];
691   [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
692   [r appendContentString:@">"];
693   if (formatOutput) [r appendContentCharacter:'\n'];
694   {
695     NSString *baseURL;
696     NSString *range;
697     id entry;
698     
699     baseURL = [self baseURLForContext:_ctx];
700     [self debugWithFormat:@"  baseURL: %@", baseURL];
701     
702     entryCount = 0; /* Note: this will clash with streamed output later */
703     while ((entry = [_entries nextObject])) {
704       [self renderSearchResultEntry:entry inContext:_ctx
705             namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
706             baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
707       entryCount++;
708     }
709     [self debugWithFormat:@"  rendered %i entries", entryCount];
710     
711     /*
712       If we got a "rows" range header, we report back the actual rows
713       delivered. Since we do not really support ranges in the moment,
714       we just report all rows ... 
715       TODO: support for row ranges.
716     */
717     if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
718       /* sample: "Content-Range: rows 0-143; total=144" */
719       NSString *v;
720       
721       v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i", 
722                               entryCount>0?(entryCount - 1):0, entryCount];
723       [r setHeader:v forKey:@"content-range"];
724       [v release];
725     }
726   }
727   [r appendContentString:@"</D:multistatus>"];
728   if (formatOutput) [r appendContentCharacter:'\n'];
729   
730   [pool release];
731 }
732
733 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
734   EOFetchSpecification *fs;
735   NSDictionary *propMap;
736   
737   if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
738     return NO;
739   
740   if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
741     propMap = [_object davAttributeMapInContext:_ctx];
742
743   if (debugOn) {
744     [self debugWithFormat:@"render search result 0x%08X<%@>",
745             _object, NSStringFromClass([_object class])];
746   }
747   
748   [self renderSearchResult:_object inContext:_ctx
749         namesOnly:[fs queryWebDAVPropertyNamesOnly]
750         attributes:[fs selectedWebDAVPropertyNames]
751         propertyMap:propMap];
752   
753   if (debugOn) 
754     [self debugWithFormat:@"finished rendering."];
755   return YES;
756 }
757
758 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
759   /* TODO: this is a fake ! */
760   WOResponse *r;
761   
762   if (_object == nil) return NO;
763   
764   r = [_ctx response];
765   
766   [r setStatus:200];
767   [r setContentEncoding:NSUTF8StringEncoding];
768   [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
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 */