]> err.no Git - scalable-opengroupware.org/blob - SOGo/SoObjects/SOGo/SOGoContentObject.m
57a5871f0d6605bf542ad4142fa7b1580085d90e
[scalable-opengroupware.org] / SOGo / SoObjects / SOGo / SOGoContentObject.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #include "SOGoContentObject.h"
23 #include "SOGoFolder.h"
24 #include <NGObjWeb/WEClientCapabilities.h>
25 #include "common.h"
26 #include <GDLContentStore/GCSFolder.h>
27
28 @interface SOGoContentObject(ETag)
29 - (NSArray *)parseETagList:(NSString *)_c;
30 @end
31
32 @implementation SOGoContentObject
33
34 static BOOL kontactGroupDAV = YES;
35
36 + (void)initialize {
37   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
38   
39   kontactGroupDAV = 
40     [ud boolForKey:@"SOGoDisableKontact34GroupDAVHack"] ? NO : YES;
41 }
42
43 - (void)dealloc {
44   [self->content release];
45   [self->ocsPath release];
46   [super dealloc];
47 }
48
49 /* notifications */
50
51 - (void)sleep {
52   [self->content release]; self->content = nil;
53   [super sleep];
54 }
55
56 /* accessors */
57
58 - (BOOL)isFolderish {
59   return NO;
60 }
61
62 - (void)setOCSPath:(NSString *)_path {
63   if ([self->ocsPath isEqualToString:_path])
64     return;
65   
66   if (self->ocsPath)
67     [self warnWithFormat:@"GCS path is already set! '%@'", _path];
68   
69   ASSIGNCOPY(self->ocsPath, _path);
70 }
71
72 - (NSString *)ocsPath {
73   if (self->ocsPath == nil) {
74     NSString *p;
75     
76     if ((p = [self ocsPathOfContainer]) != nil) {
77       if (![p hasSuffix:@"/"]) p = [p stringByAppendingString:@"/"];
78       p = [p stringByAppendingString:[self nameInContainer]];
79       self->ocsPath = [p copy];
80     }
81   }
82   return self->ocsPath;
83 }
84
85 - (NSString *)ocsPathOfContainer {
86   if (![[self container] respondsToSelector:@selector(ocsPath)])
87     return nil;
88
89   return [[self container] ocsPath];
90 }
91
92 - (GCSFolder *)ocsFolder {
93   if (![[self container] respondsToSelector:@selector(ocsFolder)])
94     return nil;
95   
96   return [[self container] ocsFolder];
97 }
98
99 /* content */
100
101 - (NSString *)contentAsString {
102   GCSFolder *folder;
103
104   if (self->content != nil)
105     return self->content;
106   
107   if ((folder = [self ocsFolder]) == nil) {
108     [self errorWithFormat:@"Did not find folder of content object."];
109     return nil;
110   }
111   
112   self->content = [[folder fetchContentWithName:[self nameInContainer]] copy];
113   return self->content;
114 }
115
116 - (NSException *)saveContentString:(NSString *)_str
117   baseVersion:(unsigned int)_baseVersion
118 {
119   /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
120   GCSFolder   *folder;
121   NSException *ex;
122   
123   if ((folder = [self ocsFolder]) == nil) {
124     [self errorWithFormat:@"Did not find folder of content object."];
125     return nil;
126   }
127   
128   ex = [folder writeContent:_str toName:[self nameInContainer]
129                baseVersion:_baseVersion];
130   if (ex != nil) {
131     [self errorWithFormat:@"write failed: %@", ex];
132     return ex;
133   }
134   return nil;
135 }
136 - (NSException *)saveContentString:(NSString *)_str {
137   return [self saveContentString:_str baseVersion:0 /* don't check */];
138 }
139
140 - (NSException *)delete {
141   /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
142   GCSFolder   *folder;
143   NSException *ex;
144   
145   // TODO: add precondition check? (or add DELETEAction?)
146   
147   if ((folder = [self ocsFolder]) == nil) {
148     [self errorWithFormat:@"Did not find folder of content object."];
149     return nil;
150   }
151   
152   if ((ex = [folder deleteContentWithName:[self nameInContainer]])) {
153     [self errorWithFormat:@"delete failed: %@", ex];
154     return ex;
155   }
156   return nil;
157 }
158
159 /* actions */
160
161 - (id)PUTAction:(WOContext *)_ctx {
162   WORequest    *rq;
163   NSException  *error;
164   unsigned int baseVersion;
165   id           etag, tmp;
166   BOOL         needsLocation;
167   
168   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
169     return error;
170   
171   rq = [_ctx request];
172   
173   /* check whether its a request to the 'special' 'new' location */
174   /*
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
178           here.
179           A correct client would loop until it has a name which doesn't not
180           yet exist (by using if-none-match).
181   */
182   needsLocation = NO;
183   tmp = [[self nameInContainer] stringByDeletingPathExtension];
184   if ([tmp isEqualToString:@"new"]) {
185     tmp = [[[self container] class] globallyUniqueObjectId];
186     needsLocation = YES;
187     
188     [self debugWithFormat:
189             @"reassigned a new location for special new-location: %@", tmp];
190     
191     /* kinda dangerous */
192     ASSIGNCOPY(self->nameInContainer, tmp);
193     ASSIGN(self->ocsPath, nil);
194   }
195   
196   /* determine base version from etag in if-match header */
197   /*
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
201           commit.
202           (between the check and the update a change could have been done)
203   */
204   tmp  = [rq headerForKey:@"if-match"];
205   tmp  = [self parseETagList:tmp];
206   etag = nil;
207   if ([tmp count] > 0) {
208     if ([tmp count] > 1) {
209       /*
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
212               etag.
213       */
214       [self warnWithFormat:
215               @"Got multiple if-match etags from client, only attempting to "
216               @"save with the first: %@", tmp];
217     }
218     
219     etag = [tmp objectAtIndex:0];
220   }
221   baseVersion = ([etag length] > 0)
222     ? [etag unsignedIntValue]
223     : 0 /* 0 means 'do not check' */;
224   
225   /* attempt a save */
226   
227   if ((error = [self saveContentString:[rq contentAsString]
228                      baseVersion:baseVersion]) != nil)
229     return error;
230   
231   /* setup response */
232   
233   // TODO: this should be automatic in the SoDispatcher if we return nil?
234   [[_ctx response] setStatus:201 /* Created */];
235   
236   if ((etag = [self davEntityTag]) != nil)
237     [[_ctx response] setHeader:etag forKey:@"etag"];
238   
239   if (needsLocation) {
240     [[_ctx response] setHeader:[self baseURLInContext:_ctx] 
241                      forKey:@"location"];
242   }
243   
244   return [_ctx response];
245 }
246
247 /* E-Tags */
248
249 - (id)davEntityTag {
250   // TODO: cache tag in ivar? => if you do, remember to flush after PUT
251   GCSFolder *folder;
252   
253   if ((folder = [self ocsFolder]) == nil) {
254     [self errorWithFormat:@"Did not find folder of content object."];
255     return nil;
256   }
257   
258   return [folder versionOfContentWithName:[self nameInContainer]];
259 }
260
261 - (NSArray *)parseETagList:(NSString *)_c {
262   NSMutableArray *ma;
263   NSArray  *etags;
264   unsigned i, count;
265   
266   if ([_c length] == 0)
267     return nil;
268   if ([_c isEqualToString:@"*"])
269     return nil;
270   
271   etags = [_c componentsSeparatedByString:@","];
272   count = [etags count];
273   ma    = [NSMutableArray arrayWithCapacity:count];
274   for (i = 0; i < count; i++) {
275     NSString *etag;
276     
277     etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
278     if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
279       etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
280     
281     if (etag != nil) [ma addObject:etag];
282   }
283   return ma;
284 }
285
286 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
287   /* only run the request if one of the etags matches the resource etag */
288   NSArray  *etags;
289   NSString *etag;
290   
291   if ([_c isEqualToString:@"*"])
292     /* to ensure that the resource exists! */
293     return nil;
294   
295   if ((etags = [self parseETagList:_c]) == nil)
296     return nil;
297   if ([etags count] == 0) /* no etags to check for? */
298     return nil;
299   
300   etag = [self davEntityTag];
301   if ([etag length] == 0) /* has no etag, ignore */
302     return nil;
303   
304   if ([etags containsObject:etag]) {
305     [self debugWithFormat:@"etag '%@' matches: %@", etag, 
306           [etags componentsJoinedByString:@","]];
307     return nil; /* one etag matches, so continue with request */
308   }
309
310   /* hack for Kontact 3.4 */
311   
312   if (kontactGroupDAV) {
313     WEClientCapabilities *cc;
314     
315     cc = [[_ctx request] clientCapabilities];
316     if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
317       if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
318         [self logWithFormat:
319                 @"WARNING: applying Kontact 3.4 GroupDAV hack"
320                 @" - etag check is disabled!"
321                 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
322         return nil;
323       }
324     }
325   }
326   
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"];
332 }
333
334 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
335   /*
336     If one of the etags is still the same, we can ignore the request.
337     
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.
340   */
341 #if 0
342   if ([_c isEqualToString:@"*"])
343     return nil;
344   
345   if ((a = [self parseETagList:_c]) == nil)
346     return nil;
347 #else
348   [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
349 #endif
350   return nil;
351 }
352
353 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
354   NSException *error;
355   WORequest *rq;
356   NSString  *c;
357   
358   if ((rq = [(WOContext *)_ctx request]) == nil)
359     return nil; /* be tolerant - no request, no condition */
360   
361   if ((c = [rq headerForKey:@"if-match"]) != nil) {
362     if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
363       return error;
364   }
365   if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
366     if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
367       return error;
368   }
369   
370   return nil;
371 }
372
373 /* WebDAV */
374
375 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
376   inContext:(id)_ctx
377 {
378   /*
379     Note: even for new objects we won't get a new name but a preinstantiated
380           object representing the new one.
381   */
382   [self logWithFormat:
383           @"TODO: move not implemented:\n  target:  %@\n  new name: %@",
384           _target, _name];
385   return [NSException exceptionWithHTTPStatus:405 /* not allowed */
386                       reason:@"this object cannot be copied via WebDAV"];
387 }
388
389 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
390   inContext:(id)_ctx
391 {
392   /*
393     Note: even for new objects we won't get a new name but a preinstantiated
394           object representing the new one.
395   */
396   [self logWithFormat:
397           @"TODO: copy not implemented:\n  target:  %@\n  new name: %@",
398           _target, _name];
399   return [NSException exceptionWithHTTPStatus:405 /* not allowed */
400                       reason:@"this object cannot be copied via WebDAV"];
401 }
402
403 - (BOOL)davIsCollection {
404   return [self isFolderish];
405 }
406
407 /* message type */
408
409 - (NSString *)outlookMessageClass {
410   return nil;
411 }
412
413 /* description */
414
415 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
416   [super appendAttributesToDescription:_ms];
417   
418   [_ms appendFormat:@" ocs=%@", [self ocsPath]];
419 }
420
421 @end /* SOGoContentObject */