Fixed critical issue with storing passwords & inconsistency recovery + dictation support.
[FIXED] Fixed an issue that caused stored passwords to be saved without encryption. [ADDED] Logic to check for any data inconsistencies and fix them. [ADDED] Support for using dictation in site search box.
This commit is contained in:
parent
fc82790b8c
commit
9fee4a2bbe
Binary file not shown.
@ -7,7 +7,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Crashlytics</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.crashlytics.sdk.mac</string>
|
||||
<string>com.crashlytics.ios</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
@ -15,16 +15,16 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.1.2</string>
|
||||
<string>2.1.7</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>macosx</string>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>9</string>
|
||||
<string>26</string>
|
||||
<key>DTPlatformName</key>
|
||||
<string>macosx</string>
|
||||
<string>iphoneos</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>10.6</string>
|
||||
<string>4.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
BIN
External/Crashlytics.framework/run
vendored
BIN
External/Crashlytics.framework/run
vendored
Binary file not shown.
@ -37,6 +37,8 @@
|
||||
- (NSString *)shortNameOfType:(MPElementType)type;
|
||||
- (NSString *)classNameOfType:(MPElementType)type;
|
||||
- (Class)classOfType:(MPElementType)type;
|
||||
- (NSArray *)allTypes;
|
||||
- (NSArray *)allTypesStartingWith:(MPElementType)startingType;
|
||||
- (MPElementType)nextType:(MPElementType)type;
|
||||
- (MPElementType)previousType:(MPElementType)type;
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||
*
|
||||
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
*
|
||||
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
*/
|
||||
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||
*
|
||||
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
*
|
||||
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
*/
|
||||
|
||||
//
|
||||
// MPAlgorithmV0
|
||||
@ -53,7 +53,7 @@
|
||||
migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", self.version, user];
|
||||
NSArray *migrationElements = [moc executeFetchRequest:migrationRequest error:&error];
|
||||
if (!migrationElements) {
|
||||
err(@"While looking for elements to migrate: %@", error);
|
||||
err( @"While looking for elements to migrate: %@", error );
|
||||
return NO;
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit {
|
||||
|
||||
if (element.version != [self version] - 1)
|
||||
// Only migrate from previous version.
|
||||
// Only migrate from previous version.
|
||||
return NO;
|
||||
|
||||
if (!explicit) {
|
||||
@ -85,18 +85,19 @@
|
||||
|
||||
- (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName {
|
||||
|
||||
uint32_t nuserNameLength = htonl(userName.length);
|
||||
uint32_t nuserNameLength = htonl( userName.length );
|
||||
NSDate *start = [NSDate date];
|
||||
NSData *keyData = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding]
|
||||
usingSalt:[NSData dataByConcatenatingDatas:
|
||||
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
[NSData dataWithBytes:&nuserNameLength
|
||||
length:sizeof(nuserNameLength)],
|
||||
[userName dataUsingEncoding:NSUTF8StringEncoding],
|
||||
nil] N:MP_N r:MP_r p:MP_p];
|
||||
usingSalt:[NSData dataByConcatenatingDatas:
|
||||
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
[NSData dataWithBytes:&nuserNameLength
|
||||
length:sizeof( nuserNameLength )],
|
||||
[userName dataUsingEncoding:NSUTF8StringEncoding],
|
||||
nil] N:MP_N r:MP_r p:MP_p];
|
||||
|
||||
MPKey *key = [self keyFromKeyData:keyData];
|
||||
trc(@"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", userName, password, [key.keyID encodeHex], -[start timeIntervalSinceNow]);
|
||||
trc( @"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", userName, password, [key.keyID encodeHex],
|
||||
-[start timeIntervalSinceNow] );
|
||||
|
||||
return key;
|
||||
}
|
||||
@ -142,7 +143,7 @@
|
||||
return @"Device Private Password";
|
||||
}
|
||||
|
||||
Throw(@"Type not supported: %lu", (long)type);
|
||||
Throw( @"Type not supported: %lu", (long)type );
|
||||
}
|
||||
|
||||
- (NSString *)shortNameOfType:(MPElementType)type {
|
||||
@ -176,7 +177,7 @@
|
||||
return @"Device";
|
||||
}
|
||||
|
||||
Throw(@"Type not supported: %lu", (long)type);
|
||||
Throw( @"Type not supported: %lu", (long)type );
|
||||
}
|
||||
|
||||
- (NSString *)classNameOfType:(MPElementType)type {
|
||||
@ -187,7 +188,7 @@
|
||||
- (Class)classOfType:(MPElementType)type {
|
||||
|
||||
if (!type)
|
||||
Throw(@"No type given.");
|
||||
Throw( @"No type given." );
|
||||
|
||||
switch (type) {
|
||||
case MPElementTypeGeneratedMaximum:
|
||||
@ -215,14 +216,27 @@
|
||||
return [MPElementStoredEntity class];
|
||||
}
|
||||
|
||||
Throw(@"Type not supported: %lu", (long)type);
|
||||
Throw( @"Type not supported: %lu", (long)type );
|
||||
}
|
||||
|
||||
- (NSArray *)allTypes {
|
||||
|
||||
return [self allTypesStartingWith:MPElementTypeGeneratedMaximum];
|
||||
}
|
||||
|
||||
- (NSArray *)allTypesStartingWith:(MPElementType)startingType {
|
||||
|
||||
NSMutableArray *allTypes = [[NSMutableArray alloc] initWithCapacity:8];
|
||||
MPElementType currentType = startingType;
|
||||
do {
|
||||
[allTypes addObject:@(currentType)];
|
||||
} while ((currentType = [self nextType:currentType]) != startingType);
|
||||
|
||||
return allTypes;
|
||||
}
|
||||
|
||||
- (MPElementType)nextType:(MPElementType)type {
|
||||
|
||||
if (!type)
|
||||
Throw(@"No type given.");
|
||||
|
||||
switch (type) {
|
||||
case MPElementTypeGeneratedMaximum:
|
||||
return MPElementTypeStoredDevicePrivate;
|
||||
@ -240,9 +254,9 @@
|
||||
return MPElementTypeGeneratedPIN;
|
||||
case MPElementTypeStoredDevicePrivate:
|
||||
return MPElementTypeStoredPersonal;
|
||||
default:
|
||||
return MPElementTypeGeneratedLong;
|
||||
}
|
||||
|
||||
Throw(@"Type not supported: %lu", (long)type);
|
||||
}
|
||||
|
||||
- (MPElementType)previousType:(MPElementType)type {
|
||||
@ -262,38 +276,38 @@
|
||||
[[NSBundle mainBundle] URLForResource:@"ciphers" withExtension:@"plist"]];
|
||||
|
||||
// Determine the seed whose bytes will be used for calculating a password
|
||||
uint32_t ncounter = htonl(counter), nnameLength = htonl(name.length);
|
||||
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof(ncounter)];
|
||||
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof(nnameLength)];
|
||||
trc(@"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64],
|
||||
[nameLengthBytes encodeHex], name, [counterBytes encodeHex]);
|
||||
uint32_t ncounter = htonl( counter ), nnameLength = htonl( name.length );
|
||||
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof( ncounter )];
|
||||
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof( nnameLength )];
|
||||
trc( @"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64],
|
||||
[nameLengthBytes encodeHex], name, [counterBytes encodeHex] );
|
||||
NSData *seed = [[NSData dataByConcatenatingDatas:
|
||||
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
nameLengthBytes, [name dataUsingEncoding:NSUTF8StringEncoding],
|
||||
counterBytes, nil]
|
||||
nameLengthBytes, [name dataUsingEncoding:NSUTF8StringEncoding],
|
||||
counterBytes, nil]
|
||||
hmacWith:PearlHashSHA256 key:key.keyData];
|
||||
trc(@"seed is: %@", [seed encodeBase64]);
|
||||
trc( @"seed is: %@", [seed encodeBase64] );
|
||||
const char *seedBytes = seed.bytes;
|
||||
|
||||
// Determine the cipher from the first seed byte.
|
||||
NSAssert([seed length], @"Missing seed.");
|
||||
NSAssert( [seed length], @"Missing seed." );
|
||||
NSString *typeClass = [self classNameOfType:type];
|
||||
NSString *typeName = [self nameOfType:type];
|
||||
id classCiphers = [MPTypes_ciphers valueForKey:typeClass];
|
||||
NSArray *typeCiphers = [classCiphers valueForKey:typeName];
|
||||
NSString *cipher = typeCiphers[htons(seedBytes[0]) % [typeCiphers count]];
|
||||
trc(@"type %@, ciphers: %@, selected: %@", typeName, typeCiphers, cipher);
|
||||
NSString *cipher = typeCiphers[htons( seedBytes[0] ) % [typeCiphers count]];
|
||||
trc( @"type %@, ciphers: %@, selected: %@", typeName, typeCiphers, cipher );
|
||||
|
||||
// Encode the content, character by character, using subsequent seed bytes and the cipher.
|
||||
NSAssert([seed length] >= [cipher length] + 1, @"Insufficient seed bytes to encode cipher.");
|
||||
NSAssert( [seed length] >= [cipher length] + 1, @"Insufficient seed bytes to encode cipher." );
|
||||
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
|
||||
for (NSUInteger c = 0; c < [cipher length]; ++c) {
|
||||
uint16_t keyByte = htons(seedBytes[c + 1]);
|
||||
uint16_t keyByte = htons( seedBytes[c + 1] );
|
||||
NSString *cipherClass = [cipher substringWithRange:NSMakeRange( c, 1 )];
|
||||
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
|
||||
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange( keyByte % [cipherClassCharacters length], 1 )];
|
||||
|
||||
trc(@"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character);
|
||||
trc( @"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character );
|
||||
[content appendString:character];
|
||||
}
|
||||
|
||||
@ -307,7 +321,7 @@
|
||||
|
||||
- (void)saveContent:(NSString *)clearContent toElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
|
||||
|
||||
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
|
||||
NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." );
|
||||
switch (element.type) {
|
||||
case MPElementTypeGeneratedMaximum:
|
||||
case MPElementTypeGeneratedLong:
|
||||
@ -315,13 +329,14 @@
|
||||
case MPElementTypeGeneratedBasic:
|
||||
case MPElementTypeGeneratedShort:
|
||||
case MPElementTypeGeneratedPIN: {
|
||||
NSAssert(NO, @"Cannot save content to element with generated type %lu.", (long)element.type);
|
||||
NSAssert( NO, @"Cannot save content to element with generated type %lu.", (long)element.type );
|
||||
break;
|
||||
}
|
||||
|
||||
case MPElementTypeStoredPersonal: {
|
||||
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
|
||||
NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
|
||||
[element class] );
|
||||
|
||||
NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding]
|
||||
encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
|
||||
@ -329,8 +344,9 @@
|
||||
break;
|
||||
}
|
||||
case MPElementTypeStoredDevicePrivate: {
|
||||
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
|
||||
NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
|
||||
[element class] );
|
||||
|
||||
NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding]
|
||||
encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
|
||||
@ -364,9 +380,9 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)resolveContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey result:(void (^)(NSString *result))resultBlock {
|
||||
- (void)resolveContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey result:(void ( ^ )(NSString *result))resultBlock {
|
||||
|
||||
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
|
||||
NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." );
|
||||
switch (element.type) {
|
||||
case MPElementTypeGeneratedMaximum:
|
||||
case MPElementTypeGeneratedLong:
|
||||
@ -374,17 +390,18 @@
|
||||
case MPElementTypeGeneratedBasic:
|
||||
case MPElementTypeGeneratedShort:
|
||||
case MPElementTypeGeneratedPIN: {
|
||||
NSAssert([element isKindOfClass:[MPElementGeneratedEntity class]],
|
||||
@"Element with generated type %lu is not an MPElementGeneratedEntity, but a %@.", (long)element.type, [element class]);
|
||||
NSAssert( [element isKindOfClass:[MPElementGeneratedEntity class]],
|
||||
@"Element with generated type %lu is not an MPElementGeneratedEntity, but a %@.", (long)element.type,
|
||||
[element class] );
|
||||
|
||||
NSString *name = element.name;
|
||||
MPElementType type = element.type;
|
||||
NSUInteger counter = ((MPElementGeneratedEntity *)element).counter;
|
||||
id<MPAlgorithm> algorithm = nil;
|
||||
if (!element.name.length)
|
||||
err(@"Missing name.");
|
||||
err( @"Missing name." );
|
||||
else if (!elementKey.keyData.length)
|
||||
err(@"Missing key.");
|
||||
err( @"Missing key." );
|
||||
else
|
||||
algorithm = element.algorithm;
|
||||
|
||||
@ -396,8 +413,9 @@
|
||||
}
|
||||
|
||||
case MPElementTypeStoredPersonal: {
|
||||
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
|
||||
NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
|
||||
[element class] );
|
||||
|
||||
NSData *encryptedContent = ((MPElementStoredEntity *)element).contentObject;
|
||||
|
||||
@ -408,8 +426,9 @@
|
||||
break;
|
||||
}
|
||||
case MPElementTypeStoredDevicePrivate: {
|
||||
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
|
||||
NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
|
||||
[element class] );
|
||||
|
||||
NSDictionary *elementQuery = [self queryForDevicePrivateElementNamed:element.name];
|
||||
NSData *encryptedContent = [PearlKeyChain dataOfItemForQuery:elementQuery];
|
||||
@ -426,7 +445,7 @@
|
||||
- (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)importKey
|
||||
intoElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
|
||||
|
||||
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
|
||||
NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." );
|
||||
switch (element.type) {
|
||||
case MPElementTypeGeneratedMaximum:
|
||||
case MPElementTypeGeneratedLong:
|
||||
@ -437,8 +456,9 @@
|
||||
break;
|
||||
|
||||
case MPElementTypeStoredPersonal: {
|
||||
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
|
||||
NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
|
||||
[element class] );
|
||||
if ([importKey.keyID isEqualToData:elementKey.keyID])
|
||||
((MPElementStoredEntity *)element).contentObject = [protectedContent decodeBase64];
|
||||
|
||||
@ -456,7 +476,7 @@
|
||||
|
||||
- (void)importClearTextContent:(NSString *)clearContent intoElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
|
||||
|
||||
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
|
||||
NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." );
|
||||
switch (element.type) {
|
||||
case MPElementTypeGeneratedMaximum:
|
||||
case MPElementTypeGeneratedLong:
|
||||
@ -478,7 +498,7 @@
|
||||
|
||||
- (NSString *)exportContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey {
|
||||
|
||||
NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user.");
|
||||
NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." );
|
||||
if (!(element.type & MPElementFeatureExportContent))
|
||||
return nil;
|
||||
|
||||
@ -495,8 +515,9 @@
|
||||
}
|
||||
|
||||
case MPElementTypeStoredPersonal: {
|
||||
NSAssert([element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]);
|
||||
NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
|
||||
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
|
||||
[element class] );
|
||||
result = [((MPElementStoredEntity *)element).contentObject encodeBase64];
|
||||
break;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:@{
|
||||
(__bridge id)kSecAttrService : @"Saved Master Password",
|
||||
(__bridge id)kSecAttrAccount : IfNotNilElse(user.name, @"")
|
||||
(__bridge id)kSecAttrAccount : IfNotNilElse( user.name, @"" )
|
||||
}
|
||||
matches:nil];
|
||||
}
|
||||
@ -25,11 +25,11 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
|
||||
NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )];
|
||||
if (!keyData) {
|
||||
inf(@"No key found in keychain for: %@", user.userID);
|
||||
inf( @"No key found in keychain for: %@", user.userID );
|
||||
return nil;
|
||||
}
|
||||
|
||||
inf(@"Found key in keychain for: %@", user.userID);
|
||||
inf( @"Found key in keychain for: %@", user.userID );
|
||||
return [MPAlgorithmDefault keyFromKeyData:keyData];
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
NSData *existingKeyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )];
|
||||
|
||||
if (![existingKeyData isEqualToData:self.key.keyData]) {
|
||||
inf(@"Saving key in keychain for: %@", user.userID);
|
||||
inf( @"Saving key in keychain for: %@", user.userID );
|
||||
|
||||
[PearlKeyChain addOrUpdateItemForQuery:keyQuery( user )
|
||||
withAttributes:@{
|
||||
@ -56,7 +56,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
|
||||
OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery( user )];
|
||||
if (result == noErr) {
|
||||
inf(@"Removed key from keychain for: %@", user.userID);
|
||||
inf( @"Removed key from keychain for: %@", user.userID );
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPKeyForgottenNotification object:self];
|
||||
}
|
||||
@ -74,7 +74,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
- (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password {
|
||||
|
||||
if (password)
|
||||
NSAssert(![NSThread isMainThread], @"Computing key must not happen from the main thread.");
|
||||
NSAssert( ![NSThread isMainThread], @"Computing key must not happen from the main thread." );
|
||||
if (!user)
|
||||
return NO;
|
||||
|
||||
@ -92,14 +92,14 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
|
||||
// Method 2: Depending on the user's saveKey, load or remove the key from the keychain.
|
||||
if (!user.saveKey)
|
||||
// Key should not be stored in keychain. Delete it.
|
||||
// Key should not be stored in keychain. Delete it.
|
||||
[self forgetSavedKeyFor:user];
|
||||
|
||||
else if (!tryKey) {
|
||||
// Key should be saved in keychain. Load it.
|
||||
if ((tryKey = [self loadSavedKeyFor:user]) && ![user.keyID isEqual:tryKey.keyID]) {
|
||||
// Loaded password doesn't match user's keyID. Forget saved password: it is incorrect.
|
||||
inf(@"Saved password doesn't match keyID for: %@", user.userID);
|
||||
inf( @"Saved password doesn't match keyID for: %@", user.userID );
|
||||
|
||||
tryKey = nil;
|
||||
[self forgetSavedKeyFor:user];
|
||||
@ -110,7 +110,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
if (!tryKey) {
|
||||
if ([password length]) if ((tryKey = [MPAlgorithmDefault keyForPassword:password
|
||||
ofUserNamed:user.name])) if (![user.keyID isEqual:tryKey.keyID]) {
|
||||
inf(@"Key derived from password doesn't match keyID for: %@", user.userID);
|
||||
inf( @"Key derived from password doesn't match keyID for: %@", user.userID );
|
||||
|
||||
tryKey = nil;
|
||||
}
|
||||
@ -119,13 +119,13 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
// No more methods left, fail if key still not known.
|
||||
if (!tryKey) {
|
||||
if (password) {
|
||||
inf(@"Login failed for: %@", user.userID);
|
||||
inf( @"Login failed for: %@", user.userID );
|
||||
MPCheckpoint( MPCheckpointSignInFailed, nil );
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
inf(@"Logged in: %@", user.userID);
|
||||
inf( @"Logged in: %@", user.userID );
|
||||
|
||||
if (![self.key isEqualToKey:tryKey]) {
|
||||
self.key = tryKey;
|
||||
@ -147,13 +147,19 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
}
|
||||
}
|
||||
@catch (id exception) {
|
||||
err(@"While setting username: %@", exception);
|
||||
err( @"While setting username: %@", exception );
|
||||
}
|
||||
|
||||
user.lastUsed = [NSDate date];
|
||||
[moc saveToStore];
|
||||
self.activeUser = user;
|
||||
|
||||
// Perform a data sanity check now that we're logged in as the user to allow fixes that require the user's key.
|
||||
if ([[MPConfig get].checkInconsistency boolValue])
|
||||
[MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
|
||||
[self findAndFixInconsistenciesSaveInContext:context];
|
||||
}];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPSignedInNotification object:self];
|
||||
MPCheckpoint( MPCheckpointSignedIn, nil );
|
||||
|
||||
@ -163,12 +169,13 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
- (void)migrateElementsForUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc toKey:(MPKey *)newKey {
|
||||
|
||||
if (![user.elements count])
|
||||
// Nothing to migrate.
|
||||
// Nothing to migrate.
|
||||
return;
|
||||
|
||||
MPKey *recoverKey = newKey;
|
||||
#ifdef PEARL_UIKIT
|
||||
PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:PearlString( @"Migrating %ld sites...", (long)[user.elements count] )];
|
||||
PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:PearlString( @"Migrating %ld sites...",
|
||||
(long)[user.elements count] )];
|
||||
#endif
|
||||
|
||||
for (MPElementEntity *element in user.elements) {
|
||||
@ -183,12 +190,12 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
dispatch_group_enter( recoverPasswordGroup );
|
||||
[PearlAlert showAlertWithTitle:@"Enter Old Master Password"
|
||||
message:PearlString( @"Your old master password is required to migrate the stored password for %@",
|
||||
element.name )
|
||||
element.name )
|
||||
viewStyle:UIAlertViewStyleSecureTextInput
|
||||
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||
@try {
|
||||
if (buttonIndex_ == [alert_ cancelButtonIndex])
|
||||
// Don't Migrate
|
||||
// Don't Migrate
|
||||
return;
|
||||
|
||||
masterPassword = [alert_ textFieldAtIndex:0].text;
|
||||
@ -200,14 +207,14 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
dispatch_group_wait( recoverPasswordGroup, DISPATCH_TIME_FOREVER );
|
||||
#endif
|
||||
if (!masterPassword)
|
||||
// Don't Migrate
|
||||
// Don't Migrate
|
||||
break;
|
||||
|
||||
recoverKey = [element.algorithm keyForPassword:masterPassword ofUserNamed:user.name];
|
||||
}
|
||||
|
||||
if (!content)
|
||||
// Don't Migrate
|
||||
// Don't Migrate
|
||||
break;
|
||||
|
||||
if (![recoverKey isEqualToKey:newKey])
|
||||
|
@ -9,6 +9,7 @@
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
#import "UbiquityStoreManager.h"
|
||||
#import "MPFixable.h"
|
||||
|
||||
typedef enum {
|
||||
MPImportResultSuccess,
|
||||
@ -27,6 +28,7 @@ typedef enum {
|
||||
+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock;
|
||||
|
||||
- (UbiquityStoreManager *)storeManager;
|
||||
- (MPFixableResult)findAndFixInconsistenciesSaveInContext:(NSManagedObjectContext *)context;
|
||||
|
||||
/** @param completion The block to execute after adding the element, executed from the main thread with the new element in the main MOC. */
|
||||
- (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion;
|
||||
|
@ -18,13 +18,13 @@
|
||||
#define MPMigrationLevelLocalStoreKey @"MPMigrationLevelLocalStoreKey"
|
||||
#define MPMigrationLevelCloudStoreKey @"MPMigrationLevelCloudStoreKey"
|
||||
|
||||
typedef NS_ENUM(NSInteger, MPMigrationLevelLocalStore) {
|
||||
typedef NS_ENUM( NSInteger, MPMigrationLevelLocalStore ) {
|
||||
MPMigrationLevelLocalStoreV1,
|
||||
MPMigrationLevelLocalStoreV2,
|
||||
MPMigrationLevelLocalStoreCurrent = MPMigrationLevelLocalStoreV2,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, MPMigrationLevelCloudStore) {
|
||||
typedef NS_ENUM( NSInteger, MPMigrationLevelCloudStore ) {
|
||||
MPMigrationLevelCloudStoreV1,
|
||||
MPMigrationLevelCloudStoreV2,
|
||||
MPMigrationLevelCloudStoreV3,
|
||||
@ -32,16 +32,18 @@ typedef NS_ENUM(NSInteger, MPMigrationLevelCloudStore) {
|
||||
};
|
||||
|
||||
@implementation MPAppDelegate_Shared(Store)
|
||||
PearlAssociatedObjectProperty(id, SaveObserver, saveObserver);
|
||||
PearlAssociatedObjectProperty(NSManagedObjectContext*, PrivateManagedObjectContext, privateManagedObjectContext);
|
||||
PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, mainManagedObjectContext);
|
||||
|
||||
PearlAssociatedObjectProperty( id, SaveObserver, saveObserver );
|
||||
|
||||
PearlAssociatedObjectProperty( NSManagedObjectContext*, PrivateManagedObjectContext, privateManagedObjectContext );
|
||||
|
||||
PearlAssociatedObjectProperty( NSManagedObjectContext*, MainManagedObjectContext, mainManagedObjectContext );
|
||||
|
||||
#pragma mark - Core Data setup
|
||||
|
||||
+ (NSManagedObjectContext *)managedObjectContextForMainThreadIfReady {
|
||||
|
||||
NSAssert([[NSThread currentThread] isMainThread], @"Can only access main MOC from the main thread.");
|
||||
NSAssert( [[NSThread currentThread] isMainThread], @"Can only access main MOC from the main thread." );
|
||||
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
|
||||
if (!mainManagedObjectContext || ![[NSThread currentThread] isMainThread])
|
||||
return nil;
|
||||
@ -49,7 +51,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
return mainManagedObjectContext;
|
||||
}
|
||||
|
||||
+ (BOOL)managedObjectContextForMainThreadPerformBlock:(void (^)(NSManagedObjectContext *mainContext))mocBlock {
|
||||
+ (BOOL)managedObjectContextForMainThreadPerformBlock:(void ( ^ )(NSManagedObjectContext *mainContext))mocBlock {
|
||||
|
||||
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
|
||||
if (!mainManagedObjectContext)
|
||||
@ -62,7 +64,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void (^)(NSManagedObjectContext *mainContext))mocBlock {
|
||||
+ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void ( ^ )(NSManagedObjectContext *mainContext))mocBlock {
|
||||
|
||||
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
|
||||
if (!mainManagedObjectContext)
|
||||
@ -75,7 +77,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *context))mocBlock {
|
||||
+ (BOOL)managedObjectContextPerformBlock:(void ( ^ )(NSManagedObjectContext *context))mocBlock {
|
||||
|
||||
NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady];
|
||||
if (!privateManagedObjectContextIfReady)
|
||||
@ -90,7 +92,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock {
|
||||
+ (BOOL)managedObjectContextPerformBlockAndWait:(void ( ^ )(NSManagedObjectContext *context))mocBlock {
|
||||
|
||||
NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady];
|
||||
if (!privateManagedObjectContextIfReady)
|
||||
@ -132,14 +134,14 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification object:UIApp
|
||||
queue:[NSOperationQueue mainQueue] usingBlock:
|
||||
^(NSNotification *note) {
|
||||
[[self mainManagedObjectContext] saveToStore];
|
||||
}];
|
||||
[[self mainManagedObjectContext] saveToStore];
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserverForName:UIApplicationWillResignActiveNotification object:UIApp
|
||||
queue:[NSOperationQueue mainQueue] usingBlock:
|
||||
^(NSNotification *note) {
|
||||
[[self mainManagedObjectContext] saveToStore];
|
||||
}];
|
||||
[[self mainManagedObjectContext] saveToStore];
|
||||
}];
|
||||
#else
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification object:NSApp
|
||||
queue:[NSOperationQueue mainQueue] usingBlock:
|
||||
@ -151,6 +153,41 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
return storeManager;
|
||||
}
|
||||
|
||||
- (MPFixableResult)findAndFixInconsistenciesSaveInContext:(NSManagedObjectContext *)context {
|
||||
|
||||
NSError *error = nil;
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest new];
|
||||
fetchRequest.fetchBatchSize = 50;
|
||||
|
||||
MPFixableResult result = MPFixableResultNoProblems;
|
||||
for (NSEntityDescription *entity in [context.persistentStoreCoordinator.managedObjectModel entities])
|
||||
if (class_conformsToProtocol( NSClassFromString( entity.managedObjectClassName ), @protocol(MPFixable) )) {
|
||||
fetchRequest.entity = entity;
|
||||
NSArray *objects = [context executeFetchRequest:fetchRequest error:&error];
|
||||
if (!objects) {
|
||||
err( @"Failed to fetch %@ objects: %@", entity, error );
|
||||
continue;
|
||||
}
|
||||
|
||||
for (NSManagedObject<MPFixable> *object in objects)
|
||||
result = MPApplyFix( result, ^MPFixableResult {
|
||||
return [object findAndFixInconsistenciesInContext:context];
|
||||
} );
|
||||
}
|
||||
|
||||
if (result == MPFixableResultNoProblems)
|
||||
inf( @"Sanity check found no problems in store." );
|
||||
|
||||
else {
|
||||
[context saveToStore];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPFoundInconsistenciesNotification object:nil userInfo:@{
|
||||
MPInconsistenciesFixResultUserKey : @(result)
|
||||
}];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)migrateStoreForManager:(UbiquityStoreManager *)manager isCloud:(BOOL)isCloudStore {
|
||||
|
||||
[self migrateLocalStore];
|
||||
@ -163,42 +200,42 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
|
||||
MPMigrationLevelLocalStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelLocalStoreKey];
|
||||
if (migrationLevel >= MPMigrationLevelLocalStoreCurrent)
|
||||
// Local store up-to-date.
|
||||
// Local store up-to-date.
|
||||
return;
|
||||
|
||||
inf(@"Local store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelLocalStoreCurrent);
|
||||
inf( @"Local store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelLocalStoreCurrent );
|
||||
if (migrationLevel <= MPMigrationLevelLocalStoreV1) if (![self migrateV1LocalStore]) {
|
||||
inf(@"Failed to migrate old V1 to new local store.");
|
||||
inf( @"Failed to migrate old V1 to new local store." );
|
||||
return;
|
||||
}
|
||||
|
||||
[[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelLocalStoreCurrent forKey:MPMigrationLevelLocalStoreKey];
|
||||
inf(@"Successfully migrated old to new local store.");
|
||||
inf( @"Successfully migrated old to new local store." );
|
||||
}
|
||||
|
||||
- (void)migrateCloudStore {
|
||||
|
||||
MPMigrationLevelCloudStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelCloudStoreKey];
|
||||
if (migrationLevel >= MPMigrationLevelCloudStoreCurrent)
|
||||
// Cloud store up-to-date.
|
||||
// Cloud store up-to-date.
|
||||
return;
|
||||
|
||||
inf(@"Cloud store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelCloudStoreCurrent);
|
||||
inf( @"Cloud store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelCloudStoreCurrent );
|
||||
if (migrationLevel <= MPMigrationLevelCloudStoreV1) {
|
||||
if (![self migrateV1CloudStore]) {
|
||||
inf(@"Failed to migrate old V1 to new cloud store.");
|
||||
inf( @"Failed to migrate old V1 to new cloud store." );
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (migrationLevel <= MPMigrationLevelCloudStoreV2) {
|
||||
if (![self migrateV2CloudStore]) {
|
||||
inf(@"Failed to migrate old V2 to new cloud store.");
|
||||
inf( @"Failed to migrate old V2 to new cloud store." );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelCloudStoreCurrent forKey:MPMigrationLevelCloudStoreKey];
|
||||
inf(@"Successfully migrated old to new cloud store.");
|
||||
inf( @"Successfully migrated old to new cloud store." );
|
||||
}
|
||||
|
||||
- (BOOL)migrateV1CloudStore {
|
||||
@ -211,11 +248,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
// Migrate cloud store.
|
||||
NSString *uuid = [[NSUserDefaults standardUserDefaults] stringForKey:@"LocalUUIDKey"];
|
||||
if (!uuid) {
|
||||
inf(@"No V1 cloud store to migrate.");
|
||||
inf( @"No V1 cloud store to migrate." );
|
||||
return YES;
|
||||
}
|
||||
|
||||
inf(@"Migrating V1 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"]);
|
||||
inf( @"Migrating V1 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"] );
|
||||
NSURL *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:MPCloudContainerIdentifier];
|
||||
NSURL *oldCloudContentURL = [[cloudContainerURL
|
||||
URLByAppendingPathComponent:@"Data" isDirectory:YES]
|
||||
@ -232,11 +269,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
// Migrate cloud store.
|
||||
NSString *uuid = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:@"USMStoreUUIDKey"];
|
||||
if (!uuid) {
|
||||
inf(@"No V2 cloud store to migrate.");
|
||||
inf( @"No V2 cloud store to migrate." );
|
||||
return YES;
|
||||
}
|
||||
|
||||
inf(@"Migrating V2 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"]);
|
||||
inf( @"Migrating V2 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"] );
|
||||
NSURL *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:MPCloudContainerIdentifier];
|
||||
NSURL *oldCloudContentURL = [[cloudContainerURL
|
||||
URLByAppendingPathComponent:@"CloudLogs" isDirectory:YES]
|
||||
@ -255,11 +292,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
NSURL *oldLocalStoreURL = [[applicationFilesDirectory
|
||||
URLByAppendingPathComponent:@"MasterPassword" isDirectory:NO] URLByAppendingPathExtension:@"sqlite"];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:oldLocalStoreURL.path isDirectory:NO]) {
|
||||
inf(@"No V1 local store to migrate.");
|
||||
inf( @"No V1 local store to migrate." );
|
||||
return YES;
|
||||
}
|
||||
|
||||
inf(@"Migrating V1 local store");
|
||||
inf( @"Migrating V1 local store" );
|
||||
return [self migrateFromLocalStore:oldLocalStoreURL];
|
||||
}
|
||||
|
||||
@ -267,7 +304,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
|
||||
NSURL *newLocalStoreURL = [self.storeManager URLForLocalStore];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:newLocalStoreURL.path isDirectory:NO]) {
|
||||
wrn(@"Can't migrate local store: A new local store already exists.");
|
||||
wrn( @"Can't migrate local store: A new local store already exists." );
|
||||
return YES;
|
||||
}
|
||||
|
||||
@ -278,14 +315,14 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
return NO;
|
||||
}
|
||||
|
||||
inf(@"Successfully migrated to new local store.");
|
||||
inf( @"Successfully migrated to new local store." );
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)migrateFromCloudStore:(NSURL *)oldCloudStoreURL cloudContent:(NSURL *)oldCloudContentURL {
|
||||
|
||||
if (![self.storeManager cloudSafeForSeeding]) {
|
||||
inf(@"Can't migrate cloud store: A new cloud store already exists.");
|
||||
inf( @"Can't migrate cloud store: A new cloud store already exists." );
|
||||
return YES;
|
||||
}
|
||||
|
||||
@ -295,7 +332,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
strategy:0 error:nil cause:nil context:nil])
|
||||
return NO;
|
||||
|
||||
inf(@"Successfully migrated to new cloud store.");
|
||||
inf( @"Successfully migrated to new cloud store." );
|
||||
return YES;
|
||||
}
|
||||
|
||||
@ -309,7 +346,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {
|
||||
|
||||
inf(@"[StoreManager] %@", message);
|
||||
inf( @"[StoreManager] %@", message );
|
||||
}
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore {
|
||||
@ -334,7 +371,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator
|
||||
isCloud:(BOOL)isCloudStore {
|
||||
|
||||
inf(@"Using iCloud? %@", @(isCloudStore));
|
||||
inf( @"Using iCloud? %@", @(isCloudStore) );
|
||||
MPCheckpoint( MPCheckpointCloud, @{
|
||||
@"enabled" : @(isCloudStore)
|
||||
} );
|
||||
@ -369,31 +406,37 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
self.saveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification
|
||||
object:privateManagedObjectContext queue:nil usingBlock:
|
||||
^(NSNotification *note) {
|
||||
// When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext.
|
||||
[mainManagedObjectContext performBlock:^{
|
||||
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note];
|
||||
}];
|
||||
}];
|
||||
// When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext.
|
||||
[mainManagedObjectContext performBlock:^{
|
||||
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note];
|
||||
}];
|
||||
}];
|
||||
|
||||
self.privateManagedObjectContext = privateManagedObjectContext;
|
||||
self.mainManagedObjectContext = mainManagedObjectContext;
|
||||
|
||||
// Perform a data sanity check on the newly loaded store to find and fix any issues.
|
||||
if ([[MPConfig get].checkInconsistency boolValue])
|
||||
[MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
|
||||
[self findAndFixInconsistenciesSaveInContext:context];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreErrorCause)cause
|
||||
context:(id)context {
|
||||
|
||||
err(@"[StoreManager] ERROR: cause=%@, context=%@, error=%@", NSStringFromUSMCause( cause ), context, error);
|
||||
err( @"[StoreManager] ERROR: cause=%@, context=%@, error=%@", NSStringFromUSMCause( cause ), context, error );
|
||||
MPCheckpoint( MPCheckpointMPErrorUbiquity, @{
|
||||
@"cause" : @(cause),
|
||||
@"error.code" : @(error.code),
|
||||
@"error.domain" : NilToNSNull(error.domain),
|
||||
@"error.reason" : NilToNSNull(IfNotNilElse( [error localizedFailureReason], [error localizedDescription] )),
|
||||
@"error.domain" : NilToNSNull( error.domain ),
|
||||
@"error.reason" : NilToNSNull( IfNotNilElse( [error localizedFailureReason], [error localizedDescription] ) ),
|
||||
} );
|
||||
}
|
||||
|
||||
#pragma mark - Utilities
|
||||
|
||||
- (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion {
|
||||
- (void)addElementNamed:(NSString *)siteName completion:(void ( ^ )(MPElementEntity *element))completion {
|
||||
|
||||
if (![siteName length]) {
|
||||
completion( nil );
|
||||
@ -402,7 +445,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
|
||||
[MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
|
||||
MPUserEntity *activeUser = [self activeUserInContext:context];
|
||||
NSAssert(activeUser, @"Missing user.");
|
||||
NSAssert( activeUser, @"Missing user." );
|
||||
if (!activeUser) {
|
||||
completion( nil );
|
||||
return;
|
||||
@ -420,7 +463,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
|
||||
NSError *error = nil;
|
||||
if (element.objectID.isTemporaryID && ![context obtainPermanentIDsForObjects:@[ element ] error:&error])
|
||||
err(@"Failed to obtain a permanent object ID after creating new element: %@", error);
|
||||
err( @"Failed to obtain a permanent object ID after creating new element: %@", error );
|
||||
|
||||
[context saveToStore];
|
||||
|
||||
@ -452,7 +495,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
|
||||
NSError *error = nil;
|
||||
if (![context obtainPermanentIDsForObjects:@[ newElement ] error:&error])
|
||||
err(@"Failed to obtain a permanent object ID after changing object type: %@", error);
|
||||
err( @"Failed to obtain a permanent object ID after changing object type: %@", error );
|
||||
|
||||
[context deleteObject:element];
|
||||
[context saveToStore];
|
||||
@ -466,10 +509,10 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
}
|
||||
|
||||
- (MPImportResult)importSites:(NSString *)importedSitesString
|
||||
askImportPassword:(NSString *(^)(NSString *userName))importPassword
|
||||
askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword {
|
||||
askImportPassword:(NSString *( ^ )(NSString *userName))importPassword
|
||||
askUserPassword:(NSString *( ^ )(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword {
|
||||
|
||||
NSAssert(![[NSThread currentThread] isMainThread], @"This method should not be invoked from the main thread.");
|
||||
NSAssert( ![[NSThread currentThread] isMainThread], @"This method should not be invoked from the main thread." );
|
||||
|
||||
__block MPImportResult result = MPImportResultCancelled;
|
||||
do {
|
||||
@ -485,8 +528,8 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
}
|
||||
|
||||
- (MPImportResult)importSites:(NSString *)importedSitesString
|
||||
askImportPassword:(NSString *(^)(NSString *userName))askImportPassword
|
||||
askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))askUserPassword
|
||||
askImportPassword:(NSString *( ^ )(NSString *userName))askImportPassword
|
||||
askUserPassword:(NSString *( ^ )(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))askUserPassword
|
||||
saveInContext:(NSManagedObjectContext *)context {
|
||||
|
||||
// Compile patterns.
|
||||
@ -497,7 +540,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
|
||||
options:(NSRegularExpressionOptions)0 error:&error];
|
||||
if (error) {
|
||||
err(@"Error loading the header pattern: %@", error);
|
||||
err( @"Error loading the header pattern: %@", error );
|
||||
return MPImportResultInternalError;
|
||||
}
|
||||
}
|
||||
@ -506,13 +549,13 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)(:[[:digit:]]+)?[[:space:]]+([^\t]+)\t(.*)"
|
||||
options:(NSRegularExpressionOptions)0 error:&error];
|
||||
if (error) {
|
||||
err(@"Error loading the site pattern: %@", error);
|
||||
err( @"Error loading the site pattern: %@", error );
|
||||
return MPImportResultInternalError;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse import data.
|
||||
inf(@"Importing sites.");
|
||||
inf( @"Importing sites." );
|
||||
__block MPUserEntity *user = nil;
|
||||
id<MPAlgorithm> importAlgorithm = nil;
|
||||
NSString *importBundleVersion = nil, *importUserName = nil;
|
||||
@ -540,7 +583,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
// Header
|
||||
if ([headerPattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0
|
||||
range:NSMakeRange( 0, [importedSiteLine length] )] != 1) {
|
||||
err(@"Invalid header format in line: %@", importedSiteLine);
|
||||
err( @"Invalid header format in line: %@", importedSiteLine );
|
||||
return MPImportResultMalformedInput;
|
||||
}
|
||||
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:(NSMatchingOptions)0
|
||||
@ -554,16 +597,16 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", importUserName];
|
||||
NSArray *users = [context executeFetchRequest:userFetchRequest error:&error];
|
||||
if (!users) {
|
||||
err(@"While looking for user: %@, error: %@", importUserName, error);
|
||||
err( @"While looking for user: %@, error: %@", importUserName, error );
|
||||
return MPImportResultInternalError;
|
||||
}
|
||||
if ([users count] > 1) {
|
||||
err(@"While looking for user: %@, found more than one: %lu", importUserName, (unsigned long)[users count]);
|
||||
err( @"While looking for user: %@, found more than one: %lu", importUserName, (unsigned long)[users count] );
|
||||
return MPImportResultInternalError;
|
||||
}
|
||||
|
||||
user = [users count]? [users lastObject]: nil;
|
||||
dbg(@"Found user: %@", [user debugDescription]);
|
||||
dbg( @"Found user: %@", [user debugDescription] );
|
||||
}
|
||||
if ([headerName isEqualToString:@"Key ID"])
|
||||
importKeyID = [headerValue decodeHex];
|
||||
@ -588,7 +631,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
// Site
|
||||
if ([sitePattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0
|
||||
range:NSMakeRange( 0, [importedSiteLine length] )] != 1) {
|
||||
err(@"Invalid site format in line: %@", importedSiteLine);
|
||||
err( @"Invalid site format in line: %@", importedSiteLine );
|
||||
return MPImportResultMalformedInput;
|
||||
}
|
||||
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:(NSMatchingOptions)0
|
||||
@ -607,25 +650,26 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
elementFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
|
||||
NSArray *existingSites = [context executeFetchRequest:elementFetchRequest error:&error];
|
||||
if (!existingSites) {
|
||||
err(@"Lookup of existing sites failed for site: %@, user: %@, error: %@", name, user.userID, error);
|
||||
err( @"Lookup of existing sites failed for site: %@, user: %@, error: %@", name, user.userID, error );
|
||||
return MPImportResultInternalError;
|
||||
}
|
||||
if ([existingSites count]) {
|
||||
dbg(@"Existing sites: %@", existingSites);
|
||||
dbg( @"Existing sites: %@", existingSites );
|
||||
[elementsToDelete addObjectsFromArray:existingSites];
|
||||
}
|
||||
}
|
||||
[importedSiteElements addObject:@[ lastUsed, uses, type, version, name, exportContent ]];
|
||||
dbg(@"Will import site: lastUsed=%@, uses=%@, type=%@, version=%@, name=%@, exportContent=%@",
|
||||
lastUsed, uses, type, version, name, exportContent);
|
||||
dbg( @"Will import site: lastUsed=%@, uses=%@, type=%@, version=%@, name=%@, exportContent=%@",
|
||||
lastUsed, uses, type, version, name, exportContent );
|
||||
}
|
||||
|
||||
// Ask for confirmation to import these sites and the master password of the user.
|
||||
inf(@"Importing %lu sites, deleting %lu sites, for user: %@", (unsigned long)[importedSiteElements count], (unsigned long)[elementsToDelete count], [MPUserEntity idFor:importUserName]);
|
||||
inf( @"Importing %lu sites, deleting %lu sites, for user: %@", (unsigned long)[importedSiteElements count],
|
||||
(unsigned long)[elementsToDelete count], [MPUserEntity idFor:importUserName] );
|
||||
NSString *userMasterPassword = askUserPassword( user? user.name: importUserName, [importedSiteElements count],
|
||||
[elementsToDelete count] );
|
||||
if (!userMasterPassword) {
|
||||
inf(@"Import cancelled.");
|
||||
inf( @"Import cancelled." );
|
||||
return MPImportResultCancelled;
|
||||
}
|
||||
MPKey *userKey = [MPAlgorithmDefault keyForPassword:userMasterPassword ofUserNamed:user? user.name: importUserName];
|
||||
@ -641,7 +685,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
// Delete existing sites.
|
||||
if (elementsToDelete.count)
|
||||
[elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
|
||||
inf(@"Deleting site: %@, it will be replaced by an imported site.", [obj name]);
|
||||
inf( @"Deleting site: %@, it will be replaced by an imported site.", [obj name] );
|
||||
[context deleteObject:obj];
|
||||
}];
|
||||
|
||||
@ -650,7 +694,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
user = [MPUserEntity insertNewObjectInContext:context];
|
||||
user.name = importUserName;
|
||||
user.keyID = importKeyID;
|
||||
dbg(@"Created User: %@", [user debugDescription]);
|
||||
dbg( @"Created User: %@", [user debugDescription] );
|
||||
}
|
||||
|
||||
// Import new sites.
|
||||
@ -678,13 +722,13 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
[element.algorithm importProtectedContent:exportContent protectedByKey:importKey intoElement:element usingKey:userKey];
|
||||
}
|
||||
|
||||
dbg(@"Created Element: %@", [element debugDescription]);
|
||||
dbg( @"Created Element: %@", [element debugDescription] );
|
||||
}
|
||||
|
||||
if (![context saveToStore])
|
||||
return MPImportResultInternalError;
|
||||
|
||||
inf(@"Import completed successfully.");
|
||||
inf( @"Import completed successfully." );
|
||||
MPCheckpoint( MPCheckpointSitesImported, nil );
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPSitesImportedNotification object:nil userInfo:@{
|
||||
@ -697,7 +741,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
|
||||
- (NSString *)exportSitesRevealPasswords:(BOOL)revealPasswords {
|
||||
|
||||
MPUserEntity *activeUser = [self activeUserForMainThread];
|
||||
inf(@"Exporting sites, %@, for: %@", revealPasswords? @"revealing passwords": @"omitting passwords", activeUser.userID);
|
||||
inf( @"Exporting sites, %@, for: %@", revealPasswords? @"revealing passwords": @"omitting passwords", activeUser.userID );
|
||||
|
||||
// Header.
|
||||
NSMutableString *export = [NSMutableString new];
|
||||
|
@ -14,5 +14,6 @@
|
||||
@property(nonatomic, retain) NSNumber *rememberLogin;
|
||||
|
||||
@property(nonatomic, retain) NSNumber *iCloudDecided;
|
||||
@property(nonatomic, retain) NSNumber *checkInconsistency;
|
||||
|
||||
@end
|
||||
|
@ -6,12 +6,11 @@
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPConfig.h"
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
@implementation MPConfig
|
||||
|
||||
@dynamic sendInfo, rememberLogin, iCloudDecided;
|
||||
@dynamic sendInfo, rememberLogin, iCloudDecided, checkInconsistency;
|
||||
|
||||
- (id)init {
|
||||
|
||||
@ -19,11 +18,12 @@
|
||||
return nil;
|
||||
|
||||
[self.defaults registerDefaults:@{
|
||||
NSStringFromSelector( @selector(askForReviews) ) : @YES,
|
||||
NSStringFromSelector( @selector( askForReviews ) ) : @YES,
|
||||
|
||||
NSStringFromSelector( @selector(sendInfo) ) : @NO,
|
||||
NSStringFromSelector( @selector(rememberLogin) ) : @NO,
|
||||
NSStringFromSelector( @selector(iCloudDecided) ) : @NO
|
||||
NSStringFromSelector( @selector( sendInfo ) ) : @NO,
|
||||
NSStringFromSelector( @selector( rememberLogin ) ) : @NO,
|
||||
NSStringFromSelector( @selector( iCloudDecided ) ) : @NO,
|
||||
NSStringFromSelector( @selector( checkInconsistency ) ) : @NO
|
||||
}];
|
||||
|
||||
self.delegate = [MPAppDelegate_Shared get];
|
||||
|
@ -8,10 +8,11 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
#import "MPFixable.h"
|
||||
|
||||
@class MPUserEntity;
|
||||
|
||||
@interface MPElementEntity : NSManagedObject
|
||||
@interface MPElementEntity : NSManagedObject <MPFixable>
|
||||
|
||||
@property(nonatomic, retain) NSDate *lastUsed;
|
||||
@property(nonatomic, retain) NSString *loginName;
|
||||
|
@ -19,4 +19,9 @@
|
||||
@dynamic version_;
|
||||
@dynamic user;
|
||||
|
||||
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context {
|
||||
|
||||
return MPFixableResultNoProblems;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -7,9 +7,49 @@
|
||||
//
|
||||
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
@implementation MPElementGeneratedEntity
|
||||
|
||||
@dynamic counter_;
|
||||
|
||||
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context {
|
||||
|
||||
MPFixableResult result = [super findAndFixInconsistenciesInContext:context];
|
||||
|
||||
if (!self.type || self.type == (MPElementType)NSNotFound || ![[self.algorithm allTypes] containsObject:self.type_])
|
||||
// Invalid self.type
|
||||
result = MPApplyFix( result, ^MPFixableResult {
|
||||
wrn( @"Invalid type for: %@ of %@, type: %ld. Will use %ld instead.",
|
||||
self.name, self.user.name, (long)self.type, (long)self.user.defaultType );
|
||||
self.type = self.user.defaultType;
|
||||
return MPFixableResultProblemsFixed;
|
||||
} );
|
||||
if (!self.type || self.type == (MPElementType)NSNotFound || ![[self.algorithm allTypes] containsObject:self.type_])
|
||||
// Invalid self.user.defaultType
|
||||
result = MPApplyFix( result, ^MPFixableResult {
|
||||
wrn( @"Invalid type for: %@ of %@, type: %ld. Will use %ld instead.",
|
||||
self.name, self.user.name, (long)self.type, (long)MPElementTypeGeneratedLong );
|
||||
self.type = MPElementTypeGeneratedLong;
|
||||
return MPFixableResultProblemsFixed;
|
||||
} );
|
||||
if (![self isKindOfClass:[self.algorithm classOfType:self.type]])
|
||||
// Mismatch between self.type and self.class
|
||||
result = MPApplyFix( result, ^MPFixableResult {
|
||||
for (MPElementType newType = self.type; self.type != (newType = [self.algorithm nextType:newType]);)
|
||||
if ([self isKindOfClass:[self.algorithm classOfType:newType]]) {
|
||||
wrn( @"Mismatching type for: %@ of %@, type: %lu, class: %@. Will use %ld instead.",
|
||||
self.name, self.user.name, (long)self.type, self.class, (long)newType );
|
||||
self.type = newType;
|
||||
return MPFixableResultProblemsFixed;
|
||||
}
|
||||
|
||||
err( @"Mismatching type for: %@ of %@, type: %lu, class: %@. Couldn't find a type to fix problem with.",
|
||||
self.name, self.user.name, (long)self.type, self.class );
|
||||
return MPFixableResultProblemsNotFixed;
|
||||
} );
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -12,6 +12,6 @@
|
||||
|
||||
@interface MPElementStoredEntity : MPElementEntity
|
||||
|
||||
@property(nonatomic, retain) id contentObject;
|
||||
@property(nonatomic, retain) NSData *contentObject;
|
||||
|
||||
@end
|
||||
|
@ -7,9 +7,31 @@
|
||||
//
|
||||
|
||||
#import "MPElementStoredEntity.h"
|
||||
#import "MPEntities.h"
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
@implementation MPElementStoredEntity
|
||||
|
||||
@dynamic contentObject;
|
||||
|
||||
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context {
|
||||
|
||||
MPFixableResult result = [super findAndFixInconsistenciesInContext:context];
|
||||
|
||||
if (self.contentObject && ![self.contentObject isKindOfClass:[NSData class]])
|
||||
result = MPApplyFix( result, ^MPFixableResult {
|
||||
MPKey *key = [MPAppDelegate_Shared get].key;
|
||||
if (key && [[MPAppDelegate_Shared get] activeUserInContext:context] == self.user) {
|
||||
wrn( @"Content object not encrypted for: %@ of %@. Will re-encrypt.", self.name, self.user.name );
|
||||
[self.algorithm saveContent:[self.contentObject description] toElement:self usingKey:key];
|
||||
return MPFixableResultProblemsFixed;
|
||||
}
|
||||
|
||||
err( @"Content object not encrypted for: %@ of %@. Couldn't fix, please sign in.", self.name, self.user.name );
|
||||
return MPFixableResultProblemsNotFixed;
|
||||
} );
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -38,34 +38,11 @@
|
||||
|
||||
- (MPElementType)type {
|
||||
|
||||
// Some people got elements with type == 0.
|
||||
MPElementType type = (MPElementType)[self.type_ unsignedIntegerValue];
|
||||
if (!type || type == (MPElementType)NSNotFound)
|
||||
type = [self.user defaultType];
|
||||
if (!type || type == (MPElementType)NSNotFound)
|
||||
type = MPElementTypeGeneratedLong;
|
||||
if (![self isKindOfClass:[self.algorithm classOfType:type]]) {
|
||||
// NSAssert(NO, @"This object's class does not support the type: %lu", (long)type);
|
||||
for (MPElementType aType = type; type != (aType = [self.algorithm nextType:aType]);)
|
||||
if ([self isKindOfClass:[self.algorithm classOfType:aType]]) {
|
||||
err(@"Invalid type for: %@, type: %lu. Will use %lu instead.", self.name, (long)type, (long)aType);
|
||||
return aType;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
return (MPElementType)[self.type_ unsignedIntegerValue];
|
||||
}
|
||||
|
||||
- (void)setType:(MPElementType)aType {
|
||||
|
||||
// Make sure we don't poison our model data with invalid values.
|
||||
if (!aType || aType == (MPElementType)NSNotFound)
|
||||
aType = [self.user defaultType];
|
||||
if (!aType || aType == (MPElementType)NSNotFound)
|
||||
aType = MPElementTypeGeneratedLong;
|
||||
if (![self isKindOfClass:[self.algorithm classOfType:aType]])
|
||||
Throw(@"This object's class does not support the type: %lu", (long)aType);
|
||||
|
||||
self.type_ = @(aType);
|
||||
}
|
||||
|
||||
@ -132,12 +109,12 @@
|
||||
|
||||
- (NSString *)description {
|
||||
|
||||
return PearlString( @"%@:%@", [self class], [self name] );
|
||||
return strf( @"%@:%@", [self class], [self name] );
|
||||
}
|
||||
|
||||
- (NSString *)debugDescription {
|
||||
|
||||
return PearlString( @"{%@: name=%@, user=%@, type=%lu, uses=%ld, lastUsed=%@, version=%ld, loginName=%@, requiresExplicitMigration=%d}",
|
||||
return strf( @"{%@: name=%@, user=%@, type=%lu, uses=%ld, lastUsed=%@, version=%ld, loginName=%@, requiresExplicitMigration=%d}",
|
||||
NSStringFromClass( [self class] ), self.name, self.user.name, (long)self.type, (long)self.uses, self.lastUsed, (long)self.version,
|
||||
self.loginName, self.requiresExplicitMigration );
|
||||
}
|
||||
|
33
MasterPassword/ObjC/MPFixable.h
Normal file
33
MasterPassword/ObjC/MPFixable.h
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||
*
|
||||
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
*
|
||||
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
*/
|
||||
|
||||
//
|
||||
// MPFixable.h
|
||||
// MPFixable
|
||||
//
|
||||
// Created by lhunath on 2014-04-26.
|
||||
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NS_ENUM( NSUInteger, MPFixableResult ) {
|
||||
MPFixableResultNoProblems,
|
||||
MPFixableResultProblemsFixed,
|
||||
MPFixableResultProblemsNotFixed,
|
||||
};
|
||||
|
||||
MPFixableResult MPApplyFix(MPFixableResult previousResult, MPFixableResult(^fixBlock)(void));
|
||||
|
||||
@protocol MPFixable<NSObject>
|
||||
|
||||
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context;
|
||||
|
||||
@end
|
40
MasterPassword/ObjC/MPFixable.m
Normal file
40
MasterPassword/ObjC/MPFixable.m
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||
*
|
||||
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
*
|
||||
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
*/
|
||||
|
||||
//
|
||||
// MPFixable.m
|
||||
// MPFixable
|
||||
//
|
||||
// Created by lhunath on 2014-04-26.
|
||||
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPFixable.h"
|
||||
|
||||
MPFixableResult MPApplyFix(MPFixableResult previousResult, MPFixableResult(^fixBlock)(void)) {
|
||||
|
||||
MPFixableResult additionalResult = fixBlock();
|
||||
switch (previousResult) {
|
||||
case MPFixableResultNoProblems:
|
||||
return additionalResult;
|
||||
case MPFixableResultProblemsFixed:
|
||||
switch (additionalResult) {
|
||||
case MPFixableResultNoProblems:
|
||||
case MPFixableResultProblemsFixed:
|
||||
return previousResult;
|
||||
case MPFixableResultProblemsNotFixed:
|
||||
return additionalResult;
|
||||
}
|
||||
case MPFixableResultProblemsNotFixed:
|
||||
return additionalResult;
|
||||
}
|
||||
|
||||
Throw( @"Unexpected previous=%ld or additional=%ld result.", (long)previousResult, (long)additionalResult );
|
||||
}
|
@ -77,8 +77,10 @@ typedef NS_ENUM(NSUInteger, MPElementType) {
|
||||
#define MPElementUpdatedNotification @"MPElementUpdatedNotification"
|
||||
#define MPCheckConfigNotification @"MPCheckConfigNotification"
|
||||
#define MPSitesImportedNotification @"MPSitesImportedNotification"
|
||||
#define MPFoundInconsistenciesNotification @"MPFoundInconsistenciesNotification"
|
||||
|
||||
#define MPSitesImportedNotificationUserKey @"MPSitesImportedNotificationUserKey"
|
||||
#define MPInconsistenciesFixResultUserKey @"MPInconsistenciesFixResultUserKey"
|
||||
|
||||
static void MPCheckpoint(NSString *checkpoint, NSDictionary *attributes) {
|
||||
|
||||
|
@ -243,8 +243,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
|
||||
|
||||
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
|
||||
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey ) userInfo:nil];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey )];
|
||||
}
|
||||
|
||||
#pragma mark - NSApplicationDelegate
|
||||
|
@ -44,10 +44,8 @@
|
||||
element = [super saveContentTypeWithElement:element saveInContext:context];
|
||||
|
||||
MPElementStoredEntity *storedElement = [self storedElement:element];
|
||||
if (storedElement) {
|
||||
storedElement.contentObject = self.contentField.text;
|
||||
[context saveToStore];
|
||||
}
|
||||
[storedElement.algorithm saveContent:self.contentField.text toElement:storedElement usingKey:[MPiOSAppDelegate get].key];
|
||||
[context saveToStore];
|
||||
|
||||
return element;
|
||||
}
|
||||
@ -78,7 +76,7 @@
|
||||
|
||||
switch (self.contentFieldMode) {
|
||||
case MPContentFieldModePassword: {
|
||||
storedElement.contentObject = newContent;
|
||||
[storedElement.algorithm saveContent:newContent toElement:storedElement usingKey:[MPiOSAppDelegate get].key];
|
||||
[context saveToStore];
|
||||
|
||||
PearlMainQueue( ^{
|
||||
|
@ -68,6 +68,7 @@
|
||||
|
||||
[self registerObservers];
|
||||
[self observeStore];
|
||||
[self updateFromConfig];
|
||||
[self updatePasswords];
|
||||
}
|
||||
|
||||
@ -464,6 +465,11 @@
|
||||
self.passwordSelectionContainer.alpha = 1;
|
||||
}];
|
||||
}],
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserverForName:MPCheckConfigNotification object:nil
|
||||
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
[self updateFromConfig];
|
||||
}],
|
||||
];
|
||||
}
|
||||
|
||||
@ -504,6 +510,11 @@
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_storeObserver];
|
||||
}
|
||||
|
||||
- (void)updateFromConfig {
|
||||
|
||||
self.passwordsSearchBar.keyboardType = [[MPiOSConfig get].dictationSearch boolValue]? UIKeyboardTypeDefault: UIKeyboardTypeURL;
|
||||
}
|
||||
|
||||
- (void)updatePasswords {
|
||||
|
||||
NSString *query = self.query;
|
||||
|
@ -15,6 +15,7 @@
|
||||
@property(weak, nonatomic) IBOutlet UITableViewCell *feedbackCell;
|
||||
@property(weak, nonatomic) IBOutlet UITableViewCell *coachmarksCell;
|
||||
@property(weak, nonatomic) IBOutlet UITableViewCell *exportCell;
|
||||
@property(weak, nonatomic) IBOutlet UITableViewCell *checkInconsistencies;
|
||||
@property(weak, nonatomic) IBOutlet UIImageView *avatarImage;
|
||||
@property(weak, nonatomic) IBOutlet UISegmentedControl *generatedTypeControl;
|
||||
@property(weak, nonatomic) IBOutlet UISegmentedControl *storedTypeControl;
|
||||
|
@ -29,7 +29,7 @@
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
|
||||
inf(@"Preferences will appear");
|
||||
inf( @"Preferences will appear" );
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread];
|
||||
@ -70,6 +70,14 @@
|
||||
[vc performSegueWithIdentifier:@"coachmarks" sender:self];
|
||||
}
|
||||
}
|
||||
if (cell == self.checkInconsistencies)
|
||||
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
|
||||
if ([[MPiOSAppDelegate get] findAndFixInconsistenciesSaveInContext:context] == MPFixableResultNoProblems)
|
||||
[PearlAlert showAlertWithTitle:@"No Inconsistencies" message:
|
||||
@"No inconsistencies were detected in your sites."
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||
tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
|
||||
}];
|
||||
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}
|
||||
@ -162,7 +170,7 @@
|
||||
case 1:
|
||||
return MPElementTypeStoredDevicePrivate;
|
||||
default:
|
||||
Throw(@"unsupported selected type index: generated=%d, stored=%d", selectedGeneratedIndex, selectedStoredIndex);
|
||||
Throw( @"unsupported selected type index: generated=%d, stored=%d", selectedGeneratedIndex, selectedStoredIndex );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,17 +111,20 @@
|
||||
#endif
|
||||
}
|
||||
@catch (id exception) {
|
||||
err(@"During Analytics Setup: %@", exception);
|
||||
err( @"During Analytics Setup: %@", exception );
|
||||
}
|
||||
@try {
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:MPCheckConfigNotification object:nil queue:nil usingBlock:
|
||||
^(NSNotification *note) {
|
||||
[self checkConfig];
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserverForName:kIASKAppSettingChanged object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification
|
||||
object:note userInfo:nil];
|
||||
[self updateFromConfig];
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil usingBlock:
|
||||
^(NSNotification *note) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:note userInfo:nil];
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification object:nil queue:nil usingBlock:
|
||||
^(NSNotification *note) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:note userInfo:nil];
|
||||
}];
|
||||
|
||||
#ifdef ADHOC
|
||||
@ -137,18 +140,41 @@
|
||||
#endif
|
||||
}
|
||||
@catch (id exception) {
|
||||
err(@"During Config Test: %@", exception);
|
||||
err( @"During Config Test: %@", exception );
|
||||
}
|
||||
@try {
|
||||
[super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
}
|
||||
@catch (id exception) {
|
||||
err(@"During Pearl Application Launch: %@", exception);
|
||||
err( @"During Pearl Application Launch: %@", exception );
|
||||
}
|
||||
@try {
|
||||
inf(@"Started up with device identifier: %@", [PearlKeyChain deviceIdentifier]);
|
||||
inf( @"Started up with device identifier: %@", [PearlKeyChain deviceIdentifier] );
|
||||
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:MPFoundInconsistenciesNotification object:nil queue:nil usingBlock:
|
||||
^(NSNotification *note) {
|
||||
switch ((MPFixableResult)[note.userInfo[MPInconsistenciesFixResultUserKey] unsignedIntegerValue]) {
|
||||
|
||||
case MPFixableResultNoProblems:
|
||||
break;
|
||||
case MPFixableResultProblemsFixed:
|
||||
[PearlAlert showAlertWithTitle:@"Inconsistencies Fixed" message:
|
||||
@"Some inconsistencies were detected in your sites.\n"
|
||||
@"All issues were fixed."
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||
tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
|
||||
break;
|
||||
case MPFixableResultProblemsNotFixed:
|
||||
[PearlAlert showAlertWithTitle:@"Inconsistencies Found" message:
|
||||
@"Some inconsistencies were detected in your sites.\n"
|
||||
@"Not all issues could be fixed. Try signing in to each user or checking the logs."
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||
tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
|
||||
break;
|
||||
}
|
||||
}];
|
||||
|
||||
PearlMainQueue( ^{
|
||||
if ([[MPiOSConfig get].showSetup boolValue])
|
||||
[self.navigationController performSegueWithIdentifier:@"setup" sender:self];
|
||||
} );
|
||||
@ -166,7 +192,7 @@
|
||||
} );
|
||||
}
|
||||
@catch (id exception) {
|
||||
err(@"During Post-Startup: %@", exception);
|
||||
err( @"During Post-Startup: %@", exception );
|
||||
}
|
||||
|
||||
return YES;
|
||||
@ -186,7 +212,7 @@
|
||||
NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url]
|
||||
returningResponse:&response error:&error];
|
||||
if (error)
|
||||
err(@"While reading imported sites from %@: %@", url, error);
|
||||
err( @"While reading imported sites from %@: %@", url, error );
|
||||
if (!importedSitesData)
|
||||
return;
|
||||
|
||||
@ -227,7 +253,7 @@
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
[PearlAlert showAlertWithTitle:PearlString( @"Master Password for\n%@", userName )
|
||||
message:PearlString( @"Imports %lu sites, overwriting %lu.",
|
||||
(unsigned long)importCount, (unsigned long)deleteCount )
|
||||
(unsigned long)importCount, (unsigned long)deleteCount )
|
||||
viewStyle:UIAlertViewStyleSecureTextInput
|
||||
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||
@try {
|
||||
@ -270,7 +296,7 @@
|
||||
|
||||
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
|
||||
|
||||
inf(@"Received memory warning.");
|
||||
inf( @"Received memory warning." );
|
||||
|
||||
[super applicationDidReceiveMemoryWarning:application];
|
||||
}
|
||||
@ -307,7 +333,7 @@
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||
|
||||
inf(@"Will deactivate");
|
||||
inf( @"Will deactivate" );
|
||||
if (![[MPiOSConfig get].rememberLogin boolValue])
|
||||
[self signOutAnimated:NO];
|
||||
|
||||
@ -321,9 +347,8 @@
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||
|
||||
inf(@"Re-activated");
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification
|
||||
object:application userInfo:nil];
|
||||
inf( @"Re-activated" );
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:application];
|
||||
|
||||
#ifdef LOCALYTICS
|
||||
[[LocalyticsSession sharedLocalyticsSession] resume];
|
||||
@ -377,14 +402,14 @@
|
||||
|
||||
[[[PearlEMail alloc] initForEMailTo:@"Master Password Development <masterpassword@lyndir.com>"
|
||||
subject:PearlString( @"Feedback for Master Password [%@]",
|
||||
[[PearlKeyChain deviceIdentifier] stringByDeletingMatchesOf:@"-.*"] )
|
||||
[[PearlKeyChain deviceIdentifier] stringByDeletingMatchesOf:@"-.*"] )
|
||||
body:PearlString( @"\n\n\n"
|
||||
@"--\n"
|
||||
@"%@"
|
||||
@"Master Password %@, build %@",
|
||||
userName? ([userName stringByAppendingString:@"\n"]): @"",
|
||||
[PearlInfoPlist get].CFBundleShortVersionString,
|
||||
[PearlInfoPlist get].CFBundleVersion )
|
||||
userName? ([userName stringByAppendingString:@"\n"]): @"",
|
||||
[PearlInfoPlist get].CFBundleShortVersionString,
|
||||
[PearlInfoPlist get].CFBundleVersion )
|
||||
|
||||
attachments:(logs
|
||||
? [[PearlEMailAttachment alloc]
|
||||
@ -392,8 +417,8 @@
|
||||
dataUsingEncoding:NSUTF8StringEncoding]
|
||||
mimeType:@"text/plain"
|
||||
fileName:PearlString( @"%@-%@.log",
|
||||
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]],
|
||||
[PearlKeyChain deviceIdentifier] )]
|
||||
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]],
|
||||
[PearlKeyChain deviceIdentifier] )]
|
||||
: nil), nil]
|
||||
showComposerForVC:viewController];
|
||||
}
|
||||
@ -405,22 +430,22 @@
|
||||
@"You can open the export with a text editor to get an overview of all your sites.\n\n"
|
||||
@"The file also acts as a personal backup of your site list in case you don't sync with iCloud/iTunes."
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
[PearlAlert showAlertWithTitle:@"Reveal Passwords?" message:
|
||||
@"Would you like to make all your passwords visible in the export?\n\n"
|
||||
@"A safe export will only include your stored passwords, in an encrypted manner, "
|
||||
@"making the result safe from falling in the wrong hands.\n\n"
|
||||
@"If all your passwords are shown and somebody else finds the export, "
|
||||
@"they could gain access to all your sites!"
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 0)
|
||||
// Safe Export
|
||||
[self showExportRevealPasswords:NO forVC:viewController];
|
||||
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1)
|
||||
// Show Passwords
|
||||
[self showExportRevealPasswords:YES forVC:viewController];
|
||||
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil];
|
||||
} otherTitles:nil];
|
||||
[PearlAlert showAlertWithTitle:@"Reveal Passwords?" message:
|
||||
@"Would you like to make all your passwords visible in the export?\n\n"
|
||||
@"A safe export will only include your stored passwords, in an encrypted manner, "
|
||||
@"making the result safe from falling in the wrong hands.\n\n"
|
||||
@"If all your passwords are shown and somebody else finds the export, "
|
||||
@"they could gain access to all your sites!"
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 0)
|
||||
// Safe Export
|
||||
[self showExportRevealPasswords:NO forVC:viewController];
|
||||
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1)
|
||||
// Show Passwords
|
||||
[self showExportRevealPasswords:YES forVC:viewController];
|
||||
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil];
|
||||
} otherTitles:nil];
|
||||
}
|
||||
|
||||
- (void)showExportRevealPasswords:(BOOL)revealPasswords forVC:(UIViewController *)viewController {
|
||||
@ -445,17 +470,17 @@
|
||||
@"--\n"
|
||||
@"%@\n"
|
||||
@"Master Password %@, build %@",
|
||||
[self activeUserForMainThread].name,
|
||||
[PearlInfoPlist get].CFBundleShortVersionString,
|
||||
[PearlInfoPlist get].CFBundleVersion );
|
||||
[self activeUserForMainThread].name,
|
||||
[PearlInfoPlist get].CFBundleShortVersionString,
|
||||
[PearlInfoPlist get].CFBundleVersion );
|
||||
else
|
||||
message = PearlString( @"Backup of Master Password sites.\n\n\n"
|
||||
@"--\n"
|
||||
@"%@\n"
|
||||
@"Master Password %@, build %@",
|
||||
[self activeUserForMainThread].name,
|
||||
[PearlInfoPlist get].CFBundleShortVersionString,
|
||||
[PearlInfoPlist get].CFBundleVersion );
|
||||
[self activeUserForMainThread].name,
|
||||
[PearlInfoPlist get].CFBundleShortVersionString,
|
||||
[PearlInfoPlist get].CFBundleVersion );
|
||||
|
||||
NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
|
||||
[exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
|
||||
@ -464,11 +489,11 @@
|
||||
attachments:[[PearlEMailAttachment alloc] initWithContent:[exportedSites dataUsingEncoding:NSUTF8StringEncoding]
|
||||
mimeType:@"text/plain" fileName:
|
||||
PearlString( @"%@ (%@).mpsites", [self activeUserForMainThread].name,
|
||||
[exportDateFormatter stringFromDate:[NSDate date]] )],
|
||||
[exportDateFormatter stringFromDate:[NSDate date]] )],
|
||||
nil];
|
||||
}
|
||||
|
||||
- (void)changeMasterPasswordFor:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc didResetBlock:(void (^)(void))didReset {
|
||||
- (void)changeMasterPasswordFor:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc didResetBlock:(void ( ^ )(void))didReset {
|
||||
|
||||
[PearlAlert showAlertWithTitle:@"Changing Master Password"
|
||||
message:
|
||||
@ -481,7 +506,7 @@
|
||||
return;
|
||||
|
||||
[moc performBlockAndWait:^{
|
||||
inf(@"Unsetting master password for: %@.", user.userID);
|
||||
inf( @"Unsetting master password for: %@.", user.userID );
|
||||
user.keyID = nil;
|
||||
[self forgetSavedKeyFor:user];
|
||||
[moc saveToStore];
|
||||
@ -497,16 +522,14 @@
|
||||
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - PearlConfigDelegate
|
||||
|
||||
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)value {
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification
|
||||
object:NSStringFromSelector( configKey ) userInfo:nil];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey )];
|
||||
}
|
||||
|
||||
- (void)checkConfig {
|
||||
- (void)updateFromConfig {
|
||||
|
||||
// iCloud enabled / disabled
|
||||
BOOL iCloudEnabled = [[MPiOSConfig get].iCloudEnabled boolValue];
|
||||
@ -519,7 +542,7 @@
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )];
|
||||
NSError *error = nil;
|
||||
if ((siteCount = [context countForFetchRequest:fetchRequest error:&error]) == NSNotFound) {
|
||||
wrn(@"Couldn't count current sites: %@", error);
|
||||
wrn( @"Couldn't count current sites: %@", error );
|
||||
return;
|
||||
}
|
||||
}];
|
||||
@ -536,11 +559,11 @@
|
||||
@"or overwrite them with your current sites."
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
setConfirmationAnswer( NO );
|
||||
if (buttonIndex == [alert firstOtherButtonIndex])
|
||||
setConfirmationAnswer( YES );
|
||||
}
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
setConfirmationAnswer( NO );
|
||||
if (buttonIndex == [alert firstOtherButtonIndex])
|
||||
setConfirmationAnswer( YES );
|
||||
}
|
||||
cancelTitle:@"Use Old" otherTitles:@"Overwrite", nil];
|
||||
}];
|
||||
else
|
||||
@ -550,7 +573,7 @@
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )];
|
||||
NSError *error = nil;
|
||||
if ((siteCount = [context countForFetchRequest:fetchRequest error:&error]) == NSNotFound) {
|
||||
wrn(@"Couldn't count current sites: %@", error);
|
||||
wrn( @"Couldn't count current sites: %@", error );
|
||||
return;
|
||||
}
|
||||
}];
|
||||
@ -566,11 +589,11 @@
|
||||
@"or overwrite them with your current iCloud sites."
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
setConfirmationAnswer( NO );
|
||||
if (buttonIndex == [alert firstOtherButtonIndex])
|
||||
setConfirmationAnswer( YES );
|
||||
}
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
setConfirmationAnswer( NO );
|
||||
if (buttonIndex == [alert firstOtherButtonIndex])
|
||||
setConfirmationAnswer( YES );
|
||||
}
|
||||
cancelTitle:@"Use Old" otherTitles:@"Overwrite", nil];
|
||||
}];
|
||||
}
|
||||
@ -630,15 +653,14 @@
|
||||
@"helpHidden" : @([[MPiOSConfig get].helpHidden boolValue]),
|
||||
@"showQuickStart" : @([[MPiOSConfig get].showSetup boolValue]),
|
||||
@"firstRun" : @([[PearlConfig get].firstRun boolValue]),
|
||||
@"launchCount" : NilToNSNull([PearlConfig get].launchCount),
|
||||
@"launchCount" : NilToNSNull( [PearlConfig get].launchCount ),
|
||||
@"askForReviews" : @([[PearlConfig get].askForReviews boolValue]),
|
||||
@"reviewAfterLaunches" : NilToNSNull([PearlConfig get].reviewAfterLaunches),
|
||||
@"reviewedVersion" : NilToNSNull([PearlConfig get].reviewedVersion)
|
||||
@"reviewAfterLaunches" : NilToNSNull( [PearlConfig get].reviewAfterLaunches ),
|
||||
@"reviewedVersion" : NilToNSNull( [PearlConfig get].reviewedVersion )
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UbiquityStoreManager
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore {
|
||||
@ -690,13 +712,13 @@
|
||||
@"To add one, go into Apple's Settings -> iCloud."
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == alert.firstOtherButtonIndex) {
|
||||
[MPiOSConfig get].iCloudEnabled = @NO;
|
||||
return;
|
||||
}
|
||||
if (buttonIndex == alert.firstOtherButtonIndex) {
|
||||
[MPiOSConfig get].iCloudEnabled = @NO;
|
||||
return;
|
||||
}
|
||||
|
||||
[self.storeManager reloadStore];
|
||||
} cancelTitle:@"Try Again" otherTitles:@"Disable iCloud", nil];
|
||||
[self.storeManager reloadStore];
|
||||
} cancelTitle:@"Try Again" otherTitles:@"Disable iCloud", nil];
|
||||
|
||||
return YES;
|
||||
}
|
||||
@ -710,30 +732,29 @@
|
||||
message:@"Waiting for your other device to auto‑correct the problem..."
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:
|
||||
^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert firstOtherButtonIndex])
|
||||
wSelf.fixCloudContentAlert = [PearlAlert showAlertWithTitle:@"Fix iCloud Now" message:
|
||||
@"This problem can be auto‑corrected by opening the app on another device where you recently made changes.\n"
|
||||
@"You can fix the problem from this device anyway, but recent changes from another device might get lost.\n\n"
|
||||
@"You can also turn iCloud off for now."
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
initAlert:nil tappedButtonBlock:
|
||||
^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||
if (buttonIndex_ == alert_.cancelButtonIndex)
|
||||
[wSelf showCloudContentAlert];
|
||||
if (buttonIndex_ == [alert_ firstOtherButtonIndex])
|
||||
[wSelf.storeManager rebuildCloudContentFromCloudStoreOrLocalStore:YES];
|
||||
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1)
|
||||
[MPiOSConfig get].iCloudEnabled = @NO;
|
||||
}
|
||||
cancelTitle:[PearlStrings get].commonButtonBack
|
||||
otherTitles:@"Fix Anyway",
|
||||
@"Turn Off", nil];
|
||||
if (buttonIndex == [alert firstOtherButtonIndex] + 1)
|
||||
[MPiOSConfig get].iCloudEnabled = @NO;
|
||||
} cancelTitle:nil otherTitles:@"Fix Now", @"Turn Off", nil];
|
||||
if (buttonIndex == [alert firstOtherButtonIndex])
|
||||
wSelf.fixCloudContentAlert = [PearlAlert showAlertWithTitle:@"Fix iCloud Now" message:
|
||||
@"This problem can be auto‑corrected by opening the app on another device where you recently made changes.\n"
|
||||
@"You can fix the problem from this device anyway, but recent changes from another device might get lost.\n\n"
|
||||
@"You can also turn iCloud off for now."
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
initAlert:nil tappedButtonBlock:
|
||||
^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||
if (buttonIndex_ == alert_.cancelButtonIndex)
|
||||
[wSelf showCloudContentAlert];
|
||||
if (buttonIndex_ == [alert_ firstOtherButtonIndex])
|
||||
[wSelf.storeManager rebuildCloudContentFromCloudStoreOrLocalStore:YES];
|
||||
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1)
|
||||
[MPiOSConfig get].iCloudEnabled = @NO;
|
||||
}
|
||||
cancelTitle:[PearlStrings get].commonButtonBack
|
||||
otherTitles:@"Fix Anyway",
|
||||
@"Turn Off", nil];
|
||||
if (buttonIndex == [alert firstOtherButtonIndex] + 1)
|
||||
[MPiOSConfig get].iCloudEnabled = @NO;
|
||||
} cancelTitle:nil otherTitles:@"Fix Now", @"Turn Off", nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - TestFlight
|
||||
|
||||
- (NSDictionary *)testFlightInfo {
|
||||
@ -748,14 +769,13 @@
|
||||
|
||||
- (NSString *)testFlightToken {
|
||||
|
||||
NSString *testFlightToken = NSNullToNil([[self testFlightInfo] valueForKeyPath:@"Application Token"]);
|
||||
NSString *testFlightToken = NSNullToNil( [[self testFlightInfo] valueForKeyPath:@"Application Token"] );
|
||||
if (![testFlightToken length])
|
||||
wrn(@"TestFlight token not set. Test Flight won't be aware of this test.");
|
||||
wrn( @"TestFlight token not set. Test Flight won't be aware of this test." );
|
||||
|
||||
return testFlightToken;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Crashlytics
|
||||
|
||||
- (NSDictionary *)crashlyticsInfo {
|
||||
@ -770,14 +790,13 @@
|
||||
|
||||
- (NSString *)crashlyticsAPIKey {
|
||||
|
||||
NSString *crashlyticsAPIKey = NSNullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]);
|
||||
NSString *crashlyticsAPIKey = NSNullToNil( [[self crashlyticsInfo] valueForKeyPath:@"API Key"] );
|
||||
if (![crashlyticsAPIKey length])
|
||||
wrn(@"Crashlytics API key not set. Crash logs won't be recorded.");
|
||||
wrn( @"Crashlytics API key not set. Crash logs won't be recorded." );
|
||||
|
||||
return crashlyticsAPIKey;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Localytics
|
||||
|
||||
- (NSDictionary *)localyticsInfo {
|
||||
@ -793,12 +812,12 @@
|
||||
- (NSString *)localyticsKey {
|
||||
|
||||
#ifdef DEBUG
|
||||
NSString *localyticsKey = NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]);
|
||||
NSString *localyticsKey = NSNullToNil( [[self localyticsInfo] valueForKeyPath:@"Key.development"] );
|
||||
#else
|
||||
NSString *localyticsKey = NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]);
|
||||
#endif
|
||||
if (![localyticsKey length])
|
||||
wrn(@"Localytics key not set. Demographics won't be collected.");
|
||||
wrn( @"Localytics key not set. Demographics won't be collected." );
|
||||
|
||||
return localyticsKey;
|
||||
}
|
||||
|
@ -18,5 +18,6 @@
|
||||
@property(nonatomic, retain) NSNumber *loginNameTipShown;
|
||||
@property(nonatomic, retain) NSNumber *traceMode;
|
||||
@property(nonatomic, retain) NSNumber *iCloudEnabled;
|
||||
@property(nonatomic, retain) NSNumber *dictationSearch;
|
||||
|
||||
@end
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
@implementation MPiOSConfig
|
||||
|
||||
@dynamic helpHidden, siteInfoHidden, showSetup, actionsTipShown, typeTipShown, loginNameTipShown, traceMode, iCloudEnabled;
|
||||
@dynamic helpHidden, siteInfoHidden, showSetup, actionsTipShown, typeTipShown, loginNameTipShown, traceMode, iCloudEnabled, dictationSearch;
|
||||
|
||||
- (id)init {
|
||||
|
||||
@ -24,7 +24,8 @@
|
||||
NSStringFromSelector( @selector(typeTipShown) ) : @(!self.firstRun),
|
||||
NSStringFromSelector( @selector(loginNameTipShown) ) : @NO,
|
||||
NSStringFromSelector( @selector(traceMode) ) : @NO,
|
||||
NSStringFromSelector( @selector(iCloudEnabled) ) : @NO
|
||||
NSStringFromSelector( @selector(iCloudEnabled) ) : @NO,
|
||||
NSStringFromSelector( @selector( dictationSearch) ) : @NO
|
||||
}];
|
||||
|
||||
return self;
|
||||
|
@ -47,6 +47,7 @@
|
||||
93D39CB5E2EC1078E898F46A /* MPPasswordLargeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3937863061C3916AF7AD2 /* MPPasswordLargeCell.m */; };
|
||||
93D39D596A2E376D6F6F5DA1 /* MPCombinedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393310223DDB35218467A /* MPCombinedViewController.m */; };
|
||||
93D39E281E3658B30550CB55 /* NSDictionary+Indexing.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */; };
|
||||
93D39EAA4D064193074D3021 /* MPFixable.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39A813CA9D7E192261ED2 /* MPFixable.m */; };
|
||||
93D39EDD960C381D64E4DCDD /* MPPasswordSmallCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3952CC60991B97D69F26A /* MPPasswordSmallCell.m */; };
|
||||
93D39F8A9254177891F38705 /* MPSetupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39A28369954D147E239BA /* MPSetupViewController.m */; };
|
||||
93D39FA97F4C3F69A75D5A03 /* MPPasswordLargeGeneratedCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3993422E207BF0B21D089 /* MPPasswordLargeGeneratedCell.m */; };
|
||||
@ -556,10 +557,12 @@
|
||||
93D39975CE5AEC99E3F086C7 /* MPPasswordCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordCell.h; sourceTree = "<group>"; };
|
||||
93D3999693660C89A7465F4E /* MPCoachmarkViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPCoachmarkViewController.h; sourceTree = "<group>"; };
|
||||
93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPUsersViewController.m; sourceTree = "<group>"; };
|
||||
93D399F244BB522A317811BB /* MPFixable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPFixable.h; sourceTree = "<group>"; };
|
||||
93D39A1DDFA09AE2E14D26DC /* UIResponder+PearlFirstResponder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+PearlFirstResponder.m"; sourceTree = "<group>"; };
|
||||
93D39A28369954D147E239BA /* MPSetupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPSetupViewController.m; sourceTree = "<group>"; };
|
||||
93D39A3CC4D8330831FC8CB4 /* LLToggleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLToggleViewController.h; sourceTree = "<group>"; };
|
||||
93D39A41340CF778E00D0E6D /* MPEmergencySegue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPEmergencySegue.h; sourceTree = "<group>"; };
|
||||
93D39A813CA9D7E192261ED2 /* MPFixable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPFixable.m; sourceTree = "<group>"; };
|
||||
93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Indexing.m"; sourceTree = "<group>"; };
|
||||
93D39ACBA9F4878B6A1CC33B /* MPEmergencyViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPEmergencyViewController.m; sourceTree = "<group>"; };
|
||||
93D39B050DD5F55E9794EFD4 /* MPPopdownSegue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPopdownSegue.m; sourceTree = "<group>"; };
|
||||
@ -2496,6 +2499,8 @@
|
||||
DABD3BB91711E2DC00CF925C /* MPUserEntity.h */,
|
||||
DABD3BBA1711E2DC00CF925C /* MPUserEntity.m */,
|
||||
DABD3BD01711E2DC00CF925C /* MasterPassword.xcdatamodeld */,
|
||||
93D399F244BB522A317811BB /* MPFixable.h */,
|
||||
93D39A813CA9D7E192261ED2 /* MPFixable.m */,
|
||||
);
|
||||
name = ObjC;
|
||||
path = ..;
|
||||
@ -3840,6 +3845,7 @@
|
||||
93D39BA1EA3CAAC8A220B4A6 /* MPAppSettingsViewController.m in Sources */,
|
||||
93D396D8B67DA6522CDBA142 /* MPCoachmarkViewController.m in Sources */,
|
||||
93D391C07818F4C2DC1B6956 /* MPPasswordsCoachmarkViewController.m in Sources */,
|
||||
93D39EAA4D064193074D3021 /* MPFixable.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -70,6 +70,24 @@
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Enabling support for dictation in the site search box will enable the dictation button next to the space bar at the bottom of the keyboard. Press this button and speak the name of your site to look it up. Enabling dictation will change your keyboard which might make it slightly more difficult to enter a site name manually.</string>
|
||||
<key>Title</key>
|
||||
<string></string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
<key>Key</key>
|
||||
<string>dictationSearch</string>
|
||||
<key>Title</key>
|
||||
<string>Dictation Search</string>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
@ -88,6 +106,24 @@
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string></string>
|
||||
<key>FooterText</key>
|
||||
<string>If the app tends to crash on login, enable this to check if there are any inconsistencies in your site data. It may slow down login a bit, so keep it off when no issues are reported on login.</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Check For Inconsistencies</string>
|
||||
<key>Key</key>
|
||||
<string>checkInconsistency</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>StringsTable</key>
|
||||
<string>Root</string>
|
||||
|
@ -714,8 +714,42 @@
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="110" id="UdB-BV-AHA" userLabel="Check Inconsistencies">
|
||||
<rect key="frame" x="0.0" y="860" width="320" height="110"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="UdB-BV-AHA" id="V2Y-nu-jhZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="109"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Check For Inconsistencies" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WXh-sg-l2h">
|
||||
<rect key="frame" x="20" y="20" width="247" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="Exo2.0-Bold" family="Exo 2.0" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Perform a check to see if there are any inconsistencies in your site data that might cause issues." lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="247" translatesAutoresizingMaskIntoConstraints="NO" id="gTs-JA-zmL">
|
||||
<rect key="frame" x="20" y="49" width="247" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="Exo2.0-Thin" family="Exo 2.0" pointSize="11"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="gTs-JA-zmL" secondAttribute="trailing" constant="20" symbolic="YES" id="E6T-PX-DeO"/>
|
||||
<constraint firstItem="gTs-JA-zmL" firstAttribute="leading" secondItem="V2Y-nu-jhZ" secondAttribute="leading" constant="20" symbolic="YES" id="KNx-Ww-XeE"/>
|
||||
<constraint firstAttribute="trailing" secondItem="WXh-sg-l2h" secondAttribute="trailing" constant="20" symbolic="YES" id="MTI-sn-fnK"/>
|
||||
<constraint firstItem="WXh-sg-l2h" firstAttribute="leading" secondItem="V2Y-nu-jhZ" secondAttribute="leading" constant="20" symbolic="YES" id="cXs-lo-6cy"/>
|
||||
<constraint firstItem="gTs-JA-zmL" firstAttribute="top" secondItem="WXh-sg-l2h" secondAttribute="bottom" constant="8" symbolic="YES" id="jA7-sy-qAs"/>
|
||||
<constraint firstItem="WXh-sg-l2h" firstAttribute="top" secondItem="V2Y-nu-jhZ" secondAttribute="top" constant="20" symbolic="YES" id="tOD-LM-bbp"/>
|
||||
<constraint firstAttribute="bottom" secondItem="gTs-JA-zmL" secondAttribute="bottom" constant="20" symbolic="YES" id="trB-mg-HbI"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="125" id="IVT-Rs-nTu" userLabel="Export">
|
||||
<rect key="frame" x="0.0" y="860" width="320" height="125"/>
|
||||
<rect key="frame" x="0.0" y="970" width="320" height="125"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="IVT-Rs-nTu" id="Q5J-2f-mmz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="124"/>
|
||||
@ -762,6 +796,7 @@
|
||||
<size key="freeformSize" width="320" height="568"/>
|
||||
<connections>
|
||||
<outlet property="avatarImage" destination="tWi-sc-DGp" id="ifT-Ct-WL6"/>
|
||||
<outlet property="checkInconsistencies" destination="UdB-BV-AHA" id="Cm2-Om-UzP"/>
|
||||
<outlet property="coachmarksCell" destination="eth-Dc-JYn" id="0Tq-I3-SwK"/>
|
||||
<outlet property="exportCell" destination="IVT-Rs-nTu" id="RU0-qr-Bdi"/>
|
||||
<outlet property="feedbackCell" destination="9QG-lM-ymM" id="18X-Ph-0ac"/>
|
||||
|
Loading…
Reference in New Issue
Block a user