]> err.no Git - scalable-opengroupware.org/blob - SoObjects/SOGo/SOGoObject.m
cf8f8843a75d02bad312b7673ab577a43f6c7774
[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 #import <NGObjWeb/WEClientCapabilities.h>
23 #import <NGObjWeb/SoObject+SoDAV.h>
24 #import <NGObjWeb/WOResponse.h>
25 #import <NGObjWeb/WOApplication.h>
26 #import <NGCards/NSDictionary+NGCards.h>
27 #import "common.h"
28
29 #import "NSArray+Utilities.h"
30 #import "NSString+Utilities.h"
31
32 #import "SOGoPermissions.h"
33 #import "SOGoUser.h"
34 #import "SOGoAclsFolder.h"
35 #import "SOGoAuthenticator.h"
36 #import "SOGoUserFolder.h"
37
38 #import "SOGoDAVRendererTypes.h"
39 #import "AgenorUserManager.h"
40
41 #import "SOGoObject.h"
42
43 @interface SOGoObject(Content)
44 - (NSString *)contentAsString;
45 @end
46
47 @interface SoClassSecurityInfo (SOGoAcls)
48
49 + (id) defaultWebDAVPermissionsMap;
50
51 - (NSArray *) allPermissions;
52 - (NSArray *) allDAVPermissions;
53 - (NSArray *) DAVPermissionsForRole: (NSString *) role;
54 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles;
55
56 @end
57
58 @implementation SoClassSecurityInfo (SOGoAcls)
59
60 + (id) defaultWebDAVPermissionsMap
61 {
62   return [NSDictionary dictionaryWithObjectsAndKeys:
63                          @"read", SoPerm_AccessContentsInformation,
64                        @"read", SoPerm_View, 
65                        @"bind", SoPerm_AddDocumentsImagesAndFiles,
66                        @"unbind", SoPerm_DeleteObjects,
67                        @"write-acl", SoPerm_ChangePermissions,
68                        @"write-content", SoPerm_ChangeImagesAndFiles,
69                        @"read-free-busy", SOGoPerm_FreeBusyLookup,
70                        NULL];
71 }
72
73 - (NSArray *) allPermissions
74 {
75   return [defRoles allKeys];
76 }
77
78 - (NSArray *) allDAVPermissions
79 {
80   NSEnumerator *allPermissions;
81   NSMutableArray *davPermissions;
82   NSDictionary *davPermissionsMap;
83   NSString *sopePermission, *davPermission;
84
85   davPermissions = [NSMutableArray array];
86
87   davPermissionsMap = [[self class] defaultWebDAVPermissionsMap];
88   allPermissions = [[self allPermissions] objectEnumerator];
89   sopePermission = [allPermissions nextObject];
90   while (sopePermission)
91     {
92       davPermission = [davPermissionsMap objectForCaseInsensitiveKey: sopePermission];
93       if (davPermission && ![davPermissions containsObject: davPermission])
94         [davPermissions addObject: davPermission];
95       sopePermission = [allPermissions nextObject];
96     }
97
98   return davPermissions;
99 }
100
101 - (NSArray *) DAVPermissionsForRole: (NSString *) role
102 {
103   return [self DAVPermissionsForRoles: [NSArray arrayWithObject: role]];
104 }
105
106 - (NSArray *) DAVPermissionsForRoles: (NSArray *) roles
107 {
108   NSEnumerator *allPermissions;
109   NSMutableArray *davPermissions;
110   NSDictionary *davPermissionsMap;
111   NSString *sopePermission, *davPermission;
112
113   davPermissions = [NSMutableArray array];
114
115   davPermissionsMap = [[self class] defaultWebDAVPermissionsMap];
116   allPermissions = [[self allPermissions] objectEnumerator];
117   sopePermission = [allPermissions nextObject];
118   while (sopePermission)
119     {
120       if ([[defRoles objectForCaseInsensitiveKey: sopePermission]
121             firstObjectCommonWithArray: roles])
122         {
123           davPermission
124             = [davPermissionsMap objectForCaseInsensitiveKey: sopePermission];
125           if (davPermission
126               && ![davPermissions containsObject: davPermission])
127             [davPermissions addObject: davPermission];
128         }
129       sopePermission = [allPermissions nextObject];
130     }
131
132   return davPermissions;
133 }
134
135 @end
136
137 @implementation SOGoObject
138
139 static BOOL kontactGroupDAV = YES;
140 static NSTimeZone *serverTimeZone = nil;
141
142 + (int)version {
143   return 0;
144 }
145
146 + (void) initialize
147 {
148   NSString *tzName;
149
150   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
151
152   kontactGroupDAV = 
153     [ud boolForKey:@"SOGoDisableKontact34GroupDAVHack"] ? NO : YES;
154
155   /* SoClass security declarations */
156   
157   /* require View permission to access the root (bound to authenticated ...) */
158   [[self soClassSecurityInfo] declareObjectProtected: SoPerm_View];
159
160   /* to allow public access to all contained objects (subkeys) */
161   [[self soClassSecurityInfo] setDefaultAccess: @"allow"];
162   
163   /* require Authenticated role for View and WebDAV */
164   [[self soClassSecurityInfo] declareRole: SoRole_Owner
165                               asDefaultForPermission: SoPerm_View];
166   [[self soClassSecurityInfo] declareRole: SoRole_Owner
167                               asDefaultForPermission: SoPerm_WebDAVAccess];
168
169   if (!serverTimeZone)
170     {
171       tzName = [ud stringForKey: @"SOGoServerTimeZone"];
172       if (!tzName)
173         tzName = @"Canada/Eastern";
174       serverTimeZone = [NSTimeZone timeZoneWithName: tzName];
175       [serverTimeZone retain];
176     }
177 }
178
179 + (void) _fillDictionary: (NSMutableDictionary *) dictionary
180           withDAVMethods: (NSString *) firstMethod, ...
181 {
182   va_list ap;
183   NSString *aclMethodName;
184   NSString *methodName;
185   SEL methodSel;
186
187   va_start (ap, firstMethod);
188   aclMethodName = firstMethod;
189   while (aclMethodName)
190     {
191       methodName = [aclMethodName davMethodToObjC];
192       methodSel = NSSelectorFromString (methodName);
193       if (methodSel && [self instancesRespondToSelector: methodSel])
194         [dictionary setObject: methodName
195                     forKey: [NSString stringWithFormat: @"{DAV:}%@",
196                                       aclMethodName]];
197       else
198         NSLog(@"************ method '%@' is still unimplemented!",
199               methodName);
200       aclMethodName = va_arg (ap, NSString *);
201     }
202
203   va_end (ap);
204 }
205
206 + (NSDictionary *) defaultWebDAVAttributeMap
207 {
208   static NSMutableDictionary *map = nil;
209
210   if (!map)
211     {
212       map = [NSMutableDictionary
213               dictionaryWithDictionary: [super defaultWebDAVAttributeMap]];
214       [map retain];
215       [self _fillDictionary: map
216             withDAVMethods: @"owner", @"group", @"supported-privilege-set",
217             @"current-user-privilege-set", @"acl", @"acl-restrictions",
218             @"inherited-acl-set", @"principal-collection-set", nil];
219     }
220
221   return map;
222 }
223
224 /* containment */
225
226 + (id) objectWithName: (NSString *)_name inContainer:(id)_container
227 {
228   SOGoObject *object;
229
230   object = [[self alloc] initWithName: _name inContainer: _container];
231   [object autorelease];
232
233   return object;
234 }
235
236 /* DAV ACL properties */
237 - (NSString *) _principalForUser: (NSString *) user
238 {
239   WOContext *context;
240
241   context = [[WOApplication application] context];
242
243   return [NSString stringWithFormat: @"%@users/%@",
244                    [self rootURLInContext: context],
245                    user];
246 }
247
248 - (NSString *) davOwner
249 {
250   return [self _principalForUser: [self ownerInContext: nil]];
251 }
252
253 - (NSString *) davAclRestrictions
254 {
255   NSMutableString *restrictions;
256
257   restrictions = [NSMutableString string];
258   [restrictions appendString: @"<D:grant-only/>"];
259   [restrictions appendString: @"<D:no-invert/>"];
260
261   return restrictions;
262 }
263
264 - (SOGoDAVSet *) davPrincipalCollectionSet
265 {
266   NSString *usersUrl;
267   WOContext *context;
268
269   context = [[WOApplication application] context];
270   usersUrl = [NSString stringWithFormat: @"%@users",
271                        [self rootURLInContext: context]];
272
273   return [SOGoDAVSet davSetWithArray: [NSArray arrayWithObject: usersUrl]
274                      ofValuesTaggedAs: @"D:href"];
275 }
276
277 - (SOGoDAVSet *) davCurrentUserPrivilegeSet
278 {
279   SOGoAuthenticator *sAuth;
280   SoUser *user;
281   NSArray *roles;
282   WOContext *context;
283   SoClassSecurityInfo *sInfo;
284   NSArray *davPermissions;
285
286   sAuth = [SOGoAuthenticator sharedSOGoAuthenticator];
287   context = [[WOApplication application] context];
288   user = [sAuth userInContext: context];
289   roles = [user rolesForObject: self inContext: context];
290   sInfo = [[self class] soClassSecurityInfo];
291
292   davPermissions
293     = [[sInfo DAVPermissionsForRoles: roles] stringsWithFormat: @"<D:%@/>"];
294
295   return [SOGoDAVSet davSetWithArray: davPermissions
296                      ofValuesTaggedAs: @"D:privilege"];
297 }
298
299 - (SOGoDAVSet *) davSupportedPrivilegeSet
300 {
301   SoClassSecurityInfo *sInfo;
302   NSArray *allPermissions;
303
304   sInfo = [[self class] soClassSecurityInfo];
305
306   allPermissions = [[sInfo allDAVPermissions] stringsWithFormat: @"<D:%@/>"];
307
308   return [SOGoDAVSet davSetWithArray: allPermissions
309                      ofValuesTaggedAs: @"D:privilege"];
310 }
311
312 - (NSArray *) _davAcesFromAclsDictionary: (NSDictionary *) aclsDictionary
313 {
314   NSEnumerator *keys;
315   NSArray *privileges;
316   NSMutableString *currentAce;
317   NSMutableArray *davAces;
318   NSString *currentKey;
319   SOGoDAVSet *privilegesDS;
320
321   davAces = [NSMutableArray array];
322   keys = [[aclsDictionary allKeys] objectEnumerator];
323   currentKey = [keys nextObject];
324   while (currentKey)
325     {
326       currentAce = [NSMutableString string];
327       if ([currentKey hasPrefix: @":"])
328         [currentAce
329           appendFormat: @"<D:principal><D:property><D:%@/></D:property></D:principal>",
330           [currentKey substringFromIndex: 1]];
331       else
332         [currentAce
333           appendFormat: @"<D:principal><D:href>%@</D:href></D:principal>",
334           [self _principalForUser: currentKey]];
335       privileges = [[aclsDictionary objectForKey: currentKey]
336                      stringsWithFormat: @"<D:%@/>"];
337       privilegesDS = [SOGoDAVSet davSetWithArray: privileges
338                                  ofValuesTaggedAs: @"privilege"];
339       [currentAce appendString: [privilegesDS stringForTag: @"{DAV:}grant"
340                                               rawName: @"grant"
341                                               inContext: nil prefixes: nil]];
342       [davAces addObject: currentAce];
343       currentKey = [keys nextObject];
344     }
345
346   return davAces;
347 }
348
349 - (void) _appendRolesForPseudoPrincipals: (NSMutableDictionary *) aclsDictionary
350                    withClassSecurityInfo: (SoClassSecurityInfo *) sInfo
351 {
352   NSArray *perms;
353
354   perms = [sInfo DAVPermissionsForRole: SoRole_Owner];
355   if ([perms count])
356     [aclsDictionary setObject: perms forKey: @":owner"];
357   perms = [sInfo DAVPermissionsForRole: SoRole_Authenticated];
358   if ([perms count])
359     [aclsDictionary setObject: perms forKey: @":authenticated"];
360   perms = [sInfo DAVPermissionsForRole: SoRole_Anonymous];
361   if ([perms count])
362     [aclsDictionary setObject: perms forKey: @":unauthenticated"];
363 }
364
365 - (SOGoDAVSet *) davAcl
366 {
367   NSArray *role;
368   NSEnumerator *acls;
369   NSMutableDictionary *aclsDictionary;
370   NSDictionary *currentAcl;
371   SoClassSecurityInfo *sInfo;
372
373   acls = [[[SOGoAclsFolder aclsFolder] aclsForObject: self] objectEnumerator];
374   aclsDictionary = [NSMutableDictionary dictionary];
375   sInfo = [[self class] soClassSecurityInfo];
376
377   currentAcl = [acls nextObject];
378   while (currentAcl)
379     {
380       role = [NSArray arrayWithObject: [currentAcl objectForKey: @"role"]];
381       [aclsDictionary setObject: [sInfo DAVPermissionsForRoles: role]
382                       forKey: [currentAcl objectForKey: @"uid"]];
383       currentAcl = [acls nextObject];
384     }
385   [self _appendRolesForPseudoPrincipals: aclsDictionary
386         withClassSecurityInfo: sInfo];
387
388   return [SOGoDAVSet davSetWithArray:
389                        [self _davAcesFromAclsDictionary: aclsDictionary]
390                      ofValuesTaggedAs: @"D:ace"];
391 }
392
393 /* end of properties */
394
395 - (BOOL)doesRetainContainer {
396   return YES;
397 }
398
399 - (id)initWithName:(NSString *)_name inContainer:(id)_container {
400   if ((self = [super init])) {
401     nameInContainer = [_name copy];
402     container = 
403       [self doesRetainContainer] ? [_container retain] : _container;
404     userTimeZone = nil;
405     customOwner = nil;
406   }
407   return self;
408 }
409
410 - (id)init {
411   return [self initWithName:nil inContainer:nil];
412 }
413
414 - (void)dealloc {
415   if (customOwner)
416     [customOwner release];
417   if ([self doesRetainContainer])
418     [container release];
419   if (userTimeZone)
420     [userTimeZone release];
421   [nameInContainer release];
422   [super dealloc];
423 }
424
425 /* accessors */
426
427 - (NSString *)nameInContainer {
428   return nameInContainer;
429 }
430 - (id)container {
431   return container;
432 }
433
434 /* ownership */
435
436 - (void) setOwner: (NSString *) newOwner
437 {
438   ASSIGN (customOwner, newOwner);
439 }
440
441 - (NSString *)ownerInContext:(id)_ctx {
442   return ((customOwner)
443           ? customOwner
444           : [[self container] ownerInContext:_ctx]);
445 }
446
447 /* hierarchy */
448
449 - (NSArray *)fetchSubfolders {
450   NSMutableArray *ma;
451   NSArray  *names;
452   unsigned i, count;
453   
454   if ((names = [self toManyRelationshipKeys]) == nil)
455     return nil;
456   
457   count = [names count];
458   ma    = [NSMutableArray arrayWithCapacity:count + 1];
459   for (i = 0; i < count; i++) {
460     id folder;
461
462     folder = [self lookupName: [names objectAtIndex:i]
463                    inContext: nil 
464                    acquire: NO];
465     if (folder == nil)
466       continue;
467     if ([folder isKindOfClass:[NSException class]])
468       continue;
469     
470     [ma addObject:folder];
471   }
472   return ma;
473 }
474
475 /* looking up shared objects */
476
477 - (SOGoUserFolder *)lookupUserFolder {
478   if (![container respondsToSelector:_cmd])
479     return nil;
480   
481   return [container lookupUserFolder];
482 }
483 - (SOGoGroupsFolder *)lookupGroupsFolder {
484   return [[self lookupUserFolder] lookupGroupsFolder];
485 }
486
487 - (void)sleep {
488   if ([self doesRetainContainer])
489     [container release];
490   container = nil;
491 }
492
493 /* operations */
494
495 - (NSException *)delete {
496   return [NSException exceptionWithHTTPStatus:501 /* not implemented */
497                       reason:@"delete not yet implemented, sorry ..."];
498 }
499
500 /* KVC hacks */
501
502 - (id)valueForUndefinedKey:(NSString *)_key {
503   return nil;
504 }
505
506 /* WebDAV */
507
508 - (NSString *)davDisplayName {
509   return [self nameInContainer];
510 }
511
512 /* actions */
513
514 - (id)DELETEAction:(id)_ctx {
515   NSException *error;
516
517   if ((error = [self delete]) != nil)
518     return error;
519   
520   /* Note: returning 'nil' breaks in SoObjectRequestHandler */
521   return [NSNumber numberWithBool:YES]; /* delete worked out ... */
522 }
523
524 - (id)GETAction:(id)_ctx {
525   // TODO: I guess this should really be done by SOPE (redirect to
526   //       default method)
527   WORequest  *rq;
528   WOResponse *r;
529   NSString *uri;
530   
531   r  = [(WOContext *)_ctx response];
532   rq = [(WOContext *)_ctx request];
533   
534   if ([rq isSoWebDAVRequest]) {
535     if ([self respondsToSelector:@selector(contentAsString)]) {
536       NSException *error;
537       id etag;
538       
539       if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
540         return error;
541       
542       [r appendContentString:[self contentAsString]];
543       
544       if ((etag = [self davEntityTag]) != nil)
545         [r setHeader:etag forKey:@"etag"];
546
547       return r;
548     }
549     
550     return [NSException exceptionWithHTTPStatus:501 /* not implemented */
551                         reason:@"no WebDAV GET support?!"];
552   }
553   
554   uri = [rq uri];
555   [r setStatus:302 /* moved */];
556   [r setHeader: [uri composeURLWithAction: @"view"
557                      parameters: [rq formValues]
558                      andHash: NO]
559      forKey:@"location"];
560
561   return r;
562 }
563
564 /* etag support */
565
566 - (NSArray *)parseETagList:(NSString *)_c {
567   NSMutableArray *ma;
568   NSArray  *etags;
569   unsigned i, count;
570   
571   if ([_c length] == 0)
572     return nil;
573   if ([_c isEqualToString:@"*"])
574     return nil;
575   
576   etags = [_c componentsSeparatedByString:@","];
577   count = [etags count];
578   ma    = [NSMutableArray arrayWithCapacity:count];
579   for (i = 0; i < count; i++) {
580     NSString *etag;
581     
582     etag = [[etags objectAtIndex:i] stringByTrimmingSpaces];
583 #if 0 /* this is non-sense, right? */
584     if ([etag hasPrefix:@"\""] && [etag hasSuffix:@"\""])
585       etag = [etag substringWithRange:NSMakeRange(1, [etag length] - 2)];
586 #endif
587     
588     if (etag != nil) [ma addObject:etag];
589   }
590   return ma;
591 }
592
593 - (NSException *)checkIfMatchCondition:(NSString *)_c inContext:(id)_ctx {
594   /* 
595      Only run the request if one of the etags matches the resource etag,
596      usually used to ensure consistent PUTs.
597   */
598   NSArray  *etags;
599   NSString *etag;
600   
601   if ([_c isEqualToString:@"*"])
602     /* to ensure that the resource exists! */
603     return nil;
604   
605   if ((etags = [self parseETagList:_c]) == nil)
606     return nil;
607   if ([etags count] == 0) /* no etags to check for? */
608     return nil;
609   
610   etag = [self davEntityTag];
611   if ([etag length] == 0) /* has no etag, ignore */
612     return nil;
613   
614   if ([etags containsObject:etag]) {
615     [self debugWithFormat:@"etag '%@' matches: %@", etag, 
616           [etags componentsJoinedByString:@","]];
617     return nil; /* one etag matches, so continue with request */
618   }
619
620   /* hack for Kontact 3.4 */
621   
622   if (kontactGroupDAV) {
623     WEClientCapabilities *cc;
624     
625     cc = [[(WOContext *)_ctx request] clientCapabilities];
626     if ([[cc userAgentType] isEqualToString:@"Konqueror"]) {
627       if ([cc majorVersion] == 3 && [cc minorVersion] == 4) {
628         [self logWithFormat:
629                 @"WARNING: applying Kontact 3.4 GroupDAV hack"
630                 @" - etag check is disabled!"
631                 @" (can be enabled using 'ZSDisableKontact34GroupDAVHack')"];
632         return nil;
633       }
634     }
635   }
636   
637   // TODO: we might want to return the davEntityTag in the response
638   [self debugWithFormat:@"etag '%@' does not match: %@", etag, 
639         [etags componentsJoinedByString:@","]];
640   return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
641                       reason:@"Precondition Failed"];
642 }
643
644 - (NSException *)checkIfNoneMatchCondition:(NSString *)_c inContext:(id)_ctx {
645   /*
646     If one of the etags is still the same, we can ignore the request.
647     
648     Can be used for PUT to ensure that the object does not exist in the store
649     and for GET to retrieve the content only if if the etag changed.
650   */
651   
652   if (![_c isEqualToString:@"*"] && 
653       [[[_ctx request] method] isEqualToString:@"GET"]) {
654     NSString *etag;
655     NSArray  *etags;
656     
657     if ((etags = [self parseETagList:_c]) == nil)
658       return nil;
659     if ([etags count] == 0) /* no etags to check for? */
660       return nil;
661     
662     etag = [self davEntityTag];
663     if ([etag length] == 0) /* has no etag, ignore */
664       return nil;
665     
666     if ([etags containsObject:etag]) {
667       [self debugWithFormat:@"etag '%@' matches: %@", etag, 
668               [etags componentsJoinedByString:@","]];
669       /* one etag matches, so stop the request */
670       return [NSException exceptionWithHTTPStatus:304 /* Not Modified */
671                           reason:@"object was not modified"];
672     }
673     
674     return nil;
675   }
676   
677 #if 0
678   if ([_c isEqualToString:@"*"])
679     return nil;
680   
681   if ((a = [self parseETagList:_c]) == nil)
682     return nil;
683 #else
684   [self logWithFormat:@"TODO: implement if-none-match for etag: '%@'", _c];
685 #endif
686   return nil;
687 }
688
689 - (NSException *)matchesRequestConditionInContext:(id)_ctx {
690   NSException *error;
691   WORequest *rq;
692   NSString  *c;
693   
694   if ((rq = [(WOContext *)_ctx request]) == nil)
695     return nil; /* be tolerant - no request, no condition */
696   
697   if ((c = [rq headerForKey:@"if-match"]) != nil) {
698     if ((error = [self checkIfMatchCondition:c inContext:_ctx]) != nil)
699       return error;
700   }
701   if ((c = [rq headerForKey:@"if-none-match"]) != nil) {
702     if ((error = [self checkIfNoneMatchCondition:c inContext:_ctx]) != nil)
703       return error;
704   }
705   
706   return nil;
707 }
708
709 - (NSTimeZone *) serverTimeZone
710 {
711   return serverTimeZone;
712 }
713
714 /* TODO: should be moved into SOGoUser */
715 - (NSTimeZone *) userTimeZone
716 {
717   NSUserDefaults *userPrefs;
718   WOContext *context;
719
720   if (!userTimeZone)
721     {
722       context = [[WOApplication application] context];
723       userPrefs = [[context activeUser] userDefaults];
724       userTimeZone = [NSTimeZone
725                        timeZoneWithName: [userPrefs stringForKey: @"timezonename"]];
726       if (!userTimeZone)
727         userTimeZone = [self serverTimeZone];
728       [userTimeZone retain];
729     }
730
731   return userTimeZone;
732 }
733
734 - (NSTimeZone *) userTimeZone: (NSString *) username
735 {
736   NSUserDefaults *userPrefs;
737   AgenorUserManager *am;
738
739   am = [AgenorUserManager sharedUserManager];
740   userPrefs = [am getUserDefaultsForUID: username];
741   userTimeZone = [NSTimeZone timeZoneWithName: [userPrefs stringForKey: @"timezonename"]];
742   if (!userTimeZone)
743     userTimeZone = [self serverTimeZone];
744
745   return userTimeZone;
746 }
747
748 /* description */
749
750 - (void)appendAttributesToDescription:(NSMutableString *)_ms {
751   if (nameInContainer) 
752     [_ms appendFormat:@" name=%@", nameInContainer];
753   if (container)
754     [_ms appendFormat:@" container=0x%08X/%@", 
755          container, [container valueForKey:@"nameInContainer"]];
756 }
757
758 - (NSString *)description {
759   NSMutableString *ms;
760
761   ms = [NSMutableString stringWithCapacity:64];
762   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
763   [self appendAttributesToDescription:ms];
764   [ms appendString:@">"];
765
766   return ms;
767 }
768
769 @end /* SOGoObject */