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