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