2 Copyright (C) 2004-2005 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 "SOGoContentObject.h"
23 #include "SOGoFolder.h"
24 #include <NGObjWeb/WEClientCapabilities.h>
26 #include <GDLContentStore/GCSFolder.h>
28 @interface SOGoContentObject(ETag)
29 - (NSArray *)parseETagList:(NSString *)_c;
32 @implementation SOGoContentObject
34 static BOOL kontactGroupDAV = YES;
37 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
40 [ud boolForKey:@"SOGoDisableKontact34GroupDAVHack"] ? NO : YES;
44 [self->content release];
45 [self->ocsPath release];
52 [self->content release]; self->content = nil;
62 - (void)setOCSPath:(NSString *)_path {
63 if ([self->ocsPath isEqualToString:_path])
67 [self warnWithFormat:@"GCS path is already set! '%@'", _path];
69 ASSIGNCOPY(self->ocsPath, _path);
72 - (NSString *)ocsPath {
73 if (self->ocsPath == nil) {
76 if ((p = [self ocsPathOfContainer]) != nil) {
77 if (![p hasSuffix:@"/"]) p = [p stringByAppendingString:@"/"];
78 p = [p stringByAppendingString:[self nameInContainer]];
79 self->ocsPath = [p copy];
85 - (NSString *)ocsPathOfContainer {
86 if (![[self container] respondsToSelector:@selector(ocsPath)])
89 return [[self container] ocsPath];
92 - (GCSFolder *)ocsFolder {
93 if (![[self container] respondsToSelector:@selector(ocsFolder)])
96 return [[self container] ocsFolder];
101 - (NSString *)contentAsString {
104 if (self->content != nil)
105 return self->content;
107 if ((folder = [self ocsFolder]) == nil) {
108 [self errorWithFormat:@"Did not find folder of content object."];
112 self->content = [[folder fetchContentWithName:[self nameInContainer]] copy];
113 return self->content;
116 - (NSException *)saveContentString:(NSString *)_str
117 baseVersion:(unsigned int)_baseVersion
119 /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
123 if ((folder = [self ocsFolder]) == nil) {
124 [self errorWithFormat:@"Did not find folder of content object."];
128 ex = [folder writeContent:_str toName:[self nameInContainer]
129 baseVersion:_baseVersion];
131 [self errorWithFormat:@"write failed: %@", ex];
136 - (NSException *)saveContentString:(NSString *)_str {
137 return [self saveContentString:_str baseVersion:0 /* don't check */];
140 - (NSException *)delete {
141 /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
145 // TODO: add precondition check? (or add DELETEAction?)
147 if ((folder = [self ocsFolder]) == nil) {
148 [self errorWithFormat:@"Did not find folder of content object."];
152 if ((ex = [folder deleteContentWithName:[self nameInContainer]])) {
153 [self errorWithFormat:@"delete failed: %@", ex];
161 - (id)PUTAction:(WOContext *)_ctx {
164 unsigned int baseVersion;
168 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
173 /* check whether its a request to the 'special' 'new' location */
175 Note: this is kinda hack. The OGo ZideStore detects writes to 'new' as
176 object creations and will assign a server side identifier. Most
177 current GroupDAV clients rely on this behaviour, so we reproduce it
179 A correct client would loop until it has a name which doesn't not
180 yet exist (by using if-none-match).
183 tmp = [[self nameInContainer] stringByDeletingPathExtension];
184 if ([tmp isEqualToString:@"new"]) {
185 tmp = [[[self container] class] globallyUniqueObjectId];
188 [self debugWithFormat:
189 @"reassigned a new location for special new-location: %@", tmp];
191 /* kinda dangerous */
192 ASSIGNCOPY(self->nameInContainer, tmp);
193 ASSIGN(self->ocsPath, nil);
196 /* determine base version from etag in if-match header */
198 Note: The -matchesRequestConditionInContext: already checks whether the
199 etag matches and returns an HTTP exception in case it doesn't.
200 We retrieve the etag again here to _ensure_ a transactionally save
202 (between the check and the update a change could have been done)
204 tmp = [rq headerForKey:@"if-match"];
205 tmp = [self parseETagList:tmp];
207 if ([tmp count] > 0) {
208 if ([tmp count] > 1) {
210 Note: we would have to attempt a save for _each_ of the etags being
211 passed in! In practice most WebDAV clients submit exactly one
214 [self warnWithFormat:
215 @"Got multiple if-match etags from client, only attempting to "
216 @"save with the first: %@", tmp];
219 etag = [tmp objectAtIndex:0];
221 baseVersion = ([etag length] > 0)
222 ? [etag unsignedIntValue]
223 : 0 /* 0 means 'do not check' */;
227 if ((error = [self saveContentString:[rq contentAsString]
228 baseVersion:baseVersion]) != nil)
233 // TODO: this should be automatic in the SoDispatcher if we return nil?
234 [[_ctx response] setStatus:201 /* Created */];
236 if ((etag = [self davEntityTag]) != nil)
237 [[_ctx response] setHeader:etag forKey:@"etag"];
240 [[_ctx response] setHeader:[self baseURLInContext:_ctx]
244 return [_ctx response];
250 // TODO: cache tag in ivar? => if you do, remember to flush after PUT
253 if ((folder = [self ocsFolder]) == nil) {
254 [self errorWithFormat:@"Did not find folder of content object."];
258 return [folder versionOfContentWithName:[self nameInContainer]];
261 - (NSArray *)parseETagList:(NSString *)_c {
266 if ([_c length] == 0)
268 if ([_c isEqualToString:@"*"])
271 etags = [_c componentsSeparatedByString:@","];
272 count = [etags count];
273 ma = [NSMutableArray arrayWithCapacity:count];
274 for (i = 0; i < count; i++) {
277 etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
278 if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
279 etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
281 if (etag != nil) [ma addObject:etag];
286 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
287 /* only run the request if one of the etags matches the resource etag */
291 if ([_c isEqualToString:@"*"])
292 /* to ensure that the resource exists! */
295 if ((etags = [self parseETagList:_c]) == nil)
297 if ([etags count] == 0) /* no etags to check for? */
300 etag = [self davEntityTag];
301 if ([etag length] == 0) /* has no etag, ignore */
304 if ([etags containsObject:etag]) {
305 [self debugWithFormat:@"etag '%@' matches: %@", etag,
306 [etags componentsJoinedByString:@","]];
307 return nil; /* one etag matches, so continue with request */
310 /* hack for Kontact 3.4 */
312 if (kontactGroupDAV) {
313 WEClientCapabilities *cc;
315 cc = [[(WOContext *)_ctx request] clientCapabilities];
316 if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
317 if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
319 @"WARNING: applying Kontact 3.4 GroupDAV hack"
320 @" - etag check is disabled!"
321 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
327 // TODO: we might want to return the davEntityTag in the response
328 [self debugWithFormat:@"etag '%@' does not match: %@", etag,
329 [etags componentsJoinedByString:@","]];
330 return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
331 reason:@"Precondition Failed"];
334 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
336 If one of the etags is still the same, we can ignore the request.
338 Can be used for PUT to ensure that the object does not exist in the store
339 and for GET to retrieve the content only if if the etag changed.
342 if ([_c isEqualToString:@"*"])
345 if ((a = [self parseETagList:_c]) == nil)
348 [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
353 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
358 if ((rq = [(WOContext *)_ctx request]) == nil)
359 return nil; /* be tolerant - no request, no condition */
361 if ((c = [rq headerForKey:@"if-match"]) != nil) {
362 if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
365 if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
366 if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
375 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
379 Note: even for new objects we won't get a new name but a preinstantiated
380 object representing the new one.
383 @"TODO: move not implemented:\n target: %@\n new name: %@",
385 return [NSException exceptionWithHTTPStatus:405 /* not allowed */
386 reason:@"this object cannot be copied via WebDAV"];
389 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
393 Note: even for new objects we won't get a new name but a preinstantiated
394 object representing the new one.
397 @"TODO: copy not implemented:\n target: %@\n new name: %@",
399 return [NSException exceptionWithHTTPStatus:405 /* not allowed */
400 reason:@"this object cannot be copied via WebDAV"];
403 - (BOOL)davIsCollection {
404 return [self isFolderish];
409 - (NSString *)outlookMessageClass {
415 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
416 [super appendAttributesToDescription:_ms];
418 [_ms appendFormat:@" ocs=%@", [self ocsPath]];
421 @end /* SOGoContentObject */