]> err.no Git - scalable-opengroupware.org/blob - SoObjects/SOGo/SOGoContentObject.m
344e9c2860e0e909209632c323fd031c8d3ad533
[scalable-opengroupware.org] / 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 #import <GDLContentStore/GCSFolder.h>
23
24 #import "common.h"
25 #import "SOGoFolder.h"
26 #import "SOGoUser.h"
27 #import "SOGoPermissions.h"
28 #import "SOGoContentObject.h"
29
30 @interface SOGoContentObject(ETag)
31 - (NSArray *)parseETagList:(NSString *)_c;
32 @end
33
34 @implementation SOGoContentObject
35
36 // TODO: check superclass version
37
38 - (id) initWithName: (NSString *) newName
39         inContainer: (id) newContainer
40 {
41   if ((self = [super initWithName: newName inContainer: newContainer]))
42     {
43       ocsPath = nil;
44       content = [[self ocsFolder] fetchContentWithName: newName];
45       [content retain];
46       isNew = (!content);
47     }
48
49   return self;
50 }
51
52 - (void) dealloc
53 {
54   [content release];
55   [ocsPath release];
56   [super dealloc];
57 }
58
59 /* notifications */
60
61 - (void) sleep
62 {
63   [content release]; content = nil;
64   [super sleep];
65 }
66
67 /* accessors */
68
69 - (BOOL) isFolderish
70 {
71   return NO;
72 }
73
74 - (void) setOCSPath: (NSString *) newOCSPath
75 {
76   if (![ocsPath isEqualToString: newOCSPath])
77     {
78       if (ocsPath)
79         [self warnWithFormat:@"GCS path is already set! '%@'", newOCSPath];
80   
81       ASSIGNCOPY (ocsPath, newOCSPath);
82     }
83 }
84
85 - (NSString *) ocsPath
86 {
87   NSMutableString *newOCSPath;
88
89   if (!ocsPath)
90     {
91       newOCSPath = [NSMutableString new];
92       [newOCSPath appendString: [self ocsPathOfContainer]];
93       if ([newOCSPath length] > 0)
94         {
95           if (![newOCSPath hasSuffix:@"/"])
96             [newOCSPath appendString: @"/"];
97           [newOCSPath appendString: nameInContainer];
98           ocsPath = newOCSPath;
99         }
100     }
101
102   return ocsPath;
103 }
104
105 - (NSString *) ocsPathOfContainer
106 {
107   NSString *ocsPathOfContainer;
108
109   if ([container respondsToSelector: @selector (ocsPath)])
110     ocsPathOfContainer = [container ocsPath];
111   else
112     ocsPathOfContainer = nil;
113
114   return ocsPath;
115 }
116
117 - (GCSFolder *) ocsFolder
118 {
119   return [container ocsFolder];
120 }
121
122 /* content */
123
124 - (BOOL) isNew
125 {
126   return isNew;
127 }
128
129 - (NSString *) contentAsString
130 {
131   return content;
132 }
133
134 - (NSException *) saveContentString: (NSString *) newContent
135                         baseVersion: (unsigned int) newBaseVersion
136 {
137   /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
138   GCSFolder   *folder;
139   NSException *ex;
140
141   ex = nil;
142
143   ASSIGN (content, newContent);
144   folder = [container ocsFolder];
145   if (folder)
146     {
147       ex = [folder writeContent: newContent
148                    toName: nameInContainer
149                    baseVersion: newBaseVersion];
150       if (ex)
151         [self errorWithFormat:@"write failed: %@", ex];
152     }
153   else
154     [self errorWithFormat:@"Did not find folder of content object."];
155   
156   return ex;
157 }
158
159 - (NSException *) saveContentString: (NSString *) newContent
160 {
161   return [self saveContentString: newContent baseVersion: 0];
162 }
163
164 - (NSException *) delete
165 {
166   /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
167   GCSFolder   *folder;
168   NSException *ex;
169   
170   // TODO: add precondition check? (or add DELETEAction?)
171   
172   if ((folder = [self ocsFolder]) == nil) {
173     [self errorWithFormat:@"Did not find folder of content object."];
174     return nil;
175   }
176   
177   if ((ex = [folder deleteContentWithName:[self nameInContainer]])) {
178     [self errorWithFormat:@"delete failed: %@", ex];
179     return ex;
180   }
181   return nil;
182 }
183
184 /* actions */
185
186 // - (id) lookupName:
187 // {
188 //   SoSelectorInvocation *invocation;
189 //   NSString *name;
190
191 //   name = [NSString stringWithFormat: @"%@:", [_key davMethodToObjC]];
192
193 //   invocation = [[SoSelectorInvocation alloc]
194 //                  initWithSelectorNamed: name
195 //                  addContextParameter: YES];
196 //   [invocation autorelease];
197
198 //   return invocation;
199
200 // }
201
202 - (id) PUTAction: (WOContext *) _ctx
203 {
204   WORequest    *rq;
205   NSException  *error;
206   unsigned int baseVersion;
207   id           etag, tmp;
208   BOOL         needsLocation;
209   
210   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
211     return error;
212   
213   rq = [_ctx request];
214   
215   /* check whether its a request to the 'special' 'new' location */
216   /*
217     Note: this is kinda hack. The OGo ZideStore detects writes to 'new' as
218           object creations and will assign a server side identifier. Most
219           current GroupDAV clients rely on this behaviour, so we reproduce it
220           here.
221           A correct client would loop until it has a name which doesn't not
222           yet exist (by using if-none-match).
223   */
224   needsLocation = NO;
225   tmp = [[self nameInContainer] stringByDeletingPathExtension];
226   if ([tmp isEqualToString:@"new"]) {
227     tmp = [[[self container] class] globallyUniqueObjectId];
228     needsLocation = YES;
229     
230     [self debugWithFormat:
231             @"reassigned a new location for special new-location: %@", tmp];
232     
233     /* kinda dangerous */
234     ASSIGNCOPY(nameInContainer, tmp);
235     ASSIGN(ocsPath, nil);
236   }
237   
238   /* determine base version from etag in if-match header */
239   /*
240     Note: The -matchesRequestConditionInContext: already checks whether the
241           etag matches and returns an HTTP exception in case it doesn't.
242           We retrieve the etag again here to _ensure_ a transactionally save
243           commit.
244           (between the check and the update a change could have been done)
245   */
246   tmp  = [rq headerForKey:@"if-match"];
247   tmp  = [self parseETagList:tmp];
248   etag = nil;
249   if ([tmp count] > 0) {
250     if ([tmp count] > 1) {
251       /*
252         Note: we would have to attempt a save for _each_ of the etags being
253               passed in! In practice most WebDAV clients submit exactly one
254               etag.
255       */
256       [self warnWithFormat:
257               @"Got multiple if-match etags from client, only attempting to "
258               @"save with the first: %@", tmp];
259     }
260     
261     etag = [tmp objectAtIndex:0];
262   }
263   baseVersion = ([etag length] > 0)
264     ? [etag unsignedIntValue]
265     : 0 /* 0 means 'do not check' */;
266   
267   /* attempt a save */
268
269   if ((error = [self saveContentString: [rq contentAsString]
270                      baseVersion: baseVersion]) != nil)
271     return error;
272   
273   /* setup response */
274   
275   // TODO: this should be automatic in the SoDispatcher if we return nil?
276   [[_ctx response] setStatus: 201 /* Created */];
277   
278   if ((etag = [self davEntityTag]) != nil)
279     [[_ctx response] setHeader:etag forKey:@"etag"];
280   
281   if (needsLocation) {
282     [[_ctx response] setHeader:[self baseURLInContext:_ctx] 
283                      forKey:@"location"];
284   }
285   
286   return [_ctx response];
287 }
288
289 /* E-Tags */
290
291 - (id) davEntityTag
292 {
293   // TODO: cache tag in ivar? => if you do, remember to flush after PUT
294   GCSFolder *folder;
295   char buf[64];
296   NSString *entityTag;
297   NSNumber *versionValue;
298   
299   folder = [self ocsFolder];
300   if (folder)
301     {
302       versionValue = [folder versionOfContentWithName: [self nameInContainer]];
303       sprintf (buf, "\"gcs%08d\"", [versionValue unsignedIntValue]);
304       entityTag = [NSString stringWithCString: buf];
305     }
306   else
307     {
308       [self errorWithFormat:@"Did not find folder of content object."];
309       entityTag = nil;
310     }
311
312   return entityTag;
313 }
314
315 /* WebDAV */
316
317 - (NSException *) davMoveToTargetObject: (id) _target
318                                 newName: (NSString *) _name
319                               inContext: (id) _ctx
320 {
321   /*
322     Note: even for new objects we won't get a new name but a preinstantiated
323           object representing the new one.
324   */
325   [self logWithFormat:
326           @"TODO: move not implemented:\n  target:  %@\n  new name: %@",
327           _target, _name];
328   return [NSException exceptionWithHTTPStatus:405 /* not allowed */
329                       reason:@"this object cannot be copied via WebDAV"];
330 }
331
332 - (NSException *) davCopyToTargetObject: (id)_target
333                                 newName: (NSString *) _name
334                               inContext: (id) _ctx
335 {
336   /*
337     Note: even for new objects we won't get a new name but a preinstantiated
338           object representing the new one.
339   */
340   [self logWithFormat:
341           @"TODO: copy not implemented:\n  target:  %@\n  new name: %@",
342           _target, _name];
343   return [NSException exceptionWithHTTPStatus:405 /* not allowed */
344                       reason:@"this object cannot be copied via WebDAV"];
345 }
346
347 - (BOOL)davIsCollection {
348   return [self isFolderish];
349 }
350
351 /* acls */
352
353 - (NSArray *) aclUsers
354 {
355   return [container aclUsersForObjectAtPath: [self pathArrayToSoObject]];
356 }
357
358 - (NSArray *) aclsForUser: (NSString *) uid
359 {
360   NSMutableArray *acls;
361   NSArray *ownAcls, *containerAcls;
362
363   acls = [NSMutableArray array];
364   ownAcls = [container aclsForUser: uid
365                        forObjectAtPath: [self pathArrayToSoObject]];
366   [acls addObjectsFromArray: ownAcls];
367   containerAcls = [container aclsForUser: uid];
368   if ([containerAcls count] > 0)
369     {
370       if ([containerAcls containsObject: SOGoRole_ObjectCreator])
371         {
372           [acls addObject: SOGoRole_ObjectCreator];
373           if (isNew)
374             [acls addObject: SOGoRole_ObjectEditor];
375         }
376       if ([containerAcls containsObject: SOGoRole_ObjectReader])
377         [acls addObject: SOGoRole_ObjectViewer];
378     }
379
380   return acls;
381 }
382
383 - (void) setRoles: (NSArray *) roles
384           forUser: (NSString *) uid
385 {
386   return [container setRoles: roles
387                     forUser: uid
388                     forObjectAtPath: [self pathArrayToSoObject]];
389 }
390
391 - (void) removeAclsForUsers: (NSArray *) users
392 {
393   return [container removeAclsForUsers: users
394                     forObjectAtPath: [self pathArrayToSoObject]];
395 }
396
397 - (NSString *) defaultUserID
398 {
399   return @"<default>";
400 }
401
402 /* message type */
403
404 - (NSString *) outlookMessageClass
405 {
406   return nil;
407 }
408
409 /* description */
410
411 - (void) appendAttributesToDescription: (NSMutableString *) _ms
412 {
413   [super appendAttributesToDescription:_ms];
414   
415   [_ms appendFormat:@" ocs=%@", [self ocsPath]];
416 }
417
418 @end /* SOGoContentObject */