]> err.no Git - scalable-opengroupware.org/blob - SoObjects/SOGo/SOGoObject.m
8691beff0c08a5dee51f85190cd82831657ed28c
[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 <NGExtensions/NSString+misc.h>
44 #import <NGCards/NSDictionary+NGCards.h>
45 #import <UI/SOGoUI/SOGoACLAdvisory.h>
46
47 #import "SOGoPermissions.h"
48 #import "SOGoUser.h"
49 #import "SOGoAuthenticator.h"
50 #import "SOGoUserFolder.h"
51
52 #import "SOGoDAVRendererTypes.h"
53
54 #import "NSArray+Utilities.h"
55 #import "NSString+Utilities.h"
56
57 #import "SOGoObject.h"
58
59 @interface SOGoObject(Content)
60 - (NSString *)contentAsString;
61 @end
62
63 @interface SoClassSecurityInfo (SOGoAcls)
64
65 + (id) defaultWebDAVPermissionsMap;
66
67 - (NSArray *) allPermissions;
68 - (NSArray *) allDAVPermissions;
69 - (NSArray *) DAVPermissionsForRole: (NSString *) role;
70 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles;
71
72 @end
73
74 @implementation SoClassSecurityInfo (SOGoAcls)
75
76 + (id) defaultWebDAVPermissionsMap
77 {
78   return [NSDictionary dictionaryWithObjectsAndKeys:
79                          @"read", SoPerm_AccessContentsInformation,
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) init
397 {
398   if ((self = [super init]))
399     {
400       context = nil;
401       nameInContainer = nil;
402       container = nil;
403       owner = nil;
404     }
405
406   return self;
407 }
408
409 - (id) initWithName: (NSString *) _name
410         inContainer: (id) _container
411 {
412   if ((self = [self init]))
413     {
414       context = [[WOApplication application] context];
415       [context retain];
416       nameInContainer = [_name copy];
417       container = _container;
418       if ([self doesRetainContainer])
419         [_container retain];
420       owner = [self ownerInContext: context];
421       if (owner)
422         [owner retain];
423     }
424
425   return self;
426 }
427
428 - (void) dealloc
429 {
430   [context release];
431   [owner release];
432   if ([self doesRetainContainer])
433     [container release];
434   [nameInContainer release];
435   [super dealloc];
436 }
437
438 /* accessors */
439
440 - (NSString *) nameInContainer
441 {
442   return nameInContainer;
443 }
444
445 - (id) container
446 {
447   return container;
448 }
449
450 /* ownership */
451
452 - (void) setOwner: (NSString *) newOwner
453 {
454   ASSIGN (owner, newOwner);
455 }
456
457 - (NSString *) ownerInContext: (id) localContext
458 {
459   if (!owner)
460     owner = [container ownerInContext: context];
461
462   return owner;
463 }
464
465 /* hierarchy */
466
467 - (NSArray *) fetchSubfolders
468 {
469   NSMutableArray *ma;
470   NSArray  *names;
471   unsigned i, count;
472   
473   if ((names = [self toManyRelationshipKeys]) == nil)
474     return nil;
475   
476   count = [names count];
477   ma    = [NSMutableArray arrayWithCapacity:count + 1];
478   for (i = 0; i < count; i++) {
479     id folder;
480
481     folder = [self lookupName: [names objectAtIndex:i]
482                    inContext: nil 
483                    acquire: NO];
484     if (folder == nil)
485       continue;
486     if ([folder isKindOfClass:[NSException class]])
487       continue;
488     
489     [ma addObject:folder];
490   }
491   return ma;
492 }
493
494 /* looking up shared objects */
495
496 - (SOGoUserFolder *)lookupUserFolder {
497   if (![container respondsToSelector:_cmd])
498     return nil;
499   
500   return [container lookupUserFolder];
501 }
502
503 - (SOGoGroupsFolder *)lookupGroupsFolder {
504   return [[self lookupUserFolder] lookupGroupsFolder];
505 }
506
507 - (void) sleep
508 {
509   if ([self doesRetainContainer])
510     [container release];
511   container = nil;
512 }
513
514 /* operations */
515
516 - (NSException *) delete
517 {
518   return [NSException exceptionWithHTTPStatus: 501 /* not implemented */
519                       reason: @"delete not yet implemented, sorry ..."];
520 }
521
522 /* KVC hacks */
523
524 - (id) valueForUndefinedKey: (NSString *) _key
525 {
526   return nil;
527 }
528
529 /* WebDAV */
530
531 - (NSString *) davDisplayName
532 {
533   return [self nameInContainer];
534 }
535
536 /* actions */
537
538 - (id) DELETEAction: (id) _ctx
539 {
540   NSException *error;
541
542   if ((error = [self delete]) != nil)
543     return error;
544   
545   /* Note: returning 'nil' breaks in SoObjectRequestHandler */
546   return [NSNumber numberWithBool:YES]; /* delete worked out ... */
547 }
548
549 - (NSString *) davContentType
550 {
551   return @"text/plain";
552 }
553
554 - (WOResponse *) _webDAVResponse: (WOContext *) localContext
555 {
556   WOResponse *response;
557   NSString *contentType;
558   id etag;
559
560   response = [localContext response];
561   contentType = [NSString stringWithFormat: @"%@; charset=utf8",
562                           [self davContentType]];
563   [response setHeader: contentType forKey: @"content-type"];
564   [response appendContentString: [self contentAsString]];
565   etag = [self davEntityTag];
566   if (etag)
567     [response setHeader: etag forKey: @"etag"];
568
569   return response;
570 }
571
572 - (id) GETAction: (id) localContext
573 {
574   // TODO: I guess this should really be done by SOPE (redirect to
575   //       default method)
576   WORequest *request;
577   NSString *uri;
578   NSException *error;
579   id value;
580
581   request = [localContext request];  
582   if ([request isSoWebDAVRequest])
583     {
584       if ([self respondsToSelector: @selector (contentAsString)])
585         {
586           error = [self matchesRequestConditionInContext: localContext];
587           if (error)
588             value = error;
589           else
590             value = [self _webDAVResponse: localContext];
591         }
592       else
593         value = [NSException exceptionWithHTTPStatus: 501 /* not implemented */
594                              reason: @"no WebDAV GET support?!"];
595     }
596   else
597     {
598       value = [localContext response];
599       uri = [[request uri] composeURLWithAction: @"view"
600                            parameters: [request formValues]
601                            andHash: NO];
602       [value setStatus: 302 /* moved */];
603       [value setHeader: uri forKey: @"location"];
604     }
605
606   return value;
607 }
608
609 /* etag support */
610
611 - (NSArray *)parseETagList:(NSString *)_c {
612   NSMutableArray *ma;
613   NSArray  *etags;
614   unsigned i, count;
615   
616   if ([_c length] == 0)
617     return nil;
618   if ([_c isEqualToString:@"*"])
619     return nil;
620   
621   etags = [_c componentsSeparatedByString:@","];
622   count = [etags count];
623   ma    = [NSMutableArray arrayWithCapacity:count];
624   for (i = 0; i < count; i++) {
625     NSString *etag;
626     
627     etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
628 #if 0 /* this is non-sense, right? */
629     if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
630       etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
631 #endif
632     
633     if (etag != nil) [ma addObject:etag];
634   }
635   return ma;
636 }
637
638 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
639   /* 
640      Only run the request if one of the etags matches the resource etag,
641      usually used to ensure consistent PUTs.
642   */
643   NSArray  *etags;
644   NSString *etag;
645   
646   if ([_c isEqualToString:@"*"])
647     /* to ensure that the resource exists! */
648     return nil;
649   
650   if ((etags = [self parseETagList:_c]) == nil)
651     return nil;
652   if ([etags count] == 0) /* no etags to check for? */
653     return nil;
654   
655   etag = [self davEntityTag];
656   if ([etag length] == 0) /* has no etag, ignore */
657     return nil;
658   
659   if ([etags containsObject:etag]) {
660     [self debugWithFormat:@"etag '%@' matches: %@", etag, 
661           [etags componentsJoinedByString:@","]];
662     return nil; /* one etag matches, so continue with request */
663   }
664
665   /* hack for Kontact 3.4 */
666   
667   if (kontactGroupDAV) {
668     WEClientCapabilities *cc;
669     
670     cc = [[(WOContext *)_ctx request] clientCapabilities];
671     if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
672       if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
673         [self logWithFormat:
674                 @"WARNING: applying Kontact 3.4 GroupDAV hack"
675                 @" - etag check is disabled!"
676                 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
677         return nil;
678       }
679     }
680   }
681   
682   // TODO: we might want to return the davEntityTag in the response
683   [self debugWithFormat:@"etag '%@' does not match: %@", etag, 
684         [etags componentsJoinedByString:@","]];
685   return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
686                       reason:@"Precondition Failed"];
687 }
688
689 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
690   /*
691     If one of the etags is still the same, we can ignore the request.
692     
693     Can be used for PUT to ensure that the object does not exist in the store
694     and for GET to retrieve the content only if if the etag changed.
695   */
696   
697   if (![_c isEqualToString:@"*"] && 
698       [[[_ctx request] method] isEqualToString:@"GET"]) {
699     NSString *etag;
700     NSArray  *etags;
701     
702     if ((etags = [self parseETagList:_c]) == nil)
703       return nil;
704     if ([etags count] == 0) /* no etags to check for? */
705       return nil;
706     
707     etag = [self davEntityTag];
708     if ([etag length] == 0) /* has no etag, ignore */
709       return nil;
710     
711     if ([etags containsObject:etag]) {
712       [self debugWithFormat:@"etag '%@' matches: %@", etag, 
713               [etags componentsJoinedByString:@","]];
714       /* one etag matches, so stop the request */
715       return [NSException exceptionWithHTTPStatus:304 /* Not Modified */
716                           reason:@"object was not modified"];
717     }
718     
719     return nil;
720   }
721   
722 #if 0
723   if ([_c isEqualToString:@"*"])
724     return nil;
725   
726   if ((a = [self parseETagList:_c]) == nil)
727     return nil;
728 #else
729   [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
730 #endif
731   return nil;
732 }
733
734 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
735   NSException *error;
736   WORequest *rq;
737   NSString  *c;
738   
739   if ((rq = [(WOContext *)_ctx request]) == nil)
740     return nil; /* be tolerant - no request, no condition */
741   
742   if ((c = [rq headerForKey:@"if-match"]) != nil) {
743     if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
744       return error;
745   }
746   if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
747     if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
748       return error;
749   }
750   
751   return nil;
752 }
753
754 /* acls */
755
756 - (NSArray *) aclUsers
757 {
758   [self subclassResponsibility: _cmd];
759
760   return nil;
761 }
762
763 - (NSArray *) aclsForUser: (NSString *) uid
764 {
765   [self subclassResponsibility: _cmd];
766
767   return nil;
768 }
769
770 - (void) setRoles: (NSArray *) roles
771           forUser: (NSString *) uid
772 {
773   [self subclassResponsibility: _cmd];
774 }
775
776 - (void) removeAclsForUsers: (NSArray *) users
777 {
778   [self subclassResponsibility: _cmd];
779 }
780
781 - (NSString *) defaultUserID
782 {
783   [self subclassResponsibility: _cmd];
784
785   return nil;
786 }
787
788 - (void) sendACLAdvisoryTemplate: (NSString *) template
789                           toUser: (NSString *) uid
790 {
791   NSString *language, *pageName;
792   SOGoUser *user;
793   SOGoACLAdvisory *page;
794   WOApplication *app;
795
796   user = [SOGoUser userWithLogin: uid roles: nil];
797   language = [user language];
798   pageName = [NSString stringWithFormat: @"SOGoACL%@%@Advisory",
799                        language, template];
800
801   app = [WOApplication application];
802   page = [app pageWithName: pageName inContext: context];
803   [page setACLObject: self];
804   [page setRecipientUID: uid];
805   [page send];
806 }
807
808 - (void) sendACLAdditionAdvisoryToUser: (NSString *) uid
809 {
810   return [self sendACLAdvisoryTemplate: @"Addition"
811                toUser: uid];
812 }
813
814 - (void) sendACLRemovalAdvisoryToUser: (NSString *) uid
815 {
816   return [self sendACLAdvisoryTemplate: @"Removal"
817                toUser: uid];
818 }
819
820 - (NSURL *) _urlPreferringParticle: (NSString *) expected
821                        overThisOne: (NSString *) possible
822 {
823   NSURL *serverURL, *url;
824   NSMutableArray *path;
825   NSString *baseURL, *urlMethod;
826
827   serverURL = [context serverURL];
828   baseURL = [[self baseURLInContext: context] stringByUnescapingURL];
829   path = [NSMutableArray arrayWithArray: [baseURL componentsSeparatedByString:
830                                                     @"/"]];
831   if ([baseURL hasPrefix: @"http"])
832     {
833       [path removeObjectAtIndex: 1];
834       [path removeObjectAtIndex: 0];
835       [path replaceObjectAtIndex: 0 withObject: @""];
836     }
837   urlMethod = [path objectAtIndex: 2];
838   if (![urlMethod isEqualToString: expected])
839     {
840       if ([urlMethod isEqualToString: possible])
841         [path replaceObjectAtIndex: 2 withObject: expected];
842       else
843         [path insertObject: expected atIndex: 2];
844     }
845
846   url = [[NSURL alloc] initWithScheme: [serverURL scheme]
847                        host: [serverURL host]
848                        path: [path componentsJoinedByString: @"/"]];
849   [url autorelease];
850
851   return url;
852 }
853
854 - (NSURL *) davURL
855 {
856   return [self _urlPreferringParticle: @"dav" overThisOne: @"so"];
857 }
858
859 - (NSURL *) soURL
860 {
861   return [self _urlPreferringParticle: @"so" overThisOne: @"dav"];
862 }
863
864 - (NSURL *) soURLToBaseContainerForUser: (NSString *) uid
865 {
866   NSURL *soURL, *baseSoURL;
867   NSArray *basePath;
868   NSMutableArray *newPath;
869
870   soURL = [self soURL];
871   basePath = [[soURL path] componentsSeparatedByString: @"/"];
872   newPath
873     = [NSMutableArray arrayWithArray:
874                         [basePath subarrayWithRange: NSMakeRange (0, 5)]];
875   [newPath replaceObjectAtIndex: 3 withObject: uid];
876
877   baseSoURL = [[NSURL alloc] initWithScheme: [soURL scheme]
878                              host: [soURL host]
879                              path: [newPath componentsJoinedByString: @"/"]];
880   [baseSoURL autorelease];
881
882   return baseSoURL;
883 }
884
885 - (NSURL *) soURLToBaseContainerForCurrentUser
886 {
887   NSString *currentLogin;
888
889   currentLogin = [[context activeUser] login];
890
891   return [self soURLToBaseContainerForUser: currentLogin];
892 }
893
894 - (NSString *) httpURLForAdvisoryToUser: (NSString *) uid
895 {
896   [self subclassResponsibility: _cmd];
897
898   return nil;
899 }
900
901 - (NSString *) resourceURLForAdvisoryToUser: (NSString *) uid
902 {
903   [self subclassResponsibility: _cmd];
904
905   return nil;
906 }
907
908 /* description */
909
910 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
911   if (nameInContainer) 
912     [_ms appendFormat:@" name=%@", nameInContainer];
913   if (container)
914     [_ms appendFormat:@" container=0x%08X/%@", 
915          container, [container valueForKey:@"nameInContainer"]];
916 }
917
918 - (NSString *)description {
919   NSMutableString *ms;
920
921   ms = [NSMutableString stringWithCapacity:64];
922   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
923   [self appendAttributesToDescription:ms];
924   [ms appendString:@">"];
925
926   return ms;
927 }
928
929 @end /* SOGoObject */