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