*/
#include "SOGoContentObject.h"
+#include <NGObjWeb/WEClientCapabilities.h>
#include "common.h"
#include <GDLContentStore/GCSFolder.h>
@implementation SOGoContentObject
+static BOOL kontactGroupDAV = YES;
+
++ (void)initialize {
+ NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
+
+ kontactGroupDAV =
+ [ud boolForKey:@"SOGoDisableKontact34GroupDAVHack"] ? NO : YES;
+}
+
- (void)dealloc {
[self->content release];
[self->ocsPath release];
return self->content;
if ((folder = [self ocsFolder]) == nil) {
- [self logWithFormat:@"did not find folder of appointment."];
+ [self errorWithFormat:@"Did not find folder of content object."];
return nil;
}
/* Note: "iCal multifolder saves" are implemented in the apt subclass! */
GCSFolder *folder;
NSException *ex;
-
+
if ((folder = [self ocsFolder]) == nil) {
- [self logWithFormat:@"did not find folder of appointment."];
+ [self errorWithFormat:@"Did not find folder of content object."];
return nil;
}
if ((ex = [folder writeContent:_str toName:[self nameInContainer]])) {
- [self logWithFormat:@"write failed: %@", ex];
+ [self errorWithFormat:@"write failed: %@", ex];
return ex;
}
return nil;
GCSFolder *folder;
NSException *ex;
+ // TODO: add precondition check? (or add DELETEAction?)
+
if ((folder = [self ocsFolder]) == nil) {
- [self logWithFormat:@"did not find folder of appointment."];
+ [self errorWithFormat:@"Did not find folder of content object."];
return nil;
}
if ((ex = [folder deleteContentWithName:[self nameInContainer]])) {
- [self logWithFormat:@"delete failed: %@", ex];
+ [self errorWithFormat:@"delete failed: %@", ex];
return ex;
}
return nil;
- (id)PUTAction:(WOContext *)_ctx {
WORequest *rq;
NSException *error;
+ id etag;
+
+ if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
+ return error;
rq = [_ctx request];
if ((error = [self saveContentString:[rq contentAsString]]) != nil)
// TODO: this should be automatic if we return nil?
[[_ctx response] setStatus:201 /* Created */];
+
+ if ((etag = [self davEntityTag]) != nil)
+ [[_ctx response] setHeader:etag forKey:@"etag"];
return [_ctx response];
}
+/* E-Tags */
+
+- (id)davEntityTag {
+ // TODO: cache tag in ivar? => if you do, remember to flush after PUT
+ GCSFolder *folder;
+
+ if ((folder = [self ocsFolder]) == nil) {
+ [self errorWithFormat:@"Did not find folder of content object."];
+ return nil;
+ }
+
+ return [folder versionOfContentWithName:[self nameInContainer]];
+}
+
+- (NSArray *)parseETagList:(NSString *)_c {
+ NSMutableArray *ma;
+ NSArray *etags;
+ unsigned i, count;
+
+ if ([_c length] == 0)
+ return nil;
+ if ([_c isEqualToString:@"*"])
+ return nil;
+
+ etags = [_c componentsSeparatedByString:@","];
+ count = [etags count];
+ ma = [NSMutableArray arrayWithCapacity:count];
+ for (i = 0; i < count; i++) {
+ NSString *etag;
+
+ etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
+ if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
+ etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
+
+ if (etag != nil) [ma addObject:etag];
+ }
+ return ma;
+}
+
+- (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
+ /* only run the request if one of the etags matches the resource etag */
+ NSArray *etags;
+ NSString *etag;
+
+ if ([_c isEqualToString:@"*"])
+ /* to ensure that the resource exists! */
+ return nil;
+
+ if ((etags = [self parseETagList:_c]) == nil)
+ return nil;
+ if ([etags count] == 0) /* no etags to check for? */
+ return nil;
+
+ etag = [self davEntityTag];
+ if ([etag length] == 0) /* has no etag, ignore */
+ return nil;
+
+ if ([etags containsObject:etag]) {
+ [self debugWithFormat:@"etag '%@' matches: %@", etag,
+ [etags componentsJoinedByString:@","]];
+ return nil; /* one etag matches, so continue with request */
+ }
+
+ /* hack for Kontact 3.4 */
+
+ if (kontactGroupDAV) {
+ WEClientCapabilities *cc;
+
+ cc = [[_ctx request] clientCapabilities];
+ if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
+ if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
+ [self logWithFormat:
+ @"WARNING: applying Kontact 3.4 GroupDAV hack"
+ @" - etag check is disabled!"
+ @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
+ return nil;
+ }
+ }
+ }
+
+ // TODO: we might want to return the davEntityTag in the response
+ [self debugWithFormat:@"etag '%@' does not match: %@", etag,
+ [etags componentsJoinedByString:@","]];
+ return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
+ reason:@"Precondition Failed"];
+}
+
+- (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
+ /*
+ If one of the etags is still the same, we can ignore the request.
+
+ Can be used for PUT to ensure that the object does not exist in the store
+ and for GET to retrieve the content only if if the etag changed.
+ */
+#if 0
+ if ([_c isEqualToString:@"*"])
+ return nil;
+
+ if ((a = [self parseETagList:_c]) == nil)
+ return nil;
+#else
+ [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
+#endif
+ return nil;
+}
+
+- (NSException *)matchesRequestConditionInContext:(id)_ctx {
+ NSException *error;
+ WORequest *rq;
+ NSString *c;
+
+ if ((rq = [(WOContext *)_ctx request]) == nil)
+ return nil; /* be tolerant - no request, no condition */
+
+ if ((c = [rq headerForKey:@"if-match"]) != nil) {
+ if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
+ return error;
+ }
+ if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
+ if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
+ return error;
+ }
+
+ return nil;
+}
+
/* WebDAV */
- (BOOL)davIsCollection {
#include "SOGoObject.h"
#include "SOGoUserFolder.h"
+#include <NGObjWeb/SoObject+SoDAV.h>
#include "common.h"
@interface SOGoObject(Content)
WOResponse *r;
NSString *uri;
+ r = [(WOContext *)_ctx response];
rq = [(WOContext *)_ctx request];
+
if ([rq isSoWebDAVRequest]) {
- if ([self respondsToSelector:@selector(contentAsString)])
- return [self contentAsString];
+ if ([self respondsToSelector:@selector(contentAsString)]) {
+ id etag;
+
+ [r appendContentString:[self contentAsString]];
+
+ if ((etag = [self davEntityTag]) != nil)
+ [r setHeader:etag forKey:@"etag"];
+ return r;
+ }
return [NSException exceptionWithHTTPStatus:501 /* not implemented */
reason:@"no WebDAV GET support?!"];
if (![uri hasSuffix:@"/"]) uri = [uri stringByAppendingString:@"/"];
uri = [uri stringByAppendingString:@"view"];
- r = [(WOContext *)_ctx response];
[r setStatus:302 /* moved */];
[r setHeader:uri forKey:@"location"];
return r;