Retry failed browser or node service requests that may have an updated URL in the...
[pithos-macos] / pithos-macos / PithosAccount.m
1 //
2 //  PithosAccount.m
3 //  pithos-macos
4 //
5 // Copyright 2012-2013 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 static NSString *defaultAuthURLString = @"https://accounts.okeanos.grnet.gr/identity/v2.0";
48 static NSString *defaultManualURLString = @"https://pithos.okeanos.grnet.gr";
49
50 @interface NSString(Additions)
51 - (BOOL)isValidURL;
52 - (NSString *)stringByRemovingTrailingSlashes;
53 @end
54
55 @implementation NSString(Additions)
56 - (BOOL)isValidURL {
57     NSURL *URL = [NSURL URLWithString:self];
58     return (URL && URL.scheme && URL.host);
59 }
60
61 - (NSString *)stringByRemovingTrailingSlashes {
62     return [self stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]];
63 }
64 @end
65
66 @implementation PithosAccount
67 @synthesize uniqueName, active, name, clientVersion;
68 @synthesize syncActive, syncDirectoryPath, syncAccountsDictionary, syncSkipHidden, syncLastCompleted, syncDaemon;
69 @synthesize authURLString, pithosObjectStoreURLString, astakosAccountURLString, astakosWebloginURLString, manual;
70 @synthesize authToken, authUser, userCatalog;
71 @synthesize ignoreSSLErrors;
72 @synthesize pithos, accountNode, sharingAccountsNode;
73 @synthesize tokensURL, storageURLPrefix, loginURL, userCatalogURL, publicURLPrefix;
74
75 #pragma mark -
76 #pragma mark Object Lifecycle
77
78 + (id)pithosAccount {
79     PithosAccount *pithosAccount = [[self alloc] init];
80     pithosAccount.uniqueName = [NSString stringWithFormat:@"pithosAccount-%f", [NSDate timeIntervalSinceReferenceDate]];
81     pithosAccount.clientVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
82     pithosAccount.syncSkipHidden = YES;
83     pithosAccount.authURLString = defaultAuthURLString;
84     return pithosAccount;
85 }
86
87 - (NSString *)description {
88     return [NSString stringWithFormat:@"uniqueName: %@, active: %d, name: %@, clientVersion: %@, syncActive: %d, syncDirectoryPath: %@, syncAccountsDictionary: %@, syncSkipHidden: %d, syncLastCompleted: %@, authURLString: %@, pithosObjectStoreURLString:%@ , astakosAccountURLString: %@, astakosWebloginURLString: %@, manual: %d, authToken: %@, authUser: %@",
89             uniqueName, active, name, clientVersion, syncActive, syncDirectoryPath, syncAccountsDictionary, syncSkipHidden, syncLastCompleted, authURLString, pithosObjectStoreURLString, astakosAccountURLString, astakosWebloginURLString, manual, authToken, authUser];
90 }
91
92 #pragma mark -
93 #pragma mark Properties
94
95 - (NSString *)name {
96     if (![name length]) {
97         NSDictionary *pithosAccountsDictionary = [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] pithosAccountsDictionary];
98         NSString *namePrefix = @"okeanos";
99         NSUInteger nameSuffix = 1;
100         name = @"okeanos";
101         NSString *documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
102         NSFileManager *fileManager = [NSFileManager defaultManager];
103         while ([pithosAccountsDictionary objectForKey:name] || 
104                [fileManager fileExistsAtPath:[documentsDirectoryPath stringByAppendingPathComponent:name]]) {
105             name = [NSString stringWithFormat:@"%@%ld", namePrefix, ++nameSuffix];
106         }
107     }
108     return name;
109 }
110
111 - (void)setName:(NSString *)aName {
112     NSMutableDictionary *pithosAccountsDictionary = [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] pithosAccountsDictionary];
113     if (![self.name isEqualToString:aName] && [aName length] && ![pithosAccountsDictionary objectForKey:aName]) {
114         [pithosAccountsDictionary setObject:self forKey:aName];
115         [pithosAccountsDictionary removeObjectForKey:name];
116         name = aName;
117     }
118 }
119
120 - (BOOL)syncActive {
121     if (active)
122         return syncActive;
123     else
124         return NO;
125 }
126
127 - (void)setSyncActive:(BOOL)aSyncActive {
128     syncActive = aSyncActive;
129     if (syncDaemon && !self.syncActive)
130         [syncDaemon resetDaemon];
131 }
132
133 - (NSString *)syncDirectoryPath {
134     if (![syncDirectoryPath length]) {
135         syncDirectoryPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
136                               stringByAppendingPathComponent:self.name];
137     }
138     return syncDirectoryPath;
139 }
140
141 - (void)setSyncDirectoryPath:(NSString *)aSyncDirectoryPath {
142     if (![self.syncDirectoryPath isEqualToString:aSyncDirectoryPath] && [aSyncDirectoryPath length]) {
143         BOOL isDirectory;
144         if (![[NSFileManager defaultManager] fileExistsAtPath:aSyncDirectoryPath isDirectory:&isDirectory] || isDirectory) {
145             syncDirectoryPath = aSyncDirectoryPath;
146         } else {
147             return;
148         }
149
150         @synchronized(self) {
151             resetSyncDaemonLocalState = YES;
152             syncLastCompleted = nil;
153         }
154     }
155 }
156
157 - (NSMutableDictionary *)syncAccountsDictionary {
158     if (!syncAccountsDictionary) {
159         syncAccountsDictionary = [NSMutableDictionary dictionaryWithObject:[NSMutableDictionary dictionaryWithObject:[NSMutableSet set] 
160                                                                                                               forKey:@"pithos"]
161                                                                     forKey:@""];
162     }        
163     return syncAccountsDictionary;
164 }
165
166 - (void)setSyncAccountsDictionary:(NSMutableDictionary *)aSyncAccountsDictionary {
167     if (aSyncAccountsDictionary && ![self.syncAccountsDictionary isEqualToDictionary:aSyncAccountsDictionary]) {
168         syncAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[aSyncAccountsDictionary count]];
169         for (NSString *accountName in aSyncAccountsDictionary) {
170             NSDictionary *aSyncContainersDictionary = [aSyncAccountsDictionary objectForKey:accountName];
171             NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
172             for (NSString *containerName in aSyncContainersDictionary) {
173                 if (![accountName isEqualToString:@""] || ![[containerName lowercaseString] isEqualToString:@"shared with me"])
174                     [syncContainersDictionary setObject:[NSMutableSet setWithSet:[aSyncContainersDictionary objectForKey:containerName]] 
175                                                  forKey:containerName];
176             }
177             if ([syncContainersDictionary count])
178                 [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
179         }
180         
181         @synchronized(self) {
182             resetSyncDaemonLocalState = YES;
183             syncLastCompleted = nil;
184         }
185     }
186 }
187
188 - (NSDate *)syncLastCompleted {
189     if (self.syncDaemon.lastCompletedSync && ![self.syncDaemon.lastCompletedSync isEqualToDate:syncLastCompleted]) {
190         syncLastCompleted = [self.syncDaemon.lastCompletedSync copy];
191     }
192     return syncLastCompleted;
193 }
194
195 - (PithosSyncDaemon *)syncDaemon {
196     @synchronized(self) {
197         if (self.syncActive && !syncDaemon)
198             syncDaemon = [[PithosSyncDaemon alloc] initWithDirectoryPath:self.syncDirectoryPath 
199                                                            pithosAccount:self 
200                                                       accountsDictionary:self.syncAccountsDictionary 
201                                                               skipHidden:self.syncSkipHidden 
202                                                          resetLocalState:resetSyncDaemonLocalState];
203         resetSyncDaemonLocalState = NO;
204     }
205     return syncDaemon;
206 }
207
208 - (void)setAuthURLString:(NSString *)anAuthURLString {
209     NSString *tmpURLString = [anAuthURLString stringByRemovingTrailingSlashes];
210     if (![authURLString isEqualToString:tmpURLString]) {
211         authURLString = tmpURLString;
212
213         @synchronized(self) {
214             updatePithos = YES;
215             resetSyncDaemonLocalState = YES;
216             syncLastCompleted = nil;
217         }
218     }
219 }
220
221 - (void)setPithosObjectStoreURLString:(NSString *)aPithosObjectStoreURLString {
222     NSString *tmpURLString = [aPithosObjectStoreURLString stringByRemovingTrailingSlashes];
223     if (![pithosObjectStoreURLString isEqualToString:tmpURLString]) {
224         pithosObjectStoreURLString = tmpURLString;
225
226         @synchronized(self) {
227             updatePithos = YES;
228         }
229     }
230 }
231
232 - (void)setAstakosAccountURLString:(NSString *)anAstakosAccountURLString {
233     NSString *tmpURLString = [anAstakosAccountURLString stringByRemovingTrailingSlashes];
234     if (![astakosAccountURLString isEqualToString:tmpURLString]) {
235         astakosAccountURLString = tmpURLString;
236         
237         @synchronized(self) {
238             updatePithos = YES;
239         }
240     }
241 }
242
243 - (void)setAstakosWebloginURLString:(NSString *)anAstakosWebloginURLString {
244     NSString *tmpURLString = [anAstakosWebloginURLString stringByRemovingTrailingSlashes];
245     if (![astakosWebloginURLString isEqualToString:tmpURLString]) {
246         astakosWebloginURLString = tmpURLString;
247     }
248 }
249
250 - (void)setAuthToken:(NSString *)anAuthToken {
251     if (anAuthToken.length && ![anAuthToken isEqualToString:authToken]) {
252         authToken = anAuthToken;
253         
254         @synchronized(self) {
255             updatePithos = YES;
256         }
257     }
258 }
259
260 - (void)setAuthUser:(NSString *)anAuthUser {
261     if (anAuthUser.length && ![anAuthUser isEqualToString:authUser]) {
262         authUser = anAuthUser;
263         
264         @synchronized(self) {
265             updatePithos = YES;
266             resetSyncDaemonLocalState = YES;
267             syncLastCompleted = nil;
268
269         }
270     }
271 }
272
273 - (void)setIgnoreSSLErrors:(BOOL)anIgnoreSSLErrors {
274     if (anIgnoreSSLErrors != ignoreSSLErrors) {
275         ignoreSSLErrors = anIgnoreSSLErrors;
276         
277         @synchronized(self) {
278             updatePithos = YES;
279         }
280     }
281 }
282
283 - (NSMutableDictionary *)userCatalog {
284     if (!userCatalog) {
285         userCatalog = [NSMutableDictionary dictionary];
286     }
287     return userCatalog;
288 }
289
290 - (ASIPithos *)pithos {
291     @synchronized(self) {
292         if (!pithos || updatePithos) {
293             pithos = [ASIPithos pithos];
294             pithos.authToken = authToken;
295             pithos.authUser = authUser;
296             
297             pithos.ignoreSSLErrors = ignoreSSLErrors;
298             
299             pithos.tokensURL = self.tokensURL;
300             pithos.storageURLPrefix = self.storageURLPrefix;
301             pithos.userCatalogURL = self.userCatalogURL;
302             pithos.publicURLPrefix = self.publicURLPrefix;
303             updatePithos = NO;
304         }
305     }
306     return pithos;
307 }
308
309 - (PithosAccountNode *)accountNode {
310     if (!accountNode) {
311         accountNode = [[PithosAccountNode alloc] initWithPithosAccountManager:self];
312         accountNode.childrenUpdatedNotificationName = nil;
313         accountNode.inheritChildrenUpdatedNotificationName = YES;
314     }
315     return accountNode;
316 }
317
318 - (PithosSharingAccountsNode *)sharingAccountsNode {
319     if (!sharingAccountsNode) {
320         sharingAccountsNode = [[PithosSharingAccountsNode alloc] initWithPithosAccountManager:self];
321         sharingAccountsNode.childrenUpdatedNotificationName = nil;
322         sharingAccountsNode.inheritChildrenUpdatedNotificationName = YES;
323     }
324     return sharingAccountsNode;
325 }
326
327 - (NSString *)tokensURL {
328     return [authURLString stringByAppendingString:@"/tokens"];
329 }
330
331 - (NSString *)storageURLPrefix {
332     return [pithosObjectStoreURLString copy];
333 }
334
335 - (NSString *)loginURL {
336     return [astakosWebloginURLString stringByAppendingString:@"/login"];
337 }
338
339 - (NSString *)publicURLPrefix {
340     return [pithosObjectStoreURLString copy];
341 }
342
343 - (NSString *)userCatalogURL {
344     return [astakosAccountURLString stringByAppendingString:@"/user_catalogs"];
345 }
346
347 #pragma mark -
348 #pragma mark Actions
349
350 - (void)updateWithAuthURLString:(NSString *)anAuthURLString
351      pithosObjectStoreURLString:(NSString *)aPithosObjectStoreURLString
352         astakosAccountURLString:(NSString *)anAstakosAccountURLString
353        astakosWebloginURLString:(NSString *)anAstakosWebloginURLString
354                          manual:(BOOL)aManual
355                       authToken:(NSString *)anAuthToken
356                       authUser:(NSString *)anAuthUser
357                 ignoreSSLErrors:(BOOL)anIgnoreSSLErrors
358                      resetNodes:(BOOL)resetNodes {
359     BOOL pithosUpdated = (![authURLString isEqualToString:[anAuthURLString stringByRemovingTrailingSlashes]] ||
360                           ![pithosObjectStoreURLString isEqualToString:[aPithosObjectStoreURLString stringByRemovingTrailingSlashes]] ||
361                           ![astakosAccountURLString isEqualToString:[anAstakosAccountURLString stringByRemovingTrailingSlashes]] ||
362                           ![authToken isEqualToString:anAuthToken] || ![authUser isEqualToString:anAuthUser] ||
363                           (ignoreSSLErrors != anIgnoreSSLErrors));
364     self.authURLString = anAuthURLString;
365     self.pithosObjectStoreURLString = aPithosObjectStoreURLString;
366     self.astakosAccountURLString = anAstakosAccountURLString;
367     self.astakosWebloginURLString = anAstakosWebloginURLString;
368     self.manual = aManual;
369     self.authToken = anAuthToken;
370     self.authUser = anAuthUser;
371     self.ignoreSSLErrors = anIgnoreSSLErrors;
372     DLog(@"Account updated: %@", self);
373     if (!authToken.length || !authUser.length) {
374         self.active = NO;
375         self.syncActive = NO;
376     } else  {
377         [self updateUserCatalogForDisplaynames:nil UUIDs:[NSArray arrayWithObject:authUser]];
378
379         self.active = YES;
380         if (pithosUpdated) {
381             if (syncDaemon) {
382                 // For the sync daemon we keep using a separate pithos object.
383                 self.syncDaemon.pithos = self.pithos;
384                 if (self.syncActive)
385                     [self.syncDaemon startDaemon];
386             }
387             if (resetNodes) {
388                 [accountNode reset];
389                 [accountNode refreshInfo];
390                 [sharingAccountsNode reset];
391             
392                 [[NSNotificationCenter defaultCenter] postNotificationName:@"PithosAccountPithosChanged" object:self];
393             }
394         }
395     }
396 }
397
398 - (void)updateWithAuthURLString:(NSString *)anAuthURLString
399      pithosObjectStoreURLString:(NSString *)aPithosObjectStoreURLString
400         astakosAccountURLString:(NSString *)anAstakosAccountURLString
401        astakosWebloginURLString:(NSString *)anAstakosWebloginURLString
402                          manual:(BOOL)aManual
403                       authToken:(NSString *)anAuthToken
404                        authUser:(NSString *)anAuthUser
405                 ignoreSSLErrors:(BOOL)anIgnoreSSLErrors {
406     [self updateWithAuthURLString:anAuthURLString
407        pithosObjectStoreURLString:aPithosObjectStoreURLString
408           astakosAccountURLString:anAstakosAccountURLString
409          astakosWebloginURLString:anAstakosWebloginURLString
410                            manual:aManual
411                         authToken:anAuthToken
412                          authUser:anAuthUser
413                   ignoreSSLErrors:anIgnoreSSLErrors
414                        resetNodes:YES];
415 }
416
417 - (void)updateSyncWithSyncActive:(BOOL)aSyncActive
418                syncDirectoryPath:(NSString *)aSyncDirectoryPath 
419           syncAccountsDictionary:(NSMutableDictionary *)aSyncAccountsDictionary 
420                   syncSkipHidden:(BOOL)aSyncSkipHidden {
421     self.syncAccountsDictionary = aSyncAccountsDictionary;
422     self.syncDirectoryPath = aSyncDirectoryPath;
423     self.syncSkipHidden = aSyncSkipHidden;
424     self.syncActive = aSyncActive;
425     if (syncDaemon) {
426         self.syncDaemon.accountsDictionary = self.syncAccountsDictionary;
427         self.syncDaemon.directoryPath = self.syncDirectoryPath;
428         self.syncDaemon.skipHidden = self.syncSkipHidden;
429         if (self.syncActive)
430             [self.syncDaemon startDaemon];
431     }    
432 }
433
434 - (NSMutableDictionary *)servicesFromServiceCatalogRequest:(ASIPithosRequest *)serviceCatalogRequest {
435     NSMutableDictionary *services = [NSMutableDictionary dictionary];
436     if (serviceCatalogRequest.responseStatusCode == 200) {
437         BOOL pithosObjectStoreFound = NO;
438         BOOL astakosAccountFound = NO;
439         BOOL astakosWebloginFound = NO;
440         NSArray *serviceCatalog = [serviceCatalogRequest serviceCatalog];
441         for (NSDictionary *service in serviceCatalog) {
442             NSString *serviceName = [service objectForKey:@"name"];
443             if (!pithosObjectStoreFound && [serviceName isEqualToString:@"pithos_object-store"]) {
444                 [services setObject:[[[service objectForKey:@"endpoints"] objectAtIndex:0] objectForKey:@"publicURL"]
445                              forKey:@"PithosObjectStoreURLString"];
446                 pithosObjectStoreFound = YES;
447             } else if (!astakosAccountFound && [serviceName isEqualToString:@"astakos_account"]) {
448                 [services setObject:[[[service objectForKey:@"endpoints"] objectAtIndex:0] objectForKey:@"publicURL"]
449                              forKey:@"AstakosAccountURLString"];
450                 astakosAccountFound = YES;
451             } else if (!astakosWebloginFound && [serviceName isEqualToString:@"astakos_weblogin"]) {
452                 [services setObject:[[[service objectForKey:@"endpoints"] objectAtIndex:0] objectForKey:@"SNF:webloginURL"]
453                              forKey:@"AstakosWebloginURLString"];
454                 astakosWebloginFound = YES;
455             }
456             if (pithosObjectStoreFound && astakosAccountFound && astakosWebloginFound)
457                 break;
458         }
459     }
460     return services;
461 }
462
463 - (void)updateServicesFromServiceCatalogRequest:(ASIPithosRequest *)serviceCatalogRequest {
464     if (serviceCatalogRequest.responseStatusCode == 200) {
465         NSMutableDictionary *services = [self servicesFromServiceCatalogRequest:serviceCatalogRequest];
466         NSString *newAuthToken = nil;
467         NSString *newAuthUser = nil;
468         if (authToken.length) {
469             NSDictionary *token = [serviceCatalogRequest token];
470             newAuthToken = [token objectForKey:@"id"];
471             newAuthUser = [[token objectForKey:@"tenant"] objectForKey:@"id"];
472         }
473         [self updateWithAuthURLString:authURLString
474            pithosObjectStoreURLString:[services objectForKey:@"PithosObjectStoreURLString"]
475               astakosAccountURLString:[services objectForKey:@"AstakosAccountURLString"]
476              astakosWebloginURLString:[services objectForKey:@"AstakosWebloginURLString"]
477                                manual:NO
478                             authToken:(newAuthToken ? newAuthToken : authToken)
479                              authUser:(newAuthUser ? newAuthUser : authUser)
480                       ignoreSSLErrors:ignoreSSLErrors
481                            resetNodes:NO];
482     } else if (serviceCatalogRequest.responseStatusCode == 404) {
483         [self updateWithAuthURLString:authURLString
484            pithosObjectStoreURLString:[[[NSURL URLWithString:authURLString] URLByAppendingPathComponent:@"v1"] description]
485               astakosAccountURLString:[authURLString copy]
486              astakosWebloginURLString:[authURLString copy]
487                                manual:YES
488                             authToken:authToken
489                              authUser:authUser
490                       ignoreSSLErrors:ignoreSSLErrors
491                            resetNodes:YES];
492     }
493 }
494
495 - (ASIPithosRequest *)updateUserCatalogForDisplaynames:(NSArray *)displaynames UUIDs:(NSArray *)UUIDs {
496     ASIPithosRequest *userCatalogRequest = [ASIPithosRequest userCatalogRequestWithPithos:self.pithos
497                                                                              displaynames:displaynames
498                                                                                     UUIDs:UUIDs];
499     [PithosUtilities startAndWaitForRequest:userCatalogRequest];
500     if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
501         // Don't show alert on 404, since it can be a pre-UUID server.
502         [PithosUtilities httpRequestErrorAlertWithRequest:userCatalogRequest];
503     } else if (userCatalogRequest.responseStatusCode == 200) {
504         NSDictionary *catalogs = [userCatalogRequest catalogs];
505         NSDictionary *displaynameCatalog = [catalogs objectForKey:@"displayname_catalog"];
506         for (NSString *displayname in displaynameCatalog) {
507             [self.userCatalog setObject:displayname forKey:[displaynameCatalog objectForKey:displayname]];
508         }
509         if (UUIDs) {
510             NSDictionary *UUIDCatalog = [catalogs objectForKey:@"uuid_catalog"];
511             for (NSString *UUID in UUIDs) {
512                 NSString *displayname = [UUIDCatalog objectForKey:UUID];
513                 if (displayname) {
514                     [self.userCatalog setObject:displayname forKey:UUID];
515                 } else {
516                     [self.userCatalog removeObjectForKey:UUID];
517                 }
518             }
519         }
520     }
521     return userCatalogRequest;
522 }
523
524 - (NSString *)displaynameForUUID:(NSString *)UUID safe:(BOOL)safe {
525     NSString *displayName = [userCatalog objectForKey:UUID];
526     if (safe && !displayName) {
527         return UUID;
528     } else {
529         return displayName;
530     }
531 }
532
533 - (NSString *)displaynameForUUID:(NSString *)UUID {
534     return [self displaynameForUUID:UUID safe:NO];
535 }
536
537 #pragma mark -
538 #pragma mark NSCoding
539
540 - (id)initWithCoder:(NSCoder *)decoder {
541     if ((self = [super init])) {
542         self.uniqueName = [decoder decodeObjectForKey:@"uniqueName"];
543         self.active = [decoder decodeBoolForKey:@"active"];
544         name = [decoder decodeObjectForKey:@"name"];
545         self.clientVersion = [decoder decodeObjectForKey:@"clientVersion"];
546
547         self.syncActive = [decoder decodeBoolForKey:@"syncActive"];
548         self.syncDirectoryPath = [decoder decodeObjectForKey:@"syncDirectoryPath"];
549         self.syncAccountsDictionary = [decoder decodeObjectForKey:@"syncAccountsDictionary"];
550         self.syncSkipHidden = [decoder decodeBoolForKey:@"syncSkipHidden"];
551         self.syncLastCompleted = [decoder decodeObjectForKey:@"syncLastCompleted"];
552         
553         self.authURLString = [decoder decodeObjectForKey:@"authURLString"];
554         self.pithosObjectStoreURLString = [decoder decodeObjectForKey:@"pithosObjectStoreURLString"];
555         self.astakosAccountURLString = [decoder decodeObjectForKey:@"astakosAccountURLString"];
556         self.astakosWebloginURLString = [decoder decodeObjectForKey:@"astakosWebloginURLString"];
557         self.manual = [decoder decodeBoolForKey:@"manual"];
558         
559         self.ignoreSSLErrors = [decoder decodeBoolForKey:@"ignoreSSLErrors"];
560         
561         // Support for older versions.
562         if (!authURLString && !pithosObjectStoreURLString && !astakosAccountURLString && !astakosWebloginURLString) {
563             NSString *tmpURLString = [decoder decodeObjectForKey:@"serverURL"];
564             if (!tmpURLString ||
565                 [[[NSURL URLWithString:tmpURLString] URLByAppendingPathComponent:@""] isEqual:[[NSURL URLWithString:defaultManualURLString] URLByAppendingPathComponent:@""]]) {
566                 self.authURLString = defaultAuthURLString;
567                 self.manual = NO;
568             } else {
569                 self.pithosObjectStoreURLString = [tmpURLString stringByAppendingString:@"/v1"];
570                 self.astakosAccountURLString = tmpURLString;
571                 self.astakosWebloginURLString = tmpURLString;
572                 self.manual = YES;
573             }
574         }
575         
576         self.authToken = [decoder decodeObjectForKey:@"authToken"];
577         self.authUser = [decoder decodeObjectForKey:@"authUser"];
578         self.userCatalog = [decoder decodeObjectForKey:@"userCatalog"];
579         
580         if (![authUser length] || ![authToken length])
581             self.active = NO;
582         
583         NSString *currentVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
584         if (!clientVersion || ![clientVersion isEqualToString:currentVersion]) {
585             resetSyncDaemonLocalState = YES;
586             self.clientVersion = currentVersion;
587         } else {
588             resetSyncDaemonLocalState = NO;
589         }
590     }
591     return self;
592 }
593
594 - (void)encodeWithCoder:(NSCoder *)encoder {
595     [encoder encodeObject:uniqueName forKey:@"uniqueName"];
596     [encoder encodeBool:active forKey:@"active"];
597     [encoder encodeObject:name forKey:@"name"];
598     [encoder encodeObject:clientVersion forKey:@"clientVersion"];
599     
600     [encoder encodeBool:syncActive forKey:@"syncActive"];
601     [encoder encodeObject:syncDirectoryPath forKey:@"syncDirectoryPath"];
602     [encoder encodeObject:syncAccountsDictionary forKey:@"syncAccountsDictionary"];
603     [encoder encodeBool:syncSkipHidden forKey:@"syncSkipHidden"];
604     [encoder encodeObject:self.syncLastCompleted forKey:@"syncLastCompleted"];
605
606     [encoder encodeObject:authURLString forKey:@"authURLString"];
607     [encoder encodeObject:pithosObjectStoreURLString forKey:@"pithosObjectStoreURLString"];
608     [encoder encodeObject:astakosAccountURLString forKey:@"astakosAccountURLString"];
609     [encoder encodeObject:astakosWebloginURLString forKey:@"astakosWebloginURLString"];
610     [encoder encodeBool:manual forKey:@"manual"];
611     
612     [encoder encodeObject:authToken forKey:@"authToken"];
613     [encoder encodeObject:authUser forKey:@"authUser"];
614     [encoder encodeObject:userCatalog forKey:@"userCatalog"];
615     
616     [encoder encodeBool:ignoreSSLErrors forKey:@"ignoreSSLErrors"];
617 }
618
619 @end