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