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