2
0

Adapt macOS for new APIs.

This commit is contained in:
Maarten Billemont 2017-08-12 22:26:48 -04:00
parent f5c7d11f0e
commit d7193f7753
9 changed files with 105 additions and 96 deletions

View File

@ -23,11 +23,11 @@
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
//#ifdef NS_ENUM #ifdef NS_ENUM
//#define enum(_type, _name) NS_ENUM(_type, _name) #define enum(_type, _name) NS_ENUM(_type, _name)
//#else #else
#define enum(_type, _name) _type _name; enum #define enum(_type, _name) _type _name; enum
//#endif #endif
//// Types. //// Types.

View File

@ -64,10 +64,10 @@ NSString *NSStringFromTimeToCrack(TimeToCrack timeToCrack);
- (NSString *)mpwLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key; - (NSString *)mpwLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key;
- (NSString *)mpwTemplateForSiteNamed:(NSString *)name ofType:(MPResultType)type - (NSString *)mpwTemplateForSiteNamed:(NSString *)name ofType:(MPResultType)type
withCounter:(NSUInteger)counter usingKey:(MPKey *)key; withCounter:(MPCounterValue)counter usingKey:(MPKey *)key;
- (NSString *)mpwAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key; - (NSString *)mpwAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key;
- (NSString *)mpwResultForSiteNamed:(NSString *)name ofType:(MPResultType)type parameter:(NSString *)parameter - (NSString *)mpwResultForSiteNamed:(NSString *)name ofType:(MPResultType)type parameter:(NSString *)parameter
withCounter:(NSUInteger)counter variant:(MPKeyPurpose)purpose context:(NSString *)context usingKey:(MPKey *)key; withCounter:(MPCounterValue)counter variant:(MPKeyPurpose)purpose context:(NSString *)context usingKey:(MPKey *)key;
- (BOOL)savePassword:(NSString *)clearPassword toSite:(MPSiteEntity *)site usingKey:(MPKey *)key; - (BOOL)savePassword:(NSString *)clearPassword toSite:(MPSiteEntity *)site usingKey:(MPKey *)key;

View File

@ -180,6 +180,9 @@ static NSOperationQueue *_mpwQueue = nil;
case MPResultTypeStatefulDevice: case MPResultTypeStatefulDevice:
return @"Device Private Password"; return @"Device Private Password";
case MPResultTypeDeriveKey:
return @"Crypto Key";
} }
Throw( @"Type not supported: %lu", (long)type ); Throw( @"Type not supported: %lu", (long)type );
@ -220,6 +223,9 @@ static NSOperationQueue *_mpwQueue = nil;
case MPResultTypeStatefulDevice: case MPResultTypeStatefulDevice:
return @"Device"; return @"Device";
case MPResultTypeDeriveKey:
return @"Key";
} }
Throw( @"Type not supported: %lu", (long)type ); Throw( @"Type not supported: %lu", (long)type );
@ -265,6 +271,9 @@ static NSOperationQueue *_mpwQueue = nil;
case MPResultTypeStatefulDevice: case MPResultTypeStatefulDevice:
return [MPStoredSiteEntity class]; return [MPStoredSiteEntity class];
case MPResultTypeDeriveKey:
break;
} }
Throw( @"Type not supported: %lu", (long)type ); Throw( @"Type not supported: %lu", (long)type );
@ -314,6 +323,8 @@ static NSOperationQueue *_mpwQueue = nil;
return MPResultTypeStatefulDevice; return MPResultTypeStatefulDevice;
case MPResultTypeStatefulDevice: case MPResultTypeStatefulDevice:
return MPResultTypeTemplatePhrase; return MPResultTypeTemplatePhrase;
case MPResultTypeDeriveKey:
break;
} }
return [self defaultType]; return [self defaultType];
@ -330,12 +341,12 @@ static NSOperationQueue *_mpwQueue = nil;
- (NSString *)mpwLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key { - (NSString *)mpwLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key {
return [self mpwResultForSiteNamed:name ofType:MPResultTypeTemplateName parameter:nil withCounter:1 return [self mpwResultForSiteNamed:name ofType:MPResultTypeTemplateName parameter:nil withCounter:MPCounterValueInitial
variant:MPKeyPurposeIdentification context:nil usingKey:key]; variant:MPKeyPurposeIdentification context:nil usingKey:key];
} }
- (NSString *)mpwTemplateForSiteNamed:(NSString *)name ofType:(MPResultType)type - (NSString *)mpwTemplateForSiteNamed:(NSString *)name ofType:(MPResultType)type
withCounter:(NSUInteger)counter usingKey:(MPKey *)key { withCounter:(MPCounterValue)counter usingKey:(MPKey *)key {
return [self mpwResultForSiteNamed:name ofType:type parameter:nil withCounter:counter return [self mpwResultForSiteNamed:name ofType:type parameter:nil withCounter:counter
variant:MPKeyPurposeAuthentication context:nil usingKey:key]; variant:MPKeyPurposeAuthentication context:nil usingKey:key];
@ -343,17 +354,18 @@ static NSOperationQueue *_mpwQueue = nil;
- (NSString *)mpwAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key { - (NSString *)mpwAnswerForSiteNamed:(NSString *)name onQuestion:(NSString *)question usingKey:(MPKey *)key {
return [self mpwResultForSiteNamed:name ofType:MPResultTypeTemplatePhrase parameter:nil withCounter:1 return [self mpwResultForSiteNamed:name ofType:MPResultTypeTemplatePhrase parameter:nil withCounter:MPCounterValueInitial
variant:MPKeyPurposeRecovery context:question usingKey:key]; variant:MPKeyPurposeRecovery context:question usingKey:key];
} }
- (NSString *)mpwResultForSiteNamed:(NSString *)name ofType:(MPResultType)type parameter:(NSString *)parameter - (NSString *)mpwResultForSiteNamed:(NSString *)name ofType:(MPResultType)type parameter:(NSString *)parameter
withCounter:(NSUInteger)counter variant:(MPKeyPurpose)purpose context:(NSString *)context usingKey:(MPKey *)key { withCounter:(MPCounterValue)counter variant:(MPKeyPurpose)purpose context:(NSString *)context
usingKey:(MPKey *)key {
__block NSString *result = nil; __block NSString *result = nil;
[self mpw_perform:^{ [self mpw_perform:^{
char const *resultBytes = mpw_siteResult( [key keyForAlgorithm:self], char const *resultBytes = mpw_siteResult( [key keyForAlgorithm:self],
name.UTF8String, (uint32_t)counter, purpose, context.UTF8String, type, parameter.UTF8String, [self version] ); name.UTF8String, counter, purpose, context.UTF8String, type, parameter.UTF8String, [self version] );
if (resultBytes) { if (resultBytes) {
result = [NSString stringWithCString:resultBytes encoding:NSUTF8StringEncoding]; result = [NSString stringWithCString:resultBytes encoding:NSUTF8StringEncoding];
mpw_free_string( resultBytes ); mpw_free_string( resultBytes );
@ -490,7 +502,7 @@ static NSOperationQueue *_mpwQueue = nil;
break; break;
} }
NSUInteger counter = ((MPGeneratedSiteEntity *)site).counter; MPCounterValue counter = ((MPGeneratedSiteEntity *)site).counter;
PearlNotMainQueue( ^{ PearlNotMainQueue( ^{
resultBlock( [algorithm mpwTemplateForSiteNamed:name ofType:type withCounter:counter usingKey:key] ); resultBlock( [algorithm mpwTemplateForSiteNamed:name ofType:type withCounter:counter usingKey:key] );
@ -517,7 +529,12 @@ static NSOperationQueue *_mpwQueue = nil;
} ); } );
break; break;
} }
case MPResultTypeDeriveKey:
break;
} }
Throw( @"Type not supported: %lu", (long)type );
} }
- (void)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)key result:(void ( ^ )(NSString *result))resultBlock { - (void)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)key result:(void ( ^ )(NSString *result))resultBlock {

View File

@ -49,13 +49,16 @@ __END_DECLS
} \ } \
error; \ error; \
}) })
#else
#define MPError(error_, message, ...) ({ \
NSError *error = error_; \
err( message @"%@%@", ##__VA_ARGS__, error? @"\n": @"", [error fullDescription]?: @"" ); \
error; \
})
#endif
#define MPMakeError(message, ...) ({ \ #define MPMakeError(message, ...) ({ \
MPError( [NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{ \ MPError( [NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{ \
NSLocalizedDescriptionKey: strf( message, ##__VA_ARGS__ ) \ NSLocalizedDescriptionKey: strf( message, ##__VA_ARGS__ ) \
}], @"" ); \ }], @"" ); \
}) })
#else
#define MPError(error_, message, ...) ({ \
err( message @"%@%@", ##__VA_ARGS__, error_? @"\n": @"", [error_ fullDescription]?: @"" ); \
})
#endif

View File

@ -269,23 +269,22 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
[openPanel close]; [openPanel close];
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler: [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:
^(NSData *importedSitesData, NSURLResponse *response, NSError *error) { ^(NSData *importedSitesData, NSURLResponse *response, NSError *urlError) {
if (error) if (urlError)
MPError( error, @"While reading imported sites from %@.", url ); [[NSAlert alertWithError:MPError( urlError, @"While reading imported sites from %@.", url )] runModal];
if (!importedSitesData) if (!importedSitesData)
return; return;
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding]; NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
MPImportResult result = [self importSites:importedSitesString askImportPassword:^NSString *(NSString *userName) { [self importSites:importedSitesString askImportPassword:^NSString *(NSString *userName) {
__block NSString *masterPassword = nil; __block NSString *masterPassword = nil;
PearlMainQueueWait( ^{ PearlMainQueueWait( ^{
NSAlert *alert = [NSAlert new]; NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Unlock"]; [alert addButtonWithTitle:@"Unlock"];
[alert addButtonWithTitle:@"Cancel"]; [alert addButtonWithTitle:@"Cancel"];
alert.messageText = @"Import File's Master Password"; alert.messageText = strf( @"Importing Sites For\n%@", userName );
alert.informativeText = strf( @"%@'s export was done using a different master password.\n" alert.informativeText = @"Enter the master password used to create this export file.";
@"Enter that master password to unlock the exported data.", userName );
alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]; alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
[alert layout]; [alert layout];
if ([alert runModal] == NSAlertFirstButtonReturn) if ([alert runModal] == NSAlertFirstButtonReturn)
@ -293,16 +292,15 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
} ); } );
return masterPassword; return masterPassword;
} askUserPassword:^NSString *(NSString *userName, NSUInteger importCount, NSUInteger deleteCount) { } askUserPassword:^NSString *(NSString *userName) {
__block NSString *masterPassword = nil; __block NSString *masterPassword = nil;
PearlMainQueueWait( ^{ PearlMainQueueWait( ^{
NSAlert *alert = [NSAlert new]; NSAlert *alert = [NSAlert new];
[alert addButtonWithTitle:@"Import"]; [alert addButtonWithTitle:@"Import"];
[alert addButtonWithTitle:@"Cancel"]; [alert addButtonWithTitle:@"Cancel"];
alert.messageText = strf( @"Master Password for\n%@", userName ); alert.messageText = strf( @"Master Password For\n%@", userName );
alert.informativeText = strf( @"Imports %lu sites, overwriting %lu.", alert.informativeText = @"Enter the current master password for this user.";
(unsigned long)importCount, (unsigned long)deleteCount );
alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]; alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
[alert layout]; [alert layout];
if ([alert runModal] == NSAlertFirstButtonReturn) if ([alert runModal] == NSAlertFirstButtonReturn)
@ -310,37 +308,12 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
} ); } );
return masterPassword; return masterPassword;
}]; } result:^(NSError *error) {
PearlMainQueue( ^{
switch (result) {
case MPImportResultSuccess: {
[self updateUsers]; [self updateUsers];
NSAlert *alert = [NSAlert new]; if (error && !(error.domain == NSCocoaErrorDomain && error.code == NSUserCancelledError))
alert.messageText = @"Successfully imported sites."; [[NSAlert alertWithError:error] runModal];
[alert runModal]; }];
break;
}
case MPImportResultCancelled:
break;
case MPImportResultInternalError:
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey: @"Import failed because of an internal error."
}]] runModal];
break;
case MPImportResultMalformedInput:
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey: @"The import doesn't look like a Master Password export."
}]] runModal];
break;
case MPImportResultInvalidPassword:
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
NSLocalizedDescriptionKey: @"Incorrect master password for the import sites."
}]] runModal];
break;
}
} );
}] resume]; }] resume];
} }
@ -509,25 +482,43 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
if ([savePanel runModal] == NSFileHandlingPanelCancelButton) if ([savePanel runModal] == NSFileHandlingPanelCancelButton)
return; return;
NSError *coordinateError = nil; [self exportSitesRevealPasswords:revealPasswords
NSString *exportedSites = [self exportSitesRevealPasswords:revealPasswords]; askExportPassword:^NSString *(NSString *userName) {
[[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:savePanel.URL options:0 return PearlMainQueueAwait( ^id {
error:&coordinateError byAccessor: NSAlert *alert = [NSAlert new];
^(NSURL *newURL) { [alert addButtonWithTitle:@"Import"];
NSError *writeError = nil; [alert addButtonWithTitle:@"Cancel"];
if (![exportedSites writeToURL:newURL atomically:NO encoding:NSUTF8StringEncoding error:&writeError]) alert.messageText = strf( @"Master Password For\n%@", userName );
MPError( writeError, @"Could not write to the export file." ); alert.informativeText = @"Enter the current master password for this user.";
alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
[alert layout];
if ([alert runModal] == NSAlertFirstButtonReturn)
return ((NSTextField *)alert.accessoryView).stringValue;
else
return nil;
} );
} result:^(NSString *mpsites, NSError *error) {
if (!mpsites || error) {
PearlMainQueue( ^{ PearlMainQueue( ^{
[[NSAlert alertWithError:writeError] runModal]; [[NSAlert alertWithError:MPError( error, @"Failed to export mpsites." )] runModal];
} );
return;
}
NSError *coordinateError = nil;
[[[NSFileCoordinator alloc] initWithFilePresenter:nil]
coordinateWritingItemAtURL:savePanel.URL options:0 error:&coordinateError byAccessor:^(NSURL *newURL) {
NSError *writeError = nil;
if (![mpsites writeToURL:newURL atomically:NO encoding:NSUTF8StringEncoding error:&writeError])
PearlMainQueue( ^{
[[NSAlert alertWithError:MPError( writeError, @"Could not write to the export file." )] runModal];
} ); } );
}]; }];
if (coordinateError) { if (coordinateError)
MPError( coordinateError, @"Write access to the export file could not be obtained." );
PearlMainQueue( ^{ PearlMainQueue( ^{
[[NSAlert alertWithError:coordinateError] runModal]; [[NSAlert alertWithError:MPError( coordinateError, @"Could not gain access to the export file." )] runModal];
} ); } );
} }];
} }
- (void)updateUsers { - (void)updateUsers {

View File

@ -27,7 +27,7 @@
@property(nonatomic) NSString *name; @property(nonatomic) NSString *name;
@property(nonatomic) NSAttributedString *displayedName; @property(nonatomic) NSAttributedString *displayedName;
@property(nonatomic) MPSiteType type; @property(nonatomic) MPResultType type;
@property(nonatomic) NSString *typeName; @property(nonatomic) NSString *typeName;
@property(nonatomic) NSString *content; @property(nonatomic) NSString *content;
@property(nonatomic) NSString *displayedContent; @property(nonatomic) NSString *displayedContent;
@ -36,7 +36,7 @@
@property(nonatomic) NSString *loginName; @property(nonatomic) NSString *loginName;
@property(nonatomic) BOOL loginGenerated; @property(nonatomic) BOOL loginGenerated;
@property(nonatomic) NSNumber *uses; @property(nonatomic) NSNumber *uses;
@property(nonatomic) NSUInteger counter; @property(nonatomic) MPCounterValue counter;
@property(nonatomic) NSDate *lastUsed; @property(nonatomic) NSDate *lastUsed;
@property(nonatomic) id<MPAlgorithm> algorithm; @property(nonatomic) id<MPAlgorithm> algorithm;
@property(nonatomic) MPAlgorithmVersion algorithmVersion; @property(nonatomic) MPAlgorithmVersion algorithmVersion;

View File

@ -81,7 +81,7 @@
self.type = entity.type; self.type = entity.type;
self.typeName = entity.typeName; self.typeName = entity.typeName;
self.uses = entity.uses_; self.uses = entity.uses_;
self.counter = [entity isKindOfClass:[MPGeneratedSiteEntity class]]? [(MPGeneratedSiteEntity *)entity counter]: 0; self.counter = [entity isKindOfClass:[MPGeneratedSiteEntity class]]? [(MPGeneratedSiteEntity *)entity counter]: MPCounterValueInitial;
self.loginGenerated = entity.loginGenerated; self.loginGenerated = entity.loginGenerated;
// Find all password types and the index of the current type amongst them. // Find all password types and the index of the current type amongst them.
@ -104,7 +104,7 @@
self.type = user.defaultType; self.type = user.defaultType;
self.typeName = [self.algorithm nameOfType:self.type]; self.typeName = [self.algorithm nameOfType:self.type];
self.uses = @0; self.uses = @0;
self.counter = 1; self.counter = MPCounterValueDefault;
// Find all password types and the index of the current type amongst them. // Find all password types and the index of the current type amongst them.
[self updateContent]; [self updateContent];
@ -116,14 +116,14 @@
return nil; return nil;
NSError *error; NSError *error;
MPSiteEntity *entity = (MPSiteEntity *)[moc existingObjectWithID:self.entityOID error:&error]; MPSiteEntity *entity = [moc existingObjectWithID:self.entityOID error:&error];
if (!entity) if (!entity)
MPError( error, @"Couldn't retrieve active site." ); MPError( error, @"Couldn't retrieve active site." );
return entity; return entity;
} }
- (void)setCounter:(NSUInteger)counter { - (void)setCounter:(MPCounterValue)counter {
if (self.counter == counter) if (self.counter == counter)
return; return;
@ -210,12 +210,12 @@
- (BOOL)generated { - (BOOL)generated {
return self.type & MPSiteTypeClassGenerated; return self.type & MPResultTypeClassTemplate;
} }
- (BOOL)stored { - (BOOL)stored {
return self.type & MPSiteTypeClassStored; return self.type & MPResultTypeClassStateful;
} }
- (BOOL)transient { - (BOOL)transient {

View File

@ -373,9 +373,9 @@
NSArray *types = [site.algorithm allTypes]; NSArray *types = [site.algorithm allTypes];
[self.passwordTypesMatrix renewRows:(NSInteger)[types count] columns:1]; [self.passwordTypesMatrix renewRows:(NSInteger)[types count] columns:1];
for (NSUInteger t = 0; t < [types count]; ++t) { for (NSUInteger t = 0; t < [types count]; ++t) {
MPSiteType type = (MPSiteType)[types[t] unsignedIntegerValue]; MPResultType type = (MPResultType)[types[t] unsignedIntegerValue];
NSString *title = [site.algorithm nameOfType:type]; NSString *title = [site.algorithm nameOfType:type];
if (type & MPSiteTypeClassGenerated) if (type & MPResultTypeClassTemplate)
title = strf( @"%@ %@", [site.algorithm mpwTemplateForSiteNamed:site.name ofType:type withCounter:site.counter title = strf( @"%@ %@", [site.algorithm mpwTemplateForSiteNamed:site.name ofType:type withCounter:site.counter
usingKey:[MPMacAppDelegate get].key], title ); usingKey:[MPMacAppDelegate get].key], title );
@ -397,7 +397,7 @@
switch (returnCode) { switch (returnCode) {
case NSAlertFirstButtonReturn: { case NSAlertFirstButtonReturn: {
// "Save" button. // "Save" button.
MPSiteType type = (MPSiteType)[self.passwordTypesMatrix.selectedCell tag]; MPResultType type = (MPResultType)[self.passwordTypesMatrix.selectedCell tag];
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *entity = [[MPMacAppDelegate get] changeSite:[self.selectedSite entityInContext:context] MPSiteEntity *entity = [[MPMacAppDelegate get] changeSite:[self.selectedSite entityInContext:context]
saveInContext:context toType:type]; saveInContext:context toType:type];

View File

@ -436,17 +436,15 @@
[self exportSitesRevealPasswords:revealPasswords askExportPassword:^NSString *(NSString *userName) { [self exportSitesRevealPasswords:revealPasswords askExportPassword:^NSString *(NSString *userName) {
return PearlAwait( ^(void (^setResult)(id)) { return PearlAwait( ^(void (^setResult)(id)) {
[PearlAlert showAlertWithTitle:@"Import File's Master Password" [PearlAlert showAlertWithTitle:strf( @"Master Password For:\n%@", userName )
message:strf( @"%@'s export was done using a different master password.\n" message:@"Enter the user's master password to create an export file."
@"Enter that master password to unlock the exported data.", userName )
viewStyle:UIAlertViewStyleSecureTextInput viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) { initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
if (buttonIndex_ == [alert_ cancelButtonIndex]) if (buttonIndex_ == [alert_ cancelButtonIndex])
setResult( nil ); setResult( nil );
else else
setResult( [alert_ textFieldAtIndex:0].text ); setResult( [alert_ textFieldAtIndex:0].text );
} } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Export", nil];
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Unlock Import", nil];
} ); } );
} result:^(NSString *mpsites, NSError *error) { } result:^(NSString *mpsites, NSError *error) {
if (!mpsites || error) { if (!mpsites || error) {