]> err.no Git - scalable-opengroupware.org/blob - SoObjects/SOGo/SOGoObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1282 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SoObjects / SOGo / SOGoObject.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 #if LIB_FOUNDATION_LIBRARY
23 #error SOGo will not work properly with libFoundation. \
24        Please use gnustep-base instead.
25 #endif
26
27 #import <unistd.h>
28
29 #import <Foundation/NSBundle.h>
30 #import <Foundation/NSClassDescription.h>
31 #import <Foundation/NSString.h>
32 #import <Foundation/NSUserDefaults.h>
33 #import <Foundation/NSURL.h>
34 #import <Foundation/NSValue.h>
35
36 #import <NGObjWeb/SoClass.h>
37 #import <NGObjWeb/SoClassSecurityInfo.h>
38 #import <NGObjWeb/SoObject+SoDAV.h>
39 #import <NGObjWeb/WEClientCapabilities.h>
40 #import <NGObjWeb/WOApplication.h>
41 #import <NGObjWeb/WOContext.h>
42 #import <NGObjWeb/WOResourceManager.h>
43 #import <NGObjWeb/WOResponse.h>
44 #import <NGObjWeb/WORequest.h>
45 #import <NGObjWeb/WORequest+So.h>
46 #import <NGObjWeb/NSException+HTTP.h>
47 #import <NGExtensions/NSObject+Logs.h>
48 #import <NGExtensions/NSString+misc.h>
49 #import <NGCards/NSDictionary+NGCards.h>
50 #import <UI/SOGoUI/SOGoACLAdvisory.h>
51
52 #import "SOGoPermissions.h"
53 #import "SOGoUser.h"
54 #import "SOGoDAVAuthenticator.h"
55 #import "SOGoUserFolder.h"
56
57 #import "SOGoDAVRendererTypes.h"
58
59 #import "NSArray+Utilities.h"
60 #import "NSDictionary+Utilities.h"
61 #import "NSString+Utilities.h"
62
63 #import "SOGoObject.h"
64
65 @interface SOGoObject(Content)
66 - (NSString *)contentAsString;
67 @end
68
69 @interface SoClassSecurityInfo (SOGoAcls)
70
71 + (id) defaultWebDAVPermissionsMap;
72
73 - (NSArray *) allPermissions;
74 - (NSArray *) allDAVPermissions;
75 - (NSArray *) DAVPermissionsForRole: (NSString *) role;
76 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles;
77
78 @end
79
80 @implementation SoClassSecurityInfo (SOGoAcls)
81
82 + (id) defaultWebDAVPermissionsMap
83 {
84   return [NSDictionary dictionaryWithObjectsAndKeys:
85                          @"read", SoPerm_AccessContentsInformation,
86                        @"bind", SoPerm_AddDocumentsImagesAndFiles,
87                        @"unbind", SoPerm_DeleteObjects,
88                        @"write-acl", SoPerm_ChangePermissions,
89                        @"write-content", SoPerm_ChangeImagesAndFiles,
90                        @"read-free-busy", SOGoPerm_FreeBusyLookup,
91                        NULL];
92 }
93
94 - (NSArray *) allPermissions
95 {
96   return [defRoles allKeys];
97 }
98
99 - (NSArray *) allDAVPermissions
100 {
101   NSEnumerator *allPermissions;
102   NSMutableArray *davPermissions;
103   NSDictionary *davPermissionsMap;
104   NSString *sopePermission, *davPermission;
105
106   davPermissions = [NSMutableArray array];
107
108   davPermissionsMap = [[self class] defaultWebDAVPermissionsMap];
109   allPermissions = [[self allPermissions] objectEnumerator];
110   sopePermission = [allPermissions nextObject];
111   while (sopePermission)
112     {
113       davPermission = [davPermissionsMap objectForCaseInsensitiveKey: sopePermission];
114       if (davPermission && ![davPermissions containsObject: davPermission])
115         [davPermissions addObject: davPermission];
116       sopePermission = [allPermissions nextObject];
117     }
118
119   return davPermissions;
120 }
121
122 - (NSArray *) DAVPermissionsForRole: (NSString *) role
123 {
124   return [self DAVPermissionsForRoles: [NSArray arrayWithObject: role]];
125 }
126
127 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles
128 {
129   NSEnumerator *allPermissions;
130   NSMutableArray *davPermissions;
131   NSDictionary *davPermissionsMap;
132   NSString *sopePermission, *davPermission;
133
134   davPermissions = [NSMutableArray array];
135
136   davPermissionsMap = [[self class] defaultWebDAVPermissionsMap];
137   allPermissions = [[self allPermissions] objectEnumerator];
138   sopePermission = [allPermissions nextObject];
139   while (sopePermission)
140     {
141       if ([[defRoles objectForCaseInsensitiveKey: sopePermission]
142             firstObjectCommonWithArray: roles])
143         {
144           davPermission
145             = [davPermissionsMap objectForCaseInsensitiveKey: sopePermission];
146           if (davPermission
147               && ![davPermissions containsObject: davPermission])
148             [davPermissions addObject: davPermission];
149         }
150       sopePermission = [allPermissions nextObject];
151     }
152
153   return davPermissions;
154 }
155
156 @end
157
158 @implementation SOGoObject
159
160 static BOOL kontactGroupDAV = YES;
161
162 + (int)version {
163   return 0;
164 }
165
166 + (void) initialize
167 {
168   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
169
170   kontactGroupDAV = 
171     [ud boolForKey:@"SOGoDisableKontact34GroupDAVHack"] ? NO : YES;
172
173 //   SoClass security declarations
174   
175 //   require View permission to access the root (bound to authenticated ...)
176 //   [[self soClassSecurityInfo] declareObjectProtected: SoPerm_View];
177
178 //   to allow public access to all contained objects (subkeys)
179 //   [[self soClassSecurityInfo] setDefaultAccess: @"allow"];
180   
181 //   /* require Authenticated role for View and WebDAV */
182 //   [[self soClassSecurityInfo] declareRole: SoRole_Owner
183 //                               asDefaultForPermission: SoPerm_View];
184 //   [[self soClassSecurityInfo] declareRole: SoRole_Owner
185 //                               asDefaultForPermission: SoPerm_WebDAVAccess];
186 }
187
188 + (NSString *) globallyUniqueObjectId
189 {
190   /*
191     4C08AE1A-A808-11D8-AC5A-000393BBAFF6
192     SOGo-Web-28273-18283-288182
193     printf( "%x", *(int *) &f);
194   */
195   static int pid = 0;
196   static int sequence = 0;
197   static float rndm = 0;
198   float f;
199
200   if (pid == 0)
201     { /* break if we fork ;-) */
202       pid = getpid();
203       rndm = random();
204     }
205   sequence++;
206   f = [[NSDate date] timeIntervalSince1970];
207
208   return [NSString stringWithFormat:@"%0X-%0X-%0X-%0X",
209                    pid, (int) f, sequence++, random];
210 }
211
212 - (NSString *) globallyUniqueObjectId
213 {
214   return [[self class] globallyUniqueObjectId];
215 }
216
217 + (void) _fillDictionary: (NSMutableDictionary *) dictionary
218           withDAVMethods: (NSString *) firstMethod, ...
219 {
220   va_list ap;
221   NSString *aclMethodName;
222   NSString *methodName;
223   SEL methodSel;
224
225   va_start (ap, firstMethod);
226   aclMethodName = firstMethod;
227   while (aclMethodName)
228     {
229       methodName = [aclMethodName davMethodToObjC];
230       methodSel = NSSelectorFromString (methodName);
231       if (methodSel && [self instancesRespondToSelector: methodSel])
232         [dictionary setObject: methodName
233                     forKey: [NSString stringWithFormat: @"{DAV:}%@",
234                                       aclMethodName]];
235       else
236         NSLog(@"************ method '%@' is still unimplemented!",
237               methodName);
238       aclMethodName = va_arg (ap, NSString *);
239     }
240
241   va_end (ap);
242 }
243
244 + (NSDictionary *) defaultWebDAVAttributeMap
245 {
246   static NSMutableDictionary *map = nil;
247
248   if (!map)
249     {
250       map = [NSMutableDictionary
251               dictionaryWithDictionary: [super defaultWebDAVAttributeMap]];
252       [map retain];
253       [self _fillDictionary: map
254             withDAVMethods: @"owner", @"group", @"supported-privilege-set",
255             @"current-user-privilege-set", @"acl", @"acl-restrictions",
256             @"inherited-acl-set", @"principal-collection-set", nil];
257     }
258
259   return map;
260 }
261
262 /* containment */
263
264 + (id) objectWithName: (NSString *)_name inContainer:(id)_container
265 {
266   SOGoObject *object;
267
268   object = [[self alloc] initWithName: _name inContainer: _container];
269   [object autorelease];
270
271   return object;
272 }
273
274 /* DAV ACL properties */
275 - (NSString *) davOwner
276 {
277   return [NSString stringWithFormat: @"%@users/%@",
278                    [self rootURLInContext: context],
279                    [self ownerInContext: nil]];
280 }
281
282 - (NSString *) davAclRestrictions
283 {
284   NSMutableString *restrictions;
285
286   restrictions = [NSMutableString string];
287   [restrictions appendString: @"<D:grant-only/>"];
288   [restrictions appendString: @"<D:no-invert/>"];
289
290   return restrictions;
291 }
292
293 - (SOGoDAVSet *) davPrincipalCollectionSet
294 {
295   NSString *usersUrl;
296
297   usersUrl = [NSString stringWithFormat: @"%@users",
298                        [self rootURLInContext: context]];
299
300   return [SOGoDAVSet davSetWithArray: [NSArray arrayWithObject: usersUrl]
301                      ofValuesTaggedAs: @"D:href"];
302 }
303
304 - (SOGoDAVSet *) davCurrentUserPrivilegeSet
305 {
306   SOGoDAVAuthenticator *sAuth;
307   SoUser *user;
308   NSArray *roles;
309   SoClassSecurityInfo *sInfo;
310   NSArray *davPermissions;
311
312   sAuth = [SOGoDAVAuthenticator sharedSOGoDAVAuthenticator];
313   user = [sAuth userInContext: context];
314   roles = [user rolesForObject: self inContext: context];
315   sInfo = [[self class] soClassSecurityInfo];
316
317   davPermissions
318     = [[sInfo DAVPermissionsForRoles: roles] stringsWithFormat: @"<D:%@/>"];
319
320   return [SOGoDAVSet davSetWithArray: davPermissions
321                      ofValuesTaggedAs: @"D:privilege"];
322 }
323
324 - (SOGoDAVSet *) davSupportedPrivilegeSet
325 {
326   SoClassSecurityInfo *sInfo;
327   NSArray *allPermissions;
328
329   sInfo = [[self class] soClassSecurityInfo];
330
331   allPermissions = [[sInfo allDAVPermissions] stringsWithFormat: @"<D:%@/>"];
332
333   return [SOGoDAVSet davSetWithArray: allPermissions
334                      ofValuesTaggedAs: @"D:privilege"];
335 }
336
337 - (NSArray *) _davAcesFromAclsDictionary: (NSDictionary *) aclsDictionary
338 {
339   NSEnumerator *keys;
340   NSArray *privileges;
341   NSMutableString *currentAce;
342   NSMutableArray *davAces;
343   NSString *currentKey, *principal;
344   SOGoDAVSet *privilegesDS;
345
346   davAces = [NSMutableArray array];
347   keys = [[aclsDictionary allKeys] objectEnumerator];
348   currentKey = [keys nextObject];
349   while (currentKey)
350     {
351       currentAce = [NSMutableString string];
352       if ([currentKey hasPrefix: @":"])
353         [currentAce
354           appendFormat: @"<D:principal><D:property><D:%@/></D:property></D:principal>",
355           [currentKey substringFromIndex: 1]];
356       else
357         {
358           principal = [NSString stringWithFormat: @"%@users/%@",
359                                 [self rootURLInContext: context],
360                                 currentKey];
361           [currentAce
362             appendFormat: @"<D:principal><D:href>%@</D:href></D:principal>",
363             principal];
364         }
365
366       privileges = [[aclsDictionary objectForKey: currentKey]
367                      stringsWithFormat: @"<D:%@/>"];
368       privilegesDS = [SOGoDAVSet davSetWithArray: privileges
369                                  ofValuesTaggedAs: @"privilege"];
370       [currentAce appendString: [privilegesDS stringForTag: @"{DAV:}grant"
371                                               rawName: @"grant"
372                                               inContext: nil prefixes: nil]];
373       [davAces addObject: currentAce];
374       currentKey = [keys nextObject];
375     }
376
377   return davAces;
378 }
379
380 - (void) _appendRolesForPseudoPrincipals: (NSMutableDictionary *) aclsDictionary
381                    withClassSecurityInfo: (SoClassSecurityInfo *) sInfo
382 {
383   NSArray *perms;
384
385   perms = [sInfo DAVPermissionsForRole: SoRole_Owner];
386   if ([perms count])
387     [aclsDictionary setObject: perms forKey: @":owner"];
388   perms = [sInfo DAVPermissionsForRole: SoRole_Authenticated];
389   if ([perms count])
390     [aclsDictionary setObject: perms forKey: @":authenticated"];
391   perms = [sInfo DAVPermissionsForRole: SoRole_Anonymous];
392   if ([perms count])
393     [aclsDictionary setObject: perms forKey: @":unauthenticated"];
394 }
395
396 - (SOGoDAVSet *) davAcl
397 {
398   NSArray *roles;
399   NSEnumerator *uids;
400   NSMutableDictionary *aclsDictionary;
401   NSString *currentUID;
402   SoClassSecurityInfo *sInfo;
403
404   aclsDictionary = [NSMutableDictionary dictionary];
405   uids = [[self aclUsers] objectEnumerator];
406   sInfo = [[self class] soClassSecurityInfo];
407
408   currentUID = [uids nextObject];
409   while (currentUID)
410     {
411       roles = [self aclsForUser: currentUID];
412       [aclsDictionary setObject: [sInfo DAVPermissionsForRoles: roles]
413                       forKey: currentUID];
414       currentUID = [uids nextObject];
415     }
416   [self _appendRolesForPseudoPrincipals: aclsDictionary
417         withClassSecurityInfo: sInfo];
418
419   return [SOGoDAVSet davSetWithArray:
420                        [self _davAcesFromAclsDictionary: aclsDictionary]
421                      ofValuesTaggedAs: @"D:ace"];
422 }
423
424 /* end of properties */
425
426 - (BOOL) doesRetainContainer
427 {
428   return YES;
429 }
430
431 - (id) init
432 {
433   if ((self = [super init]))
434     {
435       context = nil;
436       nameInContainer = nil;
437       container = nil;
438       owner = nil;
439     }
440
441   return self;
442 }
443
444 - (id) initWithName: (NSString *) _name
445         inContainer: (id) _container
446 {
447   if ((self = [self init]))
448     {
449       context = [[WOApplication application] context];
450       [context retain];
451       nameInContainer = [_name copy];
452       container = _container;
453       if ([self doesRetainContainer])
454         [_container retain];
455       owner = [self ownerInContext: context];
456       if (owner)
457         [owner retain];
458     }
459
460   return self;
461 }
462
463 - (void) dealloc
464 {
465   [context release];
466   [owner release];
467   if ([self doesRetainContainer])
468     [container release];
469   [nameInContainer release];
470   [super dealloc];
471 }
472
473 /* accessors */
474
475 - (NSString *) nameInContainer
476 {
477   return nameInContainer;
478 }
479
480 - (id) container
481 {
482   return container;
483 }
484
485 - (NSArray *) pathArrayToSOGoObject
486 {
487   NSMutableArray *realPathArray;
488   NSString *objectName;
489   NSArray *objectDescription;
490
491   realPathArray
492     = [NSMutableArray arrayWithArray: [self pathArrayToSoObject]];
493   if ([realPathArray count] > 2)
494     {
495       objectName = [realPathArray objectAtIndex: 2];
496       if ([objectName isKindOfClass: [NSString class]])
497         {
498           objectDescription = [objectName componentsSeparatedByString: @"_"];
499           if ([objectDescription count] > 1)
500             {
501               [realPathArray replaceObjectAtIndex: 0
502                              withObject: [objectDescription objectAtIndex: 0]];
503               [realPathArray replaceObjectAtIndex: 2
504                              withObject: [objectDescription objectAtIndex: 1]];
505             }
506         }
507     }
508
509   return realPathArray;
510 }
511
512 /* ownership */
513
514 - (void) setOwner: (NSString *) newOwner
515 {
516   ASSIGN (owner, newOwner);
517 }
518
519 - (NSString *) ownerInContext: (id) localContext
520 {
521   if (!owner)
522     owner = [container ownerInContext: context];
523
524   return owner;
525 }
526
527 /* hierarchy */
528
529 - (NSArray *) fetchSubfolders
530 {
531   NSMutableArray *ma;
532   NSArray  *names;
533   unsigned i, count;
534   
535   if ((names = [self toManyRelationshipKeys]) == nil)
536     return nil;
537   
538   count = [names count];
539   ma    = [NSMutableArray arrayWithCapacity:count + 1];
540   for (i = 0; i < count; i++) {
541     id folder;
542
543     folder = [self lookupName: [names objectAtIndex:i]
544                    inContext: nil 
545                    acquire: NO];
546     if (folder == nil)
547       continue;
548     if ([folder isKindOfClass:[NSException class]])
549       continue;
550     
551     [ma addObject:folder];
552   }
553   return ma;
554 }
555
556 - (id) lookupName: (NSString *) lookupName
557         inContext: (id) localContext
558           acquire: (BOOL) acquire
559 {
560   id obj;
561
562   obj = [[self soClass] lookupKey: lookupName inContext: localContext];
563   if (obj)
564     [obj bindToObject: self inContext: localContext];
565
566   return obj;
567 }
568
569 /* looking up shared objects */
570
571 - (SOGoUserFolder *) lookupUserFolder
572 {
573   if (![container respondsToSelector:_cmd])
574     return nil;
575   
576   return [container lookupUserFolder];
577 }
578
579 - (SOGoGroupsFolder *) lookupGroupsFolder
580 {
581   return [[self lookupUserFolder] lookupGroupsFolder];
582 }
583
584 - (void) sleep
585 {
586   if ([self doesRetainContainer])
587     [container release];
588   container = nil;
589 }
590
591 /* operations */
592
593 - (NSException *) delete
594 {
595   return [NSException exceptionWithHTTPStatus: 501 /* not implemented */
596                       reason: @"delete not yet implemented, sorry ..."];
597 }
598
599 /* KVC hacks */
600
601 - (id) valueForUndefinedKey: (NSString *) _key
602 {
603   return nil;
604 }
605
606 /* WebDAV */
607
608 - (NSString *) davDisplayName
609 {
610   return [self nameInContainer];
611 }
612
613 /* actions */
614
615 - (id) DELETEAction: (id) _ctx
616 {
617   NSException *error;
618
619   if ((error = [self delete]) != nil)
620     return error;
621   
622   /* Note: returning 'nil' breaks in SoObjectRequestHandler */
623   return [NSNumber numberWithBool:YES]; /* delete worked out ... */
624 }
625
626 - (NSString *) davContentType
627 {
628   return @"text/plain";
629 }
630
631 - (WOResponse *) _webDAVResponse: (WOContext *) localContext
632 {
633   WOResponse *response;
634   NSString *contentType;
635   id etag;
636
637   response = [localContext response];
638   contentType = [NSString stringWithFormat: @"%@; charset=utf8",
639                           [self davContentType]];
640   [response setHeader: contentType forKey: @"content-type"];
641   [response appendContentString: [self contentAsString]];
642   etag = [self davEntityTag];
643   if (etag)
644     [response setHeader: etag forKey: @"etag"];
645
646   return response;
647 }
648
649 - (id) GETAction: (id) localContext
650 {
651   // TODO: I guess this should really be done by SOPE (redirect to
652   //       default method)
653   WORequest *request;
654   NSString *uri;
655   NSException *error;
656   id value;
657
658   request = [localContext request];  
659   if ([request isSoWebDAVRequest])
660     {
661       if ([self respondsToSelector: @selector (contentAsString)])
662         {
663           error = [self matchesRequestConditionInContext: localContext];
664           if (error)
665             value = error;
666           else
667             value = [self _webDAVResponse: localContext];
668         }
669       else
670         value = [NSException exceptionWithHTTPStatus: 501 /* not implemented */
671                              reason: @"no WebDAV GET support?!"];
672     }
673   else
674     {
675       value = [localContext response];
676       uri = [[request uri] composeURLWithAction: @"view"
677                            parameters: [request formValues]
678                            andHash: NO];
679       [value setStatus: 302 /* moved */];
680       [value setHeader: uri forKey: @"location"];
681     }
682
683   return value;
684 }
685
686 /* etag support */
687
688 - (NSArray *)parseETagList:(NSString *)_c {
689   NSMutableArray *ma;
690   NSArray  *etags;
691   unsigned i, count;
692   
693   if ([_c length] == 0)
694     return nil;
695   if ([_c isEqualToString:@"*"])
696     return nil;
697   
698   etags = [_c componentsSeparatedByString:@","];
699   count = [etags count];
700   ma    = [NSMutableArray arrayWithCapacity:count];
701   for (i = 0; i < count; i++) {
702     NSString *etag;
703     
704     etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
705 #if 0 /* this is non-sense, right? */
706     if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
707       etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
708 #endif
709     
710     if (etag != nil) [ma addObject:etag];
711   }
712   return ma;
713 }
714
715 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
716   /* 
717      Only run the request if one of the etags matches the resource etag,
718      usually used to ensure consistent PUTs.
719   */
720   NSArray  *etags;
721   NSString *etag;
722   
723   if ([_c isEqualToString:@"*"])
724     /* to ensure that the resource exists! */
725     return nil;
726   
727   if ((etags = [self parseETagList:_c]) == nil)
728     return nil;
729   if ([etags count] == 0) /* no etags to check for? */
730     return nil;
731   
732   etag = [self davEntityTag];
733   if ([etag length] == 0) /* has no etag, ignore */
734     return nil;
735   
736   if ([etags containsObject:etag]) {
737     [self debugWithFormat:@"etag '%@' matches: %@", etag, 
738           [etags componentsJoinedByString:@","]];
739     return nil; /* one etag matches, so continue with request */
740   }
741
742   /* hack for Kontact 3.4 */
743   
744   if (kontactGroupDAV) {
745     WEClientCapabilities *cc;
746     
747     cc = [[(WOContext *)_ctx request] clientCapabilities];
748     if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
749       if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
750         [self logWithFormat:
751                 @"WARNING: applying Kontact 3.4 GroupDAV hack"
752                 @" - etag check is disabled!"
753                 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
754         return nil;
755       }
756     }
757   }
758   
759   // TODO: we might want to return the davEntityTag in the response
760   [self debugWithFormat:@"etag '%@' does not match: %@", etag, 
761         [etags componentsJoinedByString:@","]];
762   return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
763                       reason:@"Precondition Failed"];
764 }
765
766 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
767   /*
768     If one of the etags is still the same, we can ignore the request.
769     
770     Can be used for PUT to ensure that the object does not exist in the store
771     and for GET to retrieve the content only if if the etag changed.
772   */
773   
774   if (![_c isEqualToString:@"*"] && 
775       [[[_ctx request] method] isEqualToString:@"GET"]) {
776     NSString *etag;
777     NSArray  *etags;
778     
779     if ((etags = [self parseETagList:_c]) == nil)
780       return nil;
781     if ([etags count] == 0) /* no etags to check for? */
782       return nil;
783     
784     etag = [self davEntityTag];
785     if ([etag length] == 0) /* has no etag, ignore */
786       return nil;
787     
788     if ([etags containsObject:etag]) {
789       [self debugWithFormat:@"etag '%@' matches: %@", etag, 
790               [etags componentsJoinedByString:@","]];
791       /* one etag matches, so stop the request */
792       return [NSException exceptionWithHTTPStatus:304 /* Not Modified */
793                           reason:@"object was not modified"];
794     }
795     
796     return nil;
797   }
798   
799 #if 0
800   if ([_c isEqualToString:@"*"])
801     return nil;
802   
803   if ((a = [self parseETagList:_c]) == nil)
804     return nil;
805 #else
806   [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
807 #endif
808   return nil;
809 }
810
811 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
812   NSException *error;
813   WORequest *rq;
814   NSString  *c;
815   
816   if ((rq = [(WOContext *)_ctx request]) == nil)
817     return nil; /* be tolerant - no request, no condition */
818   
819   if ((c = [rq headerForKey:@"if-match"]) != nil) {
820     if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
821       return error;
822   }
823   if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
824     if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
825       return error;
826   }
827   
828   return nil;
829 }
830
831 /* acls */
832
833 /* roles required to obtain the "authorized subscriber" role */
834 - (NSArray *) subscriptionRoles
835 {
836   return [container subscriptionRoles];
837 }
838
839 - (NSArray *) aclUsers
840 {
841   [self subclassResponsibility: _cmd];
842
843   return nil;
844 }
845
846 - (NSArray *) aclsForUser: (NSString *) uid
847 {
848   [self subclassResponsibility: _cmd];
849
850   return nil;
851 }
852
853 - (void) setRoles: (NSArray *) roles
854           forUser: (NSString *) uid
855 {
856   [self subclassResponsibility: _cmd];
857 }
858
859 - (void) removeAclsForUsers: (NSArray *) users
860 {
861   [self subclassResponsibility: _cmd];
862 }
863
864 - (NSString *) defaultUserID
865 {
866   [self subclassResponsibility: _cmd];
867
868   return nil;
869 }
870
871 - (void) sendACLAdvisoryTemplate: (NSString *) template
872                           toUser: (NSString *) uid
873 {
874   NSString *language, *pageName;
875   SOGoUser *user;
876   SOGoACLAdvisory *page;
877   WOApplication *app;
878
879   user = [SOGoUser userWithLogin: uid roles: nil];
880   language = [user language];
881   pageName = [NSString stringWithFormat: @"SOGoACL%@%@Advisory",
882                        language, template];
883
884   app = [WOApplication application];
885   page = [app pageWithName: pageName inContext: context];
886   [page setACLObject: self];
887   [page setRecipientUID: uid];
888   [page send];
889 }
890
891 - (void) sendACLAdditionAdvisoryToUser: (NSString *) uid
892 {
893   return [self sendACLAdvisoryTemplate: @"Addition"
894                toUser: uid];
895 }
896
897 - (void) sendACLRemovalAdvisoryToUser: (NSString *) uid
898 {
899   return [self sendACLAdvisoryTemplate: @"Removal"
900                toUser: uid];
901 }
902
903 - (NSURL *) _urlPreferringParticle: (NSString *) expected
904                        overThisOne: (NSString *) possible
905 {
906   NSURL *serverURL, *url;
907   NSMutableArray *path;
908   NSString *baseURL, *urlMethod;
909
910   serverURL = [context serverURL];
911   baseURL = [[self baseURLInContext: context] stringByUnescapingURL];
912   path = [NSMutableArray arrayWithArray: [baseURL componentsSeparatedByString:
913                                                     @"/"]];
914   if ([baseURL hasPrefix: @"http"])
915     {
916       [path removeObjectAtIndex: 1];
917       [path removeObjectAtIndex: 0];
918       [path replaceObjectAtIndex: 0 withObject: @""];
919     }
920   urlMethod = [path objectAtIndex: 2];
921   if (![urlMethod isEqualToString: expected])
922     {
923       if ([urlMethod isEqualToString: possible])
924         [path replaceObjectAtIndex: 2 withObject: expected];
925       else
926         [path insertObject: expected atIndex: 2];
927     }
928
929   url = [[NSURL alloc] initWithScheme: [serverURL scheme]
930                        host: [serverURL host]
931                        path: [path componentsJoinedByString: @"/"]];
932   [url autorelease];
933
934   return url;
935 }
936
937 - (NSURL *) davURL
938 {
939   return [self _urlPreferringParticle: @"dav" overThisOne: @"so"];
940 }
941
942 - (NSURL *) soURL
943 {
944   return [self _urlPreferringParticle: @"so" overThisOne: @"dav"];
945 }
946
947 - (NSURL *) soURLToBaseContainerForUser: (NSString *) uid
948 {
949   NSURL *soURL, *baseSoURL;
950   NSArray *basePath;
951   NSMutableArray *newPath;
952
953   soURL = [self soURL];
954   basePath = [[soURL path] componentsSeparatedByString: @"/"];
955   newPath
956     = [NSMutableArray arrayWithArray:
957                         [basePath subarrayWithRange: NSMakeRange (0, 5)]];
958   [newPath replaceObjectAtIndex: 3 withObject: uid];
959
960   baseSoURL = [[NSURL alloc] initWithScheme: [soURL scheme]
961                              host: [soURL host]
962                              path: [newPath componentsJoinedByString: @"/"]];
963   [baseSoURL autorelease];
964
965   return baseSoURL;
966 }
967
968 - (NSURL *) soURLToBaseContainerForCurrentUser
969 {
970   NSString *currentLogin;
971
972   currentLogin = [[context activeUser] login];
973
974   return [self soURLToBaseContainerForUser: currentLogin];
975 }
976
977 - (NSString *) httpURLForAdvisoryToUser: (NSString *) uid
978 {
979   [self subclassResponsibility: _cmd];
980
981   return nil;
982 }
983
984 - (NSString *) resourceURLForAdvisoryToUser: (NSString *) uid
985 {
986   [self subclassResponsibility: _cmd];
987
988   return nil;
989 }
990
991 - (NSString *) labelForKey: (NSString *) key
992 {
993   NSString *userLanguage, *label;
994   NSArray *paths;
995   NSBundle *bundle;
996   NSDictionary *strings;
997
998   bundle = [NSBundle bundleForClass: [self class]];
999   if (!bundle)
1000     bundle = [NSBundle mainBundle];
1001
1002   userLanguage = [[context activeUser] language];
1003   paths = [bundle pathsForResourcesOfType: @"strings"
1004                   inDirectory: [NSString stringWithFormat: @"%@.lproj",
1005                                          userLanguage]
1006                   forLocalization: userLanguage];
1007   if ([paths count] > 0)
1008     {
1009       strings = [NSDictionary
1010                   dictionaryFromStringsFile: [paths objectAtIndex: 0]];
1011       label = [strings objectForKey: key];
1012       if (!label)
1013         label = key;
1014     }
1015   else
1016     label = key;
1017   
1018   return label;
1019 }
1020
1021 /* description */
1022
1023 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
1024   if (nameInContainer) 
1025     [_ms appendFormat:@" name=%@", nameInContainer];
1026   if (container)
1027     [_ms appendFormat:@" container=0x%08X/%@", 
1028          container, [container valueForKey:@"nameInContainer"]];
1029 }
1030
1031 - (NSString *)description {
1032   NSMutableString *ms;
1033
1034   ms = [NSMutableString stringWithCapacity:64];
1035   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1036   [self appendAttributesToDescription:ms];
1037   [ms appendString:@">"];
1038
1039   return ms;
1040 }
1041
1042 - (NSString *) loggingPrefix
1043 {
1044   return [NSString stringWithFormat:@"<0x%08X[%@]:%@>",
1045                    self, NSStringFromClass([self class]),
1046                    [self nameInContainer]];
1047 }
1048
1049 @end /* SOGoObject */