2 Copyright (C) 2000-2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo 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 OGo 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 OGo; 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
147 WOResponse *r = [_ctx response];
151 TODO: implement proper etag support. This probably implies that we need
152 to pass in some structure or store the etag in the context?
153 We cannot use davEntityTag on the input parameter, since this is
154 usually the plain object.
156 tmp = @"0"; // fallback, cannot use the thing above
157 [r setHeader:tmp forKey:@"ETag"]; // required for WebFolder PUTs
159 if ([_object isKindOfClass:[NSData class]]) {
160 [r setHeader:[self mimeTypeForData:_object inContext:_ctx]
161 forKey:@"content-type"];
163 [r setHeader:[NSString stringWithFormat:@"%d", [_object length]]
164 forKey:@"content-length"];
165 if (!_onlyHead) [r setContent:_object];
169 if ([_object isKindOfClass:[NSString class]]) {
172 [r setHeader:[self mimeTypeForString:_object inContext:_ctx]
173 forKey:@"content-type"];
175 data = [_object dataUsingEncoding:NSUTF8StringEncoding];
176 [r setHeader:[NSString stringWithFormat:@"%d", [data length]]
177 forKey:@"content-length"];
182 if ([_object respondsToSelector:@selector(appendToResponse:inContext:)]) {
186 [_object appendToResponse:r inContext:_ctx];
189 if ([[r headerForKey:@"content-type"] length] == 0) {
190 [r setHeader:[self mimeTypeForData:data inContext:_ctx]
191 forKey:@"content-type"];
194 if (_onlyHead) [r setContent:nil];
196 [r setHeader:[NSString stringWithFormat:@"%d", len]
197 forKey:@"content-length"];
201 [self logWithFormat:@"ERROR: don't know how to render: %@", _object];
205 - (NSException *)renderObject:(id)_object inContext:(WOContext *)_ctx {
210 if ([_object isKindOfClass:[WOResponse class]]) {
211 if (_object != [_ctx response]) {
212 [self logWithFormat:@"response mismatch"];
213 return [NSException exceptionWithHTTPStatus:500 /* internal error */];
215 [self _fixupResponse:_object inContext:_ctx];
219 m = [[_ctx request] method];
220 if ([m length] == 0) {
221 return [NSException exceptionWithHTTPStatus:400 /* bad request */
222 reason:@"missing method name!"];
224 c1 = [m characterAtIndex:0];
229 if ([m isEqualToString:@"BPROPFIND"])
230 ok = [self renderSearchResult:_object inContext:_ctx];
233 if ([m isEqualToString:@"COPY"]) {
234 ok = [self renderStatusResult:_object
235 withDefaultStatus:201 /* Created */
240 if ([m isEqualToString:@"DELETE"])
241 ok = [self renderDeleteResult:_object inContext:_ctx];
244 if ([m isEqualToString:@"GET"])
245 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:NO];
248 if ([m isEqualToString:@"HEAD"])
249 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:YES];
252 if ([m isEqualToString:@"LOCK"])
253 ok = [self renderLockToken:_object inContext:_ctx];
256 if ([m isEqualToString:@"MKCOL"])
257 ok = [self renderMkColResult:_object inContext:_ctx];
258 else if ([m isEqualToString:@"MOVE"]) {
259 ok = [self renderStatusResult:_object
260 withDefaultStatus:201 /* Created */
265 if ([m isEqualToString:@"OPTIONS"])
266 ok = [self renderOptions:_object inContext:_ctx];
269 if ([m isEqualToString:@"PUT"])
270 ok = [self renderUploadResult:_object inContext:_ctx];
271 else if ([m isEqualToString:@"PROPFIND"])
272 ok = [self renderSearchResult:_object inContext:_ctx];
273 else if ([m isEqualToString:@"PROPPATCH"])
274 ok = [self renderPropPatchResult:_object inContext:_ctx];
275 else if ([m isEqualToString:@"POLL"])
276 ok = [self renderPollResult:_object inContext:_ctx];
279 if ([m isEqualToString:@"SEARCH"])
280 ok = [self renderSearchResult:_object inContext:_ctx];
281 else if ([m isEqualToString:@"SUBSCRIBE"])
282 ok = [self renderSubscription:_object inContext:_ctx];
290 if (ok) [self _fixupResponse:[_ctx response] inContext:_ctx];
293 : [NSException exceptionWithHTTPStatus:500 /* server error */];
296 - (BOOL)canRenderObject:(id)_object inContext:(WOContext *)_ctx {
297 if ([_object isKindOfClass:[NSException class]])
302 - (NSString *)stringForValue:(id)_value ofProperty:(NSString *)_prop
303 prefixes:(NSDictionary *)_prefixes
305 /* seems like this is the default date value */
306 NSString *datefmt = @"%a, %d %b %Y %H:%M:%S GMT";
310 if (![_value isNotNull])
313 /* special processing for some properties */
315 if ([_prop isEqualToString:@"{DAV:}resourcetype"]) {
316 _value = [_value stringValue];
317 if ([_value length] == 0) return nil;
319 return [NSString stringWithFormat:@"<%@:%@/>",
320 [_prefixes objectForKey:@"DAV:"], _value];
322 else if ([_prop isEqualToString:@"{DAV:}creationdate"])
323 datefmt = @"%Y-%m-%dT%H:%M:%S%zZ";
325 /* special processing for some properties */
327 // TODO: move this to user-level code !
328 // HH: what is that ? it does not do anything anyway ?
329 if ([_prop hasPrefix:XMLNS_INTTASK]) {
330 if ([_prop hasSuffix:@"}0x00008102"]) {
334 /* special processing for some classes */
336 if ([_value isKindOfClass:[NSString class]])
337 return [_value stringByEscapingXMLString];
339 if ([_value isKindOfClass:[NSNumber class]])
340 return [_value stringValue];
342 if ([_value isKindOfClass:[NSDate class]]) {
343 return [_value descriptionWithCalendarFormat:datefmt
348 return [[_value stringValue] stringByEscapingXMLString];
351 - (NSString *)baseURLForContext:(WOContext *)_ctx {
353 Note: Evolution doesn't correctly transfer the "Host:" header, it
354 misses the port argument :-(
363 if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
365 if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
366 hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
368 else if ((tmp = [rq headerForKey:@"host"]))
371 hostport = [[NSHost currentHost] name];
373 baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
377 - (NSString *)tidyHref:(id)_href baseURL:(id)baseURL {
380 href = [_href stringValue];
383 // TODO: this happens if we access using Goliath
384 if ([href hasPrefix:@"http:/"] && ![href hasPrefix:@"http://"]) {
385 [self logWithFormat:@"BROKEN URL: %@", _href];
393 @"WARNING: using baseURL for href, "
394 @"entry did not provide a URL: %@", baseURL];
396 href = [baseURL stringValue];
398 else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
399 // TODO: use "real" URL processing
400 href = [baseURL stringByAppendingPathComponent:href];
404 - (id)tidyStatus:(id)stat {
406 stat = @"HTTP/1.1 200 OK";
407 else if ([stat isKindOfClass:[NSException class]]) {
410 if ((i = [stat httpStatus]) > 0)
411 stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
413 stat = [(NSException *)stat name];
414 stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
420 - (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx
421 namesOnly:(BOOL)_namesOnly
422 attributes:(NSArray *)_attrs
423 propertyMap:(NSDictionary *)_propMap
424 baseURL:(NSString *)baseURL
425 tagToPrefix:(NSDictionary *)extNameCache
426 nsToPrefix:(NSDictionary *)nsToPrefix
428 /* Note: the entry is an NSArray in case _namesOnly is requested! */
429 // TODO: use -valueForKey: to improve NSNull handling ?
438 isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
441 [self debugWithFormat:@" render entry: 0x%08X<%@>%s%s",
442 entry, NSStringFromClass([entry class]),
443 isBrief ? " brief" : "",
444 _namesOnly ? " names-only" : ""];
447 /* we do not map these DAV properties because they are very special */
449 if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
450 if ((key = [_propMap objectForKey:@"{DAV:}href"]) != nil) {
451 if ((href = [entry valueForKey:key]) == nil) {
453 [self debugWithFormat:
454 @"WARNING: no value for {DAV:}href key '%@': %@",
460 [self debugWithFormat:
461 @"WARNING: no key for {DAV:}href in property map !"];
464 if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
465 if ((key = [_propMap objectForKey:@"{DAV:}status"]))
466 stat = [entry valueForKey:key];
470 href = [self tidyHref:href baseURL:baseURL];
473 stat = [self tidyStatus:stat];
475 else { /* propnames only */
476 href = [baseURL stringValue];
477 stat = @"HTTP/1.1 200 OK";
481 [self debugWithFormat:@" status: %@", stat];
482 [self debugWithFormat:@" href: %@", href];
486 [r appendContentString:@"<D:response>"];
487 if (formatOutput) [r appendContentCharacter:'\n'];
489 if ([href isNotNull]) {
490 [r appendContentString:@"<D:href>"];
492 TODO: need to find out what is appropriate! While Cadaver and ZideLook
493 (both Neon+Expat) seem to be fine with this, OSX reports invalid
494 characters (displayed as '#') for umlauts.
495 It might be that we are supposed to use *URL* escaping in any
496 case! (notably entering a directory with an umlaut doesn't seem
497 to work in Cadaver either because of a URL mismatch!)
498 Note: we cannot apply URL encoding in this place, because it will encode
499 all URL special chars ... where are URLs escaped?
500 Note: we always need to apply XML escaping (even though higher-level
501 characters might be already encoded)!
503 [r appendContentXMLString:[href stringValue]];
504 [r appendContentString:@"</D:href>"];
505 if (formatOutput) [r appendContentCharacter:'\n'];
509 @"WARNING: WebDAV result entry has no valid href: %@", entry];
512 [r appendContentString:@"<D:propstat>"];
514 [r appendContentString:@"<D:status>"];
515 [r appendContentXMLString:[stat stringValue]];
516 [r appendContentString:@"</D:status>"];
519 [r appendContentString:@"<D:prop>"];
520 if (formatOutput) [r appendContentCharacter:'\n'];
522 /* now the properties */
524 keys = [_attrs objectEnumerator] ;
525 while ((key = [keys nextObject])) {
530 #if 0 /* this filter probably doesn't make sense ? */
531 /* filter out predefined props */
532 if ([key isEqualToString:@"{DAV:}href"]) continue;
533 if ([key isEqualToString:@"{DAV:}status"]) continue;
536 extName = [extNameCache objectForKey:key];
539 [r appendContentCharacter:'<'];
540 [r appendContentString:extName];
541 [r appendContentString:@"/>"];
542 if (formatOutput) [r appendContentCharacter:'\n'];
546 // TODO: we should support property status (eg encode 404 on NSNull)
548 if ((okey = [_propMap objectForKey:key]) == nil)
551 if ([key isEqualToString:@"{DAV:}href"])
554 value = [entry valueForKey:okey];
556 if ([value isNotNull]) {
559 if ([value isKindOfClass:[SoWebDAVValue class]]) {
560 s = [value stringForTag:key rawName:extName
561 inContext:_ctx prefixes:nsToPrefix];
562 [r appendContentString:s];
565 [r appendContentCharacter:'<'];
566 [r appendContentString:extName];
567 [r appendContentCharacter:'>'];
569 s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
570 [r appendContentString:s];
572 [r appendContentString:@"</"];
573 [r appendContentString:extName];
574 [r appendContentString:@">"];
575 if (formatOutput) [r appendContentCharacter:'\n'];
582 Not sure whether this is correct, do we need to encode null attrs?
583 Seems like Evo gets confused on that.
584 TODO: probably add a 404 property status for that!
586 [r appendContentCharacter:'<'];
587 [r appendContentString:extName];
588 [r appendContentString:@"/>"];
589 if (formatOutput) [r appendContentCharacter:'\n'];
593 [r appendContentString:@"</D:prop></D:propstat></D:response>"];
594 if (formatOutput) [r appendContentCharacter:'\n'];
597 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
598 tagToExtName:(NSMutableDictionary *)_tagToExtName
599 nsToPrefix:(NSMutableDictionary *)_nsToPrefix
601 unichar autoPrefix[2] = { ('a' - 1), 0 };
605 e = [_attrs objectEnumerator];
606 while ((fqn = [e nextObject])) {
607 NSString *ns, *localName, *prefix, *extName;
609 if ([_tagToExtName objectForKey:fqn]) continue;
611 if (![fqn xmlIsFQN]) {
612 /* hm, no namespace given :-(, using DAV */
617 ns = [fqn xmlNamespaceURI];
618 localName = [fqn xmlLocalName];
621 if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
622 if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
624 prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
626 [_nsToPrefix setObject:prefix forKey:ns];
629 extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
630 [_tagToExtName setObject:extName forKey:fqn];
634 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
639 ms = [NSMutableString stringWithCapacity:256];
640 nse = [_nsToPrefix keyEnumerator];
641 while ((ns = [nse nextObject])) {
642 [ms appendString:@" xmlns:"];
643 [ms appendString:[_nsToPrefix objectForKey:ns]];
644 [ms appendString:@"=\""];
645 [ms appendString:ns];
646 [ms appendString:@"\""];
651 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx
652 namesOnly:(BOOL)_namesOnly
653 attributes:(NSArray *)_attrs
654 propertyMap:(NSDictionary *)_propMap
656 NSMutableDictionary *extNameCache = nil;
657 NSMutableDictionary *nsToPrefix = nil;
658 NSAutoreleasePool *pool;
662 pool = [[NSAutoreleasePool alloc] init];
665 if (![_entries isKindOfClass:[NSEnumerator class]]) {
666 if ([_entries isKindOfClass:[NSArray class]]) {
667 [self debugWithFormat:@" render %i entries", [_entries count]];
668 _entries = [_entries objectEnumerator];
671 [self debugWithFormat:@" render a single object ..."];
672 _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
676 /* collect used namespaces */
678 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
679 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
682 the extNameCache is used to map fully qualified tag names to their
683 prefixed external representation
685 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
687 // TODO: only walk attrs, if available
689 Walk all attributes of all entries to collect names. We might be able
690 to take a look at just the first record if it is guaranteed, that all
691 records have all properties (even if the value is NSNull) ?
693 [self buildPrefixMapForAttributes:_attrs
694 tagToExtName:extNameCache
695 nsToPrefix:nsToPrefix];
697 /* generate multistatus */
699 [r setStatus:207 /* multistatus */];
700 [r setHeader:@"no-cache" forKey:@"pragma"];
701 [r setHeader:@"no-cache" forKey:@"cache-control"];
703 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
704 [r appendContentString:@"<D:multistatus"];
705 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
706 [r appendContentString:@">"];
707 if (formatOutput) [r appendContentCharacter:'\n'];
713 baseURL = [self baseURLForContext:_ctx];
714 [self debugWithFormat:@" baseURL: %@", baseURL];
716 entryCount = 0; /* Note: this will clash with streamed output later */
717 while ((entry = [_entries nextObject])) {
718 [self renderSearchResultEntry:entry inContext:_ctx
719 namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
720 baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
723 [self debugWithFormat:@" rendered %i entries", entryCount];
726 If we got a "rows" range header, we report back the actual rows
727 delivered. Since we do not really support ranges in the moment,
728 we just report all rows ...
729 TODO: support for row ranges.
731 if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
732 /* sample: "Content-Range: rows 0-143; total=144" */
735 v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i",
736 entryCount>0?(entryCount - 1):0, entryCount];
737 [r setHeader:v forKey:@"content-range"];
741 [r appendContentString:@"</D:multistatus>"];
742 if (formatOutput) [r appendContentCharacter:'\n'];
747 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
748 EOFetchSpecification *fs;
749 NSDictionary *propMap;
751 if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
754 if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
755 propMap = [_object davAttributeMapInContext:_ctx];
758 [self debugWithFormat:@"render search result 0x%08X<%@>",
759 _object, NSStringFromClass([_object class])];
762 [self renderSearchResult:_object inContext:_ctx
763 namesOnly:[fs queryWebDAVPropertyNamesOnly]
764 attributes:[fs selectedWebDAVPropertyNames]
765 propertyMap:propMap];
768 [self debugWithFormat:@"finished rendering."];
772 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
773 /* TODO: this is a fake ! */
776 if (_object == nil) return NO;
781 [r setContentEncoding:NSUTF8StringEncoding];
782 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
783 [r setHeader:[_object stringValue] forKey:@"lock-token"];
784 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
785 [r appendContentString:@"<D:prop xmlns:D=\"DAV:\">"];
786 [r appendContentString:@"<D:lockdiscovery>"];
787 [r appendContentString:@"<D:activelock>"];
788 if (formatOutput) [r appendContentCharacter:'\n'];
790 /* this is the href of the lock, not of the locked resource */
791 [r appendContentString:@"<D:locktoken><D:href>"];
792 [r appendContentString:[_object stringValue]];
793 [r appendContentString:@"</D:href></D:locktoken>"];
794 if (formatOutput) [r appendContentCharacter:'\n'];
796 // TODO: locktype, eg <D:locktype><D:write/></D:locktype>
797 // TODO: lockscope, eg <D:lockscope><D:exclusive/></D:lockscope>
798 // TODO: depth, eg <D:depth>Infinitiy</D:depth>
799 // TODO: owner, eg <D:owner><D:href>...</D:href></D:owner>
800 // TODO: timeout, eg <D:timeout>Second-604800</D:timeout>
802 [r appendContentString:@"</D:activelock>"];
803 [r appendContentString:@"</D:lockdiscovery>"];
804 [r appendContentString:@"</D:prop>"];
805 if (formatOutput) [r appendContentCharacter:'\n'];
809 - (BOOL)renderOptions:(id)_object inContext:(WOContext *)_ctx {
810 WOResponse *r = [_ctx response];
813 [r setHeader:@"1,2" forKey:@"DAV"]; // TODO: select protocol level
814 //[r setHeader:@"" forKey:@"Etag"];
815 [r setHeader:[_object componentsJoinedByString:@", "] forKey:@"allow"];
819 - (BOOL)renderSubscription:(id)_object inContext:(WOContext *)_ctx {
820 // TODO: this is fake, mirrors request
821 WOResponse *r = [_ctx response];
824 NSString *notificationType;
828 callback = [rq headerForKey:@"call-back"];
829 notificationType = [rq headerForKey:@"notification-type"];
830 lifetime = [rq headerForKey:@"subscription-lifetime"];
833 if (notificationType)
834 [r setHeader:notificationType forKey:@"notification-type"];
836 [r setHeader:lifetime forKey:@"subscription-lifetime"];
838 [r setHeader:callback forKey:@"callback"];
839 [r setHeader:[self baseURLForContext:_ctx] forKey:@"content-location"];
840 [r setHeader:_object forKey:@"subscription-id"];
844 - (BOOL)renderPropPatchResult:(id)_object inContext:(WOContext *)_ctx {
845 NSMutableDictionary *extNameCache = nil;
846 NSMutableDictionary *nsToPrefix = nil;
847 WOResponse *r = [_ctx response];
849 if (_object == nil) return NO;
851 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
852 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
853 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
854 [self buildPrefixMapForAttributes:_object
855 tagToExtName:extNameCache
856 nsToPrefix:nsToPrefix];
858 [r setStatus:207 /* multistatus */];
859 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
860 [r appendContentString:@"<D:multistatus"];
861 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
862 [r appendContentString:@">"];
863 if (formatOutput) [r appendContentCharacter:'\n'];
865 [r appendContentString:@"<D:response>"];
866 if (formatOutput) [r appendContentCharacter:'\n'];
867 [r appendContentString:@"<D:href>"];
868 [r appendContentString:[[_ctx request] uri]];
869 [r appendContentString:@"</D:href>"];
870 if (formatOutput) [r appendContentCharacter:'\n'];
871 [r appendContentString:@"<D:propstat><D:status>HTTP/1.1 200 OK</D:status>"];
872 if (formatOutput) [r appendContentCharacter:'\n'];
873 [r appendContentString:@"<D:prop>"];
874 if (formatOutput) [r appendContentCharacter:'\n'];
876 /* encode properties */
881 e = [_object objectEnumerator];
882 while ((tag = [e nextObject])) {
885 extName = [extNameCache objectForKey:tag];
886 [r appendContentCharacter:'<'];
887 [r appendContentString:extName];
888 [r appendContentString:@"/>"];
889 if (formatOutput) [r appendContentCharacter:'\n'];
893 [r appendContentString:@"</D:prop></D:propstat></D:response>"];
894 if (formatOutput) [r appendContentCharacter:'\n'];
895 [r appendContentString:@"</D:multistatus>"];
896 if (formatOutput) [r appendContentCharacter:'\n'];
901 - (BOOL)renderDeleteResult:(id)_object inContext:(WOContext *)_ctx {
902 WOResponse *r = [_ctx response];
904 if (_object == nil || [_object boolValue]) {
905 [r setStatus:204 /* no content */];
906 //[r appendContentString:@"object was deleted."];
910 if ([_object isKindOfClass:[NSNumber class]]) {
911 [r setStatus:[_object intValue]];
912 if ([r status] != 204 /* No Content */)
913 [r appendContentString:@"object could not be deleted."];
916 [r setStatus:500 /* server error */];
917 [r appendContentString:@"object could not be deleted. reason: "];
918 [r appendContentHTMLString:[_object stringValue]];
923 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
924 inContext:(WOContext *)_ctx
926 WOResponse *r = [_ctx response];
928 if (_object == nil) {
929 [r setStatus:_defStatus /* no content */];
933 if ([_object isKindOfClass:[NSNumber class]]) {
934 if ([_object intValue] < 100) {
935 [r setStatus:_defStatus /* no content */];
939 [r setStatus:[_object intValue]];
943 [r setStatus:_defStatus /* no content */];
947 - (BOOL)renderUploadResult:(id)_object inContext:(WOContext *)_ctx {
948 WOResponse *r = [_ctx response];
950 if (_object == nil) {
951 [r setStatus:204 /* no content */];
955 if ([_object isKindOfClass:[NSNumber class]]) {
956 if ([_object intValue] < 100) {
957 [r setStatus:204 /* no content */];
961 [r setStatus:[_object intValue]];
962 if ([_object intValue] >= 300) {
963 [r setHeader:@"text/html" forKey:@"content-type"];
964 [r appendContentString:@"object could not be stored."];
970 [r setStatus:204 /* no content */];
974 - (void)renderPollList:(NSArray *)_sids code:(int)_code
975 inContext:(WOContext *)_ctx
977 WOResponse *r = [_ctx response];
982 if ([_sids count] == 0) return;
983 href = [self baseURLForContext:_ctx];
985 [r appendContentString:@"<D:response>"];
986 if (formatOutput) [r appendContentCharacter:'\n'];
987 [r appendContentString:@"<D:href>"];
988 [r appendContentString:href];
989 [r appendContentString:@"</D:href>"];
990 if (formatOutput) [r appendContentCharacter:'\n'];
992 [r appendContentString:@"<D:status>HTTP/1.1 "];
994 [r appendContentString:@"200 OK"];
995 else if (_code == 204)
996 [r appendContentString:@"204 No Content"];
999 s = [NSString stringWithFormat:@"%i code%i"];
1000 [r appendContentString:s];
1002 [r appendContentString:@"</D:status>"];
1003 if (formatOutput) [r appendContentCharacter:'\n'];
1005 [r appendContentString:@"<E:subscriptionID>"];
1006 if (formatOutput) [r appendContentCharacter:'\n'];
1007 e = [_sids objectEnumerator];
1008 while ((sid = [e nextObject])) {
1009 if (formatOutput) [r appendContentString:@" "];
1010 [r appendContentString:@"<li>"];
1011 [r appendContentString:sid];
1012 [r appendContentString:@"</li>"];
1013 if (formatOutput) [r appendContentCharacter:'\n'];
1015 [r appendContentString:@"</E:subscriptionID>"];
1016 if (formatOutput) [r appendContentCharacter:'\n'];
1017 [r appendContentString:@"</D:response>"];
1018 if (formatOutput) [r appendContentCharacter:'\n'];
1021 - (BOOL)renderPollResult:(id)_object inContext:(WOContext *)_ctx {
1022 WOResponse *r = [_ctx response];
1024 if (_object == nil) {
1025 [r setStatus:204 /* no content */];
1029 if ([_object isKindOfClass:[NSDictionary class]]) {
1030 NSArray *pending, *inactive;
1032 pending = [_object objectForKey:@"pending"];
1033 inactive = [_object objectForKey:@"inactive"];
1035 [r setStatus:207 /* Multi-Status */];
1036 [r setHeader:@"text/xml" forKey:@"content-type"];
1038 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1039 [r appendContentString:@"<D:multistatus "];
1040 [r appendContentString:@" xmlns:D=\"DAV:\""];
1041 [r appendContentString:
1042 @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1043 [r appendContentString:@">"];
1044 if (formatOutput) [r appendContentCharacter:'\n'];
1046 [self renderPollList:pending code:200 inContext:_ctx];
1047 [self renderPollList:inactive code:204 inContext:_ctx];
1049 [r appendContentString:@"</D:multistatus>"];
1050 if (formatOutput) [r appendContentCharacter:'\n'];
1052 else if ([_object isKindOfClass:[NSArray class]]) {
1053 [r setStatus:207 /* Multi-Status */];
1054 [r setHeader:@"text/xml" forKey:@"content-type"];
1056 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1057 [r appendContentString:@"<D:multistatus "];
1058 [r appendContentString:@" xmlns:D=\"DAV:\""];
1059 [r appendContentString:
1060 @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1061 [r appendContentString:@">"];
1062 if (formatOutput) [r appendContentCharacter:'\n'];
1064 [self renderPollList:_object code:200 inContext:_ctx];
1066 [r appendContentString:@"</D:multistatus>"];
1067 if (formatOutput) [r appendContentCharacter:'\n'];
1070 [r setStatus:204 /* no content */];
1071 //[r appendContentString:@"object was stored."];
1076 - (BOOL)renderMkColResult:(id)_object inContext:(WOContext *)_ctx {
1077 WOResponse *r = [_ctx response];
1079 if (_object == nil || [_object boolValue]) {
1080 [r setStatus:201 /* Created */];
1084 if ([_object isKindOfClass:[NSNumber class]]) {
1085 [r setStatus:[_object intValue]];
1086 [r appendContentString:@"object could not be created."];
1089 [r setStatus:500 /* server error */];
1090 [r appendContentString:@"object could not be deleted. reason: "];
1091 [r appendContentHTMLString:[_object stringValue]];
1098 - (BOOL)isDebuggingEnabled {
1102 @end /* SoWebDAVRenderer */