]> err.no Git - scalable-opengroupware.org/blob - SoObjects/SOGo/SOGoContentObject.m
945382da5b1d2d27cb399318e43d7e4e307fbdc4
[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 "SOGoFolder.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 - (NSException *) saveContentString: (NSString *) newContent
142                         baseVersion: (unsigned int) newBaseVersion
143 {
144   /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
145   GCSFolder   *folder;
146   NSException *ex;
147
148   ex = nil;
149
150   ASSIGN (content, newContent);
151   folder = [container ocsFolder];
152   if (folder)
153     {
154       ex = [folder writeContent: newContent
155                    toName: nameInContainer
156                    baseVersion: newBaseVersion];
157       if (ex)
158         [self errorWithFormat:@"write failed: %@", ex];
159     }
160   else
161     [self errorWithFormat:@"Did not find folder of content object."];
162   
163   return ex;
164 }
165
166 - (NSException *) saveContentString: (NSString *) newContent
167 {
168   return [self saveContentString: newContent baseVersion: 0];
169 }
170
171 - (NSException *) delete
172 {
173   /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
174   GCSFolder   *folder;
175   NSException *ex;
176   
177   // TODO: add precondition check? (or add DELETEAction?)
178   
179   if ((folder = [self ocsFolder]) == nil) {
180     [self errorWithFormat:@"Did not find folder of content object."];
181     return nil;
182   }
183   
184   if ((ex = [folder deleteContentWithName:[self nameInContainer]])) {
185     [self errorWithFormat:@"delete failed: %@", ex];
186     return ex;
187   }
188   return nil;
189 }
190
191 /* actions */
192
193 // - (id) lookupName:
194 // {
195 //   SoSelectorInvocation *invocation;
196 //   NSString *name;
197
198 //   name = [NSString stringWithFormat: @"%@:", [_key davMethodToObjC]];
199
200 //   invocation = [[SoSelectorInvocation alloc]
201 //                  initWithSelectorNamed: name
202 //                  addContextParameter: YES];
203 //   [invocation autorelease];
204
205 //   return invocation;
206
207 // }
208
209 - (id) PUTAction: (WOContext *) _ctx
210 {
211   WORequest    *rq;
212   NSException  *error;
213   unsigned int baseVersion;
214   id           etag, tmp;
215   BOOL         needsLocation;
216   
217   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
218     return error;
219   
220   rq = [_ctx request];
221   
222   /* check whether its a request to the 'special' 'new' location */
223   /*
224     Note: this is kinda hack. The OGo ZideStore detects writes to 'new' as
225           object creations and will assign a server side identifier. Most
226           current GroupDAV clients rely on this behaviour, so we reproduce it
227           here.
228           A correct client would loop until it has a name which doesn't not
229           yet exist (by using if-none-match).
230   */
231   needsLocation = NO;
232   tmp = [[self nameInContainer] stringByDeletingPathExtension];
233   if ([tmp isEqualToString:@"new"]) {
234     tmp = [[[self container] class] globallyUniqueObjectId];
235     needsLocation = YES;
236     
237     [self debugWithFormat:
238             @"reassigned a new location for special new-location: %@", tmp];
239     
240     /* kinda dangerous */
241     ASSIGNCOPY(nameInContainer, tmp);
242     ASSIGN(ocsPath, nil);
243   }
244   
245   /* determine base version from etag in if-match header */
246   /*
247     Note: The -matchesRequestConditionInContext: already checks whether the
248           etag matches and returns an HTTP exception in case it doesn't.
249           We retrieve the etag again here to _ensure_ a transactionally save
250           commit.
251           (between the check and the update a change could have been done)
252   */
253   tmp  = [rq headerForKey:@"if-match"];
254   tmp  = [self parseETagList:tmp];
255   etag = nil;
256   if ([tmp count] > 0) {
257     if ([tmp count] > 1) {
258       /*
259         Note: we would have to attempt a save for _each_ of the etags being
260               passed in! In practice most WebDAV clients submit exactly one
261               etag.
262       */
263       [self warnWithFormat:
264               @"Got multiple if-match etags from client, only attempting to "
265               @"save with the first: %@", tmp];
266     }
267     
268     etag = [tmp objectAtIndex:0];
269   }
270   baseVersion = ([etag length] > 0)
271     ? [etag unsignedIntValue]
272     : 0 /* 0 means 'do not check' */;
273   
274   /* attempt a save */
275
276   if ((error = [self saveContentString: [rq contentAsString]
277                      baseVersion: baseVersion]) != nil)
278     return error;
279   
280   /* setup response */
281   
282   // TODO: this should be automatic in the SoDispatcher if we return nil?
283   [[_ctx response] setStatus: 201 /* Created */];
284   
285   if ((etag = [self davEntityTag]) != nil)
286     [[_ctx response] setHeader:etag forKey:@"etag"];
287   
288   if (needsLocation) {
289     [[_ctx response] setHeader:[self baseURLInContext:_ctx] 
290                      forKey:@"location"];
291   }
292   
293   return [_ctx response];
294 }
295
296 /* E-Tags */
297
298 - (id) davEntityTag
299 {
300   // TODO: cache tag in ivar? => if you do, remember to flush after PUT
301   GCSFolder *folder;
302   char buf[64];
303   NSString *entityTag;
304   NSNumber *versionValue;
305   
306   folder = [self ocsFolder];
307   if (folder)
308     {
309       versionValue = [folder versionOfContentWithName: [self nameInContainer]];
310       sprintf (buf, "\"gcs%08d\"", [versionValue unsignedIntValue]);
311       entityTag = [NSString stringWithCString: buf];
312     }
313   else
314     {
315       [self errorWithFormat:@"Did not find folder of content object."];
316       entityTag = nil;
317     }
318
319   return entityTag;
320 }
321
322 /* WebDAV */
323
324 - (NSException *) davMoveToTargetObject: (id) _target
325                                 newName: (NSString *) _name
326                               inContext: (id) _ctx
327 {
328   /*
329     Note: even for new objects we won't get a new name but a preinstantiated
330           object representing the new one.
331   */
332   [self logWithFormat:
333           @"TODO: move not implemented:\n  target:  %@\n  new name: %@",
334           _target, _name];
335   return [NSException exceptionWithHTTPStatus:405 /* not allowed */
336                       reason:@"this object cannot be copied via WebDAV"];
337 }
338
339 - (NSException *) davCopyToTargetObject: (id)_target
340                                 newName: (NSString *) _name
341                               inContext: (id) _ctx
342 {
343   /*
344     Note: even for new objects we won't get a new name but a preinstantiated
345           object representing the new one.
346   */
347   [self logWithFormat:
348           @"TODO: copy not implemented:\n  target:  %@\n  new name: %@",
349           _target, _name];
350   return [NSException exceptionWithHTTPStatus:405 /* not allowed */
351                       reason:@"this object cannot be copied via WebDAV"];
352 }
353
354 - (BOOL)davIsCollection {
355   return [self isFolderish];
356 }
357
358 /* acls */
359
360 - (NSArray *) aclUsers
361 {
362   return [container aclUsersForObjectAtPath: [self pathArrayToSoObject]];
363 }
364
365 - (NSArray *) aclsForUser: (NSString *) uid
366 {
367   NSMutableArray *acls;
368   NSArray *ownAcls, *containerAcls;
369
370   acls = [NSMutableArray array];
371   ownAcls = [container aclsForUser: uid
372                        forObjectAtPath: [self pathArrayToSoObject]];
373   [acls addObjectsFromArray: ownAcls];
374   containerAcls = [container aclsForUser: uid];
375   if ([containerAcls count] > 0)
376     {
377       if ([containerAcls containsObject: SOGoRole_ObjectCreator])
378         {
379           [acls addObject: SOGoRole_ObjectCreator];
380           if (isNew)
381             [acls addObject: SOGoRole_ObjectEditor];
382         }
383       if ([containerAcls containsObject: SOGoRole_ObjectReader])
384         [acls addObject: SOGoRole_ObjectViewer];
385     }
386
387   return acls;
388 }
389
390 - (void) setRoles: (NSArray *) roles
391           forUser: (NSString *) uid
392 {
393   return [container setRoles: roles
394                     forUser: uid
395                     forObjectAtPath: [self pathArrayToSoObject]];
396 }
397
398 - (void) removeAclsForUsers: (NSArray *) users
399 {
400   return [container removeAclsForUsers: users
401                     forObjectAtPath: [self pathArrayToSoObject]];
402 }
403
404 - (NSString *) defaultUserID
405 {
406   return @"<default>";
407 }
408
409 /* message type */
410
411 - (NSString *) outlookMessageClass
412 {
413   return nil;
414 }
415
416 /* description */
417
418 - (void) appendAttributesToDescription: (NSMutableString *) _ms
419 {
420   [super appendAttributesToDescription:_ms];
421   
422   [_ms appendFormat:@" ocs=%@", [self ocsPath]];
423 }
424
425 @end /* SOGoContentObject */