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