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 static NSString *server = nil;
102 server = [[NSString alloc] initWithFormat:@"SOPE %i.%i.%i/WebDAV",
103 SOPE_MAJOR_VERSION, SOPE_MINOR_VERSION,
104 SOPE_SUBMINOR_VERSION];
107 [_r setHeader:server forKey:@"server"];
110 [_r setHeader:@"close" forKey:@"connection"];
111 [_r setHeader:@"DAV" forKey:@"Ms-Author-Via"];
113 // what program uses that header ?
114 [_r setHeader:@"200 No error" forKey:@"X-Dav-Error"];
116 if ((tmp = [_r headerForKey:@"content-type"]) == nil)
117 [_r setHeader:@"text/xml" forKey:@"content-type"];
120 nowHttpString = [now descriptionWithCalendarFormat:
121 @"%a, %d %b %Y %H:%M:%S GMT"
125 if ((tmp = [_r headerForKey:@"date"]) == nil)
126 [_r setHeader:nowHttpString forKey:@"date"];
128 #if 0 /* currently none of the clients allows zipping, retry later ... */
130 if ([_r shouldZipResponseToRequest:nil]) {
131 [self logWithFormat:@"zipping DAV result ..."];
137 - (NSString *)mimeTypeForData:(NSData *)_data inContext:(WOContext *)_ctx {
138 /* should check extension for MIME type */
139 return @"application/octet-stream";
141 - (NSString *)mimeTypeForString:(NSString *)_str inContext:(WOContext *)_ctx {
142 /* should check extension for MIME type */
144 if ([_str hasPrefix:@"<?xml"])
145 return @"text/xml; charset=\"utf-8\"";
146 if ([_str hasPrefix:@"<html"])
147 return @"text/html; charset=\"utf-8\"";
149 return @"text/plain; charset=\"utf-8\"";
152 - (BOOL)renderObjectBodyResult:(id)_object inContext:(WOContext *)_ctx
153 onlyHead:(BOOL)_onlyHead
157 unsigned char buf[128];
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.
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
172 if ([_object isKindOfClass:[NSData class]]) {
175 [r setHeader:[self mimeTypeForData:_object inContext:_ctx]
176 forKey:@"content-type"];
178 sprintf((char *)buf, "%d", [_object length]);
179 s = [[NSString alloc] initWithCString:(const char *)buf];
180 if (s != nil) [r setHeader:s forKey:@"content-length"];
182 if (!_onlyHead) [r setContent:_object];
186 if ([_object isKindOfClass:[NSString class]]) {
190 [r setHeader:[self mimeTypeForString:_object inContext:_ctx]
191 forKey:@"content-type"];
193 data = [_object dataUsingEncoding:NSUTF8StringEncoding];
194 sprintf((char *)buf, "%d", [data length]);
195 s = [[NSString alloc] initWithCString:(const char *)buf];
196 [r setHeader:s forKey:@"content-length"];
202 if ([_object respondsToSelector:@selector(appendToResponse:inContext:)]) {
206 [_object appendToResponse:r inContext:_ctx];
209 if ([[r headerForKey:@"content-type"] length] == 0) {
210 [r setHeader:[self mimeTypeForData:data inContext:_ctx]
211 forKey:@"content-type"];
214 if (_onlyHead) [r setContent:nil];
216 [r setHeader:[NSString stringWithFormat:@"%d", len]
217 forKey:@"content-length"];
221 [self errorWithFormat:@"don't know how to render: %@", _object];
225 - (NSException *)renderObject:(id)_object inContext:(WOContext *)_ctx {
230 if ([_object isKindOfClass:[WOResponse class]]) {
231 if (_object != [_ctx response]) {
232 [self logWithFormat:@"response mismatch"];
233 return [NSException exceptionWithHTTPStatus:500 /* internal error */];
235 [self _fixupResponse:_object inContext:_ctx];
239 m = [[_ctx request] method];
240 if ([m length] == 0) {
241 return [NSException exceptionWithHTTPStatus:400 /* bad request */
242 reason:@"missing method name!"];
244 c1 = [m characterAtIndex:0];
249 if ([m isEqualToString:@"BPROPFIND"])
250 ok = [self renderSearchResult:_object inContext:_ctx];
253 if ([m isEqualToString:@"COPY"]) {
254 ok = [self renderStatusResult:_object
255 withDefaultStatus:201 /* Created */
260 if ([m isEqualToString:@"DELETE"])
261 ok = [self renderDeleteResult:_object inContext:_ctx];
264 if ([m isEqualToString:@"GET"])
265 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:NO];
268 if ([m isEqualToString:@"HEAD"])
269 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:YES];
272 if ([m isEqualToString:@"LOCK"])
273 ok = [self renderLockToken:_object inContext:_ctx];
276 if ([m isEqualToString:@"MKCOL"])
277 ok = [self renderMkColResult:_object inContext:_ctx];
278 else if ([m isEqualToString:@"MOVE"]) {
279 ok = [self renderStatusResult:_object
280 withDefaultStatus:201 /* Created */
285 if ([m isEqualToString:@"OPTIONS"])
286 ok = [self renderOptions:_object inContext:_ctx];
289 if ([m isEqualToString:@"PUT"])
290 ok = [self renderUploadResult:_object inContext:_ctx];
291 else if ([m isEqualToString:@"PROPFIND"])
292 ok = [self renderSearchResult:_object inContext:_ctx];
293 else if ([m isEqualToString:@"PROPPATCH"])
294 ok = [self renderPropPatchResult:_object inContext:_ctx];
295 else if ([m isEqualToString:@"POLL"])
296 ok = [self renderPollResult:_object inContext:_ctx];
299 if ([m isEqualToString:@"SEARCH"])
300 ok = [self renderSearchResult:_object inContext:_ctx];
301 else if ([m isEqualToString:@"SUBSCRIBE"])
302 ok = [self renderSubscription:_object inContext:_ctx];
310 if (ok) [self _fixupResponse:[_ctx response] inContext:_ctx];
313 : [NSException exceptionWithHTTPStatus:500 /* server error */];
316 - (BOOL)canRenderObject:(id)_object inContext:(WOContext *)_ctx {
317 if ([_object isKindOfClass:[NSException class]])
322 - (NSString *)stringForResourceType:(id)_value ofProperty:(NSString *)_prop
323 prefixes:(NSDictionary *)_prefixes
327 davNS = [_prefixes objectForKey:@"DAV:"];
329 if ([_value isKindOfClass:[NSArray class]]) {
331 Use arrays to allow for something like this:
333 <C:todos xmlns:C="urn:ietf:params:xml:ns:caldav"/>
335 ( TAG ) => tag in DAV: namespace
336 ( TAG, NS ) => tag in NS namespace
337 ( TAG, NS, PREFIX ) => tag in NS namespace with PREFIX
343 if ([_value count] == 0)
346 ms = [NSMutableString stringWithCapacity:16];
347 e = [_value objectEnumerator];
348 while ((item = [e nextObject]) != nil) {
351 if (![item isKindOfClass:[NSArray class]]) {
352 item = [item stringValue];
353 if ([item length] == 0) continue;
354 [ms appendFormat:@"<%@:%@ />", davNS, item];
358 /* process array tags */
360 if ((count = [item count]) == 0)
364 [ms appendFormat:@"<%@:%@ />", davNS, [item objectAtIndex:0]];
365 else if (count == 2) {
367 [ms appendFormat:@"<%@ xmlns=\"%@\" />",
368 [item objectAtIndex:0], [item objectAtIndex:1]];
371 /* 0=tag, 1=nsuri, 2=nsprefix */
372 [ms appendFormat:@"<%@:%@ xmlns:%@=\"%@\" />",
373 [item objectAtIndex:2], [item objectAtIndex:0],
374 [item objectAtIndex:2], [item objectAtIndex:1]];
380 _value = [_value stringValue];
381 if ([_value length] == 0) return nil;
383 return [NSString stringWithFormat:@"<%@:%@/>", davNS, _value];
385 - (NSString *)stringForValue:(id)_value ofProperty:(NSString *)_prop
386 prefixes:(NSDictionary *)_prefixes
388 /* seems like this is the default date value */
389 NSString *datefmt = @"%a, %d %b %Y %H:%M:%S GMT";
393 if (![_value isNotNull])
396 /* special processing for some properties */
398 if ([_prop isEqualToString:@"{DAV:}resourcetype"]) {
399 return [self stringForResourceType:_value ofProperty:_prop
402 else if ([_prop isEqualToString:@"{DAV:}creationdate"])
403 datefmt = @"%Y-%m-%dT%H:%M:%S%zZ";
405 /* special processing for some properties */
407 // TODO: move this to user-level code !
408 // HH: what is that ? it does not do anything anyway ?
409 if ([_prop hasPrefix:XMLNS_INTTASK]) {
410 if ([_prop hasSuffix:@"}0x00008102"]) {
414 /* special processing for some classes */
416 if ([_value isKindOfClass:[NSString class]])
417 return [_value stringByEscapingXMLString];
419 if ([_value isKindOfClass:[NSNumber class]])
420 return [_value stringValue];
422 if ([_value isKindOfClass:[NSDate class]]) {
423 return [_value descriptionWithCalendarFormat:datefmt
428 return [[_value stringValue] stringByEscapingXMLString];
431 - (NSString *)baseURLForContext:(WOContext *)_ctx {
433 Note: Evolution doesn't correctly transfer the "Host:" header, it
434 misses the port argument :-(
443 if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
445 if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
446 hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
448 else if ((tmp = [rq headerForKey:@"host"]))
451 hostport = [[NSHost currentHost] name];
453 baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
457 - (NSString *)tidyHref:(id)_href baseURL:(id)baseURL {
460 href = [_href stringValue];
463 // TODO: this happens if we access using Goliath
464 if ([href hasPrefix:@"http:/"] && ![href hasPrefix:@"http://"]) {
465 [self logWithFormat:@"BROKEN URL: %@", _href];
472 [self warnWithFormat:
473 @"using baseURL for href, entry did not provide a URL: %@",
476 href = [baseURL stringValue];
478 else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
479 // TODO: use "real" URL processing
480 href = [baseURL stringByAppendingPathComponent:href];
484 - (id)tidyStatus:(id)stat {
486 stat = @"HTTP/1.1 200 OK";
487 else if ([stat isKindOfClass:[NSException class]]) {
490 if ((i = [stat httpStatus]) > 0)
491 stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
493 stat = [(NSException *)stat name];
494 stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
500 - (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx
501 namesOnly:(BOOL)_namesOnly
502 attributes:(NSArray *)_attrs
503 propertyMap:(NSDictionary *)_propMap
504 baseURL:(NSString *)baseURL
505 tagToPrefix:(NSDictionary *)extNameCache
506 nsToPrefix:(NSDictionary *)nsToPrefix
508 /* Note: the entry is an NSArray in case _namesOnly is requested! */
509 // TODO: use -valueForKey: to improve NSNull handling ?
518 isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
521 [self debugWithFormat:@" render entry: 0x%08X<%@>%s%s",
522 entry, NSStringFromClass([entry class]),
523 isBrief ? " brief" : "",
524 _namesOnly ? " names-only" : ""];
527 /* we do not map these DAV properties because they are very special */
529 if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
530 if ((key = [_propMap objectForKey:@"{DAV:}href"]) != nil) {
531 if ((href = [entry valueForKey:key]) == nil) {
533 [self warnWithFormat:
534 @"no value for {DAV:}href key '%@': %@", key, entry];
539 [self warnWithFormat:@"no key for {DAV:}href in property map !"];
542 if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
543 if ((key = [_propMap objectForKey:@"{DAV:}status"]))
544 stat = [entry valueForKey:key];
548 href = [self tidyHref:href baseURL:baseURL];
551 stat = [self tidyStatus:stat];
553 else { /* propnames only */
554 href = [baseURL stringValue];
555 stat = @"HTTP/1.1 200 OK";
559 [self debugWithFormat:@" status: %@", stat];
560 [self debugWithFormat:@" href: %@", href];
564 [r appendContentString:@"<D:response>"];
565 if (formatOutput) [r appendContentCharacter:'\n'];
567 if ([href isNotNull]) {
568 [r appendContentString:@"<D:href>"];
570 TODO: need to find out what is appropriate! While Cadaver and ZideLook
571 (both Neon+Expat) seem to be fine with this, OSX reports invalid
572 characters (displayed as '#') for umlauts.
573 It might be that we are supposed to use *URL* escaping in any
574 case! (notably entering a directory with an umlaut doesn't seem
575 to work in Cadaver either because of a URL mismatch!)
576 Note: we cannot apply URL encoding in this place, because it will encode
577 all URL special chars ... where are URLs escaped?
578 Note: we always need to apply XML escaping (even though higher-level
579 characters might be already encoded)!
581 [r appendContentXMLString:[href stringValue]];
582 [r appendContentString:@"</D:href>"];
583 if (formatOutput) [r appendContentCharacter:'\n'];
586 [self warnWithFormat:@"WebDAV result entry has no valid href: %@", entry];
589 [r appendContentString:@"<D:propstat>"];
590 [r appendContentString:@"<D:prop>"];
591 if (formatOutput) [r appendContentCharacter:'\n'];
593 /* now the properties */
595 keys = [_attrs objectEnumerator] ;
596 while ((key = [keys nextObject])) {
601 #if 0 /* this filter probably doesn't make sense ? */
602 /* filter out predefined props */
603 if ([key isEqualToString:@"{DAV:}href"]) continue;
604 if ([key isEqualToString:@"{DAV:}status"]) continue;
607 extName = [extNameCache objectForKey:key];
610 [r appendContentCharacter:'<'];
611 [r appendContentString:extName];
612 [r appendContentString:@"/>"];
613 if (formatOutput) [r appendContentCharacter:'\n'];
617 // TODO: we should support property status (eg encode 404 on NSNull)
619 if ((okey = [_propMap objectForKey:key]) == nil)
622 if ([key isEqualToString:@"{DAV:}href"])
625 value = [entry valueForKey:okey];
627 if ([value isNotNull]) {
630 if ([value isKindOfClass:[SoWebDAVValue class]]) {
631 s = [value stringForTag:key rawName:extName
632 inContext:_ctx prefixes:nsToPrefix];
633 [r appendContentString:s];
636 [r appendContentCharacter:'<'];
637 [r appendContentString:extName];
638 [r appendContentCharacter:'>'];
640 s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
641 [r appendContentString:s];
643 [r appendContentString:@"</"];
644 [r appendContentString:extName];
645 [r appendContentString:@">"];
646 if (formatOutput) [r appendContentCharacter:'\n'];
653 Not sure whether this is correct, do we need to encode null attrs?
654 Seems like Evo gets confused on that.
655 TODO: probably add a 404 property status for that!
657 [r appendContentCharacter:'<'];
658 [r appendContentString:extName];
659 [r appendContentString:@"/>"];
660 if (formatOutput) [r appendContentCharacter:'\n'];
664 [r appendContentString:@"</D:prop>"];
665 if (formatOutput) [r appendContentCharacter:'\n'];
668 [r appendContentString:@"<D:status>"];
669 [r appendContentXMLString:[stat stringValue]];
670 [r appendContentString:@"</D:status>"];
673 [r appendContentString:@"</D:propstat></D:response>"];
674 if (formatOutput) [r appendContentCharacter:'\n'];
677 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
678 tagToExtName:(NSMutableDictionary *)_tagToExtName
679 nsToPrefix:(NSMutableDictionary *)_nsToPrefix
681 unichar autoPrefix[2] = { ('a' - 1), 0 };
685 e = [_attrs objectEnumerator];
686 while ((fqn = [e nextObject])) {
687 NSString *ns, *localName, *prefix, *extName;
689 if ([_tagToExtName objectForKey:fqn]) continue;
691 if (![fqn xmlIsFQN]) {
692 /* hm, no namespace given :-(, using DAV */
697 ns = [fqn xmlNamespaceURI];
698 localName = [fqn xmlLocalName];
701 if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
702 if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
704 prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
706 [_nsToPrefix setObject:prefix forKey:ns];
709 extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
710 [_tagToExtName setObject:extName forKey:fqn];
714 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
719 ms = [NSMutableString stringWithCapacity:256];
720 nse = [_nsToPrefix keyEnumerator];
721 while ((ns = [nse nextObject])) {
722 [ms appendString:@" xmlns:"];
723 [ms appendString:[_nsToPrefix objectForKey:ns]];
724 [ms appendString:@"=\""];
725 [ms appendString:ns];
726 [ms appendString:@"\""];
731 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx
732 namesOnly:(BOOL)_namesOnly
733 attributes:(NSArray *)_attrs
734 propertyMap:(NSDictionary *)_propMap
736 NSMutableDictionary *extNameCache = nil;
737 NSMutableDictionary *nsToPrefix = nil;
738 NSAutoreleasePool *pool;
742 pool = [[NSAutoreleasePool alloc] init];
745 if (![_entries isKindOfClass:[NSEnumerator class]]) {
746 if ([_entries isKindOfClass:[NSArray class]]) {
747 [self debugWithFormat:@" render %i entries", [_entries count]];
748 _entries = [_entries objectEnumerator];
751 [self debugWithFormat:@" render a single object ..."];
752 _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
756 /* collect used namespaces */
758 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
759 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
762 the extNameCache is used to map fully qualified tag names to their
763 prefixed external representation
765 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
767 // TODO: only walk attrs, if available
769 Walk all attributes of all entries to collect names. We might be able
770 to take a look at just the first record if it is guaranteed, that all
771 records have all properties (even if the value is NSNull) ?
773 [self buildPrefixMapForAttributes:_attrs
774 tagToExtName:extNameCache
775 nsToPrefix:nsToPrefix];
777 /* generate multistatus */
779 [r setStatus:207 /* multistatus */];
780 [r setContentEncoding:NSUTF8StringEncoding];
781 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
782 [r setHeader:@"no-cache" forKey:@"pragma"];
783 [r setHeader:@"no-cache" forKey:@"cache-control"];
785 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
786 [r appendContentString:@"<D:multistatus"];
787 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
788 [r appendContentString:@">"];
789 if (formatOutput) [r appendContentCharacter:'\n'];
795 baseURL = [self baseURLForContext:_ctx];
796 [self debugWithFormat:@" baseURL: %@", baseURL];
798 entryCount = 0; /* Note: this will clash with streamed output later */
799 while ((entry = [_entries nextObject])) {
800 [self renderSearchResultEntry:entry inContext:_ctx
801 namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
802 baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
805 [self debugWithFormat:@" rendered %i entries", entryCount];
808 If we got a "rows" range header, we report back the actual rows
809 delivered. Since we do not really support ranges in the moment,
810 we just report all rows ...
811 TODO: support for row ranges.
813 if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
814 /* sample: "Content-Range: rows 0-143; total=144" */
817 v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i",
818 entryCount>0?(entryCount - 1):0, entryCount];
819 [r setHeader:v forKey:@"content-range"];
823 [r appendContentString:@"</D:multistatus>"];
824 if (formatOutput) [r appendContentCharacter:'\n'];
829 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
830 EOFetchSpecification *fs;
831 NSDictionary *propMap;
833 if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
836 if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
837 propMap = [_object davAttributeMapInContext:_ctx];
840 [self debugWithFormat:@"render search result 0x%08X<%@>",
841 _object, NSStringFromClass([_object class])];
844 [self renderSearchResult:_object inContext:_ctx
845 namesOnly:[fs queryWebDAVPropertyNamesOnly]
846 attributes:[fs selectedWebDAVPropertyNames]
847 propertyMap:propMap];
850 [self debugWithFormat:@"finished rendering."];
854 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
855 /* TODO: this is a fake ! */
858 if (_object == nil) return NO;
862 [r setStatus:200 /* OK */];
863 [r setContentEncoding:NSUTF8StringEncoding];
864 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
865 [r setHeader:[_object stringValue] forKey:@"lock-token"];
866 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
867 [r appendContentString:@"<D:prop xmlns:D=\"DAV:\">"];
868 [r appendContentString:@"<D:lockdiscovery>"];
869 [r appendContentString:@"<D:activelock>"];
870 if (formatOutput) [r appendContentCharacter:'\n'];
872 /* this is the href of the lock, not of the locked resource */
873 [r appendContentString:@"<D:locktoken><D:href>"];
874 [r appendContentString:[_object stringValue]];
875 [r appendContentString:@"</D:href></D:locktoken>"];
876 if (formatOutput) [r appendContentCharacter:'\n'];
878 // TODO: locktype, eg <D:locktype><D:write/></D:locktype>
879 // TODO: lockscope, eg <D:lockscope><D:exclusive/></D:lockscope>
880 // TODO: depth, eg <D:depth>Infinitiy</D:depth>
881 // TODO: owner, eg <D:owner><D:href>...</D:href></D:owner>
882 // TODO: timeout, eg <D:timeout>Second-604800</D:timeout>
884 [r appendContentString:@"</D:activelock>"];
885 [r appendContentString:@"</D:lockdiscovery>"];
886 [r appendContentString:@"</D:prop>"];
887 if (formatOutput) [r appendContentCharacter:'\n'];
891 - (BOOL)renderOptions:(id)_object inContext:(WOContext *)_ctx {
895 [r setStatus:200 /* OK */];
896 [r setHeader:@"1,2" forKey:@"DAV"]; // TODO: select protocol level
897 //[r setHeader:@"" forKey:@"Etag"];
899 if (![_object isNotNull])
901 else if ([_object isKindOfClass:[NSArray class]]) {
903 [r setHeader:[_object componentsJoinedByString:@", "] forKey:@"allow"];
906 [self logWithFormat:@"ERROR: unexpected options result: %@ (class=%@)",
907 _object, [_object class]];
912 - (BOOL)renderSubscription:(id)_object inContext:(WOContext *)_ctx {
913 // TODO: this is fake, mirrors request
914 WOResponse *r = [_ctx response];
917 NSString *notificationType;
921 callback = [rq headerForKey:@"call-back"];
922 notificationType = [rq headerForKey:@"notification-type"];
923 lifetime = [rq headerForKey:@"subscription-lifetime"];
925 [r setStatus:200 /* OK */];
926 if (notificationType != nil)
927 [r setHeader:notificationType forKey:@"notification-type"];
929 [r setHeader:lifetime forKey:@"subscription-lifetime"];
931 [r setHeader:callback forKey:@"callback"];
932 [r setHeader:[self baseURLForContext:_ctx] forKey:@"content-location"];
933 [r setHeader:_object forKey:@"subscription-id"];
937 - (BOOL)renderPropPatchResult:(id)_object inContext:(WOContext *)_ctx {
938 NSMutableDictionary *extNameCache = nil;
939 NSMutableDictionary *nsToPrefix = nil;
940 WOResponse *r = [_ctx response];
942 if (_object == nil) return NO;
944 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
945 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
946 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
947 [self buildPrefixMapForAttributes:_object
948 tagToExtName:extNameCache
949 nsToPrefix:nsToPrefix];
951 [r setStatus:207 /* multistatus */];
952 [r setContentEncoding:NSUTF8StringEncoding];
953 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
954 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
955 [r appendContentString:@"<D:multistatus"];
956 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
957 [r appendContentString:@">"];
958 if (formatOutput) [r appendContentCharacter:'\n'];
960 [r appendContentString:@"<D:response>"];
961 if (formatOutput) [r appendContentCharacter:'\n'];
962 [r appendContentString:@"<D:href>"];
963 [r appendContentString:[[_ctx request] uri]];
964 [r appendContentString:@"</D:href>"];
965 if (formatOutput) [r appendContentCharacter:'\n'];
966 [r appendContentString:@"<D:propstat>"];
967 if (formatOutput) [r appendContentCharacter:'\n'];
968 [r appendContentString:@"<D:prop>"];
969 if (formatOutput) [r appendContentCharacter:'\n'];
971 /* encode properties */
976 e = [_object objectEnumerator];
977 while ((tag = [e nextObject])) {
980 extName = [extNameCache objectForKey:tag];
981 [r appendContentCharacter:'<'];
982 [r appendContentString:extName];
983 [r appendContentString:@"/>"];
984 if (formatOutput) [r appendContentCharacter:'\n'];
988 [r appendContentString:@"</D:prop>"];
989 if (formatOutput) [r appendContentCharacter:'\n'];
990 [r appendContentString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
991 [r appendContentString:@"</D:propstat></D:response>"];
992 if (formatOutput) [r appendContentCharacter:'\n'];
993 [r appendContentString:@"</D:multistatus>"];
994 if (formatOutput) [r appendContentCharacter:'\n'];
999 - (BOOL)renderDeleteResult:(id)_object inContext:(WOContext *)_ctx {
1000 WOResponse *r = [_ctx response];
1002 if (_object == nil || [_object boolValue]) {
1003 [r setStatus:204 /* no content */];
1004 //[r appendContentString:@"object was deleted."];
1008 if ([_object isKindOfClass:[NSNumber class]]) {
1009 [r setStatus:[_object intValue]];
1010 if ([r status] != 204 /* No Content */)
1011 [r appendContentString:@"object could not be deleted."];
1014 [r setStatus:500 /* server error */];
1015 [r appendContentString:@"object could not be deleted. reason: "];
1016 [r appendContentHTMLString:[_object stringValue]];
1021 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
1022 inContext:(WOContext *)_ctx
1024 WOResponse *r = [_ctx response];
1026 if (_object == nil) {
1027 [r setStatus:_defStatus /* no content */];
1031 if ([_object isKindOfClass:[NSNumber class]]) {
1032 if ([_object intValue] < 100) {
1033 [r setStatus:_defStatus /* no content */];
1037 [r setStatus:[_object intValue]];
1041 [r setStatus:_defStatus /* no content */];
1045 - (BOOL)renderUploadResult:(id)_object inContext:(WOContext *)_ctx {
1046 WOResponse *r = [_ctx response];
1048 if (_object == nil) {
1049 [r setStatus:204 /* no content */];
1053 if ([_object isKindOfClass:[NSNumber class]]) {
1054 if ([_object intValue] < 100) {
1055 [r setStatus:204 /* no content */];
1059 [r setStatus:[_object intValue]];
1060 if ([_object intValue] >= 300) {
1061 [r setHeader:@"text/html" forKey:@"content-type"];
1062 [r appendContentString:@"object could not be stored."];
1068 [r setStatus:204 /* no content */];
1072 - (void)renderPollList:(NSArray *)_sids code:(int)_code
1073 inContext:(WOContext *)_ctx
1075 WOResponse *r = [_ctx response];
1080 if ([_sids count] == 0) return;
1081 href = [self baseURLForContext:_ctx];
1083 [r appendContentString:@"<D:response>"];
1084 if (formatOutput) [r appendContentCharacter:'\n'];
1085 [r appendContentString:@"<D:href>"];
1086 [r appendContentString:href];
1087 [r appendContentString:@"</D:href>"];
1088 if (formatOutput) [r appendContentCharacter:'\n'];
1090 [r appendContentString:@"<D:status>HTTP/1.1 "];
1092 [r appendContentString:@"200 OK"];
1093 else if (_code == 204)
1094 [r appendContentString:@"204 No Content"];
1097 s = [NSString stringWithFormat:@"%i code%i"];
1098 [r appendContentString:s];
1100 [r appendContentString:@"</D:status>"];
1101 if (formatOutput) [r appendContentCharacter:'\n'];
1103 [r appendContentString:@"<E:subscriptionID>"];
1104 if (formatOutput) [r appendContentCharacter:'\n'];
1105 e = [_sids objectEnumerator];
1106 while ((sid = [e nextObject])) {
1107 if (formatOutput) [r appendContentString:@" "];
1108 [r appendContentString:@"<li>"];
1109 [r appendContentString:sid];
1110 [r appendContentString:@"</li>"];
1111 if (formatOutput) [r appendContentCharacter:'\n'];
1113 [r appendContentString:@"</E:subscriptionID>"];
1114 if (formatOutput) [r appendContentCharacter:'\n'];
1115 [r appendContentString:@"</D:response>"];
1116 if (formatOutput) [r appendContentCharacter:'\n'];
1119 - (BOOL)renderPollResult:(id)_object inContext:(WOContext *)_ctx {
1120 WOResponse *r = [_ctx response];
1122 if (_object == nil) {
1123 [r setStatus:204 /* no content */];
1127 if ([_object isKindOfClass:[NSDictionary class]]) {
1128 NSArray *pending, *inactive;
1130 pending = [(NSDictionary *)_object objectForKey:@"pending"];
1131 inactive = [(NSDictionary *)_object objectForKey:@"inactive"];
1133 [r setStatus:207 /* Multi-Status */];
1134 [r setContentEncoding:NSUTF8StringEncoding];
1135 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
1137 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1138 [r appendContentString:@"<D:multistatus "];
1139 [r appendContentString:@" xmlns:D=\"DAV:\""];
1140 [r appendContentString:
1141 @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1142 [r appendContentString:@">"];
1143 if (formatOutput) [r appendContentCharacter:'\n'];
1145 [self renderPollList:pending code:200 inContext:_ctx];
1146 [self renderPollList:inactive code:204 inContext:_ctx];
1148 [r appendContentString:@"</D:multistatus>"];
1149 if (formatOutput) [r appendContentCharacter:'\n'];
1151 else if ([_object isKindOfClass:[NSArray class]]) {
1152 [r setStatus:207 /* Multi-Status */];
1153 [r setContentEncoding:NSUTF8StringEncoding];
1154 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
1156 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1157 [r appendContentString:@"<D:multistatus "];
1158 [r appendContentString:@" xmlns:D=\"DAV:\""];
1159 [r appendContentString:
1160 @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1161 [r appendContentString:@">"];
1162 if (formatOutput) [r appendContentCharacter:'\n'];
1164 [self renderPollList:_object code:200 inContext:_ctx];
1166 [r appendContentString:@"</D:multistatus>"];
1167 if (formatOutput) [r appendContentCharacter:'\n'];
1170 [r setStatus:204 /* no content */];
1171 //[r appendContentString:@"object was stored."];
1176 - (BOOL)renderMkColResult:(id)_object inContext:(WOContext *)_ctx {
1177 WOResponse *r = [_ctx response];
1179 if (_object == nil || [_object boolValue]) {
1180 [r setStatus:201 /* Created */];
1184 if ([_object isKindOfClass:[NSNumber class]]) {
1185 [r setStatus:[_object intValue]];
1186 [r appendContentString:@"object could not be created."];
1189 [r setStatus:500 /* server error */];
1190 [r appendContentString:@"object could not be deleted. reason: "];
1191 [r appendContentHTMLString:[_object stringValue]];
1198 - (BOOL)isDebuggingEnabled {
1202 @end /* SoWebDAVRenderer */