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