]> err.no Git - scalable-opengroupware.org/blob - SOGo/SoObjects/SOGo/SOGoContentObject.m
e942dc4facd911d08fe6bf9643bf37d4900b131e
[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 <NGObjWeb/WEClientCapabilities.h>
24 #include "common.h"
25 #include <GDLContentStore/GCSFolder.h>
26
27 @implementation SOGoContentObject
28
29 static BOOL kontactGroupDAV = YES;
30
31 + (void)initialize {
32   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
33   
34   kontactGroupDAV = 
35     [ud boolForKey:@"SOGoDisableKontact34GroupDAVHack"] ? NO : YES;
36 }
37
38 - (void)dealloc {
39   [self->content release];
40   [self->ocsPath release];
41   [super dealloc];
42 }
43
44 /* notifications */
45
46 - (void)sleep {
47   [self->content release]; self->content = nil;
48   [super sleep];
49 }
50
51 /* accessors */
52
53 - (BOOL)isFolderish {
54   return NO;
55 }
56
57 - (void)setOCSPath:(NSString *)_path {
58   if ([self->ocsPath isEqualToString:_path])
59     return;
60   
61   if (self->ocsPath)
62     [self warnWithFormat:@"GCS path is already set! '%@'", _path];
63   
64   ASSIGNCOPY(self->ocsPath, _path);
65 }
66
67 - (NSString *)ocsPath {
68   if (self->ocsPath == nil) {
69     NSString *p;
70     
71     if ((p = [self ocsPathOfContainer]) != nil) {
72       if (![p hasSuffix:@"/"]) p = [p stringByAppendingString:@"/"];
73       p = [p stringByAppendingString:[self nameInContainer]];
74       self->ocsPath = [p copy];
75     }
76   }
77   return self->ocsPath;
78 }
79
80 - (NSString *)ocsPathOfContainer {
81   if (![[self container] respondsToSelector:@selector(ocsPath)])
82     return nil;
83
84   return [[self container] ocsPath];
85 }
86
87 - (GCSFolder *)ocsFolder {
88   if (![[self container] respondsToSelector:@selector(ocsFolder)])
89     return nil;
90   
91   return [[self container] ocsFolder];
92 }
93
94 /* content */
95
96 - (NSString *)contentAsString {
97   GCSFolder *folder;
98
99   if (self->content != nil)
100     return self->content;
101   
102   if ((folder = [self ocsFolder]) == nil) {
103     [self errorWithFormat:@"Did not find folder of content object."];
104     return nil;
105   }
106   
107   self->content = [[folder fetchContentWithName:[self nameInContainer]] copy];
108   return self->content;
109 }
110
111 - (NSException *)saveContentString:(NSString *)_str {
112   /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
113   GCSFolder   *folder;
114   NSException *ex;
115   
116   if ((folder = [self ocsFolder]) == nil) {
117     [self errorWithFormat:@"Did not find folder of content object."];
118     return nil;
119   }
120   if ((ex = [folder writeContent:_str toName:[self nameInContainer]])) {
121     [self errorWithFormat:@"write failed: %@", ex];
122     return ex;
123   }
124   return nil;
125 }
126
127 - (NSException *)delete {
128   /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
129   GCSFolder   *folder;
130   NSException *ex;
131   
132   // TODO: add precondition check? (or add DELETEAction?)
133   
134   if ((folder = [self ocsFolder]) == nil) {
135     [self errorWithFormat:@"Did not find folder of content object."];
136     return nil;
137   }
138   
139   if ((ex = [folder deleteContentWithName:[self nameInContainer]])) {
140     [self errorWithFormat:@"delete failed: %@", ex];
141     return ex;
142   }
143   return nil;
144 }
145
146 /* actions */
147
148 - (id)PUTAction:(WOContext *)_ctx {
149   WORequest   *rq;
150   NSException *error;
151   id etag;
152   
153   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
154     return error;
155   
156   rq = [_ctx request];
157   if ((error = [self saveContentString:[rq contentAsString]]) != nil)
158     return error;
159   
160   // TODO: this should be automatic if we return nil?
161   [[_ctx response] setStatus:201 /* Created */];
162   
163   if ((etag = [self davEntityTag]) != nil)
164     [[_ctx response] setHeader:etag forKey:@"etag"];
165   return [_ctx response];
166 }
167
168 /* E-Tags */
169
170 - (id)davEntityTag {
171   // TODO: cache tag in ivar? => if you do, remember to flush after PUT
172   GCSFolder *folder;
173   
174   if ((folder = [self ocsFolder]) == nil) {
175     [self errorWithFormat:@"Did not find folder of content object."];
176     return nil;
177   }
178   
179   return [folder versionOfContentWithName:[self nameInContainer]];
180 }
181
182 - (NSArray *)parseETagList:(NSString *)_c {
183   NSMutableArray *ma;
184   NSArray  *etags;
185   unsigned i, count;
186   
187   if ([_c length] == 0)
188     return nil;
189   if ([_c isEqualToString:@"*"])
190     return nil;
191   
192   etags = [_c componentsSeparatedByString:@","];
193   count = [etags count];
194   ma    = [NSMutableArray arrayWithCapacity:count];
195   for (i = 0; i < count; i++) {
196     NSString *etag;
197     
198     etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
199     if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
200       etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
201     
202     if (etag != nil) [ma addObject:etag];
203   }
204   return ma;
205 }
206
207 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
208   /* only run the request if one of the etags matches the resource etag */
209   NSArray  *etags;
210   NSString *etag;
211   
212   if ([_c isEqualToString:@"*"])
213     /* to ensure that the resource exists! */
214     return nil;
215   
216   if ((etags = [self parseETagList:_c]) == nil)
217     return nil;
218   if ([etags count] == 0) /* no etags to check for? */
219     return nil;
220   
221   etag = [self davEntityTag];
222   if ([etag length] == 0) /* has no etag, ignore */
223     return nil;
224   
225   if ([etags containsObject:etag]) {
226     [self debugWithFormat:@"etag '%@' matches: %@", etag, 
227           [etags componentsJoinedByString:@","]];
228     return nil; /* one etag matches, so continue with request */
229   }
230
231   /* hack for Kontact 3.4 */
232   
233   if (kontactGroupDAV) {
234     WEClientCapabilities *cc;
235     
236     cc = [[_ctx request] clientCapabilities];
237     if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
238       if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
239         [self logWithFormat:
240                 @"WARNING: applying Kontact 3.4 GroupDAV hack"
241                 @" - etag check is disabled!"
242                 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
243         return nil;
244       }
245     }
246   }
247   
248   // TODO: we might want to return the davEntityTag in the response
249   [self debugWithFormat:@"etag '%@' does not match: %@", etag, 
250         [etags componentsJoinedByString:@","]];
251   return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
252                       reason:@"Precondition Failed"];
253 }
254
255 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
256   /*
257     If one of the etags is still the same, we can ignore the request.
258     
259     Can be used for PUT to ensure that the object does not exist in the store
260     and for GET to retrieve the content only if if the etag changed.
261   */
262 #if 0
263   if ([_c isEqualToString:@"*"])
264     return nil;
265   
266   if ((a = [self parseETagList:_c]) == nil)
267     return nil;
268 #else
269   [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
270 #endif
271   return nil;
272 }
273
274 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
275   NSException *error;
276   WORequest *rq;
277   NSString  *c;
278   
279   if ((rq = [(WOContext *)_ctx request]) == nil)
280     return nil; /* be tolerant - no request, no condition */
281   
282   if ((c = [rq headerForKey:@"if-match"]) != nil) {
283     if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
284       return error;
285   }
286   if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
287     if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
288       return error;
289   }
290   
291   return nil;
292 }
293
294 /* WebDAV */
295
296 - (BOOL)davIsCollection {
297   return [self isFolderish];
298 }
299
300 /* message type */
301
302 - (NSString *)outlookMessageClass {
303   return nil;
304 }
305
306 /* description */
307
308 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
309   [super appendAttributesToDescription:_ms];
310   
311   [_ms appendFormat:@" ocs=%@", [self ocsPath]];
312 }
313
314 @end /* SOGoContentObject */