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 {
378 href = [href stringValue];
382 @"WARNING: using baseURL for href, "
383 @"entry did not provide a URL: %@", baseURL];
385 href = [baseURL stringValue];
387 else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
388 // TODO: use "real" URL processing
389 href = [baseURL stringByAppendingPathComponent:href];
393 - (id)tidyStatus:(id)stat {
395 stat = @"HTTP/1.1 200 OK";
396 else if ([stat isKindOfClass:[NSException class]]) {
399 if ((i = [stat httpStatus]) > 0)
400 stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
402 stat = [(NSException *)stat name];
403 stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
409 - (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx
410 namesOnly:(BOOL)_namesOnly
411 attributes:(NSArray *)_attrs
412 propertyMap:(NSDictionary *)_propMap
413 baseURL:(NSString *)baseURL
414 tagToPrefix:(NSDictionary *)extNameCache
415 nsToPrefix:(NSDictionary *)nsToPrefix
417 /* Note: the entry is an NSArray in case _namesOnly is requested! */
418 // TODO: use -valueForKey: to improve NSNull handling ?
427 isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
430 [self debugWithFormat:@" render entry: 0x%08X<%@>%s%s",
431 entry, NSStringFromClass([entry class]),
432 isBrief ? " brief" : "",
433 _namesOnly ? " names-only" : ""];
436 /* we do not map these DAV properties because they are very special */
438 if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
439 if ((key = [_propMap objectForKey:@"{DAV:}href"])) {
440 if ((href = [entry valueForKey:key]) == nil) {
442 [self debugWithFormat:
443 @"WARNING: no value for {DAV:}href key '%@': %@",
449 [self debugWithFormat:
450 @"WARNING: no key for {DAV:}href in property map !"];
453 if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
454 if ((key = [_propMap objectForKey:@"{DAV:}status"]))
455 stat = [entry valueForKey:key];
459 href = [self tidyHref:href baseURL:baseURL];
462 stat = [self tidyStatus:stat];
464 else { /* propnames only */
465 href = [baseURL stringValue];
466 stat = @"HTTP/1.1 200 OK";
470 [self debugWithFormat:@" status: %@", stat];
471 [self debugWithFormat:@" href: %@", href];
475 [r appendContentString:@"<D:response>"];
476 if (formatOutput) [r appendContentCharacter:'\n'];
479 [r appendContentString:@"<D:href>"];
481 TODO: need to find out what is appropriate! While Cadaver and ZideLook
482 (both Neon+Expat) seem to be fine with this, OSX reports invalid
483 characters (displayed as '#') for umlauts.
484 It might be that we are supposed to use *URL* escaping in any
485 case! (notably entering a directory with an umlaut doesn't seem
486 to work in Cadaver either because of a URL mismatch!)
487 Note: we cannot apply URL encoding in this place, because it will encode
488 all URL special chars ... where are URLs escaped?
489 Note: we always need to apply XML escaping (even though higher-level
490 characters might be already encoded)!
492 [r appendContentXMLString:[href stringValue]];
493 [r appendContentString:@"</D:href>"];
494 if (formatOutput) [r appendContentCharacter:'\n'];
497 [r appendContentString:@"<D:propstat>"];
499 [r appendContentString:@"<D:status>"];
500 [r appendContentXMLString:[stat stringValue]];
501 [r appendContentString:@"</D:status>"];
504 [r appendContentString:@"<D:prop>"];
505 if (formatOutput) [r appendContentCharacter:'\n'];
507 /* now the properties */
509 keys = [_attrs objectEnumerator] ;
510 while ((key = [keys nextObject])) {
515 #if 0 /* this filter probably doesn't make sense ? */
516 /* filter out predefined props */
517 if ([key isEqualToString:@"{DAV:}href"]) continue;
518 if ([key isEqualToString:@"{DAV:}status"]) continue;
521 extName = [extNameCache objectForKey:key];
524 [r appendContentCharacter:'<'];
525 [r appendContentString:extName];
526 [r appendContentString:@"/>"];
527 if (formatOutput) [r appendContentCharacter:'\n'];
531 // TODO: we should support property status (eg encode 404 on NSNull)
533 if ((okey = [_propMap objectForKey:key]) == nil)
536 if ([key isEqualToString:@"{DAV:}href"])
539 value = [entry valueForKey:okey];
541 if ([value isNotNull]) {
544 if ([value isKindOfClass:[SoWebDAVValue class]]) {
545 s = [value stringForTag:key rawName:extName
546 inContext:_ctx prefixes:nsToPrefix];
547 [r appendContentString:s];
550 [r appendContentCharacter:'<'];
551 [r appendContentString:extName];
552 [r appendContentCharacter:'>'];
554 s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
555 [r appendContentString:s];
557 [r appendContentString:@"</"];
558 [r appendContentString:extName];
559 [r appendContentString:@">"];
560 if (formatOutput) [r appendContentCharacter:'\n'];
567 Not sure whether this is correct, do we need to encode null attrs?
568 Seems like Evo gets confused on that.
569 TODO: probably add a 404 property status for that!
571 [r appendContentCharacter:'<'];
572 [r appendContentString:extName];
573 [r appendContentString:@"/>"];
574 if (formatOutput) [r appendContentCharacter:'\n'];
578 [r appendContentString:@"</D:prop></D:propstat></D:response>"];
579 if (formatOutput) [r appendContentCharacter:'\n'];
582 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
583 tagToExtName:(NSMutableDictionary *)_tagToExtName
584 nsToPrefix:(NSMutableDictionary *)_nsToPrefix
586 unichar autoPrefix[2] = { ('a' - 1), 0 };
590 e = [_attrs objectEnumerator];
591 while ((fqn = [e nextObject])) {
592 NSString *ns, *localName, *prefix, *extName;
594 if ([_tagToExtName objectForKey:fqn]) continue;
596 if (![fqn xmlIsFQN]) {
597 /* hm, no namespace given :-(, using DAV */
602 ns = [fqn xmlNamespaceURI];
603 localName = [fqn xmlLocalName];
606 if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
607 if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
609 prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
611 [_nsToPrefix setObject:prefix forKey:ns];
614 extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
615 [_tagToExtName setObject:extName forKey:fqn];
619 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
624 ms = [NSMutableString stringWithCapacity:256];
625 nse = [_nsToPrefix keyEnumerator];
626 while ((ns = [nse nextObject])) {
627 [ms appendString:@" xmlns:"];
628 [ms appendString:[_nsToPrefix objectForKey:ns]];
629 [ms appendString:@"=\""];
630 [ms appendString:ns];
631 [ms appendString:@"\""];
636 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx
637 namesOnly:(BOOL)_namesOnly
638 attributes:(NSArray *)_attrs
639 propertyMap:(NSDictionary *)_propMap
641 NSMutableDictionary *extNameCache = nil;
642 NSMutableDictionary *nsToPrefix = nil;
643 NSAutoreleasePool *pool;
647 pool = [[NSAutoreleasePool alloc] init];
650 if (![_entries isKindOfClass:[NSEnumerator class]]) {
651 if ([_entries isKindOfClass:[NSArray class]]) {
652 [self debugWithFormat:@" render %i entries", [_entries count]];
653 _entries = [_entries objectEnumerator];
656 [self debugWithFormat:@" render a single object ..."];
657 _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
661 /* collect used namespaces */
663 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
664 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
667 the extNameCache is used to map fully qualified tag names to their
668 prefixed external representation
670 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
672 // TODO: only walk attrs, if available
674 Walk all attributes of all entries to collect names. We might be able
675 to take a look at just the first record if it is guaranteed, that all
676 records have all properties (even if the value is NSNull) ?
678 [self buildPrefixMapForAttributes:_attrs
679 tagToExtName:extNameCache
680 nsToPrefix:nsToPrefix];
682 /* generate multistatus */
684 [r setStatus:207 /* multistatus */];
685 [r setHeader:@"no-cache" forKey:@"pragma"];
686 [r setHeader:@"no-cache" forKey:@"cache-control"];
688 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
689 [r appendContentString:@"<D:multistatus"];
690 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
691 [r appendContentString:@">"];
692 if (formatOutput) [r appendContentCharacter:'\n'];
698 baseURL = [self baseURLForContext:_ctx];
699 [self debugWithFormat:@" baseURL: %@", baseURL];
701 entryCount = 0; /* Note: this will clash with streamed output later */
702 while ((entry = [_entries nextObject])) {
703 [self renderSearchResultEntry:entry inContext:_ctx
704 namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
705 baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
708 [self debugWithFormat:@" rendered %i entries", entryCount];
711 If we got a "rows" range header, we report back the actual rows
712 delivered. Since we do not really support ranges in the moment,
713 we just report all rows ...
714 TODO: support for row ranges.
716 if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
717 /* sample: "Content-Range: rows 0-143; total=144" */
720 v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i",
721 entryCount>0?(entryCount - 1):0, entryCount];
722 [r setHeader:v forKey:@"content-range"];
726 [r appendContentString:@"</D:multistatus>"];
727 if (formatOutput) [r appendContentCharacter:'\n'];
732 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
733 EOFetchSpecification *fs;
734 NSDictionary *propMap;
736 if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
739 if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
740 propMap = [_object davAttributeMapInContext:_ctx];
743 [self debugWithFormat:@"render search result 0x%08X<%@>",
744 _object, NSStringFromClass([_object class])];
747 [self renderSearchResult:_object inContext:_ctx
748 namesOnly:[fs queryWebDAVPropertyNamesOnly]
749 attributes:[fs selectedWebDAVPropertyNames]
750 propertyMap:propMap];
753 [self debugWithFormat:@"finished rendering."];
757 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
758 /* TODO: this is a fake ! */
761 if (_object == nil) return NO;
766 [r setContentEncoding:NSUTF8StringEncoding];
767 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
768 [r setHeader:[_object stringValue] forKey:@"lock-token"];
769 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
770 [r appendContentString:@"<D:prop xmlns:D=\"DAV:\">"];
771 [r appendContentString:@"<D:lockdiscovery>"];
772 [r appendContentString:@"<D:activelock>"];
773 if (formatOutput) [r appendContentCharacter:'\n'];
775 /* this is the href of the lock, not of the locked resource */
776 [r appendContentString:@"<D:locktoken><D:href>"];
777 [r appendContentString:[_object stringValue]];
778 [r appendContentString:@"</D:href></D:locktoken>"];
779 if (formatOutput) [r appendContentCharacter:'\n'];
781 // TODO: locktype, eg <D:locktype><D:write/></D:locktype>
782 // TODO: lockscope, eg <D:lockscope><D:exclusive/></D:lockscope>
783 // TODO: depth, eg <D:depth>Infinitiy</D:depth>
784 // TODO: owner, eg <D:owner><D:href>...</D:href></D:owner>
785 // TODO: timeout, eg <D:timeout>Second-604800</D:timeout>
787 [r appendContentString:@"</D:activelock>"];
788 [r appendContentString:@"</D:lockdiscovery>"];
789 [r appendContentString:@"</D:prop>"];
790 if (formatOutput) [r appendContentCharacter:'\n'];
794 - (BOOL)renderOptions:(id)_object inContext:(WOContext *)_ctx {
795 WOResponse *r = [_ctx response];
798 [r setHeader:@"1,2" forKey:@"DAV"]; // TODO: select protocol level
799 //[r setHeader:@"" forKey:@"Etag"];
800 [r setHeader:[_object componentsJoinedByString:@", "] forKey:@"allow"];
804 - (BOOL)renderSubscription:(id)_object inContext:(WOContext *)_ctx {
805 // TODO: this is fake, mirrors request
806 WOResponse *r = [_ctx response];
809 NSString *notificationType;
813 callback = [rq headerForKey:@"call-back"];
814 notificationType = [rq headerForKey:@"notification-type"];
815 lifetime = [rq headerForKey:@"subscription-lifetime"];
818 if (notificationType)
819 [r setHeader:notificationType forKey:@"notification-type"];
821 [r setHeader:lifetime forKey:@"subscription-lifetime"];
823 [r setHeader:callback forKey:@"callback"];
824 [r setHeader:[self baseURLForContext:_ctx] forKey:@"content-location"];
825 [r setHeader:_object forKey:@"subscription-id"];
829 - (BOOL)renderPropPatchResult:(id)_object inContext:(WOContext *)_ctx {
830 NSMutableDictionary *extNameCache = nil;
831 NSMutableDictionary *nsToPrefix = nil;
832 WOResponse *r = [_ctx response];
834 if (_object == nil) return NO;
836 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
837 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
838 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
839 [self buildPrefixMapForAttributes:_object
840 tagToExtName:extNameCache
841 nsToPrefix:nsToPrefix];
843 [r setStatus:207 /* multistatus */];
844 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
845 [r appendContentString:@"<D:multistatus"];
846 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
847 [r appendContentString:@">"];
848 if (formatOutput) [r appendContentCharacter:'\n'];
850 [r appendContentString:@"<D:response>"];
851 if (formatOutput) [r appendContentCharacter:'\n'];
852 [r appendContentString:@"<D:href>"];
853 [r appendContentString:[[_ctx request] uri]];
854 [r appendContentString:@"</D:href>"];
855 if (formatOutput) [r appendContentCharacter:'\n'];
856 [r appendContentString:@"<D:propstat><D:status>HTTP/1.1 200 OK</D:status>"];
857 if (formatOutput) [r appendContentCharacter:'\n'];
858 [r appendContentString:@"<D:prop>"];
859 if (formatOutput) [r appendContentCharacter:'\n'];
861 /* encode properties */
866 e = [_object objectEnumerator];
867 while ((tag = [e nextObject])) {
870 extName = [extNameCache objectForKey:tag];
871 [r appendContentCharacter:'<'];
872 [r appendContentString:extName];
873 [r appendContentString:@"/>"];
874 if (formatOutput) [r appendContentCharacter:'\n'];
878 [r appendContentString:@"</D:prop></D:propstat></D:response>"];
879 if (formatOutput) [r appendContentCharacter:'\n'];
880 [r appendContentString:@"</D:multistatus>"];
881 if (formatOutput) [r appendContentCharacter:'\n'];
886 - (BOOL)renderDeleteResult:(id)_object inContext:(WOContext *)_ctx {
887 WOResponse *r = [_ctx response];
889 if (_object == nil || [_object boolValue]) {
890 [r setStatus:204 /* no content */];
891 //[r appendContentString:@"object was deleted."];
895 if ([_object isKindOfClass:[NSNumber class]]) {
896 [r setStatus:[_object intValue]];
897 if ([r status] != 204 /* No Content */)
898 [r appendContentString:@"object could not be deleted."];
901 [r setStatus:500 /* server error */];
902 [r appendContentString:@"object could not be deleted. reason: "];
903 [r appendContentHTMLString:[_object stringValue]];
908 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
909 inContext:(WOContext *)_ctx
911 WOResponse *r = [_ctx response];
913 if (_object == nil) {
914 [r setStatus:_defStatus /* no content */];
918 if ([_object isKindOfClass:[NSNumber class]]) {
919 if ([_object intValue] < 100) {
920 [r setStatus:_defStatus /* no content */];
924 [r setStatus:[_object intValue]];
928 [r setStatus:_defStatus /* no content */];
932 - (BOOL)renderUploadResult:(id)_object inContext:(WOContext *)_ctx {
933 WOResponse *r = [_ctx response];
935 if (_object == nil) {
936 [r setStatus:204 /* no content */];
940 if ([_object isKindOfClass:[NSNumber class]]) {
941 if ([_object intValue] < 100) {
942 [r setStatus:204 /* no content */];
946 [r setStatus:[_object intValue]];
947 if ([_object intValue] >= 300) {
948 [r setHeader:@"text/html" forKey:@"content-type"];
949 [r appendContentString:@"object could not be stored."];
955 [r setStatus:204 /* no content */];
959 - (void)renderPollList:(NSArray *)_sids code:(int)_code
960 inContext:(WOContext *)_ctx
962 WOResponse *r = [_ctx response];
967 if ([_sids count] == 0) return;
968 href = [self baseURLForContext:_ctx];
970 [r appendContentString:@"<D:response>"];
971 if (formatOutput) [r appendContentCharacter:'\n'];
972 [r appendContentString:@"<D:href>"];
973 [r appendContentString:href];
974 [r appendContentString:@"</D:href>"];
975 if (formatOutput) [r appendContentCharacter:'\n'];
977 [r appendContentString:@"<D:status>HTTP/1.1 "];
979 [r appendContentString:@"200 OK"];
980 else if (_code == 204)
981 [r appendContentString:@"204 No Content"];
984 s = [NSString stringWithFormat:@"%i code%i"];
985 [r appendContentString:s];
987 [r appendContentString:@"</D:status>"];
988 if (formatOutput) [r appendContentCharacter:'\n'];
990 [r appendContentString:@"<E:subscriptionID>"];
991 if (formatOutput) [r appendContentCharacter:'\n'];
992 e = [_sids objectEnumerator];
993 while ((sid = [e nextObject])) {
994 if (formatOutput) [r appendContentString:@" "];
995 [r appendContentString:@"<li>"];
996 [r appendContentString:sid];
997 [r appendContentString:@"</li>"];
998 if (formatOutput) [r appendContentCharacter:'\n'];
1000 [r appendContentString:@"</E:subscriptionID>"];
1001 if (formatOutput) [r appendContentCharacter:'\n'];
1002 [r appendContentString:@"</D:response>"];
1003 if (formatOutput) [r appendContentCharacter:'\n'];
1006 - (BOOL)renderPollResult:(id)_object inContext:(WOContext *)_ctx {
1007 WOResponse *r = [_ctx response];
1009 if (_object == nil) {
1010 [r setStatus:204 /* no content */];
1014 if ([_object isKindOfClass:[NSDictionary class]]) {
1015 NSArray *pending, *inactive;
1017 pending = [_object objectForKey:@"pending"];
1018 inactive = [_object objectForKey:@"inactive"];
1020 [r setStatus:207 /* Multi-Status */];
1021 [r setHeader:@"text/xml" forKey:@"content-type"];
1023 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1024 [r appendContentString:@"<D:multistatus "];
1025 [r appendContentString:@" xmlns:D=\"DAV:\""];
1026 [r appendContentString:
1027 @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1028 [r appendContentString:@">"];
1029 if (formatOutput) [r appendContentCharacter:'\n'];
1031 [self renderPollList:pending code:200 inContext:_ctx];
1032 [self renderPollList:inactive code:204 inContext:_ctx];
1034 [r appendContentString:@"</D:multistatus>"];
1035 if (formatOutput) [r appendContentCharacter:'\n'];
1037 else if ([_object isKindOfClass:[NSArray class]]) {
1038 [r setStatus:207 /* Multi-Status */];
1039 [r setHeader:@"text/xml" forKey:@"content-type"];
1041 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1042 [r appendContentString:@"<D:multistatus "];
1043 [r appendContentString:@" xmlns:D=\"DAV:\""];
1044 [r appendContentString:
1045 @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1046 [r appendContentString:@">"];
1047 if (formatOutput) [r appendContentCharacter:'\n'];
1049 [self renderPollList:_object code:200 inContext:_ctx];
1051 [r appendContentString:@"</D:multistatus>"];
1052 if (formatOutput) [r appendContentCharacter:'\n'];
1055 [r setStatus:204 /* no content */];
1056 //[r appendContentString:@"object was stored."];
1061 - (BOOL)renderMkColResult:(id)_object inContext:(WOContext *)_ctx {
1062 WOResponse *r = [_ctx response];
1064 if (_object == nil || [_object boolValue]) {
1065 [r setStatus:201 /* Created */];
1069 if ([_object isKindOfClass:[NSNumber class]]) {
1070 [r setStatus:[_object intValue]];
1071 [r appendContentString:@"object could not be created."];
1074 [r setStatus:500 /* server error */];
1075 [r appendContentString:@"object could not be deleted. reason: "];
1076 [r appendContentHTMLString:[_object stringValue]];
1083 - (BOOL)isDebuggingEnabled {
1087 @end /* SoWebDAVRenderer */