Use user catalog displayname when displaying an account node
[pithos-macos] / pithos-macos / PithosAccount.m
1 //
2 //  PithosAccount.m
3 //  pithos-macos
4 //
5 // Copyright 2012 GRNET S.A. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
9 // conditions are met:
10 // 
11 //   1. Redistributions of source code must retain the above
12 //      copyright notice, this list of conditions and the following
13 //      disclaimer.
14 // 
15 //   2. Redistributions in binary form must reproduce the above
16 //      copyright notice, this list of conditions and the following
17 //      disclaimer in the documentation and/or other materials
18 //      provided with the distribution.
19 // 
20 // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 // 
33 // The views and conclusions contained in the software and
34 // documentation are those of the authors and should not be
35 // interpreted as representing official policies, either expressed
36 // or implied, of GRNET S.A.
37
38 #import "PithosAccount.h"
39 #import "PithosSyncDaemon.h"
40 #import "ASIPithos.h"
41 #import "ASIPithosRequest.h"
42 #import "PithosAccountNode.h"
43 #import "PithosSharingAccountsNode.h"
44 #import "PithosUtilities.h"
45 #import "pithos_macosAppDelegate.h"
46
47 @interface PithosAccount (Internal)
48 - (BOOL)urlIsValid:(NSString *)urlString;
49 @end
50
51 @implementation PithosAccount
52 @synthesize uniqueName, active, name;
53 @synthesize syncActive, syncDirectoryPath, syncAccountsDictionary, syncSkipHidden, syncLastCompleted, syncDaemon;
54 @synthesize serverURL, versionResource, loginResource, publicResource, userCatalogResource;
55 @synthesize authUser, authToken, storageURLPrefix, authURL, loginURLPrefix, publicURLPrefix, userCatalogURL, userCatalog;
56 @synthesize pithos, accountNode, sharingAccountsNode;
57
58 #pragma mark -
59 #pragma mark Object Lifecycle
60
61 + (id)pithosAccount {
62     PithosAccount *pithosAccount = [[self alloc] init];
63     pithosAccount.uniqueName = [NSString stringWithFormat:@"pithosAccount-%f", [NSDate timeIntervalSinceReferenceDate]];
64     pithosAccount.syncSkipHidden = YES;
65     return pithosAccount;
66 }
67
68
69 - (NSString *)description {
70     return [NSString stringWithFormat:@"uniqueName: %@, active: %d, name: %@, syncActive: %d, syncDirectoryPath: %@, syncAccountsDictionary: %@, syncSkipHidden: %d, syncLastCompleted: %@, serverURL: %@, versionResource: %@, loginResource: %@, publicResource: %@, userCatalogResource: %@, authUser: %@, authToken: %@, storageURLPrefix: %@, authURL: %@, loginURLPrefix: %@, publicURLPrefix: %@, userCatalogResource: %@",
71             uniqueName, active, name, syncActive, syncDirectoryPath, syncAccountsDictionary, syncSkipHidden, syncLastCompleted, serverURL, versionResource, loginResource, publicResource, userCatalogResource, authUser, authToken, storageURLPrefix, authURL, loginURLPrefix, publicURLPrefix, userCatalogResource];
72 }
73
74 #pragma mark -
75 #pragma mark Internal
76
77 - (BOOL)urlIsValid:(NSString *)urlString {
78     if (urlString) {
79         NSURL *url = [NSURL URLWithString:urlString];
80         if (url && url.scheme && url.host)
81             return YES;
82     }
83     return NO;
84 }
85
86 #pragma mark -
87 #pragma mark Properties
88
89 - (NSString *)name {
90     if (![name length]) {
91         NSDictionary *pithosAccountsDictionary = [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] pithosAccountsDictionary];
92         NSString *namePrefix = @"okeanos";
93         NSUInteger nameSuffix = 1;
94         name = @"okeanos";
95         NSString *documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
96         NSFileManager *fileManager = [NSFileManager defaultManager];
97         while ([pithosAccountsDictionary objectForKey:name] || 
98                [fileManager fileExistsAtPath:[documentsDirectoryPath stringByAppendingPathComponent:name]]) {
99             name = [NSString stringWithFormat:@"%@%ld", namePrefix, ++nameSuffix];
100         }
101     }
102     return name;
103 }
104
105 - (void)setName:(NSString *)aName {
106     NSMutableDictionary *pithosAccountsDictionary = [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] pithosAccountsDictionary];
107     if (![self.name isEqualToString:aName] && [aName length] && ![pithosAccountsDictionary objectForKey:aName]) {
108         [pithosAccountsDictionary setObject:self forKey:aName];
109         [pithosAccountsDictionary removeObjectForKey:name];
110         name = aName;
111     }
112 }
113
114 - (BOOL)syncActive {
115     if (active)
116         return syncActive;
117     else
118         return NO;
119 }
120
121 - (void)setSyncActive:(BOOL)aSyncActive {
122     syncActive = aSyncActive;
123     if (syncDaemon && !self.syncActive)
124         [syncDaemon resetDaemon];
125 }
126
127 - (NSString *)syncDirectoryPath {
128     if (![syncDirectoryPath length]) {
129         syncDirectoryPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
130                               stringByAppendingPathComponent:self.name];
131     }
132     return syncDirectoryPath;
133 }
134
135 - (void)setSyncDirectoryPath:(NSString *)aSyncDirectoryPath {
136     if (![self.syncDirectoryPath isEqualToString:aSyncDirectoryPath] && [aSyncDirectoryPath length]) {
137         BOOL isDirectory;
138         if (![[NSFileManager defaultManager] fileExistsAtPath:aSyncDirectoryPath isDirectory:&isDirectory] || isDirectory) {
139             syncDirectoryPath = aSyncDirectoryPath;
140         } else {
141             return;
142         }
143
144         @synchronized(self) {
145             resetSyncDaemonLocalState = YES;
146             syncLastCompleted = nil;
147         }
148     }
149 }
150
151 - (NSMutableDictionary *)syncAccountsDictionary {
152     if (!syncAccountsDictionary) {
153         syncAccountsDictionary = [NSMutableDictionary dictionaryWithObject:[NSMutableDictionary dictionaryWithObject:[NSMutableSet set] 
154                                                                                                               forKey:@"pithos"]
155                                                                     forKey:@""];
156     }        
157     return syncAccountsDictionary;
158 }
159
160 - (void)setSyncAccountsDictionary:(NSMutableDictionary *)aSyncAccountsDictionary {
161     if (aSyncAccountsDictionary && ![self.syncAccountsDictionary isEqualToDictionary:aSyncAccountsDictionary]) {
162         syncAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[aSyncAccountsDictionary count]];
163         for (NSString *accountName in aSyncAccountsDictionary) {
164             NSDictionary *aSyncContainersDictionary = [aSyncAccountsDictionary objectForKey:accountName];
165             NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
166             for (NSString *containerName in aSyncContainersDictionary) {
167                 if (![accountName isEqualToString:@""] || ![[containerName lowercaseString] isEqualToString:@"shared with me"])
168                     [syncContainersDictionary setObject:[NSMutableSet setWithSet:[aSyncContainersDictionary objectForKey:containerName]] 
169                                                  forKey:containerName];
170             }
171             if ([syncContainersDictionary count])
172                 [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
173         }
174         
175         @synchronized(self) {
176             resetSyncDaemonLocalState = YES;
177             syncLastCompleted = nil;
178         }
179     }
180 }
181
182 - (NSDate *)syncLastCompleted {
183     if (self.syncDaemon.lastCompletedSync && ![self.syncDaemon.lastCompletedSync isEqualToDate:syncLastCompleted]) {
184         syncLastCompleted = [self.syncDaemon.lastCompletedSync copy];
185     }
186     return syncLastCompleted;
187 }
188
189 - (PithosSyncDaemon *)syncDaemon {
190     @synchronized(self) {
191         if (self.syncActive && !syncDaemon)
192             syncDaemon = [[PithosSyncDaemon alloc] initWithDirectoryPath:self.syncDirectoryPath 
193                                                            pithosAccount:self 
194                                                       accountsDictionary:self.syncAccountsDictionary 
195                                                               skipHidden:self.syncSkipHidden 
196                                                          resetLocalState:resetSyncDaemonLocalState];
197         resetSyncDaemonLocalState = NO;
198     }
199     return syncDaemon;
200 }
201
202 - (NSString *)serverURL {
203     if (![self urlIsValid:serverURL]) {
204         serverURL = @"https://pithos.okeanos.grnet.gr";
205     }
206     return serverURL;
207 }
208
209 - (void)setServerURL:(NSString *)aServerURL {
210     if (![self.serverURL isEqualToString:aServerURL] && [self urlIsValid:aServerURL]) {
211         serverURL = aServerURL;
212         storageURLPrefix = nil;
213         authURL = nil;
214         publicURLPrefix = nil;
215         loginURLPrefix = nil;
216
217         @synchronized(self) {
218             updatePithos = YES;
219             resetSyncDaemonLocalState = YES;
220             syncLastCompleted = nil;
221         }
222     }
223 }
224
225 - (NSString *)versionResource {
226     if (!versionResource) {
227         versionResource = @"v1";
228     }
229     return versionResource;
230 }
231
232 - (NSString *)loginResource {
233     if (!loginResource) {
234         loginResource = @"login";
235     }
236     return loginResource;
237 }
238
239 - (NSString *)userCatalogResource {
240     if (!userCatalogResource) {
241         userCatalogResource = @"user_catalogs";
242     }
243     return userCatalogResource;
244 }
245
246 - (void)setAuthUser:(NSString *)anAuthUser {
247     if ([anAuthUser length] && ![anAuthUser isEqualToString:authUser]) {
248         authUser = anAuthUser;
249         
250         @synchronized(self) {
251             updatePithos = YES;
252             resetSyncDaemonLocalState = YES;
253             syncLastCompleted = nil;
254
255         }
256     }
257 }
258
259 - (void)setAuthToken:(NSString *)anAuthToken {
260     if ([anAuthToken length] && ![anAuthToken isEqualToString:authToken]) {
261         authToken = anAuthToken;
262         
263         @synchronized(self) {
264             updatePithos = YES;
265         }
266     }
267 }
268
269 - (NSString *)storageURLPrefix {
270     if (![self urlIsValid:storageURLPrefix]) {
271         storageURLPrefix = [self.serverURL stringByAppendingFormat:@"/%@", self.versionResource];
272     }
273     return storageURLPrefix;
274 }
275
276 - (void)setStorageURLPrefix:(NSString *)aStorageURLPrefix {
277     if (![self.storageURLPrefix isEqualToString:aStorageURLPrefix] && [self urlIsValid:aStorageURLPrefix]) {
278         storageURLPrefix = aStorageURLPrefix;
279     }
280 }
281
282 - (NSString *)authURL {
283     if (![self urlIsValid:authURL]) {
284         authURL = [self.serverURL stringByAppendingFormat:@"/%@", self.versionResource];
285     }
286     return authURL;
287 }
288
289 - (void)setAuthURL:(NSString *)anAuthURL {
290     if (![self.authURL isEqualToString:anAuthURL] && [self urlIsValid:anAuthURL]) {
291         authURL = anAuthURL;
292     }
293 }
294
295 - (NSString *)publicURLPrefix {
296     if (![self urlIsValid:publicURLPrefix]) {
297         if (publicResource)
298             publicURLPrefix = [self.serverURL stringByAppendingFormat:@"/%@", publicResource];
299         else
300             publicURLPrefix = [self.serverURL copy];
301     }
302     return publicURLPrefix;
303 }
304
305 - (void)setPublicURLPrefix:(NSString *)aPublicURLPrefix {
306     if (![self.publicURLPrefix isEqualToString:aPublicURLPrefix] && [self urlIsValid:aPublicURLPrefix]) {
307         publicURLPrefix = aPublicURLPrefix;
308     }
309 }
310
311 - (NSString *)loginURLPrefix {
312     if (![self urlIsValid:loginURLPrefix]) {
313         loginURLPrefix = [self.serverURL stringByAppendingFormat:@"/%@", self.loginResource];
314     }
315     return loginURLPrefix;
316 }
317
318 - (void)setLoginURLPrefix:(NSString *)aLoginURLPrefix {
319     if (![self.loginURLPrefix isEqualToString:aLoginURLPrefix] && [self urlIsValid:aLoginURLPrefix]) {
320         loginURLPrefix = aLoginURLPrefix;
321     }
322 }
323
324 - (NSString *)userCatalogURL {
325     if (![self urlIsValid:userCatalogURL]) {
326         userCatalogURL = [self.serverURL stringByAppendingFormat:@"/%@", self.userCatalogResource];
327     }
328     return userCatalogURL;
329 }
330
331 - (void)setUserCatalogURL:(NSString *)aUserCatalogURL {
332     if (![self.userCatalogURL isEqualToString:aUserCatalogURL] && [self urlIsValid:aUserCatalogURL]) {
333         userCatalogURL = aUserCatalogURL;
334     }
335 }
336
337 - (NSMutableDictionary *)userCatalog {
338     if (!userCatalog) {
339         userCatalog = [NSMutableDictionary dictionary];
340     }
341     return userCatalog;
342 }
343
344 - (ASIPithos *)pithos {
345     @synchronized(self) {
346         if (!pithos || updatePithos) {
347             pithos = [ASIPithos pithos];
348             pithos.authUser = authUser;
349             pithos.authToken = authToken;
350             pithos.storageURLPrefix = self.storageURLPrefix;
351             pithos.authURL = self.authURL;
352             pithos.publicURLPrefix = self.publicURLPrefix;
353             pithos.userCatalogURL = self.userCatalogURL;
354             updatePithos = NO;
355         }
356     }
357     return pithos;
358 }
359
360 - (PithosAccountNode *)accountNode {
361     if (!accountNode) {
362         accountNode = [[PithosAccountNode alloc] initWithPithos:self.pithos];
363         accountNode.childrenUpdatedNotificationName = nil;
364         accountNode.inheritChildrenUpdatedNotificationName = YES;
365         accountNode.pithosAccountManager = self;
366     }
367     return accountNode;
368 }
369
370 - (PithosSharingAccountsNode *)sharingAccountsNode {
371     if (!sharingAccountsNode) {
372         sharingAccountsNode = [[PithosSharingAccountsNode alloc] initWithPithos:self.pithos];
373         sharingAccountsNode.childrenUpdatedNotificationName = nil;
374         sharingAccountsNode.inheritChildrenUpdatedNotificationName = YES;
375         sharingAccountsNode.pithosAccountManager = self;
376     }
377     return sharingAccountsNode;
378 }
379
380 #pragma mark -
381 #pragma mark Actions
382
383 - (void)authenticateWithServerURL:(NSString *)aServerURL authUser:(NSString *)anAuthUser authToken:(NSString *)anAuthToken {
384     self.serverURL = aServerURL;
385     self.authUser = anAuthUser;
386     self.authToken = anAuthToken;
387     DLog(@"Account: %@\nauthentication", self);
388     if (![authUser length] || ![authToken length]) {
389         self.active = NO;
390         self.syncActive = NO;
391         // XXX Show preferences with self as the selected account?
392     } else  {
393         self.active = YES;
394         if (syncDaemon) {
395             self.syncDaemon.pithos = self.pithos;
396             if (self.syncActive)
397                 [self.syncDaemon startDaemon];
398         }
399         if (accountNode)
400             self.accountNode.pithos = self.pithos;
401         if (sharingAccountsNode)
402             self.sharingAccountsNode.pithos = self.pithos;
403         if (accountNode) {
404             if (self.accountNode.children) {
405             }
406             [self.accountNode refreshInfo];
407         }
408         if (sharingAccountsNode && self.sharingAccountsNode.children) {
409         }
410         
411         [self updateUserCatalogForForDisplaynames:nil UUIDs:[NSArray arrayWithObject:authUser]];
412     }
413 }
414
415 - (void)loginWithServerURL:(NSString *)aServerURL {
416     self.serverURL = aServerURL;
417     NSProcessInfo *processInfo = [NSProcessInfo processInfo];
418     NSString *loginURL = [NSString stringWithFormat:@"%@?next=pithos://%d/%@&force=", 
419                           self.loginURLPrefix, [processInfo processIdentifier], [ASIPithosRequest encodeToPercentEscape:self.name]];
420     DLog(@"Account: %@\nloginURL: %@", self, loginURL);
421     [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:loginURL]];
422 }
423
424 - (void)updateSyncWithSyncActive:(BOOL)aSyncActive 
425                syncDirectoryPath:(NSString *)aSyncDirectoryPath 
426           syncAccountsDictionary:(NSMutableDictionary *)aSyncAccountsDictionary 
427                   syncSkipHidden:(BOOL)aSyncSkipHidden {
428     self.syncAccountsDictionary = aSyncAccountsDictionary;
429     self.syncDirectoryPath = aSyncDirectoryPath;
430     self.syncSkipHidden = aSyncSkipHidden;
431     self.syncActive = aSyncActive;
432     if (syncDaemon) {
433         self.syncDaemon.accountsDictionary = self.syncAccountsDictionary;
434         self.syncDaemon.directoryPath = self.syncDirectoryPath;
435         self.syncDaemon.skipHidden = self.syncSkipHidden;
436         if (self.syncActive)
437             [self.syncDaemon startDaemon];
438     }    
439 }
440
441 - (ASIPithosRequest *)updateUserCatalogForForDisplaynames:(NSArray *)displaynames UUIDs:(NSArray *)UUIDs {
442     ASIPithosRequest *userCatalogRequest = [ASIPithosRequest userCatalogRequestWithPithos:self.pithos
443                                                                              displaynames:displaynames
444                                                                                     UUIDs:UUIDs];
445     [PithosUtilities startAndWaitForRequest:userCatalogRequest];
446     if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
447         // Don't show alert on 404, since it can be a pre-UUID server.
448         [PithosUtilities httpRequestErrorAlertWithRequest:userCatalogRequest];
449     } else if (userCatalogRequest.responseStatusCode == 200) {
450         NSDictionary *catalogs = [userCatalogRequest catalogs];
451         NSDictionary *displaynameCatalog = [catalogs objectForKey:@"displayname_catalog"];
452         for (NSString *displayname in displaynameCatalog) {
453             [self.userCatalog setObject:displayname forKey:[displaynameCatalog objectForKey:displayname]];
454         }
455         if (UUIDs) {
456             NSDictionary *UUIDCatalog = [catalogs objectForKey:@"uuid_catalog"];
457             for (NSString *UUID in UUIDs) {
458                 NSString *displayname = [UUIDCatalog objectForKey:UUID];
459                 if (displayname) {
460                     [self.userCatalog setObject:displayname forKey:UUID];
461                 } else {
462                     [self.userCatalog removeObjectForKey:UUID];
463                 }
464             }
465         }
466     }
467     return userCatalogRequest;
468 }
469
470 - (NSString *)displaynameForUUID:(NSString *)UUID safe:(BOOL)safe {
471     NSString *displayName = [userCatalog objectForKey:UUID];
472     if (safe && !displayName) {
473         return UUID;
474     } else {
475         return displayName;
476     }
477 }
478
479 - (NSString *)displaynameForUUID:(NSString *)UUID {
480     return [self displaynameForUUID:UUID safe:NO];
481 }
482
483 #pragma mark -
484 #pragma mark NSCoding
485
486 - (id)initWithCoder:(NSCoder *)decoder {
487     if ((self = [super init])) {
488         self.uniqueName = [decoder decodeObjectForKey:@"uniqueName"];
489         self.active = [decoder decodeBoolForKey:@"active"];
490         name = [decoder decodeObjectForKey:@"name"];
491
492         self.syncActive = [decoder decodeBoolForKey:@"syncActive"];
493         self.syncDirectoryPath = [decoder decodeObjectForKey:@"syncDirectoryPath"];
494         self.syncAccountsDictionary = [decoder decodeObjectForKey:@"syncAccountsDictionary"];
495         self.syncSkipHidden = [decoder decodeBoolForKey:@"syncSkipHidden"];
496         self.syncLastCompleted = [decoder decodeObjectForKey:@"syncLastCompleted"];
497         
498         self.serverURL = [decoder decodeObjectForKey:@"serverURL"];
499         self.versionResource = [decoder decodeObjectForKey:@"versionResource"];
500         self.loginResource = [decoder decodeObjectForKey:@"loginResource"];
501         self.publicResource = [decoder decodeObjectForKey:@"publicResource"];
502         self.userCatalogResource = [decoder decodeObjectForKey:@"userCatalogResource"];
503         
504         self.authUser = [decoder decodeObjectForKey:@"authUser"];
505         self.authToken = [decoder decodeObjectForKey:@"authToken"];
506         self.storageURLPrefix = [decoder decodeObjectForKey:@"storageURLPrefix"];
507         self.authURL = [decoder decodeObjectForKey:@"authURL"];
508         self.publicURLPrefix = [decoder decodeObjectForKey:@"publicURLPrefix"];
509         self.loginURLPrefix = [decoder decodeObjectForKey:@"loginURLPrefix"];
510         self.userCatalogURL = [decoder decodeObjectForKey:@"userCatalogURL"];
511         self.userCatalog = [decoder decodeObjectForKey:@"userCatalog"];
512         
513         if (![authUser length] || ![authToken length] || ![self.storageURLPrefix length])
514             self.active = NO;
515         
516         resetSyncDaemonLocalState = NO;
517     }
518     return self;
519 }
520
521 - (void)encodeWithCoder:(NSCoder *)encoder {
522     [encoder encodeObject:uniqueName forKey:@"uniqueName"];
523     [encoder encodeBool:active forKey:@"active"];
524     [encoder encodeObject:name forKey:@"name"];
525     
526     [encoder encodeBool:syncActive forKey:@"syncActive"];
527     [encoder encodeObject:syncDirectoryPath forKey:@"syncDirectoryPath"];
528     [encoder encodeObject:syncAccountsDictionary forKey:@"syncAccountsDictionary"];
529     [encoder encodeBool:syncSkipHidden forKey:@"syncSkipHidden"];
530     [encoder encodeObject:self.syncLastCompleted forKey:@"syncLastCompleted"];
531
532     [encoder encodeObject:serverURL forKey:@"serverURL"];
533     [encoder encodeObject:versionResource forKey:@"versionResource"];
534     [encoder encodeObject:publicResource forKey:@"publicResource"];
535     [encoder encodeObject:loginResource forKey:@"loginResource"];
536     [encoder encodeObject:loginResource forKey:@"userCatalogResource"];
537     
538     [encoder encodeObject:authUser forKey:@"authUser"];
539     [encoder encodeObject:authToken forKey:@"authToken"];
540     [encoder encodeObject:storageURLPrefix forKey:@"storageURLPrefix"];
541     [encoder encodeObject:authURL forKey:@"authURL"];
542     [encoder encodeObject:publicURLPrefix forKey:@"publicURLPrefix"];
543     [encoder encodeObject:loginURLPrefix forKey:@"loginURLPrefix"];
544     [encoder encodeObject:userCatalogURL forKey:@"userCatalogURL"];
545     [encoder encodeObject:userCatalog forKey:@"userCatalog"];
546 }
547
548 @end