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]]) {
173 [r setHeader:[self mimeTypeForData:_object inContext:_ctx]
174 forKey:@"content-type"];
176 sprintf(buf, "%d", [_object length]);
177 [r setHeader:[NSString stringWithCString:buf] forKey:@"content-length"];
178 if (!_onlyHead) [r setContent:_object];
182 if ([_object isKindOfClass:[NSString class]]) {
185 [r setHeader:[self mimeTypeForString:_object inContext:_ctx]
186 forKey:@"content-type"];
188 data = [_object dataUsingEncoding:NSUTF8StringEncoding];
189 sprintf(buf, "%d", [data length]);
190 [r setHeader:[NSString stringWithCString:buf] forKey:@"content-length"];
195 if ([_object respondsToSelector:@selector(appendToResponse:inContext:)]) {
199 [_object appendToResponse:r inContext:_ctx];
202 if ([[r headerForKey:@"content-type"] length] == 0) {
203 [r setHeader:[self mimeTypeForData:data inContext:_ctx]
204 forKey:@"content-type"];
207 if (_onlyHead) [r setContent:nil];
209 [r setHeader:[NSString stringWithFormat:@"%d", len]
210 forKey:@"content-length"];
214 [self errorWithFormat:@"don't know how to render: %@", _object];
218 - (NSException *)renderObject:(id)_object inContext:(WOContext *)_ctx {
223 if ([_object isKindOfClass:[WOResponse class]]) {
224 if (_object != [_ctx response]) {
225 [self logWithFormat:@"response mismatch"];
226 return [NSException exceptionWithHTTPStatus:500 /* internal error */];
228 [self _fixupResponse:_object inContext:_ctx];
232 m = [[_ctx request] method];
233 if ([m length] == 0) {
234 return [NSException exceptionWithHTTPStatus:400 /* bad request */
235 reason:@"missing method name!"];
237 c1 = [m characterAtIndex:0];
242 if ([m isEqualToString:@"BPROPFIND"])
243 ok = [self renderSearchResult:_object inContext:_ctx];
246 if ([m isEqualToString:@"COPY"]) {
247 ok = [self renderStatusResult:_object
248 withDefaultStatus:201 /* Created */
253 if ([m isEqualToString:@"DELETE"])
254 ok = [self renderDeleteResult:_object inContext:_ctx];
257 if ([m isEqualToString:@"GET"])
258 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:NO];
261 if ([m isEqualToString:@"HEAD"])
262 ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:YES];
265 if ([m isEqualToString:@"LOCK"])
266 ok = [self renderLockToken:_object inContext:_ctx];
269 if ([m isEqualToString:@"MKCOL"])
270 ok = [self renderMkColResult:_object inContext:_ctx];
271 else if ([m isEqualToString:@"MOVE"]) {
272 ok = [self renderStatusResult:_object
273 withDefaultStatus:201 /* Created */
278 if ([m isEqualToString:@"OPTIONS"])
279 ok = [self renderOptions:_object inContext:_ctx];
282 if ([m isEqualToString:@"PUT"])
283 ok = [self renderUploadResult:_object inContext:_ctx];
284 else if ([m isEqualToString:@"PROPFIND"])
285 ok = [self renderSearchResult:_object inContext:_ctx];
286 else if ([m isEqualToString:@"PROPPATCH"])
287 ok = [self renderPropPatchResult:_object inContext:_ctx];
288 else if ([m isEqualToString:@"POLL"])
289 ok = [self renderPollResult:_object inContext:_ctx];
292 if ([m isEqualToString:@"SEARCH"])
293 ok = [self renderSearchResult:_object inContext:_ctx];
294 else if ([m isEqualToString:@"SUBSCRIBE"])
295 ok = [self renderSubscription:_object inContext:_ctx];
303 if (ok) [self _fixupResponse:[_ctx response] inContext:_ctx];
306 : [NSException exceptionWithHTTPStatus:500 /* server error */];
309 - (BOOL)canRenderObject:(id)_object inContext:(WOContext *)_ctx {
310 if ([_object isKindOfClass:[NSException class]])
315 - (NSString *)stringForResourceType:(id)_value ofProperty:(NSString *)_prop
316 prefixes:(NSDictionary *)_prefixes
320 davNS = [_prefixes objectForKey:@"DAV:"];
322 if ([_value isKindOfClass:[NSArray class]]) {
324 Use arrays to allow for something like this:
326 <C:todos xmlns:C="urn:ietf:params:xml:ns:caldav"/>
328 ( TAG ) => tag in DAV: namespace
329 ( TAG, NS ) => tag in NS namespace
330 ( TAG, NS, PREFIX ) => tag in NS namespace with PREFIX
336 if ([_value count] == 0)
339 ms = [NSMutableString stringWithCapacity:16];
340 e = [_value objectEnumerator];
341 while ((item = [e nextObject]) != nil) {
344 if (![item isKindOfClass:[NSArray class]]) {
345 item = [item stringValue];
346 if ([item length] == 0) continue;
347 [ms appendFormat:@"<%@:%@ />", davNS, item];
351 /* process array tags */
353 if ((count = [item count]) == 0)
357 [ms appendFormat:@"<%@:%@ />", davNS, [item objectAtIndex:0]];
358 else if (count == 2) {
360 [ms appendFormat:@"<%@ xmlns=\"%@\" />",
361 [item objectAtIndex:0], [item objectAtIndex:1]];
364 /* 0=tag, 1=nsuri, 2=nsprefix */
365 [ms appendFormat:@"<%@:%@ xmlns:%@=\"%@\" />",
366 [item objectAtIndex:2], [item objectAtIndex:0],
367 [item objectAtIndex:2], [item objectAtIndex:1]];
373 _value = [_value stringValue];
374 if ([_value length] == 0) return nil;
376 return [NSString stringWithFormat:@"<%@:%@/>", davNS, _value];
378 - (NSString *)stringForValue:(id)_value ofProperty:(NSString *)_prop
379 prefixes:(NSDictionary *)_prefixes
381 /* seems like this is the default date value */
382 NSString *datefmt = @"%a, %d %b %Y %H:%M:%S GMT";
386 if (![_value isNotNull])
389 /* special processing for some properties */
391 if ([_prop isEqualToString:@"{DAV:}resourcetype"]) {
392 return [self stringForResourceType:_value ofProperty:_prop
395 else if ([_prop isEqualToString:@"{DAV:}creationdate"])
396 datefmt = @"%Y-%m-%dT%H:%M:%S%zZ";
398 /* special processing for some properties */
400 // TODO: move this to user-level code !
401 // HH: what is that ? it does not do anything anyway ?
402 if ([_prop hasPrefix:XMLNS_INTTASK]) {
403 if ([_prop hasSuffix:@"}0x00008102"]) {
407 /* special processing for some classes */
409 if ([_value isKindOfClass:[NSString class]])
410 return [_value stringByEscapingXMLString];
412 if ([_value isKindOfClass:[NSNumber class]])
413 return [_value stringValue];
415 if ([_value isKindOfClass:[NSDate class]]) {
416 return [_value descriptionWithCalendarFormat:datefmt
421 return [[_value stringValue] stringByEscapingXMLString];
424 - (NSString *)baseURLForContext:(WOContext *)_ctx {
426 Note: Evolution doesn't correctly transfer the "Host:" header, it
427 misses the port argument :-(
436 if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
438 if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
439 hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
441 else if ((tmp = [rq headerForKey:@"host"]))
444 hostport = [[NSHost currentHost] name];
446 baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
450 - (NSString *)tidyHref:(id)_href baseURL:(id)baseURL {
453 href = [_href stringValue];
456 // TODO: this happens if we access using Goliath
457 if ([href hasPrefix:@"http:/"] && ![href hasPrefix:@"http://"]) {
458 [self logWithFormat:@"BROKEN URL: %@", _href];
465 [self warnWithFormat:
466 @"using baseURL for href, entry did not provide a URL: %@",
469 href = [baseURL stringValue];
471 else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
472 // TODO: use "real" URL processing
473 href = [baseURL stringByAppendingPathComponent:href];
477 - (id)tidyStatus:(id)stat {
479 stat = @"HTTP/1.1 200 OK";
480 else if ([stat isKindOfClass:[NSException class]]) {
483 if ((i = [stat httpStatus]) > 0)
484 stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
486 stat = [(NSException *)stat name];
487 stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
493 - (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx
494 namesOnly:(BOOL)_namesOnly
495 attributes:(NSArray *)_attrs
496 propertyMap:(NSDictionary *)_propMap
497 baseURL:(NSString *)baseURL
498 tagToPrefix:(NSDictionary *)extNameCache
499 nsToPrefix:(NSDictionary *)nsToPrefix
501 /* Note: the entry is an NSArray in case _namesOnly is requested! */
502 // TODO: use -valueForKey: to improve NSNull handling ?
511 isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
514 [self debugWithFormat:@" render entry: 0x%08X<%@>%s%s",
515 entry, NSStringFromClass([entry class]),
516 isBrief ? " brief" : "",
517 _namesOnly ? " names-only" : ""];
520 /* we do not map these DAV properties because they are very special */
522 if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
523 if ((key = [_propMap objectForKey:@"{DAV:}href"]) != nil) {
524 if ((href = [entry valueForKey:key]) == nil) {
526 [self warnWithFormat:
527 @"no value for {DAV:}href key '%@': %@", key, entry];
532 [self warnWithFormat:@"no key for {DAV:}href in property map !"];
535 if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
536 if ((key = [_propMap objectForKey:@"{DAV:}status"]))
537 stat = [entry valueForKey:key];
541 href = [self tidyHref:href baseURL:baseURL];
544 stat = [self tidyStatus:stat];
546 else { /* propnames only */
547 href = [baseURL stringValue];
548 stat = @"HTTP/1.1 200 OK";
552 [self debugWithFormat:@" status: %@", stat];
553 [self debugWithFormat:@" href: %@", href];
557 [r appendContentString:@"<D:response>"];
558 if (formatOutput) [r appendContentCharacter:'\n'];
560 if ([href isNotNull]) {
561 [r appendContentString:@"<D:href>"];
563 TODO: need to find out what is appropriate! While Cadaver and ZideLook
564 (both Neon+Expat) seem to be fine with this, OSX reports invalid
565 characters (displayed as '#') for umlauts.
566 It might be that we are supposed to use *URL* escaping in any
567 case! (notably entering a directory with an umlaut doesn't seem
568 to work in Cadaver either because of a URL mismatch!)
569 Note: we cannot apply URL encoding in this place, because it will encode
570 all URL special chars ... where are URLs escaped?
571 Note: we always need to apply XML escaping (even though higher-level
572 characters might be already encoded)!
574 [r appendContentXMLString:[href stringValue]];
575 [r appendContentString:@"</D:href>"];
576 if (formatOutput) [r appendContentCharacter:'\n'];
579 [self warnWithFormat:@"WebDAV result entry has no valid href: %@", entry];
582 [r appendContentString:@"<D:propstat>"];
583 [r appendContentString:@"<D:prop>"];
584 if (formatOutput) [r appendContentCharacter:'\n'];
586 /* now the properties */
588 keys = [_attrs objectEnumerator] ;
589 while ((key = [keys nextObject])) {
594 #if 0 /* this filter probably doesn't make sense ? */
595 /* filter out predefined props */
596 if ([key isEqualToString:@"{DAV:}href"]) continue;
597 if ([key isEqualToString:@"{DAV:}status"]) continue;
600 extName = [extNameCache objectForKey:key];
603 [r appendContentCharacter:'<'];
604 [r appendContentString:extName];
605 [r appendContentString:@"/>"];
606 if (formatOutput) [r appendContentCharacter:'\n'];
610 // TODO: we should support property status (eg encode 404 on NSNull)
612 if ((okey = [_propMap objectForKey:key]) == nil)
615 if ([key isEqualToString:@"{DAV:}href"])
618 value = [entry valueForKey:okey];
620 if ([value isNotNull]) {
623 if ([value isKindOfClass:[SoWebDAVValue class]]) {
624 s = [value stringForTag:key rawName:extName
625 inContext:_ctx prefixes:nsToPrefix];
626 [r appendContentString:s];
629 [r appendContentCharacter:'<'];
630 [r appendContentString:extName];
631 [r appendContentCharacter:'>'];
633 s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
634 [r appendContentString:s];
636 [r appendContentString:@"</"];
637 [r appendContentString:extName];
638 [r appendContentString:@">"];
639 if (formatOutput) [r appendContentCharacter:'\n'];
646 Not sure whether this is correct, do we need to encode null attrs?
647 Seems like Evo gets confused on that.
648 TODO: probably add a 404 property status for that!
650 [r appendContentCharacter:'<'];
651 [r appendContentString:extName];
652 [r appendContentString:@"/>"];
653 if (formatOutput) [r appendContentCharacter:'\n'];
657 [r appendContentString:@"</D:prop>"];
658 if (formatOutput) [r appendContentCharacter:'\n'];
661 [r appendContentString:@"<D:status>"];
662 [r appendContentXMLString:[stat stringValue]];
663 [r appendContentString:@"</D:status>"];
666 [r appendContentString:@"</D:propstat></D:response>"];
667 if (formatOutput) [r appendContentCharacter:'\n'];
670 - (void)buildPrefixMapForAttributes:(NSArray *)_attrs
671 tagToExtName:(NSMutableDictionary *)_tagToExtName
672 nsToPrefix:(NSMutableDictionary *)_nsToPrefix
674 unichar autoPrefix[2] = { ('a' - 1), 0 };
678 e = [_attrs objectEnumerator];
679 while ((fqn = [e nextObject])) {
680 NSString *ns, *localName, *prefix, *extName;
682 if ([_tagToExtName objectForKey:fqn]) continue;
684 if (![fqn xmlIsFQN]) {
685 /* hm, no namespace given :-(, using DAV */
690 ns = [fqn xmlNamespaceURI];
691 localName = [fqn xmlLocalName];
694 if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
695 if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
697 prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
699 [_nsToPrefix setObject:prefix forKey:ns];
702 extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
703 [_tagToExtName setObject:extName forKey:fqn];
707 - (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
712 ms = [NSMutableString stringWithCapacity:256];
713 nse = [_nsToPrefix keyEnumerator];
714 while ((ns = [nse nextObject])) {
715 [ms appendString:@" xmlns:"];
716 [ms appendString:[_nsToPrefix objectForKey:ns]];
717 [ms appendString:@"=\""];
718 [ms appendString:ns];
719 [ms appendString:@"\""];
724 - (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx
725 namesOnly:(BOOL)_namesOnly
726 attributes:(NSArray *)_attrs
727 propertyMap:(NSDictionary *)_propMap
729 NSMutableDictionary *extNameCache = nil;
730 NSMutableDictionary *nsToPrefix = nil;
731 NSAutoreleasePool *pool;
735 pool = [[NSAutoreleasePool alloc] init];
738 if (![_entries isKindOfClass:[NSEnumerator class]]) {
739 if ([_entries isKindOfClass:[NSArray class]]) {
740 [self debugWithFormat:@" render %i entries", [_entries count]];
741 _entries = [_entries objectEnumerator];
744 [self debugWithFormat:@" render a single object ..."];
745 _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
749 /* collect used namespaces */
751 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
752 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
755 the extNameCache is used to map fully qualified tag names to their
756 prefixed external representation
758 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
760 // TODO: only walk attrs, if available
762 Walk all attributes of all entries to collect names. We might be able
763 to take a look at just the first record if it is guaranteed, that all
764 records have all properties (even if the value is NSNull) ?
766 [self buildPrefixMapForAttributes:_attrs
767 tagToExtName:extNameCache
768 nsToPrefix:nsToPrefix];
770 /* generate multistatus */
772 [r setStatus:207 /* multistatus */];
773 [r setContentEncoding:NSUTF8StringEncoding];
774 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
775 [r setHeader:@"no-cache" forKey:@"pragma"];
776 [r setHeader:@"no-cache" forKey:@"cache-control"];
778 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
779 [r appendContentString:@"<D:multistatus"];
780 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
781 [r appendContentString:@">"];
782 if (formatOutput) [r appendContentCharacter:'\n'];
788 baseURL = [self baseURLForContext:_ctx];
789 [self debugWithFormat:@" baseURL: %@", baseURL];
791 entryCount = 0; /* Note: this will clash with streamed output later */
792 while ((entry = [_entries nextObject])) {
793 [self renderSearchResultEntry:entry inContext:_ctx
794 namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
795 baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
798 [self debugWithFormat:@" rendered %i entries", entryCount];
801 If we got a "rows" range header, we report back the actual rows
802 delivered. Since we do not really support ranges in the moment,
803 we just report all rows ...
804 TODO: support for row ranges.
806 if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
807 /* sample: "Content-Range: rows 0-143; total=144" */
810 v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i",
811 entryCount>0?(entryCount - 1):0, entryCount];
812 [r setHeader:v forKey:@"content-range"];
816 [r appendContentString:@"</D:multistatus>"];
817 if (formatOutput) [r appendContentCharacter:'\n'];
822 - (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
823 EOFetchSpecification *fs;
824 NSDictionary *propMap;
826 if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
829 if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
830 propMap = [_object davAttributeMapInContext:_ctx];
833 [self debugWithFormat:@"render search result 0x%08X<%@>",
834 _object, NSStringFromClass([_object class])];
837 [self renderSearchResult:_object inContext:_ctx
838 namesOnly:[fs queryWebDAVPropertyNamesOnly]
839 attributes:[fs selectedWebDAVPropertyNames]
840 propertyMap:propMap];
843 [self debugWithFormat:@"finished rendering."];
847 - (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
848 /* TODO: this is a fake ! */
851 if (_object == nil) return NO;
855 [r setStatus:200 /* OK */];
856 [r setContentEncoding:NSUTF8StringEncoding];
857 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
858 [r setHeader:[_object stringValue] forKey:@"lock-token"];
859 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
860 [r appendContentString:@"<D:prop xmlns:D=\"DAV:\">"];
861 [r appendContentString:@"<D:lockdiscovery>"];
862 [r appendContentString:@"<D:activelock>"];
863 if (formatOutput) [r appendContentCharacter:'\n'];
865 /* this is the href of the lock, not of the locked resource */
866 [r appendContentString:@"<D:locktoken><D:href>"];
867 [r appendContentString:[_object stringValue]];
868 [r appendContentString:@"</D:href></D:locktoken>"];
869 if (formatOutput) [r appendContentCharacter:'\n'];
871 // TODO: locktype, eg <D:locktype><D:write/></D:locktype>
872 // TODO: lockscope, eg <D:lockscope><D:exclusive/></D:lockscope>
873 // TODO: depth, eg <D:depth>Infinitiy</D:depth>
874 // TODO: owner, eg <D:owner><D:href>...</D:href></D:owner>
875 // TODO: timeout, eg <D:timeout>Second-604800</D:timeout>
877 [r appendContentString:@"</D:activelock>"];
878 [r appendContentString:@"</D:lockdiscovery>"];
879 [r appendContentString:@"</D:prop>"];
880 if (formatOutput) [r appendContentCharacter:'\n'];
884 - (BOOL)renderOptions:(id)_object inContext:(WOContext *)_ctx {
885 WOResponse *r = [_ctx response];
888 [r setHeader:@"1,2" forKey:@"DAV"]; // TODO: select protocol level
889 //[r setHeader:@"" forKey:@"Etag"];
890 [r setHeader:[_object componentsJoinedByString:@", "] forKey:@"allow"];
894 - (BOOL)renderSubscription:(id)_object inContext:(WOContext *)_ctx {
895 // TODO: this is fake, mirrors request
896 WOResponse *r = [_ctx response];
899 NSString *notificationType;
903 callback = [rq headerForKey:@"call-back"];
904 notificationType = [rq headerForKey:@"notification-type"];
905 lifetime = [rq headerForKey:@"subscription-lifetime"];
907 [r setStatus:200 /* OK */];
908 if (notificationType != nil)
909 [r setHeader:notificationType forKey:@"notification-type"];
911 [r setHeader:lifetime forKey:@"subscription-lifetime"];
913 [r setHeader:callback forKey:@"callback"];
914 [r setHeader:[self baseURLForContext:_ctx] forKey:@"content-location"];
915 [r setHeader:_object forKey:@"subscription-id"];
919 - (BOOL)renderPropPatchResult:(id)_object inContext:(WOContext *)_ctx {
920 NSMutableDictionary *extNameCache = nil;
921 NSMutableDictionary *nsToPrefix = nil;
922 WOResponse *r = [_ctx response];
924 if (_object == nil) return NO;
926 nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
927 [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
928 extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
929 [self buildPrefixMapForAttributes:_object
930 tagToExtName:extNameCache
931 nsToPrefix:nsToPrefix];
933 [r setStatus:207 /* multistatus */];
934 [r setContentEncoding:NSUTF8StringEncoding];
935 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
936 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
937 [r appendContentString:@"<D:multistatus"];
938 [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
939 [r appendContentString:@">"];
940 if (formatOutput) [r appendContentCharacter:'\n'];
942 [r appendContentString:@"<D:response>"];
943 if (formatOutput) [r appendContentCharacter:'\n'];
944 [r appendContentString:@"<D:href>"];
945 [r appendContentString:[[_ctx request] uri]];
946 [r appendContentString:@"</D:href>"];
947 if (formatOutput) [r appendContentCharacter:'\n'];
948 [r appendContentString:@"<D:propstat>"];
949 if (formatOutput) [r appendContentCharacter:'\n'];
950 [r appendContentString:@"<D:prop>"];
951 if (formatOutput) [r appendContentCharacter:'\n'];
953 /* encode properties */
958 e = [_object objectEnumerator];
959 while ((tag = [e nextObject])) {
962 extName = [extNameCache objectForKey:tag];
963 [r appendContentCharacter:'<'];
964 [r appendContentString:extName];
965 [r appendContentString:@"/>"];
966 if (formatOutput) [r appendContentCharacter:'\n'];
970 [r appendContentString:@"</D:prop>"];
971 if (formatOutput) [r appendContentCharacter:'\n'];
972 [r appendContentString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
973 [r appendContentString:@"</D:propstat></D:response>"];
974 if (formatOutput) [r appendContentCharacter:'\n'];
975 [r appendContentString:@"</D:multistatus>"];
976 if (formatOutput) [r appendContentCharacter:'\n'];
981 - (BOOL)renderDeleteResult:(id)_object inContext:(WOContext *)_ctx {
982 WOResponse *r = [_ctx response];
984 if (_object == nil || [_object boolValue]) {
985 [r setStatus:204 /* no content */];
986 //[r appendContentString:@"object was deleted."];
990 if ([_object isKindOfClass:[NSNumber class]]) {
991 [r setStatus:[_object intValue]];
992 if ([r status] != 204 /* No Content */)
993 [r appendContentString:@"object could not be deleted."];
996 [r setStatus:500 /* server error */];
997 [r appendContentString:@"object could not be deleted. reason: "];
998 [r appendContentHTMLString:[_object stringValue]];
1003 - (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
1004 inContext:(WOContext *)_ctx
1006 WOResponse *r = [_ctx response];
1008 if (_object == nil) {
1009 [r setStatus:_defStatus /* no content */];
1013 if ([_object isKindOfClass:[NSNumber class]]) {
1014 if ([_object intValue] < 100) {
1015 [r setStatus:_defStatus /* no content */];
1019 [r setStatus:[_object intValue]];
1023 [r setStatus:_defStatus /* no content */];
1027 - (BOOL)renderUploadResult:(id)_object inContext:(WOContext *)_ctx {
1028 WOResponse *r = [_ctx response];
1030 if (_object == nil) {
1031 [r setStatus:204 /* no content */];
1035 if ([_object isKindOfClass:[NSNumber class]]) {
1036 if ([_object intValue] < 100) {
1037 [r setStatus:204 /* no content */];
1041 [r setStatus:[_object intValue]];
1042 if ([_object intValue] >= 300) {
1043 [r setHeader:@"text/html" forKey:@"content-type"];
1044 [r appendContentString:@"object could not be stored."];
1050 [r setStatus:204 /* no content */];
1054 - (void)renderPollList:(NSArray *)_sids code:(int)_code
1055 inContext:(WOContext *)_ctx
1057 WOResponse *r = [_ctx response];
1062 if ([_sids count] == 0) return;
1063 href = [self baseURLForContext:_ctx];
1065 [r appendContentString:@"<D:response>"];
1066 if (formatOutput) [r appendContentCharacter:'\n'];
1067 [r appendContentString:@"<D:href>"];
1068 [r appendContentString:href];
1069 [r appendContentString:@"</D:href>"];
1070 if (formatOutput) [r appendContentCharacter:'\n'];
1072 [r appendContentString:@"<D:status>HTTP/1.1 "];
1074 [r appendContentString:@"200 OK"];
1075 else if (_code == 204)
1076 [r appendContentString:@"204 No Content"];
1079 s = [NSString stringWithFormat:@"%i code%i"];
1080 [r appendContentString:s];
1082 [r appendContentString:@"</D:status>"];
1083 if (formatOutput) [r appendContentCharacter:'\n'];
1085 [r appendContentString:@"<E:subscriptionID>"];
1086 if (formatOutput) [r appendContentCharacter:'\n'];
1087 e = [_sids objectEnumerator];
1088 while ((sid = [e nextObject])) {
1089 if (formatOutput) [r appendContentString:@" "];
1090 [r appendContentString:@"<li>"];
1091 [r appendContentString:sid];
1092 [r appendContentString:@"</li>"];
1093 if (formatOutput) [r appendContentCharacter:'\n'];
1095 [r appendContentString:@"</E:subscriptionID>"];
1096 if (formatOutput) [r appendContentCharacter:'\n'];
1097 [r appendContentString:@"</D:response>"];
1098 if (formatOutput) [r appendContentCharacter:'\n'];
1101 - (BOOL)renderPollResult:(id)_object inContext:(WOContext *)_ctx {
1102 WOResponse *r = [_ctx response];
1104 if (_object == nil) {
1105 [r setStatus:204 /* no content */];
1109 if ([_object isKindOfClass:[NSDictionary class]]) {
1110 NSArray *pending, *inactive;
1112 pending = [_object objectForKey:@"pending"];
1113 inactive = [_object objectForKey:@"inactive"];
1115 [r setStatus:207 /* Multi-Status */];
1116 [r setContentEncoding:NSUTF8StringEncoding];
1117 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
1119 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1120 [r appendContentString:@"<D:multistatus "];
1121 [r appendContentString:@" xmlns:D=\"DAV:\""];
1122 [r appendContentString:
1123 @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1124 [r appendContentString:@">"];
1125 if (formatOutput) [r appendContentCharacter:'\n'];
1127 [self renderPollList:pending code:200 inContext:_ctx];
1128 [self renderPollList:inactive code:204 inContext:_ctx];
1130 [r appendContentString:@"</D:multistatus>"];
1131 if (formatOutput) [r appendContentCharacter:'\n'];
1133 else if ([_object isKindOfClass:[NSArray class]]) {
1134 [r setStatus:207 /* Multi-Status */];
1135 [r setContentEncoding:NSUTF8StringEncoding];
1136 [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
1138 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
1139 [r appendContentString:@"<D:multistatus "];
1140 [r appendContentString:@" xmlns:D=\"DAV:\""];
1141 [r appendContentString:
1142 @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
1143 [r appendContentString:@">"];
1144 if (formatOutput) [r appendContentCharacter:'\n'];
1146 [self renderPollList:_object code:200 inContext:_ctx];
1148 [r appendContentString:@"</D:multistatus>"];
1149 if (formatOutput) [r appendContentCharacter:'\n'];
1152 [r setStatus:204 /* no content */];
1153 //[r appendContentString:@"object was stored."];
1158 - (BOOL)renderMkColResult:(id)_object inContext:(WOContext *)_ctx {
1159 WOResponse *r = [_ctx response];
1161 if (_object == nil || [_object boolValue]) {
1162 [r setStatus:201 /* Created */];
1166 if ([_object isKindOfClass:[NSNumber class]]) {
1167 [r setStatus:[_object intValue]];
1168 [r appendContentString:@"object could not be created."];
1171 [r setStatus:500 /* server error */];
1172 [r appendContentString:@"object could not be deleted. reason: "];
1173 [r appendContentHTMLString:[_object stringValue]];
1180 - (BOOL)isDebuggingEnabled {
1184 @end /* SoWebDAVRenderer */