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 if ([objectName isKindOfClass: [NSString class]])
497 objectDescription = [objectName componentsSeparatedByString: @"_"];
498 if ([objectDescription count] > 1)
500 [realPathArray replaceObjectAtIndex: 0
501 withObject: [objectDescription objectAtIndex: 0]];
502 [realPathArray replaceObjectAtIndex: 2
503 withObject: [objectDescription objectAtIndex: 1]];
508 return realPathArray;
513 - (void) setOwner: (NSString *) newOwner
515 ASSIGN (owner, newOwner);
518 - (NSString *) ownerInContext: (id) localContext
521 owner = [container ownerInContext: context];
528 - (NSArray *) fetchSubfolders
534 if ((names = [self toManyRelationshipKeys]) == nil)
537 count = [names count];
538 ma = [NSMutableArray arrayWithCapacity:count + 1];
539 for (i = 0; i < count; i++) {
542 folder = [self lookupName: [names objectAtIndex:i]
547 if ([folder isKindOfClass:[NSException class]])
550 [ma addObject:folder];
555 /* looking up shared objects */
557 - (SOGoUserFolder *) lookupUserFolder
559 if (![container respondsToSelector:_cmd])
562 return [container lookupUserFolder];
565 - (SOGoGroupsFolder *) lookupGroupsFolder
567 return [[self lookupUserFolder] lookupGroupsFolder];
572 if ([self doesRetainContainer])
579 - (NSException *) delete
581 return [NSException exceptionWithHTTPStatus: 501 /* not implemented */
582 reason: @"delete not yet implemented, sorry ..."];
587 - (id) valueForUndefinedKey: (NSString *) _key
594 - (NSString *) davDisplayName
596 return [self nameInContainer];
601 - (id) DELETEAction: (id) _ctx
605 if ((error = [self delete]) != nil)
608 /* Note: returning 'nil' breaks in SoObjectRequestHandler */
609 return [NSNumber numberWithBool:YES]; /* delete worked out ... */
612 - (NSString *) davContentType
614 return @"text/plain";
617 - (WOResponse *) _webDAVResponse: (WOContext *) localContext
619 WOResponse *response;
620 NSString *contentType;
623 response = [localContext response];
624 contentType = [NSString stringWithFormat: @"%@; charset=utf8",
625 [self davContentType]];
626 [response setHeader: contentType forKey: @"content-type"];
627 [response appendContentString: [self contentAsString]];
628 etag = [self davEntityTag];
630 [response setHeader: etag forKey: @"etag"];
635 - (id) GETAction: (id) localContext
637 // TODO: I guess this should really be done by SOPE (redirect to
644 request = [localContext request];
645 if ([request isSoWebDAVRequest])
647 if ([self respondsToSelector: @selector (contentAsString)])
649 error = [self matchesRequestConditionInContext: localContext];
653 value = [self _webDAVResponse: localContext];
656 value = [NSException exceptionWithHTTPStatus: 501 /* not implemented */
657 reason: @"no WebDAV GET support?!"];
661 value = [localContext response];
662 uri = [[request uri] composeURLWithAction: @"view"
663 parameters: [request formValues]
665 [value setStatus: 302 /* moved */];
666 [value setHeader: uri forKey: @"location"];
674 - (NSArray *)parseETagList:(NSString *)_c {
679 if ([_c length] == 0)
681 if ([_c isEqualToString:@"*"])
684 etags = [_c componentsSeparatedByString:@","];
685 count = [etags count];
686 ma = [NSMutableArray arrayWithCapacity:count];
687 for (i = 0; i < count; i++) {
690 etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
691 #if 0 /* this is non-sense, right? */
692 if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
693 etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
696 if (etag != nil) [ma addObject:etag];
701 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
703 Only run the request if one of the etags matches the resource etag,
704 usually used to ensure consistent PUTs.
709 if ([_c isEqualToString:@"*"])
710 /* to ensure that the resource exists! */
713 if ((etags = [self parseETagList:_c]) == nil)
715 if ([etags count] == 0) /* no etags to check for? */
718 etag = [self davEntityTag];
719 if ([etag length] == 0) /* has no etag, ignore */
722 if ([etags containsObject:etag]) {
723 [self debugWithFormat:@"etag '%@' matches: %@", etag,
724 [etags componentsJoinedByString:@","]];
725 return nil; /* one etag matches, so continue with request */
728 /* hack for Kontact 3.4 */
730 if (kontactGroupDAV) {
731 WEClientCapabilities *cc;
733 cc = [[(WOContext *)_ctx request] clientCapabilities];
734 if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
735 if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
737 @"WARNING: applying Kontact 3.4 GroupDAV hack"
738 @" - etag check is disabled!"
739 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
745 // TODO: we might want to return the davEntityTag in the response
746 [self debugWithFormat:@"etag '%@' does not match: %@", etag,
747 [etags componentsJoinedByString:@","]];
748 return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
749 reason:@"Precondition Failed"];
752 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
754 If one of the etags is still the same, we can ignore the request.
756 Can be used for PUT to ensure that the object does not exist in the store
757 and for GET to retrieve the content only if if the etag changed.
760 if (![_c isEqualToString:@"*"] &&
761 [[[_ctx request] method] isEqualToString:@"GET"]) {
765 if ((etags = [self parseETagList:_c]) == nil)
767 if ([etags count] == 0) /* no etags to check for? */
770 etag = [self davEntityTag];
771 if ([etag length] == 0) /* has no etag, ignore */
774 if ([etags containsObject:etag]) {
775 [self debugWithFormat:@"etag '%@' matches: %@", etag,
776 [etags componentsJoinedByString:@","]];
777 /* one etag matches, so stop the request */
778 return [NSException exceptionWithHTTPStatus:304 /* Not Modified */
779 reason:@"object was not modified"];
786 if ([_c isEqualToString:@"*"])
789 if ((a = [self parseETagList:_c]) == nil)
792 [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
797 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
802 if ((rq = [(WOContext *)_ctx request]) == nil)
803 return nil; /* be tolerant - no request, no condition */
805 if ((c = [rq headerForKey:@"if-match"]) != nil) {
806 if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
809 if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
810 if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
819 - (NSArray *) aclUsers
821 [self subclassResponsibility: _cmd];
826 - (NSArray *) aclsForUser: (NSString *) uid
828 [self subclassResponsibility: _cmd];
833 - (void) setRoles: (NSArray *) roles
834 forUser: (NSString *) uid
836 [self subclassResponsibility: _cmd];
839 - (void) removeAclsForUsers: (NSArray *) users
841 [self subclassResponsibility: _cmd];
844 - (NSString *) defaultUserID
846 [self subclassResponsibility: _cmd];
851 - (void) sendACLAdvisoryTemplate: (NSString *) template
852 toUser: (NSString *) uid
854 NSString *language, *pageName;
856 SOGoACLAdvisory *page;
859 user = [SOGoUser userWithLogin: uid roles: nil];
860 language = [user language];
861 pageName = [NSString stringWithFormat: @"SOGoACL%@%@Advisory",
864 app = [WOApplication application];
865 page = [app pageWithName: pageName inContext: context];
866 [page setACLObject: self];
867 [page setRecipientUID: uid];
871 - (void) sendACLAdditionAdvisoryToUser: (NSString *) uid
873 return [self sendACLAdvisoryTemplate: @"Addition"
877 - (void) sendACLRemovalAdvisoryToUser: (NSString *) uid
879 return [self sendACLAdvisoryTemplate: @"Removal"
883 - (NSURL *) _urlPreferringParticle: (NSString *) expected
884 overThisOne: (NSString *) possible
886 NSURL *serverURL, *url;
887 NSMutableArray *path;
888 NSString *baseURL, *urlMethod;
890 serverURL = [context serverURL];
891 baseURL = [[self baseURLInContext: context] stringByUnescapingURL];
892 path = [NSMutableArray arrayWithArray: [baseURL componentsSeparatedByString:
894 if ([baseURL hasPrefix: @"http"])
896 [path removeObjectAtIndex: 1];
897 [path removeObjectAtIndex: 0];
898 [path replaceObjectAtIndex: 0 withObject: @""];
900 urlMethod = [path objectAtIndex: 2];
901 if (![urlMethod isEqualToString: expected])
903 if ([urlMethod isEqualToString: possible])
904 [path replaceObjectAtIndex: 2 withObject: expected];
906 [path insertObject: expected atIndex: 2];
909 url = [[NSURL alloc] initWithScheme: [serverURL scheme]
910 host: [serverURL host]
911 path: [path componentsJoinedByString: @"/"]];
919 return [self _urlPreferringParticle: @"dav" overThisOne: @"so"];
924 return [self _urlPreferringParticle: @"so" overThisOne: @"dav"];
927 - (NSURL *) soURLToBaseContainerForUser: (NSString *) uid
929 NSURL *soURL, *baseSoURL;
931 NSMutableArray *newPath;
933 soURL = [self soURL];
934 basePath = [[soURL path] componentsSeparatedByString: @"/"];
936 = [NSMutableArray arrayWithArray:
937 [basePath subarrayWithRange: NSMakeRange (0, 5)]];
938 [newPath replaceObjectAtIndex: 3 withObject: uid];
940 baseSoURL = [[NSURL alloc] initWithScheme: [soURL scheme]
942 path: [newPath componentsJoinedByString: @"/"]];
943 [baseSoURL autorelease];
948 - (NSURL *) soURLToBaseContainerForCurrentUser
950 NSString *currentLogin;
952 currentLogin = [[context activeUser] login];
954 return [self soURLToBaseContainerForUser: currentLogin];
957 - (NSString *) httpURLForAdvisoryToUser: (NSString *) uid
959 [self subclassResponsibility: _cmd];
964 - (NSString *) resourceURLForAdvisoryToUser: (NSString *) uid
966 [self subclassResponsibility: _cmd];
971 - (NSString *) labelForKey: (NSString *) key
973 NSString *userLanguage, *label;
976 NSDictionary *strings;
978 bundle = [NSBundle bundleForClass: [self class]];
980 bundle = [NSBundle mainBundle];
982 userLanguage = [[context activeUser] language];
983 paths = [bundle pathsForResourcesOfType: @"strings"
984 inDirectory: [NSString stringWithFormat: @"%@.lproj", userLanguage]
985 forLocalization: userLanguage];
986 if ([paths count] > 0)
988 strings = [NSDictionary dictionaryFromStringsFile: [paths objectAtIndex: 0]];
989 label = [strings objectForKey: key];
1001 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
1002 if (nameInContainer)
1003 [_ms appendFormat:@" name=%@", nameInContainer];
1005 [_ms appendFormat:@" container=0x%08X/%@",
1006 container, [container valueForKey:@"nameInContainer"]];
1009 - (NSString *)description {
1010 NSMutableString *ms;
1012 ms = [NSMutableString stringWithCapacity:64];
1013 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1014 [self appendAttributesToDescription:ms];
1015 [ms appendString:@">"];
1020 @end /* SOGoObject */