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