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