2 Copyright (C) 2000-2003 SKYRIX Software AG
4 This file is part of OGo
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
23 #include "SoWebDAVRenderer.h"
24 #include "SoWebDAVValue.h"
25 #include "SoObject+SoDAV.h"
26 #include "EOFetchSpecification+SoDAV.h"
27 #include "NSException+HTTP.h"
28 #include <NGObjWeb/WOContext.h>
29 #include <NGObjWeb/WOResponse.h>
30 #include <NGObjWeb/WORequest.h>
31 #include <NGObjWeb/WOElement.h>
32 #include <SaxObjC/XMLNamespaces.h>
33 #include <NGExtensions/NSString+Ext.h>
37 What HotMail uses for responses:
38 <?xml version="1.0" encoding="Windows-1252"?>
40 Server: Microsoft-IIS/5.0
41 X-Timestamp: folders=1035823428, ACTIVE=1035813212
42 Client-Response-Num: 1
45 P3P: BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo
48 #define XMLNS_INTTASK \
49 @"{http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/}"
51 @interface SoWebDAVRenderer(Privates)
52 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
53 inContext:(WOContext *)_ctx;
56 @implementation SoWebDAVRenderer
58 static NSDictionary *predefinedNamespacePrefixes = nil;
59 static NSTimeZone *gmt = nil;
60 static BOOL debugOn = NO;
61 static BOOL formatOutput = NO;
64 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
65 static BOOL didInit = NO;
69 gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];
71 if (predefinedNamespacePrefixes == nil) {
72 predefinedNamespacePrefixes =
73 [[ud objectForKey:@"SoPreferredNamespacePrefixes"] copy];
75 formatOutput = [ud boolForKey:@"SoWebDAVFormatOutput"];
77 if ((debugOn = [ud boolForKey:@"SoRendererDebugEnabled"]))
78 NSLog(@"enabled debugging in SoWebDAVRenderer (SoRendererDebugEnabled)");
81 + (id)sharedRenderer {
82 static SoWebDAVRenderer *r = nil; // THREAD
83 if (r == nil) r = [[SoWebDAVRenderer alloc] init];
87 - (NSString *)preferredPrefixForNamespace:(NSString *)_uri {
88 return [predefinedNamespacePrefixes objectForKey:_uri];
91 /* key render entry-point */
93 - (void)_fixupResponse:(WOResponse *)_r inContext:(WOContext *)_ctx {
95 NSString *nowHttpString;
98 if ((tmp = [_r headerForKey:@"server"]) == nil) {
99 // TODO: add application name as primary name
100 [_r setHeader:@"SOPE 4.2/WebDAV" forKey:@"server"];
103 [_r setHeader:@"close" forKey:@"connection"];
104 [_r setHeader:@"DAV" forKey:@"Ms-Author-Via"];
106 // what program uses that header ?
107 [_r setHeader:@"200 No error" forKey:@"X-Dav-Error"];
109 if ((tmp = [_r headerForKey:@"content-type"]) == nil)
110 [_r setHeader:@"text/xml" forKey:@"content-type"];
113 nowHttpString = [now descriptionWithCalendarFormat:
114 @"%a, %d %b %Y %H:%M:%S GMT"
118 if ((tmp = [_r headerForKey:@"date"]) == nil)
119 [_r setHeader:nowHttpString forKey:@"date"];
121 #if 0 /* currently none of the clients allows zipping, retry later ... */
123 if ([_r shouldZipResponseToRequest:nil]) {
124 [self logWithFormat:@"zipping DAV result ..."];
130 - (NSString *)mimeTypeForData:(NSData *)_data inContext:(WOContext *)_ctx {
131 /* should check extension for MIME type */
132 return @"application/octet-stream";
134 - (NSString *)mimeTypeForString:(NSString *)_str inContext:(WOContext *)_ctx {
135 /* should check extension for MIME type */
137 if ([_str hasPrefix:@"<?xml"])
138 return @"text/xml; charset=\"utf-8\"";
139 if ([_str hasPrefix:@"<html"])
140 return @"text/html; charset=\"utf-8\"";
142 return @"text/plain; charset=\"utf-8\"";
145 - (BOOL)renderObjectBodyResult:(id)_object inContext:(WOContext *)_ctx
146 onlyHead:(BOOL)_onlyHead
148 WOResponse *r = [_ctx response];
152 TODO: implement proper etag support. This probably implies that we need
153 to pass in some structure or store the etag in the context?
154 We cannot use davEntityTag on the input parameter, since this is
155 usually the plain object.
157 tmp = @"0"; // fallback, cannot use the thing above
158 [r setHeader:tmp forKey:@"ETag"]; // required for WebFolder PUTs
160 if ([_object isKindOfClass:[NSData class]]) {
161 [r setHeader:[self mimeTypeForData:_object inContext:_ctx]
162 forKey:@"content-type"];
164 [r setHeader:[NSString stringWithFormat:@"%d", [_object length]]
165 forKey:@"content-length"];
166 if (!_onlyHead) [r setContent:_object];
170 if ([_object isKindOfClass:[NSString class]]) {
173 [r setHeader:[self mimeTypeForString:_object inContext:_ctx]
174 forKey:@"content-type"];
176 data = [_object dataUsingEncoding:NSUTF8StringEncoding];
177 [r setHeader:[NSString stringWithFormat:@"%d", [data length]]
178 forKey:@"content-length"];
183 if ([_object respondsToSelector:@selector(appendToResponse:inContext:)]) {
187 [_object appendToResponse:r inContext:_ctx];
190 if ([[r headerForKey:@"content-type"] length] == 0) {
191 [r setHeader:[self mimeTypeForData:data inContext:_ctx]
192 forKey:@"content-type"];
195 if (_onlyHead) [r setContent:nil];
197 [r setHeader:[NSString stringWithFormat:@"%d", len]
198 forKey:@"content-length"];
202 [self logWithFormat:@"ERROR: don't know how to render: %@", _object];
206 - (NSException *)renderObject:(id)_object inContext:(WOContext *)_ctx {
211 if ([_object isKindOfClass:[WOResponse class]]) {
212 if (_object != [_ctx response]) {
213 [self logWithFormat:@"response mismatch"];
214 return [NSException exceptionWithHTTPStatus:500 /* internal error */];
216 [self _fixupResponse:_object inContext:_ctx];
220 m = [[_ctx request] method];
221 if ([m length] == 0) {
222 return [NSException exceptionWithHTTPStatus:400 /* bad request */
223 reason:@"missing method name!"];
225 c1 = [m characterAtIndex:0];
230 if ([m isEqualToString:@"BPROPFIND"])
231 ok = [self renderSearchResult:_object inContext:_ctx];
234 if ([m isEqualToString:@"COPY"]) {
235 ok = [self renderStatusResult:_object
236 withDefaultStatus:201 /* Created */
241 if ([m isEqualToString:@"DELETE"])
242 ok = [self renderDeleteResult:_object inContext:_ctx];
245 if ([m isEqualToString:@"GET"])
246 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:NO];
249 if ([m isEqualToString:@"HEAD"])
250 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:YES];
253 if ([m isEqualToString:@"LOCK"])
254 ok = [self renderLockToken:_object inContext:_ctx];
257 if ([m isEqualToString:@"MKCOL"])
258 ok = [self renderMkColResult:_object inContext:_ctx];
259 else if ([m isEqualToString:@"MOVE"]) {
260 ok = [self renderStatusResult:_object
261 withDefaultStatus:201 /* Created */
266 if ([m isEqualToString:@"OPTIONS"])
267 ok = [self renderOptions:_object inContext:_ctx];
270 if ([m isEqualToString:@"PUT"])
271 ok = [self renderUploadResult:_object inContext:_ctx];
272 else if ([m isEqualToString:@"PROPFIND"])
273 ok = [self renderSearchResult:_object inContext:_ctx];
274 else if ([m isEqualToString:@"PROPPATCH"])
275 ok = [self renderPropPatchResult:_object inContext:_ctx];
276 else if ([m isEqualToString:@"POLL"])
277 ok = [self renderPollResult:_object inContext:_ctx];
280 if ([m isEqualToString:@"SEARCH"])
281 ok = [self renderSearchResult:_object inContext:_ctx];
282 else if ([m isEqualToString:@"SUBSCRIBE"])
283 ok = [self renderSubscription:_object inContext:_ctx];
291 if (ok) [self _fixupResponse:[_ctx response] inContext:_ctx];
294 : [NSException exceptionWithHTTPStatus:500 /* server error */];
297 - (BOOL)canRenderObject:(id)_object inContext:(WOContext *)_ctx {
298 if ([_object isKindOfClass:[NSException class]])
303 - (NSString *)stringForValue:(id)_value ofProperty:(NSString *)_prop
304 prefixes:(NSDictionary *)_prefixes
306 /* seems like this is the default date value */
307 NSString *datefmt = @"%a, %d %b %Y %H:%M:%S GMT";
311 if (![_value isNotNull])
314 /* special processing for some properties */
316 if ([_prop isEqualToString:@"{DAV:}resourcetype"]) {
317 _value = [_value stringValue];
318 if ([_value length] == 0) return nil;
320 return [NSString stringWithFormat:@"<%@:%@/>",
321 [_prefixes objectForKey:@"DAV:"], _value];
323 else if ([_prop isEqualToString:@"{DAV:}creationdate"])
324 datefmt = @"%Y-%m-%dT%H:%M:%S%zZ";
326 /* special processing for some properties */
328 // TODO: move this to user-level code !
329 // HH: what is that ? it does not do anything anyway ?
330 if ([_prop hasPrefix:XMLNS_INTTASK]) {
331 if ([_prop hasSuffix:@"}0x00008102"]) {
335 /* special processing for some classes */
337 if ([_value isKindOfClass:[NSString class]])
338 return [_value stringByEscapingXMLString];
340 if ([_value isKindOfClass:[NSNumber class]])
341 return [_value stringValue];
343 if ([_value isKindOfClass:[NSDate class]]) {
344 return [_value descriptionWithCalendarFormat:datefmt
349 return [[_value stringValue] stringByEscapingXMLString];
352 - (NSString *)baseURLForContext:(WOContext *)_ctx {
354 Note: Evolution doesn't correctly transfer the "Host:" header, it
355 misses the port argument :-(
364 if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
366 if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
367 hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
369 else if ((tmp = [rq headerForKey:@"host"]))
372 hostport = [[NSHost currentHost] name];
374 baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
378 - (NSString *)tidyHref:(id)href baseURL:(id)baseURL {
379 href = [href stringValue];
383 @"WARNING: using baseURL for href, "
384 @"entry did not provide a URL: %@", baseURL];
386 href = [baseURL stringValue];
388 else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
389 // TODO: use "real" URL processing
390 href = [baseURL stringByAppendingPathComponent:href];
394 - (id)tidyStatus:(id)stat {
396 stat = @"HTTP/1.1 200 OK";
397 else if ([stat isKindOfClass:[NSException class]]) {
400 if ((i = [stat httpStatus]) > 0)
401 stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
403 stat = [(NSException *)stat name];
404 stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
410 - (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx
411 namesOnly:(BOOL)_namesOnly
412 attributes:(NSArray *)_attrs
413 propertyMap:(NSDictionary *)_propMap
414 baseURL:(NSString *)baseURL
415 tagToPrefix:(NSDictionary *)extNameCache
416 nsToPrefix:(NSDictionary *)nsToPrefix
418 /* Note: the entry is an NSArray in case _namesOnly is requested! */
419 // TODO: use -valueForKey: to improve NSNull handling ?
428 isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
431 [self debugWithFormat:@" render entry: 0x%08X<%@>%s%s",
432 entry, NSStringFromClass([entry class]),
433 isBrief ? " brief" : "",
434 _namesOnly ? " names-only" : ""];
437 /* we do not map these DAV properties because they are very special */
439 if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
440 if ((key = [_propMap objectForKey:@"{DAV:}href"])) {
441 if ((href = [entry valueForKey:key]) == nil) {
443 [self debugWithFormat:
444 @"WARNING: no value for {DAV:}href key '%@': %@",
450 [self debugWithFormat:
451 @"WARNING: no key for {DAV:}href in property map !"];
454 if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
455 if ((key = [_propMap objectForKey:@"{DAV:}status"]))
456 stat = [entry valueForKey:key];
460 href = [self tidyHref:href baseURL:baseURL];
463 stat = [self tidyStatus:stat];
465 else { /* propnames only */
466 href = [baseURL stringValue];
467 stat = @"HTTP/1.1 200 OK";
471 [self debugWithFormat:@" status: %@", stat];
472 [self debugWithFormat:@" href: %@", href];
476 [r appendContentString:@"<D:response>"];
477 if (formatOutput) [r appendContentCharacter:'\n'];
480 [r appendContentString:@"<D:href>"];
482 TODO: need to find out what is appropriate! While Cadaver and ZideLook
483 (both Neon+Expat) seem to be fine with this, OSX reports invalid
484 characters (displayed as '#') for umlauts.
485 It might be that we are supposed to use *URL* escaping in any
486 case! (notably entering a directory with an umlaut doesn't seem
487 to work in Cadaver either because of a URL mismatch!)
488 Note: we cannot apply URL encoding in this place, because it will encode
489 all URL special chars ... where are URLs escaped?
490 Note: we always need to apply XML escaping (even though higher-level
491 characters might be already encoded)!
493 [r appendContentXMLString:[href stringValue]];
494 [r appendContentString:@"</D:href>"];
495 if (formatOutput) [r appendContentCharacter:'\n'];
498 [r appendContentString:@"<D:propstat>"];
500 [r appendContentString:@"<D:status>"];
501 [r appendContentXMLString:[stat stringValue]];
502 [r appendContentString:@"</D:status>"];
505 [r appendContentString:@"<D:prop>"];
506 if (formatOutput) [r appendContentCharacter:'\n'];
508 /* now the properties */
510 keys = [_attrs objectEnumerator] ;
511 while ((key = [keys nextObject])) {
516 #if 0 /* this filter probably doesn't make sense ? */
517 /* filter out predefined props */
518 if ([key isEqualToString:@"{DAV:}href"]) continue;
519 if ([key isEqualToString:@"{DAV:}status"]) continue;
522 extName = [extNameCache objectForKey:key];
525 [r appendContentCharacter:'<'];
526 [r appendContentString:extName];
527 [r appendContentString:@"/>"];
528 if (formatOutput) [r appendContentCharacter:'\n'];
532 // TODO: we should support property status (eg encode 404 on NSNull)
534 if ((okey = [_propMap objectForKey:key]) == nil)
537 if ([key isEqualToString:@"{DAV:}href"])
540 value = [entry valueForKey:okey];
542 if ([value isNotNull]) {
545 if ([value isKindOfClass:[SoWebDAVValue class]]) {
546 s = [value stringForTag:key rawName:extName
547 inContext:_ctx prefixes:nsToPrefix];
548 [r appendContentString:s];
551 [r appendContentCharacter:'<'];
552 [r appendContentString:extName];
553 [r appendContentCharacter:'>'];
555 s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
556 [r appendContentString:s];
558 [r appendContentString:@"</"];
559 [r appendContentString:extName];
560 [r appendContentString:@">"];
561 if (formatOutput) [r appendContentCharacter:'\n'];
568 Not sure whether this is correct, do we need to encode null attrs?
569 Seems like Evo gets confused on that.
570 TODO: probably add a 404 property status for that!
572 [r appendContentCharacter:'<'];
573 [r appendContentString:extName];
574 [r appendContentString:@"/>"];
575 if (formatOutput) [r appendContentCharacter:'\n'];
579 [r appendContentString:@"</D:prop></D:propstat></D:response>"];
580 if (formatOutput) [r appendContentCharacter:'\n'];
583 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
584 tagToExtName:(NSMutableDictionary *)_tagToExtName
585 nsToPrefix:(NSMutableDictionary *)_nsToPrefix
587 unichar autoPrefix[2] = { ('a' - 1), 0 };
591 e = [_attrs objectEnumerator];
592 while ((fqn = [e nextObject])) {
593 NSString *ns, *localName, *prefix, *extName;
595 if ([_tagToExtName objectForKey:fqn]) continue;
597 if (![fqn xmlIsFQN]) {
598 /* hm, no namespace given :-(, using DAV */
603 ns = [fqn xmlNamespaceURI];
604 localName = [fqn xmlLocalName];
607 if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
608 if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
610 prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
612 [_nsToPrefix setObject:prefix forKey:ns];
615 extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
616 [_tagToExtName setObject:extName forKey:fqn];
620 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
625 ms = [NSMutableString stringWithCapacity:256];
626 nse = [_nsToPrefix keyEnumerator];
627 while ((ns = [nse nextObject])) {
628 [ms appendString:@" xmlns:"];
629 [ms appendString:[_nsToPrefix objectForKey:ns]];
630 [ms appendString:@"=\""];
631 [ms appendString:ns];
632 [ms appendString:@"\""];
637 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx
638 namesOnly:(BOOL)_namesOnly
639 attributes:(NSArray *)_attrs
640 propertyMap:(NSDictionary *)_propMap
642 NSMutableDictionary *extNameCache = nil;
643 NSMutableDictionary *nsToPrefix = nil;
644 NSAutoreleasePool *pool;
648 pool = [[NSAutoreleasePool alloc] init];
651 if (![_entries isKindOfClass:[NSEnumerator class]]) {
652 if ([_entries isKindOfClass:[NSArray class]]) {
653 [self debugWithFormat:@" render %i entries", [_entries count]];
654 _entries = [_entries objectEnumerator];
657 [self debugWithFormat:@" render a single object ..."];
658 _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
662 /* collect used namespaces */
664 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
665 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
668 the extNameCache is used to map fully qualified tag names to their
669 prefixed external representation
671 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
673 // TODO: only walk attrs, if available
675 Walk all attributes of all entries to collect names. We might be able
676 to take a look at just the first record if it is guaranteed, that all
677 records have all properties (even if the value is NSNull) ?
679 [self buildPrefixMapForAttributes:_attrs
680 tagToExtName:extNameCache
681 nsToPrefix:nsToPrefix];
683 /* generate multistatus */
685 [r setStatus:207 /* multistatus */];
686 [r setHeader:@"no-cache" forKey:@"pragma"];
687 [r setHeader:@"no-cache" forKey:@"cache-control"];
689 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
690 [r appendContentString:@"<D:multistatus"];
691 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
692 [r appendContentString:@">"];
693 if (formatOutput) [r appendContentCharacter:'\n'];
699 baseURL = [self baseURLForContext:_ctx];
700 [self debugWithFormat:@" baseURL: %@", baseURL];
702 entryCount = 0; /* Note: this will clash with streamed output later */
703 while ((entry = [_entries nextObject])) {
704 [self renderSearchResultEntry:entry inContext:_ctx
705 namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
706 baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
709 [self debugWithFormat:@" rendered %i entries", entryCount];
712 If we got a "rows" range header, we report back the actual rows
713 delivered. Since we do not really support ranges in the moment,
714 we just report all rows ...
715 TODO: support for row ranges.
717 if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
718 /* sample: "Content-Range: rows 0-143; total=144" */
721 v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i",
722 entryCount>0?(entryCount - 1):0, entryCount];
723 [r setHeader:v forKey:@"content-range"];
727 [r appendContentString:@"</D:multistatus>"];
728 if (formatOutput) [r appendContentCharacter:'\n'];
733 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
734 EOFetchSpecification *fs;
735 NSDictionary *propMap;
737 if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
740 if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
741 propMap = [_object davAttributeMapInContext:_ctx];
744 [self debugWithFormat:@"render search result 0x%08X<%@>",
745 _object, NSStringFromClass([_object class])];
748 [self renderSearchResult:_object inContext:_ctx
749 namesOnly:[fs queryWebDAVPropertyNamesOnly]
750 attributes:[fs selectedWebDAVPropertyNames]
751 propertyMap:propMap];
754 [self debugWithFormat:@"finished rendering."];
758 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
759 /* TODO: this is a fake ! */
762 if (_object == nil) return NO;
767 [r setContentEncoding:NSUTF8StringEncoding];
768 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
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 */