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 <NGExtensions/NSString+misc.h>
44 #import <NGCards/NSDictionary+NGCards.h>
45 #import <UI/SOGoUI/SOGoACLAdvisory.h>
47 #import "SOGoPermissions.h"
49 #import "SOGoAuthenticator.h"
50 #import "SOGoUserFolder.h"
52 #import "SOGoDAVRendererTypes.h"
54 #import "NSArray+Utilities.h"
55 #import "NSString+Utilities.h"
57 #import "SOGoObject.h"
59 @interface SOGoObject(Content)
60 - (NSString *)contentAsString;
63 @interface SoClassSecurityInfo (SOGoAcls)
65 + (id) defaultWebDAVPermissionsMap;
67 - (NSArray *) allPermissions;
68 - (NSArray *) allDAVPermissions;
69 - (NSArray *) DAVPermissionsForRole: (NSString *) role;
70 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles;
74 @implementation SoClassSecurityInfo (SOGoAcls)
76 + (id) defaultWebDAVPermissionsMap
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,
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
398 if ((self = [super init]))
401 nameInContainer = nil;
409 - (id) initWithName: (NSString *) _name
410 inContainer: (id) _container
412 if ((self = [self init]))
414 context = [[WOApplication application] context];
416 nameInContainer = [_name copy];
417 container = _container;
418 if ([self doesRetainContainer])
420 owner = [self ownerInContext: context];
432 if ([self doesRetainContainer])
434 [nameInContainer release];
440 - (NSString *) nameInContainer
442 return nameInContainer;
452 - (void) setOwner: (NSString *) newOwner
454 ASSIGN (owner, newOwner);
457 - (NSString *) ownerInContext: (id) localContext
460 owner = [container ownerInContext: context];
467 - (NSArray *) fetchSubfolders
473 if ((names = [self toManyRelationshipKeys]) == nil)
476 count = [names count];
477 ma = [NSMutableArray arrayWithCapacity:count + 1];
478 for (i = 0; i < count; i++) {
481 folder = [self lookupName: [names objectAtIndex:i]
486 if ([folder isKindOfClass:[NSException class]])
489 [ma addObject:folder];
494 /* looking up shared objects */
496 - (SOGoUserFolder *)lookupUserFolder {
497 if (![container respondsToSelector:_cmd])
500 return [container lookupUserFolder];
503 - (SOGoGroupsFolder *)lookupGroupsFolder {
504 return [[self lookupUserFolder] lookupGroupsFolder];
509 if ([self doesRetainContainer])
516 - (NSException *) delete
518 return [NSException exceptionWithHTTPStatus: 501 /* not implemented */
519 reason: @"delete not yet implemented, sorry ..."];
524 - (id) valueForUndefinedKey: (NSString *) _key
531 - (NSString *) davDisplayName
533 return [self nameInContainer];
538 - (id) DELETEAction: (id) _ctx
542 if ((error = [self delete]) != nil)
545 /* Note: returning 'nil' breaks in SoObjectRequestHandler */
546 return [NSNumber numberWithBool:YES]; /* delete worked out ... */
549 - (NSString *) davContentType
551 return @"text/plain";
554 - (WOResponse *) _webDAVResponse: (WOContext *) localContext
556 WOResponse *response;
557 NSString *contentType;
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];
567 [response setHeader: etag forKey: @"etag"];
572 - (id) GETAction: (id) localContext
574 // TODO: I guess this should really be done by SOPE (redirect to
581 request = [localContext request];
582 if ([request isSoWebDAVRequest])
584 if ([self respondsToSelector: @selector (contentAsString)])
586 error = [self matchesRequestConditionInContext: localContext];
590 value = [self _webDAVResponse: localContext];
593 value = [NSException exceptionWithHTTPStatus: 501 /* not implemented */
594 reason: @"no WebDAV GET support?!"];
598 value = [localContext response];
599 uri = [[request uri] composeURLWithAction: @"view"
600 parameters: [request formValues]
602 [value setStatus: 302 /* moved */];
603 [value setHeader: uri forKey: @"location"];
611 - (NSArray *)parseETagList:(NSString *)_c {
616 if ([_c length] == 0)
618 if ([_c isEqualToString:@"*"])
621 etags = [_c componentsSeparatedByString:@","];
622 count = [etags count];
623 ma = [NSMutableArray arrayWithCapacity:count];
624 for (i = 0; i < count; i++) {
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)];
633 if (etag != nil) [ma addObject:etag];
638 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
640 Only run the request if one of the etags matches the resource etag,
641 usually used to ensure consistent PUTs.
646 if ([_c isEqualToString:@"*"])
647 /* to ensure that the resource exists! */
650 if ((etags = [self parseETagList:_c]) == nil)
652 if ([etags count] == 0) /* no etags to check for? */
655 etag = [self davEntityTag];
656 if ([etag length] == 0) /* has no etag, ignore */
659 if ([etags containsObject:etag]) {
660 [self debugWithFormat:@"etag '%@' matches: %@", etag,
661 [etags componentsJoinedByString:@","]];
662 return nil; /* one etag matches, so continue with request */
665 /* hack for Kontact 3.4 */
667 if (kontactGroupDAV) {
668 WEClientCapabilities *cc;
670 cc = [[(WOContext *)_ctx request] clientCapabilities];
671 if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
672 if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
674 @"WARNING: applying Kontact 3.4 GroupDAV hack"
675 @" - etag check is disabled!"
676 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
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"];
689 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
691 If one of the etags is still the same, we can ignore the request.
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.
697 if (![_c isEqualToString:@"*"] &&
698 [[[_ctx request] method] isEqualToString:@"GET"]) {
702 if ((etags = [self parseETagList:_c]) == nil)
704 if ([etags count] == 0) /* no etags to check for? */
707 etag = [self davEntityTag];
708 if ([etag length] == 0) /* has no etag, ignore */
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"];
723 if ([_c isEqualToString:@"*"])
726 if ((a = [self parseETagList:_c]) == nil)
729 [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
734 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
739 if ((rq = [(WOContext *)_ctx request]) == nil)
740 return nil; /* be tolerant - no request, no condition */
742 if ((c = [rq headerForKey:@"if-match"]) != nil) {
743 if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
746 if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
747 if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
756 - (NSArray *) aclUsers
758 [self subclassResponsibility: _cmd];
763 - (NSArray *) aclsForUser: (NSString *) uid
765 [self subclassResponsibility: _cmd];
770 - (void) setRoles: (NSArray *) roles
771 forUser: (NSString *) uid
773 [self subclassResponsibility: _cmd];
776 - (void) removeAclsForUsers: (NSArray *) users
778 [self subclassResponsibility: _cmd];
781 - (NSString *) defaultUserID
783 [self subclassResponsibility: _cmd];
788 - (void) sendACLAdvisoryTemplate: (NSString *) template
789 toUser: (NSString *) uid
791 NSString *language, *pageName;
793 SOGoACLAdvisory *page;
796 user = [SOGoUser userWithLogin: uid roles: nil];
797 language = [user language];
798 pageName = [NSString stringWithFormat: @"SOGoACL%@%@Advisory",
801 app = [WOApplication application];
802 page = [app pageWithName: pageName inContext: context];
803 [page setACLObject: self];
804 [page setRecipientUID: uid];
808 - (void) sendACLAdditionAdvisoryToUser: (NSString *) uid
810 return [self sendACLAdvisoryTemplate: @"Addition"
814 - (void) sendACLRemovalAdvisoryToUser: (NSString *) uid
816 return [self sendACLAdvisoryTemplate: @"Removal"
820 - (NSURL *) _urlPreferringParticle: (NSString *) expected
821 overThisOne: (NSString *) possible
823 NSURL *serverURL, *url;
824 NSMutableArray *path;
825 NSString *baseURL, *urlMethod;
827 serverURL = [context serverURL];
828 baseURL = [[self baseURLInContext: context] stringByUnescapingURL];
829 path = [NSMutableArray arrayWithArray: [baseURL componentsSeparatedByString:
831 if ([baseURL hasPrefix: @"http"])
833 [path removeObjectAtIndex: 1];
834 [path removeObjectAtIndex: 0];
835 [path replaceObjectAtIndex: 0 withObject: @""];
837 urlMethod = [path objectAtIndex: 2];
838 if (![urlMethod isEqualToString: expected])
840 if ([urlMethod isEqualToString: possible])
841 [path replaceObjectAtIndex: 2 withObject: expected];
843 [path insertObject: expected atIndex: 2];
846 url = [[NSURL alloc] initWithScheme: [serverURL scheme]
847 host: [serverURL host]
848 path: [path componentsJoinedByString: @"/"]];
856 return [self _urlPreferringParticle: @"dav" overThisOne: @"so"];
861 return [self _urlPreferringParticle: @"so" overThisOne: @"dav"];
864 - (NSURL *) soURLToBaseContainerForUser: (NSString *) uid
866 NSURL *soURL, *baseSoURL;
868 NSMutableArray *newPath;
870 soURL = [self soURL];
871 basePath = [[soURL path] componentsSeparatedByString: @"/"];
873 = [NSMutableArray arrayWithArray:
874 [basePath subarrayWithRange: NSMakeRange (0, 5)]];
875 [newPath replaceObjectAtIndex: 3 withObject: uid];
877 baseSoURL = [[NSURL alloc] initWithScheme: [soURL scheme]
879 path: [newPath componentsJoinedByString: @"/"]];
880 [baseSoURL autorelease];
885 - (NSURL *) soURLToBaseContainerForCurrentUser
887 NSString *currentLogin;
889 currentLogin = [[context activeUser] login];
891 return [self soURLToBaseContainerForUser: currentLogin];
894 - (NSString *) httpURLForAdvisoryToUser: (NSString *) uid
896 [self subclassResponsibility: _cmd];
901 - (NSString *) resourceURLForAdvisoryToUser: (NSString *) uid
903 [self subclassResponsibility: _cmd];
910 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
912 [_ms appendFormat:@" name=%@", nameInContainer];
914 [_ms appendFormat:@" container=0x%08X/%@",
915 container, [container valueForKey:@"nameInContainer"]];
918 - (NSString *)description {
921 ms = [NSMutableString stringWithCapacity:64];
922 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
923 [self appendAttributesToDescription:ms];
924 [ms appendString:@">"];
929 @end /* SOGoObject */