2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
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
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.
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
22 #if LIB_FOUNDATION_LIBRARY
23 #error SOGo will not work properly with libFoundation. \
24 Please use gnustep-base instead.
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>
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>
46 #import "SOGoPermissions.h"
48 #import "SOGoAuthenticator.h"
49 #import "SOGoUserFolder.h"
51 #import "SOGoDAVRendererTypes.h"
53 #import "NSArray+Utilities.h"
54 #import "NSString+Utilities.h"
56 #import "SOGoObject.h"
58 @interface SOGoObject(Content)
59 - (NSString *)contentAsString;
62 @interface SoClassSecurityInfo (SOGoAcls)
64 + (id) defaultWebDAVPermissionsMap;
66 - (NSArray *) allPermissions;
67 - (NSArray *) allDAVPermissions;
68 - (NSArray *) DAVPermissionsForRole: (NSString *) role;
69 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles;
73 @implementation SoClassSecurityInfo (SOGoAcls)
75 + (id) defaultWebDAVPermissionsMap
77 return [NSDictionary dictionaryWithObjectsAndKeys:
78 @"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,
88 - (NSArray *) allPermissions
90 return [defRoles allKeys];
93 - (NSArray *) allDAVPermissions
95 NSEnumerator *allPermissions;
96 NSMutableArray *davPermissions;
97 NSDictionary *davPermissionsMap;
98 NSString *sopePermission, *davPermission;
100 davPermissions = [NSMutableArray array];
102 davPermissionsMap = [[self class] defaultWebDAVPermissionsMap];
103 allPermissions = [[self allPermissions] objectEnumerator];
104 sopePermission = [allPermissions nextObject];
105 while (sopePermission)
107 davPermission = [davPermissionsMap objectForCaseInsensitiveKey: sopePermission];
108 if (davPermission && ![davPermissions containsObject: davPermission])
109 [davPermissions addObject: davPermission];
110 sopePermission = [allPermissions nextObject];
113 return davPermissions;
116 - (NSArray *) DAVPermissionsForRole: (NSString *) role
118 return [self DAVPermissionsForRoles: [NSArray arrayWithObject: role]];
121 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles
123 NSEnumerator *allPermissions;
124 NSMutableArray *davPermissions;
125 NSDictionary *davPermissionsMap;
126 NSString *sopePermission, *davPermission;
128 davPermissions = [NSMutableArray array];
130 davPermissionsMap = [[self class] defaultWebDAVPermissionsMap];
131 allPermissions = [[self allPermissions] objectEnumerator];
132 sopePermission = [allPermissions nextObject];
133 while (sopePermission)
135 if ([[defRoles objectForCaseInsensitiveKey: sopePermission]
136 firstObjectCommonWithArray: roles])
139 = [davPermissionsMap objectForCaseInsensitiveKey: sopePermission];
141 && ![davPermissions containsObject: davPermission])
142 [davPermissions addObject: davPermission];
144 sopePermission = [allPermissions nextObject];
147 return davPermissions;
152 @implementation SOGoObject
154 static BOOL kontactGroupDAV = YES;
162 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
165 [ud boolForKey:@"SOGoDisableKontact34GroupDAVHack"] ? NO : YES;
167 // SoClass security declarations
169 // require View permission to access the root (bound to authenticated ...)
170 [[self soClassSecurityInfo] declareObjectProtected: SoPerm_View];
172 // to allow public access to all contained objects (subkeys)
173 [[self soClassSecurityInfo] setDefaultAccess: @"allow"];
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];
182 + (void) _fillDictionary: (NSMutableDictionary *) dictionary
183 withDAVMethods: (NSString *) firstMethod, ...
186 NSString *aclMethodName;
187 NSString *methodName;
190 va_start (ap, firstMethod);
191 aclMethodName = firstMethod;
192 while (aclMethodName)
194 methodName = [aclMethodName davMethodToObjC];
195 methodSel = NSSelectorFromString (methodName);
196 if (methodSel && [self instancesRespondToSelector: methodSel])
197 [dictionary setObject: methodName
198 forKey: [NSString stringWithFormat: @"{DAV:}%@",
201 NSLog(@"************ method '%@' is still unimplemented!",
203 aclMethodName = va_arg (ap, NSString *);
209 + (NSDictionary *) defaultWebDAVAttributeMap
211 static NSMutableDictionary *map = nil;
215 map = [NSMutableDictionary
216 dictionaryWithDictionary: [super defaultWebDAVAttributeMap]];
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];
229 + (id) objectWithName: (NSString *)_name inContainer:(id)_container
233 object = [[self alloc] initWithName: _name inContainer: _container];
234 [object autorelease];
239 /* DAV ACL properties */
240 - (NSString *) davOwner
242 return [NSString stringWithFormat: @"%@users/%@",
243 [self rootURLInContext: context],
244 [self ownerInContext: nil]];
247 - (NSString *) davAclRestrictions
249 NSMutableString *restrictions;
251 restrictions = [NSMutableString string];
252 [restrictions appendString: @"<D:grant-only/>"];
253 [restrictions appendString: @"<D:no-invert/>"];
258 - (SOGoDAVSet *) davPrincipalCollectionSet
262 usersUrl = [NSString stringWithFormat: @"%@users",
263 [self rootURLInContext: context]];
265 return [SOGoDAVSet davSetWithArray: [NSArray arrayWithObject: usersUrl]
266 ofValuesTaggedAs: @"D:href"];
269 - (SOGoDAVSet *) davCurrentUserPrivilegeSet
271 SOGoAuthenticator *sAuth;
274 SoClassSecurityInfo *sInfo;
275 NSArray *davPermissions;
277 sAuth = [SOGoAuthenticator sharedSOGoAuthenticator];
278 user = [sAuth userInContext: context];
279 roles = [user rolesForObject: self inContext: context];
280 sInfo = [[self class] soClassSecurityInfo];
283 = [[sInfo DAVPermissionsForRoles: roles] stringsWithFormat: @"<D:%@/>"];
285 return [SOGoDAVSet davSetWithArray: davPermissions
286 ofValuesTaggedAs: @"D:privilege"];
289 - (SOGoDAVSet *) davSupportedPrivilegeSet
291 SoClassSecurityInfo *sInfo;
292 NSArray *allPermissions;
294 sInfo = [[self class] soClassSecurityInfo];
296 allPermissions = [[sInfo allDAVPermissions] stringsWithFormat: @"<D:%@/>"];
298 return [SOGoDAVSet davSetWithArray: allPermissions
299 ofValuesTaggedAs: @"D:privilege"];
302 - (NSArray *) _davAcesFromAclsDictionary: (NSDictionary *) aclsDictionary
306 NSMutableString *currentAce;
307 NSMutableArray *davAces;
308 NSString *currentKey, *principal;
309 SOGoDAVSet *privilegesDS;
311 davAces = [NSMutableArray array];
312 keys = [[aclsDictionary allKeys] objectEnumerator];
313 currentKey = [keys nextObject];
316 currentAce = [NSMutableString string];
317 if ([currentKey hasPrefix: @":"])
319 appendFormat: @"<D:principal><D:property><D:%@/></D:property></D:principal>",
320 [currentKey substringFromIndex: 1]];
323 principal = [NSString stringWithFormat: @"%@users/%@",
324 [self rootURLInContext: context],
327 appendFormat: @"<D:principal><D:href>%@</D:href></D:principal>",
331 privileges = [[aclsDictionary objectForKey: currentKey]
332 stringsWithFormat: @"<D:%@/>"];
333 privilegesDS = [SOGoDAVSet davSetWithArray: privileges
334 ofValuesTaggedAs: @"privilege"];
335 [currentAce appendString: [privilegesDS stringForTag: @"{DAV:}grant"
337 inContext: nil prefixes: nil]];
338 [davAces addObject: currentAce];
339 currentKey = [keys nextObject];
345 - (void) _appendRolesForPseudoPrincipals: (NSMutableDictionary *) aclsDictionary
346 withClassSecurityInfo: (SoClassSecurityInfo *) sInfo
350 perms = [sInfo DAVPermissionsForRole: SoRole_Owner];
352 [aclsDictionary setObject: perms forKey: @":owner"];
353 perms = [sInfo DAVPermissionsForRole: SoRole_Authenticated];
355 [aclsDictionary setObject: perms forKey: @":authenticated"];
356 perms = [sInfo DAVPermissionsForRole: SoRole_Anonymous];
358 [aclsDictionary setObject: perms forKey: @":unauthenticated"];
361 - (SOGoDAVSet *) davAcl
365 NSMutableDictionary *aclsDictionary;
366 NSString *currentUID;
367 SoClassSecurityInfo *sInfo;
369 aclsDictionary = [NSMutableDictionary dictionary];
370 uids = [[self aclUsers] objectEnumerator];
371 sInfo = [[self class] soClassSecurityInfo];
373 currentUID = [uids nextObject];
376 roles = [self aclsForUser: currentUID];
377 [aclsDictionary setObject: [sInfo DAVPermissionsForRoles: roles]
379 currentUID = [uids nextObject];
381 [self _appendRolesForPseudoPrincipals: aclsDictionary
382 withClassSecurityInfo: sInfo];
384 return [SOGoDAVSet davSetWithArray:
385 [self _davAcesFromAclsDictionary: aclsDictionary]
386 ofValuesTaggedAs: @"D:ace"];
389 /* end of properties */
391 - (BOOL) doesRetainContainer
396 - (id)initWithName:(NSString *)_name inContainer:(id)_container {
397 if ((self = [self init]))
399 context = [[WOApplication application] context];
401 nameInContainer = [_name copy];
403 [self doesRetainContainer] ? [_container retain] : _container;
412 if ((self = [super init]))
414 nameInContainer = nil;
423 [customOwner release];
424 if ([self doesRetainContainer])
426 [nameInContainer release];
432 - (NSString *)nameInContainer {
433 return nameInContainer;
441 - (void) setOwner: (NSString *) newOwner
443 ASSIGN (customOwner, newOwner);
446 - (NSString *)ownerInContext:(id)_ctx {
447 return ((customOwner)
449 : [[self container] ownerInContext:_ctx]);
454 - (NSArray *)fetchSubfolders {
459 if ((names = [self toManyRelationshipKeys]) == nil)
462 count = [names count];
463 ma = [NSMutableArray arrayWithCapacity:count + 1];
464 for (i = 0; i < count; i++) {
467 folder = [self lookupName: [names objectAtIndex:i]
472 if ([folder isKindOfClass:[NSException class]])
475 [ma addObject:folder];
480 /* looking up shared objects */
482 - (SOGoUserFolder *)lookupUserFolder {
483 if (![container respondsToSelector:_cmd])
486 return [container lookupUserFolder];
488 - (SOGoGroupsFolder *)lookupGroupsFolder {
489 return [[self lookupUserFolder] lookupGroupsFolder];
493 if ([self doesRetainContainer])
500 - (NSException *)delete {
501 return [NSException exceptionWithHTTPStatus:501 /* not implemented */
502 reason:@"delete not yet implemented, sorry ..."];
507 - (id)valueForUndefinedKey:(NSString *)_key {
513 - (NSString *)davDisplayName {
514 return [self nameInContainer];
519 - (id)DELETEAction:(id)_ctx {
522 if ((error = [self delete]) != nil)
525 /* Note: returning 'nil' breaks in SoObjectRequestHandler */
526 return [NSNumber numberWithBool:YES]; /* delete worked out ... */
529 - (id)GETAction:(id)_ctx {
530 // TODO: I guess this should really be done by SOPE (redirect to
536 r = [(WOContext *)_ctx response];
537 rq = [(WOContext *)_ctx request];
539 if ([rq isSoWebDAVRequest]) {
540 if ([self respondsToSelector:@selector(contentAsString)]) {
544 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
547 [r appendContentString:[self contentAsString]];
549 if ((etag = [self davEntityTag]) != nil)
550 [r setHeader:etag forKey:@"etag"];
555 return [NSException exceptionWithHTTPStatus:501 /* not implemented */
556 reason:@"no WebDAV GET support?!"];
560 [r setStatus:302 /* moved */];
561 [r setHeader: [uri composeURLWithAction: @"view"
562 parameters: [rq formValues]
571 - (NSArray *)parseETagList:(NSString *)_c {
576 if ([_c length] == 0)
578 if ([_c isEqualToString:@"*"])
581 etags = [_c componentsSeparatedByString:@","];
582 count = [etags count];
583 ma = [NSMutableArray arrayWithCapacity:count];
584 for (i = 0; i < count; i++) {
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)];
593 if (etag != nil) [ma addObject:etag];
598 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
600 Only run the request if one of the etags matches the resource etag,
601 usually used to ensure consistent PUTs.
606 if ([_c isEqualToString:@"*"])
607 /* to ensure that the resource exists! */
610 if ((etags = [self parseETagList:_c]) == nil)
612 if ([etags count] == 0) /* no etags to check for? */
615 etag = [self davEntityTag];
616 if ([etag length] == 0) /* has no etag, ignore */
619 if ([etags containsObject:etag]) {
620 [self debugWithFormat:@"etag '%@' matches: %@", etag,
621 [etags componentsJoinedByString:@","]];
622 return nil; /* one etag matches, so continue with request */
625 /* hack for Kontact 3.4 */
627 if (kontactGroupDAV) {
628 WEClientCapabilities *cc;
630 cc = [[(WOContext *)_ctx request] clientCapabilities];
631 if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
632 if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
634 @"WARNING: applying Kontact 3.4 GroupDAV hack"
635 @" - etag check is disabled!"
636 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
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"];
649 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
651 If one of the etags is still the same, we can ignore the request.
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.
657 if (![_c isEqualToString:@"*"] &&
658 [[[_ctx request] method] isEqualToString:@"GET"]) {
662 if ((etags = [self parseETagList:_c]) == nil)
664 if ([etags count] == 0) /* no etags to check for? */
667 etag = [self davEntityTag];
668 if ([etag length] == 0) /* has no etag, ignore */
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"];
683 if ([_c isEqualToString:@"*"])
686 if ((a = [self parseETagList:_c]) == nil)
689 [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
694 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
699 if ((rq = [(WOContext *)_ctx request]) == nil)
700 return nil; /* be tolerant - no request, no condition */
702 if ((c = [rq headerForKey:@"if-match"]) != nil) {
703 if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
706 if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
707 if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
716 - (NSArray *) aclUsers
718 [self subclassResponsibility: _cmd];
723 - (NSArray *) aclsForUser: (NSString *) uid
725 [self subclassResponsibility: _cmd];
730 - (void) setRoles: (NSArray *) roles
731 forUser: (NSString *) uid
733 [self subclassResponsibility: _cmd];
736 - (void) removeAclsForUsers: (NSArray *) users
738 [self subclassResponsibility: _cmd];
741 - (NSString *) defaultUserID
743 [self subclassResponsibility: _cmd];
748 - (void) sendACLAdvisoryTemplate: (NSString *) template
749 toUser: (NSString *) uid
751 NSString *language, *pageName;
753 SOGoACLAdvisory *page;
756 user = [SOGoUser userWithLogin: uid roles: nil];
757 language = [user language];
758 pageName = [NSString stringWithFormat: @"SOGoACL%@%@Advisory",
761 app = [WOApplication application];
762 page = [app pageWithName: pageName inContext: context];
763 [page setACLObject: self];
764 [page setRecipientUID: uid];
768 - (void) sendACLAdditionAdvisoryToUser: (NSString *) uid
770 return [self sendACLAdvisoryTemplate: @"Addition"
774 - (void) sendACLRemovalAdvisoryToUser: (NSString *) uid
776 return [self sendACLAdvisoryTemplate: @"Removal"
780 - (NSURL *) _urlPreferringParticle: (NSString *) expected
781 overThisOne: (NSString *) possible
783 NSURL *serverURL, *davURL;
784 NSMutableArray *path;
785 NSString *baseURL, *urlMethod;
787 serverURL = [context serverURL];
788 baseURL = [self baseURLInContext: context];
789 path = [NSMutableArray arrayWithArray: [baseURL componentsSeparatedByString:
791 urlMethod = [path objectAtIndex: 2];
792 if (![urlMethod isEqualToString: expected])
794 if ([urlMethod isEqualToString: possible])
795 [path replaceObjectAtIndex: 2 withObject: expected];
797 [path insertObject: expected atIndex: 2];
800 davURL = [[NSURL alloc] initWithScheme: [serverURL scheme]
801 host: [serverURL host]
802 path: [path componentsJoinedByString: @"/"]];
803 [davURL autorelease];
810 return [self _urlPreferringParticle: @"dav" overThisOne: @"so"];
815 return [self _urlPreferringParticle: @"so" overThisOne: @"dav"];
818 - (NSURL *) soURLToBaseContainerForUser: (NSString *) uid
820 NSURL *soURL, *baseSoURL;
822 NSMutableArray *newPath;
824 soURL = [self soURL];
825 basePath = [[soURL path] componentsSeparatedByString: @"/"];
827 = [NSMutableArray arrayWithArray:
828 [basePath subarrayWithRange: NSMakeRange (0, 5)]];
829 [newPath replaceObjectAtIndex: 3 withObject: uid];
831 baseSoURL = [[NSURL alloc] initWithScheme: [soURL scheme]
833 path: [newPath componentsJoinedByString: @"/"]];
834 [baseSoURL autorelease];
839 - (NSURL *) soURLToBaseContainerForCurrentUser
841 NSString *currentLogin;
843 currentLogin = [[context activeUser] login];
845 return [self soURLToBaseContainerForUser: currentLogin];
848 - (NSString *) httpURLForAdvisoryToUser: (NSString *) uid
850 [self subclassResponsibility: _cmd];
855 - (NSString *) resourceURLForAdvisoryToUser: (NSString *) uid
857 [self subclassResponsibility: _cmd];
864 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
866 [_ms appendFormat:@" name=%@", nameInContainer];
868 [_ms appendFormat:@" container=0x%08X/%@",
869 container, [container valueForKey:@"nameInContainer"]];
872 - (NSString *)description {
875 ms = [NSMutableString stringWithCapacity:64];
876 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
877 [self appendAttributesToDescription:ms];
878 [ms appendString:@">"];
883 @end /* SOGoObject */