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