2 Copyright (C) 2000-2005 SKYRIX Software AG
4 This file is part of SOPE.
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
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.
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
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>
36 What HotMail uses for responses:
37 <?xml version="1.0" encoding="Windows-1252"?>
39 Server: Microsoft-IIS/5.0
40 X-Timestamp: folders=1035823428, ACTIVE=1035813212
41 Client-Response-Num: 1
44 P3P: BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo
47 #define XMLNS_INTTASK \
48 @"{http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/}"
50 @interface SoWebDAVRenderer(Privates)
51 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
52 inContext:(WOContext *)_ctx;
55 @implementation SoWebDAVRenderer
57 static NSDictionary *predefinedNamespacePrefixes = nil;
58 static NSTimeZone *gmt = nil;
59 static BOOL debugOn = NO;
60 static BOOL formatOutput = NO;
63 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
64 static BOOL didInit = NO;
68 gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
70 if (predefinedNamespacePrefixes == nil) {
71 predefinedNamespacePrefixes =
72 [[ud objectForKey:@"SoPreferredNamespacePrefixes"] copy];
74 formatOutput = [ud boolForKey:@"SoWebDAVFormatOutput"];
76 if ((debugOn = [ud boolForKey:@"SoRendererDebugEnabled"]))
77 NSLog(@"enabled debugging in SoWebDAVRenderer (SoRendererDebugEnabled)");
80 + (id)sharedRenderer {
81 static SoWebDAVRenderer *r = nil; // THREAD
82 if (r == nil) r = [[SoWebDAVRenderer alloc] init];
86 - (NSString *)preferredPrefixForNamespace:(NSString *)_uri {
87 return [predefinedNamespacePrefixes objectForKey:_uri];
90 /* key render entry-point */
92 - (void)_fixupResponse:(WOResponse *)_r inContext:(WOContext *)_ctx {
94 NSString *nowHttpString;
97 if ((tmp = [_r headerForKey:@"server"]) == nil) {
98 // TODO: add application name as primary name
99 [_r setHeader:@"SOPE 4.2/WebDAV" forKey:@"server"];
102 [_r setHeader:@"close" forKey:@"connection"];
103 [_r setHeader:@"DAV" forKey:@"Ms-Author-Via"];
105 // what program uses that header ?
106 [_r setHeader:@"200 No error" forKey:@"X-Dav-Error"];
108 if ((tmp = [_r headerForKey:@"content-type"]) == nil)
109 [_r setHeader:@"text/xml" forKey:@"content-type"];
112 nowHttpString = [now descriptionWithCalendarFormat:
113 @"%a, %d %b %Y %H:%M:%S GMT"
117 if ((tmp = [_r headerForKey:@"date"]) == nil)
118 [_r setHeader:nowHttpString forKey:@"date"];
120 #if 0 /* currently none of the clients allows zipping, retry later ... */
122 if ([_r shouldZipResponseToRequest:nil]) {
123 [self logWithFormat:@"zipping DAV result ..."];
129 - (NSString *)mimeTypeForData:(NSData *)_data inContext:(WOContext *)_ctx {
130 /* should check extension for MIME type */
131 return @"application/octet-stream";
133 - (NSString *)mimeTypeForString:(NSString *)_str inContext:(WOContext *)_ctx {
134 /* should check extension for MIME type */
136 if ([_str hasPrefix:@"<?xml"])
137 return @"text/xml; charset=\"utf-8\"";
138 if ([_str hasPrefix:@"<html"])
139 return @"text/html; charset=\"utf-8\"";
141 return @"text/plain; charset=\"utf-8\"";
144 - (BOOL)renderObjectBodyResult:(id)_object inContext:(WOContext *)_ctx
145 onlyHead:(BOOL)_onlyHead
149 unsigned char buf[128];
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.
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
164 if ([_object isKindOfClass:[NSData class]]) {
165 [r setHeader:[self mimeTypeForData:_object inContext:_ctx]
166 forKey:@"content-type"];
168 sprintf(buf, "%d", [_object length]);
169 [r setHeader:[NSString stringWithCString:buf] forKey:@"content-length"];
170 if (!_onlyHead) [r setContent:_object];
174 if ([_object isKindOfClass:[NSString class]]) {
177 [r setHeader:[self mimeTypeForString:_object inContext:_ctx]
178 forKey:@"content-type"];
180 data = [_object dataUsingEncoding:NSUTF8StringEncoding];
181 sprintf(buf, "%d", [data length]);
182 [r setHeader:[NSString stringWithCString:buf] forKey:@"content-length"];
187 if ([_object respondsToSelector:@selector(appendToResponse:inContext:)]) {
191 [_object appendToResponse:r inContext:_ctx];
194 if ([[r headerForKey:@"content-type"] length] == 0) {
195 [r setHeader:[self mimeTypeForData:data inContext:_ctx]
196 forKey:@"content-type"];
199 if (_onlyHead) [r setContent:nil];
201 [r setHeader:[NSString stringWithFormat:@"%d", len]
202 forKey:@"content-length"];
206 [self errorWithFormat:@"don't know how to render: %@", _object];
210 - (NSException *)renderObject:(id)_object inContext:(WOContext *)_ctx {
215 if ([_object isKindOfClass:[WOResponse class]]) {
216 if (_object != [_ctx response]) {
217 [self logWithFormat:@"response mismatch"];
218 return [NSException exceptionWithHTTPStatus:500 /* internal error */];
220 [self _fixupResponse:_object inContext:_ctx];
224 m = [[_ctx request] method];
225 if ([m length] == 0) {
226 return [NSException exceptionWithHTTPStatus:400 /* bad request */
227 reason:@"missing method name!"];
229 c1 = [m characterAtIndex:0];
234 if ([m isEqualToString:@"BPROPFIND"])
235 ok = [self renderSearchResult:_object inContext:_ctx];
238 if ([m isEqualToString:@"COPY"]) {
239 ok = [self renderStatusResult:_object
240 withDefaultStatus:201 /* Created */
245 if ([m isEqualToString:@"DELETE"])
246 ok = [self renderDeleteResult:_object inContext:_ctx];
249 if ([m isEqualToString:@"GET"])
250 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:NO];
253 if ([m isEqualToString:@"HEAD"])
254 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:YES];
257 if ([m isEqualToString:@"LOCK"])
258 ok = [self renderLockToken:_object inContext:_ctx];
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 */
270 if ([m isEqualToString:@"OPTIONS"])
271 ok = [self renderOptions:_object inContext:_ctx];
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];
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];
295 if (ok) [self _fixupResponse:[_ctx response] inContext:_ctx];
298 : [NSException exceptionWithHTTPStatus:500 /* server error */];
301 - (BOOL)canRenderObject:(id)_object inContext:(WOContext *)_ctx {
302 if ([_object isKindOfClass:[NSException class]])
307 - (NSString *)stringForResourceType:(id)_value ofProperty:(NSString *)_prop
308 prefixes:(NSDictionary *)_prefixes
312 davNS = [_prefixes objectForKey:@"DAV:"];
314 if ([_value isKindOfClass:[NSArray class]]) {
316 Use arrays to allow for something like this:
318 <C:todos xmlns:C="urn:ietf:params:xml:ns:caldav"/>
320 ( TAG ) => tag in DAV: namespace
321 ( TAG, NS ) => tag in NS namespace
322 ( TAG, NS, PREFIX ) => tag in NS namespace with PREFIX
328 if ([_value count] == 0)
331 ms = [NSMutableString stringWithCapacity:16];
332 e = [_value objectEnumerator];
333 while ((item = [e nextObject]) != nil) {
336 if (![item isKindOfClass:[NSArray class]]) {
337 item = [item stringValue];
338 if ([item length] == 0) continue;
339 [ms appendFormat:@"<%@:%@ />", davNS, item];
343 /* process array tags */
345 if ((count = [item count]) == 0)
349 [ms appendFormat:@"<%@:%@ />", davNS, [item objectAtIndex:0]];
350 else if (count == 2) {
352 [ms appendFormat:@"<%@ xmlns=\"%@\" />",
353 [item objectAtIndex:0], [item objectAtIndex:1]];
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]];
365 _value = [_value stringValue];
366 if ([_value length] == 0) return nil;
368 return [NSString stringWithFormat:@"<%@:%@/>", davNS, _value];
370 - (NSString *)stringForValue:(id)_value ofProperty:(NSString *)_prop
371 prefixes:(NSDictionary *)_prefixes
373 /* seems like this is the default date value */
374 NSString *datefmt = @"%a, %d %b %Y %H:%M:%S GMT";
378 if (![_value isNotNull])
381 /* special processing for some properties */
383 if ([_prop isEqualToString:@"{DAV:}resourcetype"]) {
384 return [self stringForResourceType:_value ofProperty:_prop
387 else if ([_prop isEqualToString:@"{DAV:}creationdate"])
388 datefmt = @"%Y-%m-%dT%H:%M:%S%zZ";
390 /* special processing for some properties */
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"]) {
399 /* special processing for some classes */
401 if ([_value isKindOfClass:[NSString class]])
402 return [_value stringByEscapingXMLString];
404 if ([_value isKindOfClass:[NSNumber class]])
405 return [_value stringValue];
407 if ([_value isKindOfClass:[NSDate class]]) {
408 return [_value descriptionWithCalendarFormat:datefmt
413 return [[_value stringValue] stringByEscapingXMLString];
416 - (NSString *)baseURLForContext:(WOContext *)_ctx {
418 Note: Evolution doesn't correctly transfer the "Host:" header, it
419 misses the port argument :-(
428 if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
430 if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
431 hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
433 else if ((tmp = [rq headerForKey:@"host"]))
436 hostport = [[NSHost currentHost] name];
438 baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
442 - (NSString *)tidyHref:(id)_href baseURL:(id)baseURL {
445 href = [_href stringValue];
448 // TODO: this happens if we access using Goliath
449 if ([href hasPrefix:@"http:/"] && ![href hasPrefix:@"http://"]) {
450 [self logWithFormat:@"BROKEN URL: %@", _href];
457 [self warnWithFormat:
458 @"using baseURL for href, entry did not provide a URL: %@",
461 href = [baseURL stringValue];
463 else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
464 // TODO: use "real" URL processing
465 href = [baseURL stringByAppendingPathComponent:href];
469 - (id)tidyStatus:(id)stat {
471 stat = @"HTTP/1.1 200 OK";
472 else if ([stat isKindOfClass:[NSException class]]) {
475 if ((i = [stat httpStatus]) > 0)
476 stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
478 stat = [(NSException *)stat name];
479 stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
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
493 /* Note: the entry is an NSArray in case _namesOnly is requested! */
494 // TODO: use -valueForKey: to improve NSNull handling ?
503 isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
506 [self debugWithFormat:@" render entry: 0x%08X<%@>%s%s",
507 entry, NSStringFromClass([entry class]),
508 isBrief ? " brief" : "",
509 _namesOnly ? " names-only" : ""];
512 /* we do not map these DAV properties because they are very special */
514 if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
515 if ((key = [_propMap objectForKey:@"{DAV:}href"]) != nil) {
516 if ((href = [entry valueForKey:key]) == nil) {
518 [self warnWithFormat:
519 @"no value for {DAV:}href key '%@': %@", key, entry];
524 [self warnWithFormat:@"no key for {DAV:}href in property map !"];
527 if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
528 if ((key = [_propMap objectForKey:@"{DAV:}status"]))
529 stat = [entry valueForKey:key];
533 href = [self tidyHref:href baseURL:baseURL];
536 stat = [self tidyStatus:stat];
538 else { /* propnames only */
539 href = [baseURL stringValue];
540 stat = @"HTTP/1.1 200 OK";
544 [self debugWithFormat:@" status: %@", stat];
545 [self debugWithFormat:@" href: %@", href];
549 [r appendContentString:@"<D:response>"];
550 if (formatOutput) [r appendContentCharacter:'\n'];
552 if ([href isNotNull]) {
553 [r appendContentString:@"<D:href>"];
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)!
566 [r appendContentXMLString:[href stringValue]];
567 [r appendContentString:@"</D:href>"];
568 if (formatOutput) [r appendContentCharacter:'\n'];
571 [self warnWithFormat:@"WebDAV result entry has no valid href: %@", entry];
574 [r appendContentString:@"<D:propstat>"];
576 [r appendContentString:@"<D:status>"];
577 [r appendContentXMLString:[stat stringValue]];
578 [r appendContentString:@"</D:status>"];
581 [r appendContentString:@"<D:prop>"];
582 if (formatOutput) [r appendContentCharacter:'\n'];
584 /* now the properties */
586 keys = [_attrs objectEnumerator] ;
587 while ((key = [keys nextObject])) {
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;
598 extName = [extNameCache objectForKey:key];
601 [r appendContentCharacter:'<'];
602 [r appendContentString:extName];
603 [r appendContentString:@"/>"];
604 if (formatOutput) [r appendContentCharacter:'\n'];
608 // TODO: we should support property status (eg encode 404 on NSNull)
610 if ((okey = [_propMap objectForKey:key]) == nil)
613 if ([key isEqualToString:@"{DAV:}href"])
616 value = [entry valueForKey:okey];
618 if ([value isNotNull]) {
621 if ([value isKindOfClass:[SoWebDAVValue class]]) {
622 s = [value stringForTag:key rawName:extName
623 inContext:_ctx prefixes:nsToPrefix];
624 [r appendContentString:s];
627 [r appendContentCharacter:'<'];
628 [r appendContentString:extName];
629 [r appendContentCharacter:'>'];
631 s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
632 [r appendContentString:s];
634 [r appendContentString:@"</"];
635 [r appendContentString:extName];
636 [r appendContentString:@">"];
637 if (formatOutput) [r appendContentCharacter:'\n'];
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!
648 [r appendContentCharacter:'<'];
649 [r appendContentString:extName];
650 [r appendContentString:@"/>"];
651 if (formatOutput) [r appendContentCharacter:'\n'];
655 [r appendContentString:@"</D:prop></D:propstat></D:response>"];
656 if (formatOutput) [r appendContentCharacter:'\n'];
659 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
660 tagToExtName:(NSMutableDictionary *)_tagToExtName
661 nsToPrefix:(NSMutableDictionary *)_nsToPrefix
663 unichar autoPrefix[2] = { ('a' - 1), 0 };
667 e = [_attrs objectEnumerator];
668 while ((fqn = [e nextObject])) {
669 NSString *ns, *localName, *prefix, *extName;
671 if ([_tagToExtName objectForKey:fqn]) continue;
673 if (![fqn xmlIsFQN]) {
674 /* hm, no namespace given :-(, using DAV */
679 ns = [fqn xmlNamespaceURI];
680 localName = [fqn xmlLocalName];
683 if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
684 if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
686 prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
688 [_nsToPrefix setObject:prefix forKey:ns];
691 extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
692 [_tagToExtName setObject:extName forKey:fqn];
696 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
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:@"\""];
713 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx
714 namesOnly:(BOOL)_namesOnly
715 attributes:(NSArray *)_attrs
716 propertyMap:(NSDictionary *)_propMap
718 NSMutableDictionary *extNameCache = nil;
719 NSMutableDictionary *nsToPrefix = nil;
720 NSAutoreleasePool *pool;
724 pool = [[NSAutoreleasePool alloc] init];
727 if (![_entries isKindOfClass:[NSEnumerator class]]) {
728 if ([_entries isKindOfClass:[NSArray class]]) {
729 [self debugWithFormat:@" render %i entries", [_entries count]];
730 _entries = [_entries objectEnumerator];
733 [self debugWithFormat:@" render a single object ..."];
734 _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
738 /* collect used namespaces */
740 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
741 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
744 the extNameCache is used to map fully qualified tag names to their
745 prefixed external representation
747 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
749 // TODO: only walk attrs, if available
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) ?
755 [self buildPrefixMapForAttributes:_attrs
756 tagToExtName:extNameCache
757 nsToPrefix:nsToPrefix];
759 /* generate multistatus */
761 [r setStatus:207 /* multistatus */];
762 [r setHeader:@"no-cache" forKey:@"pragma"];
763 [r setHeader:@"no-cache" forKey:@"cache-control"];
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'];
775 baseURL = [self baseURLForContext:_ctx];
776 [self debugWithFormat:@" baseURL: %@", baseURL];
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];
785 [self debugWithFormat:@" rendered %i entries", entryCount];
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.
793 if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
794 /* sample: "Content-Range: rows 0-143; total=144" */
797 v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i",
798 entryCount>0?(entryCount - 1):0, entryCount];
799 [r setHeader:v forKey:@"content-range"];
803 [r appendContentString:@"</D:multistatus>"];
804 if (formatOutput) [r appendContentCharacter:'\n'];
809 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
810 EOFetchSpecification *fs;
811 NSDictionary *propMap;
813 if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
816 if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
817 propMap = [_object davAttributeMapInContext:_ctx];
820 [self debugWithFormat:@"render search result 0x%08X<%@>",
821 _object, NSStringFromClass([_object class])];
824 [self renderSearchResult:_object inContext:_ctx
825 namesOnly:[fs queryWebDAVPropertyNamesOnly]
826 attributes:[fs selectedWebDAVPropertyNames]
827 propertyMap:propMap];
830 [self debugWithFormat:@"finished rendering."];
834 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
835 /* TODO: this is a fake ! */
838 if (_object == nil) return NO;
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'];
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'];
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>
864 [r appendContentString:@"</D:activelock>"];
865 [r appendContentString:@"</D:lockdiscovery>"];
866 [r appendContentString:@"</D:prop>"];
867 if (formatOutput) [r appendContentCharacter:'\n'];
871 - (BOOL)renderOptions:(id)_object inContext:(WOContext *)_ctx {
872 WOResponse *r = [_ctx response];
875 [r setHeader:@"1,2" forKey:@"DAV"]; // TODO: select protocol level
876 //[r setHeader:@"" forKey:@"Etag"];
877 [r setHeader:[_object componentsJoinedByString:@", "] forKey:@"allow"];
881 - (BOOL)renderSubscription:(id)_object inContext:(WOContext *)_ctx {
882 // TODO: this is fake, mirrors request
883 WOResponse *r = [_ctx response];
886 NSString *notificationType;
890 callback = [rq headerForKey:@"call-back"];
891 notificationType = [rq headerForKey:@"notification-type"];
892 lifetime = [rq headerForKey:@"subscription-lifetime"];
895 if (notificationType)
896 [r setHeader:notificationType forKey:@"notification-type"];
898 [r setHeader:lifetime forKey:@"subscription-lifetime"];
900 [r setHeader:callback forKey:@"callback"];
901 [r setHeader:[self baseURLForContext:_ctx] forKey:@"content-location"];
902 [r setHeader:_object forKey:@"subscription-id"];
906 - (BOOL)renderPropPatchResult:(id)_object inContext:(WOContext *)_ctx {
907 NSMutableDictionary *extNameCache = nil;
908 NSMutableDictionary *nsToPrefix = nil;
909 WOResponse *r = [_ctx response];
911 if (_object == nil) return NO;
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];
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'];
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'];
938 /* encode properties */
943 e = [_object objectEnumerator];
944 while ((tag = [e nextObject])) {
947 extName = [extNameCache objectForKey:tag];
948 [r appendContentCharacter:'<'];
949 [r appendContentString:extName];
950 [r appendContentString:@"/>"];
951 if (formatOutput) [r appendContentCharacter:'\n'];
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'];
963 - (BOOL)renderDeleteResult:(id)_object inContext:(WOContext *)_ctx {
964 WOResponse *r = [_ctx response];
966 if (_object == nil || [_object boolValue]) {
967 [r setStatus:204 /* no content */];
968 //[r appendContentString:@"object was deleted."];
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."];
978 [r setStatus:500 /* server error */];
979 [r appendContentString:@"object could not be deleted. reason: "];
980 [r appendContentHTMLString:[_object stringValue]];
985 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
986 inContext:(WOContext *)_ctx
988 WOResponse *r = [_ctx response];
990 if (_object == nil) {
991 [r setStatus:_defStatus /* no content */];
995 if ([_object isKindOfClass:[NSNumber class]]) {
996 if ([_object intValue] < 100) {
997 [r setStatus:_defStatus /* no content */];
1001 [r setStatus:[_object intValue]];
1005 [r setStatus:_defStatus /* no content */];
1009 - (BOOL)renderUploadResult:(id)_object inContext:(WOContext *)_ctx {
1010 WOResponse *r = [_ctx response];
1012 if (_object == nil) {
1013 [r setStatus:204 /* no content */];
1017 if ([_object isKindOfClass:[NSNumber class]]) {
1018 if ([_object intValue] < 100) {
1019 [r setStatus:204 /* no content */];
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."];
1032 [r setStatus:204 /* no content */];
1036 - (void)renderPollList:(NSArray *)_sids code:(int)_code
1037 inContext:(WOContext *)_ctx
1039 WOResponse *r = [_ctx response];
1044 if ([_sids count] == 0) return;
1045 href = [self baseURLForContext:_ctx];
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'];
1054 [r appendContentString:@"<D:status>HTTP/1.1 "];
1056 [r appendContentString:@"200 OK"];
1057 else if (_code == 204)
1058 [r appendContentString:@"204 No Content"];
1061 s = [NSString stringWithFormat:@"%i code%i"];
1062 [r appendContentString:s];
1064 [r appendContentString:@"</D:status>"];
1065 if (formatOutput) [r appendContentCharacter:'\n'];
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'];
1077 [r appendContentString:@"</E:subscriptionID>"];
1078 if (formatOutput) [r appendContentCharacter:'\n'];
1079 [r appendContentString:@"</D:response>"];
1080 if (formatOutput) [r appendContentCharacter:'\n'];
1083 - (BOOL)renderPollResult:(id)_object inContext:(WOContext *)_ctx {
1084 WOResponse *r = [_ctx response];
1086 if (_object == nil) {
1087 [r setStatus:204 /* no content */];
1091 if ([_object isKindOfClass:[NSDictionary class]]) {
1092 NSArray *pending, *inactive;
1094 pending = [_object objectForKey:@"pending"];
1095 inactive = [_object objectForKey:@"inactive"];
1097 [r setStatus:207 /* Multi-Status */];
1098 [r setHeader:@"text/xml" forKey:@"content-type"];
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'];
1108 [self renderPollList:pending code:200 inContext:_ctx];
1109 [self renderPollList:inactive code:204 inContext:_ctx];
1111 [r appendContentString:@"</D:multistatus>"];
1112 if (formatOutput) [r appendContentCharacter:'\n'];
1114 else if ([_object isKindOfClass:[NSArray class]]) {
1115 [r setStatus:207 /* Multi-Status */];
1116 [r setHeader:@"text/xml" forKey:@"content-type"];
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'];
1126 [self renderPollList:_object code:200 inContext:_ctx];
1128 [r appendContentString:@"</D:multistatus>"];
1129 if (formatOutput) [r appendContentCharacter:'\n'];
1132 [r setStatus:204 /* no content */];
1133 //[r appendContentString:@"object was stored."];
1138 - (BOOL)renderMkColResult:(id)_object inContext:(WOContext *)_ctx {
1139 WOResponse *r = [_ctx response];
1141 if (_object == nil || [_object boolValue]) {
1142 [r setStatus:201 /* Created */];
1146 if ([_object isKindOfClass:[NSNumber class]]) {
1147 [r setStatus:[_object intValue]];
1148 [r appendContentString:@"object could not be created."];
1151 [r setStatus:500 /* server error */];
1152 [r appendContentString:@"object could not be deleted. reason: "];
1153 [r appendContentHTMLString:[_object stringValue]];
1160 - (BOOL)isDebuggingEnabled {
1164 @end /* SoWebDAVRenderer */