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.
29 #import <Foundation/NSBundle.h>
30 #import <Foundation/NSClassDescription.h>
31 #import <Foundation/NSString.h>
32 #import <Foundation/NSUserDefaults.h>
33 #import <Foundation/NSURL.h>
34 #import <Foundation/NSValue.h>
36 #import <NGObjWeb/SoClassSecurityInfo.h>
37 #import <NGObjWeb/SoObject+SoDAV.h>
38 #import <NGObjWeb/WEClientCapabilities.h>
39 #import <NGObjWeb/WOApplication.h>
40 #import <NGObjWeb/WOContext.h>
41 #import <NGObjWeb/WOResourceManager.h>
42 #import <NGObjWeb/WOResponse.h>
43 #import <NGObjWeb/WORequest.h>
44 #import <NGObjWeb/WORequest+So.h>
45 #import <NGObjWeb/NSException+HTTP.h>
46 #import <NGExtensions/NSObject+Logs.h>
47 #import <NGExtensions/NSString+misc.h>
48 #import <NGCards/NSDictionary+NGCards.h>
49 #import <UI/SOGoUI/SOGoACLAdvisory.h>
51 #import "SOGoPermissions.h"
53 #import "SOGoDAVAuthenticator.h"
54 #import "SOGoUserFolder.h"
56 #import "SOGoDAVRendererTypes.h"
58 #import "NSArray+Utilities.h"
59 #import "NSDictionary+Utilities.h"
60 #import "NSString+Utilities.h"
62 #import "SOGoObject.h"
64 @interface SOGoObject(Content)
65 - (NSString *)contentAsString;
68 @interface SoClassSecurityInfo (SOGoAcls)
70 + (id) defaultWebDAVPermissionsMap;
72 - (NSArray *) allPermissions;
73 - (NSArray *) allDAVPermissions;
74 - (NSArray *) DAVPermissionsForRole: (NSString *) role;
75 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles;
79 @implementation SoClassSecurityInfo (SOGoAcls)
81 + (id) defaultWebDAVPermissionsMap
83 return [NSDictionary dictionaryWithObjectsAndKeys:
84 @"read", SoPerm_AccessContentsInformation,
85 @"bind", SoPerm_AddDocumentsImagesAndFiles,
86 @"unbind", SoPerm_DeleteObjects,
87 @"write-acl", SoPerm_ChangePermissions,
88 @"write-content", SoPerm_ChangeImagesAndFiles,
89 @"read-free-busy", SOGoPerm_FreeBusyLookup,
93 - (NSArray *) allPermissions
95 return [defRoles allKeys];
98 - (NSArray *) allDAVPermissions
100 NSEnumerator *allPermissions;
101 NSMutableArray *davPermissions;
102 NSDictionary *davPermissionsMap;
103 NSString *sopePermission, *davPermission;
105 davPermissions = [NSMutableArray array];
107 davPermissionsMap = [[self class] defaultWebDAVPermissionsMap];
108 allPermissions = [[self allPermissions] objectEnumerator];
109 sopePermission = [allPermissions nextObject];
110 while (sopePermission)
112 davPermission = [davPermissionsMap objectForCaseInsensitiveKey: sopePermission];
113 if (davPermission && ![davPermissions containsObject: davPermission])
114 [davPermissions addObject: davPermission];
115 sopePermission = [allPermissions nextObject];
118 return davPermissions;
121 - (NSArray *) DAVPermissionsForRole: (NSString *) role
123 return [self DAVPermissionsForRoles: [NSArray arrayWithObject: role]];
126 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles
128 NSEnumerator *allPermissions;
129 NSMutableArray *davPermissions;
130 NSDictionary *davPermissionsMap;
131 NSString *sopePermission, *davPermission;
133 davPermissions = [NSMutableArray array];
135 davPermissionsMap = [[self class] defaultWebDAVPermissionsMap];
136 allPermissions = [[self allPermissions] objectEnumerator];
137 sopePermission = [allPermissions nextObject];
138 while (sopePermission)
140 if ([[defRoles objectForCaseInsensitiveKey: sopePermission]
141 firstObjectCommonWithArray: roles])
144 = [davPermissionsMap objectForCaseInsensitiveKey: sopePermission];
146 && ![davPermissions containsObject: davPermission])
147 [davPermissions addObject: davPermission];
149 sopePermission = [allPermissions nextObject];
152 return davPermissions;
157 @implementation SOGoObject
159 static BOOL kontactGroupDAV = YES;
167 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
170 [ud boolForKey:@"SOGoDisableKontact34GroupDAVHack"] ? NO : YES;
172 // SoClass security declarations
174 // require View permission to access the root (bound to authenticated ...)
175 // [[self soClassSecurityInfo] declareObjectProtected: SoPerm_View];
177 // to allow public access to all contained objects (subkeys)
178 // [[self soClassSecurityInfo] setDefaultAccess: @"allow"];
180 // /* require Authenticated role for View and WebDAV */
181 // [[self soClassSecurityInfo] declareRole: SoRole_Owner
182 // asDefaultForPermission: SoPerm_View];
183 // [[self soClassSecurityInfo] declareRole: SoRole_Owner
184 // asDefaultForPermission: SoPerm_WebDAVAccess];
187 + (NSString *) globallyUniqueObjectId
190 4C08AE1A-A808-11D8-AC5A-000393BBAFF6
191 SOGo-Web-28273-18283-288182
192 printf( "%x", *(int *) &f);
195 static int sequence = 0;
196 static float rndm = 0;
200 { /* break if we fork ;-) */
205 f = [[NSDate date] timeIntervalSince1970];
207 return [NSString stringWithFormat:@"%0X-%0X-%0X-%0X",
208 pid, (int) f, sequence++, random];
211 - (NSString *) globallyUniqueObjectId
213 return [[self class] globallyUniqueObjectId];
216 + (void) _fillDictionary: (NSMutableDictionary *) dictionary
217 withDAVMethods: (NSString *) firstMethod, ...
220 NSString *aclMethodName;
221 NSString *methodName;
224 va_start (ap, firstMethod);
225 aclMethodName = firstMethod;
226 while (aclMethodName)
228 methodName = [aclMethodName davMethodToObjC];
229 methodSel = NSSelectorFromString (methodName);
230 if (methodSel && [self instancesRespondToSelector: methodSel])
231 [dictionary setObject: methodName
232 forKey: [NSString stringWithFormat: @"{DAV:}%@",
235 NSLog(@"************ method '%@' is still unimplemented!",
237 aclMethodName = va_arg (ap, NSString *);
243 + (NSDictionary *) defaultWebDAVAttributeMap
245 static NSMutableDictionary *map = nil;
249 map = [NSMutableDictionary
250 dictionaryWithDictionary: [super defaultWebDAVAttributeMap]];
252 [self _fillDictionary: map
253 withDAVMethods: @"owner", @"group", @"supported-privilege-set",
254 @"current-user-privilege-set", @"acl", @"acl-restrictions",
255 @"inherited-acl-set", @"principal-collection-set", nil];
263 + (id) objectWithName: (NSString *)_name inContainer:(id)_container
267 object = [[self alloc] initWithName: _name inContainer: _container];
268 [object autorelease];
273 /* DAV ACL properties */
274 - (NSString *) davOwner
276 return [NSString stringWithFormat: @"%@users/%@",
277 [self rootURLInContext: context],
278 [self ownerInContext: nil]];
281 - (NSString *) davAclRestrictions
283 NSMutableString *restrictions;
285 restrictions = [NSMutableString string];
286 [restrictions appendString: @"<D:grant-only/>"];
287 [restrictions appendString: @"<D:no-invert/>"];
292 - (SOGoDAVSet *) davPrincipalCollectionSet
296 usersUrl = [NSString stringWithFormat: @"%@users",
297 [self rootURLInContext: context]];
299 return [SOGoDAVSet davSetWithArray: [NSArray arrayWithObject: usersUrl]
300 ofValuesTaggedAs: @"D:href"];
303 - (SOGoDAVSet *) davCurrentUserPrivilegeSet
305 SOGoDAVAuthenticator *sAuth;
308 SoClassSecurityInfo *sInfo;
309 NSArray *davPermissions;
311 sAuth = [SOGoDAVAuthenticator sharedSOGoDAVAuthenticator];
312 user = [sAuth userInContext: context];
313 roles = [user rolesForObject: self inContext: context];
314 sInfo = [[self class] soClassSecurityInfo];
317 = [[sInfo DAVPermissionsForRoles: roles] stringsWithFormat: @"<D:%@/>"];
319 return [SOGoDAVSet davSetWithArray: davPermissions
320 ofValuesTaggedAs: @"D:privilege"];
323 - (SOGoDAVSet *) davSupportedPrivilegeSet
325 SoClassSecurityInfo *sInfo;
326 NSArray *allPermissions;
328 sInfo = [[self class] soClassSecurityInfo];
330 allPermissions = [[sInfo allDAVPermissions] stringsWithFormat: @"<D:%@/>"];
332 return [SOGoDAVSet davSetWithArray: allPermissions
333 ofValuesTaggedAs: @"D:privilege"];
336 - (NSArray *) _davAcesFromAclsDictionary: (NSDictionary *) aclsDictionary
340 NSMutableString *currentAce;
341 NSMutableArray *davAces;
342 NSString *currentKey, *principal;
343 SOGoDAVSet *privilegesDS;
345 davAces = [NSMutableArray array];
346 keys = [[aclsDictionary allKeys] objectEnumerator];
347 currentKey = [keys nextObject];
350 currentAce = [NSMutableString string];
351 if ([currentKey hasPrefix: @":"])
353 appendFormat: @"<D:principal><D:property><D:%@/></D:property></D:principal>",
354 [currentKey substringFromIndex: 1]];
357 principal = [NSString stringWithFormat: @"%@users/%@",
358 [self rootURLInContext: context],
361 appendFormat: @"<D:principal><D:href>%@</D:href></D:principal>",
365 privileges = [[aclsDictionary objectForKey: currentKey]
366 stringsWithFormat: @"<D:%@/>"];
367 privilegesDS = [SOGoDAVSet davSetWithArray: privileges
368 ofValuesTaggedAs: @"privilege"];
369 [currentAce appendString: [privilegesDS stringForTag: @"{DAV:}grant"
371 inContext: nil prefixes: nil]];
372 [davAces addObject: currentAce];
373 currentKey = [keys nextObject];
379 - (void) _appendRolesForPseudoPrincipals: (NSMutableDictionary *) aclsDictionary
380 withClassSecurityInfo: (SoClassSecurityInfo *) sInfo
384 perms = [sInfo DAVPermissionsForRole: SoRole_Owner];
386 [aclsDictionary setObject: perms forKey: @":owner"];
387 perms = [sInfo DAVPermissionsForRole: SoRole_Authenticated];
389 [aclsDictionary setObject: perms forKey: @":authenticated"];
390 perms = [sInfo DAVPermissionsForRole: SoRole_Anonymous];
392 [aclsDictionary setObject: perms forKey: @":unauthenticated"];
395 - (SOGoDAVSet *) davAcl
399 NSMutableDictionary *aclsDictionary;
400 NSString *currentUID;
401 SoClassSecurityInfo *sInfo;
403 aclsDictionary = [NSMutableDictionary dictionary];
404 uids = [[self aclUsers] objectEnumerator];
405 sInfo = [[self class] soClassSecurityInfo];
407 currentUID = [uids nextObject];
410 roles = [self aclsForUser: currentUID];
411 [aclsDictionary setObject: [sInfo DAVPermissionsForRoles: roles]
413 currentUID = [uids nextObject];
415 [self _appendRolesForPseudoPrincipals: aclsDictionary
416 withClassSecurityInfo: sInfo];
418 return [SOGoDAVSet davSetWithArray:
419 [self _davAcesFromAclsDictionary: aclsDictionary]
420 ofValuesTaggedAs: @"D:ace"];
423 /* end of properties */
425 - (BOOL) doesRetainContainer
432 if ((self = [super init]))
435 nameInContainer = nil;
443 - (id) initWithName: (NSString *) _name
444 inContainer: (id) _container
446 if ((self = [self init]))
448 context = [[WOApplication application] context];
450 nameInContainer = [_name copy];
451 container = _container;
452 if ([self doesRetainContainer])
454 owner = [self ownerInContext: context];
466 if ([self doesRetainContainer])
468 [nameInContainer release];
474 - (NSString *) nameInContainer
476 return nameInContainer;
484 - (NSArray *) pathArrayToSOGoObject
486 NSMutableArray *realPathArray;
487 NSString *objectName;
488 NSArray *objectDescription;
491 = [NSMutableArray arrayWithArray: [self pathArrayToSoObject]];
492 if ([realPathArray count] > 2)
494 objectName = [realPathArray objectAtIndex: 2];
495 objectDescription = [objectName componentsSeparatedByString: @"_"];
496 if ([objectDescription count] > 1)
498 [realPathArray replaceObjectAtIndex: 0
499 withObject: [objectDescription objectAtIndex: 0]];
500 [realPathArray replaceObjectAtIndex: 2
501 withObject: [objectDescription objectAtIndex: 1]];
505 return realPathArray;
510 - (void) setOwner: (NSString *) newOwner
512 ASSIGN (owner, newOwner);
515 - (NSString *) ownerInContext: (id) localContext
518 owner = [container ownerInContext: context];
525 - (NSArray *) fetchSubfolders
531 if ((names = [self toManyRelationshipKeys]) == nil)
534 count = [names count];
535 ma = [NSMutableArray arrayWithCapacity:count + 1];
536 for (i = 0; i < count; i++) {
539 folder = [self lookupName: [names objectAtIndex:i]
544 if ([folder isKindOfClass:[NSException class]])
547 [ma addObject:folder];
552 /* looking up shared objects */
554 - (SOGoUserFolder *) lookupUserFolder
556 if (![container respondsToSelector:_cmd])
559 return [container lookupUserFolder];
562 - (SOGoGroupsFolder *) lookupGroupsFolder
564 return [[self lookupUserFolder] lookupGroupsFolder];
569 if ([self doesRetainContainer])
576 - (NSException *) delete
578 return [NSException exceptionWithHTTPStatus: 501 /* not implemented */
579 reason: @"delete not yet implemented, sorry ..."];
584 - (id) valueForUndefinedKey: (NSString *) _key
591 - (NSString *) davDisplayName
593 return [self nameInContainer];
598 - (id) DELETEAction: (id) _ctx
602 if ((error = [self delete]) != nil)
605 /* Note: returning 'nil' breaks in SoObjectRequestHandler */
606 return [NSNumber numberWithBool:YES]; /* delete worked out ... */
609 - (NSString *) davContentType
611 return @"text/plain";
614 - (WOResponse *) _webDAVResponse: (WOContext *) localContext
616 WOResponse *response;
617 NSString *contentType;
620 response = [localContext response];
621 contentType = [NSString stringWithFormat: @"%@; charset=utf8",
622 [self davContentType]];
623 [response setHeader: contentType forKey: @"content-type"];
624 [response appendContentString: [self contentAsString]];
625 etag = [self davEntityTag];
627 [response setHeader: etag forKey: @"etag"];
632 - (id) GETAction: (id) localContext
634 // TODO: I guess this should really be done by SOPE (redirect to
641 request = [localContext request];
642 if ([request isSoWebDAVRequest])
644 if ([self respondsToSelector: @selector (contentAsString)])
646 error = [self matchesRequestConditionInContext: localContext];
650 value = [self _webDAVResponse: localContext];
653 value = [NSException exceptionWithHTTPStatus: 501 /* not implemented */
654 reason: @"no WebDAV GET support?!"];
658 value = [localContext response];
659 uri = [[request uri] composeURLWithAction: @"view"
660 parameters: [request formValues]
662 [value setStatus: 302 /* moved */];
663 [value setHeader: uri forKey: @"location"];
671 - (NSArray *)parseETagList:(NSString *)_c {
676 if ([_c length] == 0)
678 if ([_c isEqualToString:@"*"])
681 etags = [_c componentsSeparatedByString:@","];
682 count = [etags count];
683 ma = [NSMutableArray arrayWithCapacity:count];
684 for (i = 0; i < count; i++) {
687 etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
688 #if 0 /* this is non-sense, right? */
689 if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
690 etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
693 if (etag != nil) [ma addObject:etag];
698 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
700 Only run the request if one of the etags matches the resource etag,
701 usually used to ensure consistent PUTs.
706 if ([_c isEqualToString:@"*"])
707 /* to ensure that the resource exists! */
710 if ((etags = [self parseETagList:_c]) == nil)
712 if ([etags count] == 0) /* no etags to check for? */
715 etag = [self davEntityTag];
716 if ([etag length] == 0) /* has no etag, ignore */
719 if ([etags containsObject:etag]) {
720 [self debugWithFormat:@"etag '%@' matches: %@", etag,
721 [etags componentsJoinedByString:@","]];
722 return nil; /* one etag matches, so continue with request */
725 /* hack for Kontact 3.4 */
727 if (kontactGroupDAV) {
728 WEClientCapabilities *cc;
730 cc = [[(WOContext *)_ctx request] clientCapabilities];
731 if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
732 if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
734 @"WARNING: applying Kontact 3.4 GroupDAV hack"
735 @" - etag check is disabled!"
736 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
742 // TODO: we might want to return the davEntityTag in the response
743 [self debugWithFormat:@"etag '%@' does not match: %@", etag,
744 [etags componentsJoinedByString:@","]];
745 return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
746 reason:@"Precondition Failed"];
749 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
751 If one of the etags is still the same, we can ignore the request.
753 Can be used for PUT to ensure that the object does not exist in the store
754 and for GET to retrieve the content only if if the etag changed.
757 if (![_c isEqualToString:@"*"] &&
758 [[[_ctx request] method] isEqualToString:@"GET"]) {
762 if ((etags = [self parseETagList:_c]) == nil)
764 if ([etags count] == 0) /* no etags to check for? */
767 etag = [self davEntityTag];
768 if ([etag length] == 0) /* has no etag, ignore */
771 if ([etags containsObject:etag]) {
772 [self debugWithFormat:@"etag '%@' matches: %@", etag,
773 [etags componentsJoinedByString:@","]];
774 /* one etag matches, so stop the request */
775 return [NSException exceptionWithHTTPStatus:304 /* Not Modified */
776 reason:@"object was not modified"];
783 if ([_c isEqualToString:@"*"])
786 if ((a = [self parseETagList:_c]) == nil)
789 [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
794 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
799 if ((rq = [(WOContext *)_ctx request]) == nil)
800 return nil; /* be tolerant - no request, no condition */
802 if ((c = [rq headerForKey:@"if-match"]) != nil) {
803 if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
806 if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
807 if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
816 - (NSArray *) aclUsers
818 [self subclassResponsibility: _cmd];
823 - (NSArray *) aclsForUser: (NSString *) uid
825 [self subclassResponsibility: _cmd];
830 - (void) setRoles: (NSArray *) roles
831 forUser: (NSString *) uid
833 [self subclassResponsibility: _cmd];
836 - (void) removeAclsForUsers: (NSArray *) users
838 [self subclassResponsibility: _cmd];
841 - (NSString *) defaultUserID
843 [self subclassResponsibility: _cmd];
848 - (void) sendACLAdvisoryTemplate: (NSString *) template
849 toUser: (NSString *) uid
851 NSString *language, *pageName;
853 SOGoACLAdvisory *page;
856 user = [SOGoUser userWithLogin: uid roles: nil];
857 language = [user language];
858 pageName = [NSString stringWithFormat: @"SOGoACL%@%@Advisory",
861 app = [WOApplication application];
862 page = [app pageWithName: pageName inContext: context];
863 [page setACLObject: self];
864 [page setRecipientUID: uid];
868 - (void) sendACLAdditionAdvisoryToUser: (NSString *) uid
870 return [self sendACLAdvisoryTemplate: @"Addition"
874 - (void) sendACLRemovalAdvisoryToUser: (NSString *) uid
876 return [self sendACLAdvisoryTemplate: @"Removal"
880 - (NSURL *) _urlPreferringParticle: (NSString *) expected
881 overThisOne: (NSString *) possible
883 NSURL *serverURL, *url;
884 NSMutableArray *path;
885 NSString *baseURL, *urlMethod;
887 serverURL = [context serverURL];
888 baseURL = [[self baseURLInContext: context] stringByUnescapingURL];
889 path = [NSMutableArray arrayWithArray: [baseURL componentsSeparatedByString:
891 if ([baseURL hasPrefix: @"http"])
893 [path removeObjectAtIndex: 1];
894 [path removeObjectAtIndex: 0];
895 [path replaceObjectAtIndex: 0 withObject: @""];
897 urlMethod = [path objectAtIndex: 2];
898 if (![urlMethod isEqualToString: expected])
900 if ([urlMethod isEqualToString: possible])
901 [path replaceObjectAtIndex: 2 withObject: expected];
903 [path insertObject: expected atIndex: 2];
906 url = [[NSURL alloc] initWithScheme: [serverURL scheme]
907 host: [serverURL host]
908 path: [path componentsJoinedByString: @"/"]];
916 return [self _urlPreferringParticle: @"dav" overThisOne: @"so"];
921 return [self _urlPreferringParticle: @"so" overThisOne: @"dav"];
924 - (NSURL *) soURLToBaseContainerForUser: (NSString *) uid
926 NSURL *soURL, *baseSoURL;
928 NSMutableArray *newPath;
930 soURL = [self soURL];
931 basePath = [[soURL path] componentsSeparatedByString: @"/"];
933 = [NSMutableArray arrayWithArray:
934 [basePath subarrayWithRange: NSMakeRange (0, 5)]];
935 [newPath replaceObjectAtIndex: 3 withObject: uid];
937 baseSoURL = [[NSURL alloc] initWithScheme: [soURL scheme]
939 path: [newPath componentsJoinedByString: @"/"]];
940 [baseSoURL autorelease];
945 - (NSURL *) soURLToBaseContainerForCurrentUser
947 NSString *currentLogin;
949 currentLogin = [[context activeUser] login];
951 return [self soURLToBaseContainerForUser: currentLogin];
954 - (NSString *) httpURLForAdvisoryToUser: (NSString *) uid
956 [self subclassResponsibility: _cmd];
961 - (NSString *) resourceURLForAdvisoryToUser: (NSString *) uid
963 [self subclassResponsibility: _cmd];
968 - (NSString *) labelForKey: (NSString *) key
970 NSString *userLanguage, *label;
973 NSDictionary *strings;
975 bundle = [NSBundle bundleForClass: [self class]];
977 bundle = [NSBundle mainBundle];
979 userLanguage = [[context activeUser] language];
980 paths = [bundle pathsForResourcesOfType: @"strings"
981 inDirectory: [NSString stringWithFormat: @"%@.lproj", userLanguage]
982 forLocalization: userLanguage];
983 if ([paths count] > 0)
985 strings = [NSDictionary dictionaryFromStringsFile: [paths objectAtIndex: 0]];
986 label = [strings objectForKey: key];
998 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
1000 [_ms appendFormat:@" name=%@", nameInContainer];
1002 [_ms appendFormat:@" container=0x%08X/%@",
1003 container, [container valueForKey:@"nameInContainer"]];
1006 - (NSString *)description {
1007 NSMutableString *ms;
1009 ms = [NSMutableString stringWithCapacity:64];
1010 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1011 [self appendAttributesToDescription:ms];
1012 [ms appendString:@">"];
1017 @end /* SOGoObject */