//============================================================================== // This file is part of Master Password. // Copyright (c) 2011-2017, Maarten Billemont. // // Master Password is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Master Password is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You can find a copy of the GNU General Public License in the // LICENSE file. Alternatively, see . //============================================================================== #import "MPiOSAppDelegate.h" #import "MPAppDelegate_Key.h" #import "MPAppDelegate_Store.h" #import "MPStoreViewController.h" #import "mpw-marshal.h" #import "MPSecrets.h" #import #import @interface CountlyPushNotifications @end @interface CountlyPushNotifications(MPNotifications) @end @implementation CountlyPushNotifications(MPNotifications) - (void)openURL:(NSString *)URLString { [[MPiOSAppDelegate get].navigationController performSegueWithIdentifier:@"web" sender:[NSURL URLWithString:URLString]]; } @end @interface MPiOSAppDelegate() @property(nonatomic, strong) UIDocumentInteractionController *interactionController; @property(nonatomic, strong) PearlHangDetector *hangDetector; @end @implementation MPiOSAppDelegate + (void)initialize { [MPiOSConfig get]; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { @try { // Sentry [SentrySDK initWithOptions:@{ @"dsn" : NilToNSNull( decrypt( sentryDSN ) ), #ifdef DEBUG @"debug" : @(YES), @"environment" : @"Development", #elif PUBLIC @"debug" : @(NO), @"environment" : @"Public", #else @"debug" : @(NO), @"environment" : @"Private", #endif @"enabled" : [MPiOSConfig get].sendInfo, @"enableAutoSessionTracking": @(YES), }]; [[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) { PearlLogLevel level = PearlLogLevelWarn; if ([[MPConfig get].sendInfo boolValue]) level = PearlLogLevelDebug; if (message.level >= level) { SentryLevel sentryLevel = kSentryLevelInfo; switch (message.level) { case PearlLogLevelTrace: sentryLevel = kSentryLevelNone; break; case PearlLogLevelDebug: sentryLevel = kSentryLevelDebug; break; case PearlLogLevelInfo: sentryLevel = kSentryLevelInfo; break; case PearlLogLevelWarn: sentryLevel = kSentryLevelWarning; break; case PearlLogLevelError: sentryLevel = kSentryLevelError; break; case PearlLogLevelFatal: sentryLevel = kSentryLevelFatal; break; } SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] initWithLevel:sentryLevel category:@"Pearl"]; breadcrumb.type = @"log"; breadcrumb.message = message.message; breadcrumb.timestamp = message.occurrence; breadcrumb.data = @{ @"file": message.fileName, @"line": @(message.lineNumber), @"function": message.function }; [SentrySDK addBreadcrumb:breadcrumb]; } return YES; }]; // Countly CountlyConfig *countlyConfig = [CountlyConfig new]; countlyConfig.host = @"https://countly.lyndir.com"; countlyConfig.appKey = decrypt( countlyKey ); countlyConfig.features = @[ CLYPushNotifications, CLYAutoViewTracking ]; countlyConfig.requiresConsent = YES; countlyConfig.alwaysUsePOST = YES; countlyConfig.deviceID = [PearlKeyChain deviceIdentifier]; countlyConfig.secretSalt = decrypt( countlySalt ); #if DEBUG countlyConfig.enableDebug = YES; countlyConfig.pushTestMode = CLYPushTestModeDevelopment; #elif ! PUBLIC countlyConfig.enableDebug = NO; countlyConfig.pushTestMode = CLYPushTestModeTestFlightOrAdHoc; #endif [Countly.sharedInstance startWithConfig:countlyConfig]; #if ! DEBUG [self.hangDetector = [[PearlHangDetector alloc] initWithHangAction:^(NSTimeInterval hangTime) { MPError( [NSError errorWithDomain:MPErrorDomain code:MPErrorHangCode userInfo:@{ @"time": @(hangTime) }], @"Timeout waiting for main thread after %fs.", hangTime ); }] start]; #endif } @catch (id exception) { err( @"During Analytics Setup: %@", exception ); } @try { PearlAddNotificationObserver( MPCheckConfigNotification, nil, [NSOperationQueue mainQueue], ^(id self, NSNotification *note) { [self updateConfigKey:note.object]; } ); PearlAddNotificationObserver( NSUserDefaultsDidChangeNotification, nil, nil, ^(id self, NSNotification *note) { [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:nil]; } ); } @catch (id exception) { err( @"During Config Test: %@", exception ); } @try { [super application:application didFinishLaunchingWithOptions:launchOptions]; } @catch (id exception) { err( @"During Pearl Application Launch: %@", exception ); } @try { inf( @"Started up with device identifier: %@", [PearlKeyChain deviceIdentifier] ); PearlAddNotificationObserver( MPFoundInconsistenciesNotification, nil, [NSOperationQueue mainQueue], ^(id self, NSNotification *note) { switch ((MPFixableResult)[note.userInfo[MPInconsistenciesFixResultUserKey] unsignedIntegerValue]) { case MPFixableResultNoProblems: break; case MPFixableResultProblemsFixed: { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Inconsistencies Fixed" message: @"Some inconsistencies were detected in your sites.\n" @"All issues were fixed." preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; break; } case MPFixableResultProblemsNotFixed: { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"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." preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; break; } } } ); SKStoreProductViewController *migrateVC = [SKStoreProductViewController new]; [migrateVC loadProductWithParameters:@{ SKStoreProductParameterCampaignToken : @"app-masterpassword.ios", /* Campaign: From MasterPassword iOS */ SKStoreProductParameterProviderToken : @153897, /* Provider: Maarten Billemont */ SKStoreProductParameterITunesItemIdentifier: @510296984, /* Application: MasterPassword iOS */ //SKStoreProductParameterITunesItemIdentifier: @1500430196, /* Application: Volto iOS */ } completionBlock:^(BOOL result, NSError *error) { if (error) err( @"Failed loading Volto product information: %@", error ); if (result) { self.voltoViewController = migrateVC; self.voltoViewController.delegate = self; } else { self.voltoViewController = nil; } }]; PearlMainQueueOperation( ^{ [self.navigationController performSegueWithIdentifier:@"web" sender:[NSURL URLWithString:@"masterpassword://foo?bar=quux"]]; if ([[MPiOSConfig get].showSetup boolValue]) [self.navigationController performSegueWithIdentifier:@"setup" sender:self]; [self consentFeatures]; } ); } @catch (id exception) { err( @"During Post-Startup: %@", exception ); } return YES; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { // No URL? if (!url) return NO; // masterpassword: URLs. if ([url.scheme isEqualToString:@"masterpassword"]) { [self openURL:url]; return YES; } // Arbitrary URL to mpsites data. [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler: ^(NSData *importedSitesData, NSURLResponse *response, NSError *error) { if (error) MPError( error, @"While reading imported sites from %@.", url ); if (!importedSitesData) { PearlMainQueue( ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message: strf( @"Master Password couldn't read the import sites.\n\n%@", (id)[error localizedDescription]?: error ) preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Continue" style:UIAlertActionStyleCancel handler:nil]]; [self.navigationController presentViewController:alert animated:YES completion:nil]; } ); return; } NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding]; if (!importedSitesString) { PearlMainQueue( ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message: @"Master Password couldn't understand the import file." preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Continue" style:UIAlertActionStyleCancel handler:nil]]; [self.navigationController presentViewController:alert animated:YES completion:nil]; } ); return; } [self importSites:importedSitesString]; }] resume]; return YES; } - (void)consentFeatures { if ([self askDiagnostics]) return; [self tryNotifications]; } - (BOOL)askDiagnostics { if ([[MPiOSConfig get].sendInfoDecided boolValue]) return NO; PearlMainQueue( ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Diagnostics" message: @"We look for bugs, sudden crashes, runtime issues & statistics.\n\n" @"Diagnostics are scrubbed and personal details will never leave your device." preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Disable" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { [MPiOSConfig get].sendInfo = @(NO); [MPiOSConfig get].sendInfoDecided = @(YES); [self consentFeatures]; }]]; [alert addAction:[UIAlertAction actionWithTitle:@"Engage" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [MPiOSConfig get].sendInfo = @(YES); [MPiOSConfig get].sendInfoDecided = @(YES); [self consentFeatures]; }]]; [(self.navigationController.presentedViewController?: (UIViewController *)self.navigationController) presentViewController:alert animated:YES completion:nil]; } ); return YES; } - (void)tryNotifications { [Countly.sharedInstance giveConsentForFeature:CLYConsentPushNotifications]; if (@available( iOS 12, * )) { [Countly.sharedInstance askForNotificationPermissionWithOptions:UNAuthorizationOptionProvisional | UNAuthorizationOptionAlert completionHandler:^(BOOL granted, NSError *error) { if (!granted) err( @"No provisional notification permission: %@", error ); [self askNotifications]; }]; return; } [self askNotifications]; } - (void)askNotifications { if ([[MPiOSConfig get].notificationsDecided boolValue]) return; PearlMainQueue( ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Coming Soon" message: @"Master Password is rolling out a brand new, updated version and we're excited to bring you along.\n\n" @"When it's time, we'll send you a notification to help you make an effortless transition." preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Thanks" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { if (@available( iOS 12, * )) { [Countly.sharedInstance askForNotificationPermissionWithOptions:UNAuthorizationOptionAlert completionHandler: ^(BOOL granted, NSError *error) { [MPiOSConfig get].notificationsDecided = @(YES); }]; } else { [Countly.sharedInstance askForNotificationPermission]; [MPiOSConfig get].notificationsDecided = @(YES); } }]]; [(self.navigationController.presentedViewController?: (UIViewController *)self.navigationController) presentViewController:alert animated:YES completion:nil]; } ); } - (void)importSites:(NSString *)importData { if ([NSThread isMainThread]) { PearlNotMainQueue( ^{ [self importSites:importData]; } ); return; } PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:@"Importing"]; [self importSites:importData askImportPassword:^NSString *(NSString *userName) { return PearlAwait( ^(void (^setResult)(id)) { PearlMainQueue( ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:strf( @"Importing Sites For\n%@", userName ) message: @"Enter the master password used to create this export file." preferredStyle:UIAlertControllerStyleAlert]; [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.secureTextEntry = YES; }]; [alert addAction:[UIAlertAction actionWithTitle:@"Import" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { setResult( alert.textFields.firstObject.text ); }]]; [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { setResult( nil ); }]]; [self.navigationController presentViewController:alert animated:YES completion:nil]; } ); } ); } askUserPassword:^NSString *(NSString *userName) { return PearlAwait( (id)^(void (^setResult)(id)) { PearlMainQueue( ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:strf( @"Master Password For\n%@", userName ) message: @"Enter the current master password for this user." preferredStyle:UIAlertControllerStyleAlert]; [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.secureTextEntry = YES; }]; [alert addAction:[UIAlertAction actionWithTitle:@"Import" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { setResult( alert.textFields.firstObject.text ); }]]; [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { setResult( nil ); }]]; [self.navigationController presentViewController:alert animated:YES completion:nil]; } ); } ); } result:^(NSError *error) { PearlMainQueue( ^{ [activityOverlay cancelOverlayAnimated:YES]; if (error && !(error.domain == NSCocoaErrorDomain && error.code == NSUserCancelledError)) { UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Error" message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert]; [controller addAction:[UIAlertAction actionWithTitle:@"Continue" style:UIAlertActionStyleCancel handler:nil]]; [self.navigationController presentViewController:controller animated:YES completion:nil]; } } ); }]; } - (void)applicationWillEnterForeground:(UIApplication *)application { inf( @"Will foreground" ); [super applicationWillEnterForeground:application]; [self.hangDetector start]; } - (void)applicationDidBecomeActive:(UIApplication *)application { inf( @"Re-activated" ); [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:nil]; PearlNotMainQueue( ^{ NSString *importData = [UIPasteboard generalPasteboard].string; MPMarshalledFile *importFile = mpw_marshal_read( NULL, importData.UTF8String ); if (importFile && importFile->error.type == MPMarshalSuccess && importFile->info->format != MPMarshalFormatNone) { PearlMainQueue( ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Import Sites?" message: @"We've detected Master Password import sites on your pasteboard, would you like to import them?" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Import Sites" style:UIAlertActionStyleDefault handler: ^(UIAlertAction *action) { [self importSites:importData]; [UIPasteboard generalPasteboard].string = @""; }]]; [alert addAction:[UIAlertAction actionWithTitle:@"No" style:UIAlertActionStyleCancel handler:nil]]; [self.navigationController presentViewController:alert animated:YES completion:nil]; } ); } mpw_marshal_file_free( &importFile ); } ); [super applicationDidBecomeActive:application]; } - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { inf( @"Received memory warning." ); [super applicationDidReceiveMemoryWarning:application]; } - (void)applicationDidEnterBackground:(UIApplication *)application { inf( @"Did background" ); if (![[MPiOSConfig get].rememberLogin boolValue]) { [UIView setAnimationsEnabled:NO]; [self signOut]; [UIView setAnimationsEnabled:YES]; } [self.hangDetector stop]; [super applicationDidEnterBackground:application]; } #pragma mark - Behavior - (void)openURL:(NSURL *)url { if ([url.scheme isEqualToString:@"masterpassword"]) { if ([url.host isEqualToString:@"open-url"]) { for (NSURLQueryItem *item in [NSURLComponents componentsWithString:[url absoluteString]].queryItems) if ([item.name isEqualToString:@"url"]) { [UIApp openURL:[NSURL URLWithString:item.value]]; return; } } else if ([url.host isEqualToString:@"show-url"]) { for (NSURLQueryItem *item in [NSURLComponents componentsWithString:[url absoluteString]].queryItems) if ([item.name isEqualToString:@"url"]) { [[MPiOSAppDelegate get].navigationController performSegueWithIdentifier:@"web" sender:[NSURL URLWithString:item.value]]; return; } } else if ([url.host isEqualToString:@"migrate"]) { for (NSURLQueryItem *item in [NSURLComponents componentsWithString:[url absoluteString]].queryItems) if ([item.name isEqualToString:@"fullName"]) { [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", item.value]; NSArray *users = [context executeFetchRequest:fetchRequest error:nil]; [self migrateFor:users.firstObject]; }]; return; } [self migrateFor:nil]; return; } } else [UIApp openURL:url]; } - (void)showFeedbackWithLogs:(BOOL)logs forVC:(UIViewController *)viewController { if (![PearlEMail canSendMail]) { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Feedback" message: @"Have a question, comment, issue or just saying thanks?\n\n" @"We'd love to hear what you think!\n" @"help@masterpassword.app" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleCancel handler:nil]]; [self.navigationController presentViewController:alert animated:YES completion:nil]; } else if (logs) { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Feedback" message: @"Have a question, comment, issue or just saying thanks?\n\n" @"If you're having trouble, it may help us if you can first reproduce the problem " @"and then include log files in your message." preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Include Logs" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self openFeedbackWithLogs:YES forVC:viewController]; }]]; [alert addAction:[UIAlertAction actionWithTitle:@"No Logs" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self openFeedbackWithLogs:NO forVC:viewController]; }]]; [self.navigationController presentViewController:alert animated:YES completion:nil]; } else [self openFeedbackWithLogs:NO forVC:viewController]; } - (void)openFeedbackWithLogs:(BOOL)logs forVC:(UIViewController *)viewController { NSString *userName = [[MPiOSAppDelegate get] activeUserForMainThread].name; PearlLogLevel logLevel = PearlLogLevelInfo; if (logs && ([[MPConfig get].sendInfo boolValue] || [[MPiOSConfig get].traceMode boolValue])) logLevel = PearlLogLevelDebug; [[[PearlEMail alloc] initForEMailTo:@"Master Password Development PearlLogLevelInfo) [PearlLogger get].printLevel = PearlLogLevelInfo; [SentrySDK.currentHub getClient].options.enabled = @YES; [SentrySDK configureScope:^(SentryScope *scope) { [scope setExtraValue:[MPConfig get].rememberLogin forKey:@"rememberLogin"]; [scope setExtraValue:[MPConfig get].sendInfo forKey:@"sendInfo"]; [scope setExtraValue:[MPiOSConfig get].helpHidden forKey:@"helpHidden"]; [scope setExtraValue:[MPiOSConfig get].showSetup forKey:@"showQuickStart"]; [scope setExtraValue:[PearlConfig get].firstRun forKey:@"firstRun"]; [scope setExtraValue:[PearlConfig get].launchCount forKey:@"launchCount"]; [scope setExtraValue:[PearlConfig get].askForReviews forKey:@"askForReviews"]; [scope setExtraValue:[PearlConfig get].reviewAfterLaunches forKey:@"reviewAfterLaunches"]; [scope setExtraValue:[PearlConfig get].reviewedVersion forKey:@"reviewedVersion"]; [scope setExtraValue:@([PearlDeviceUtils isSimulator]) forKey:@"simulator"]; [scope setExtraValue:@([PearlDeviceUtils isAppEncrypted]) forKey:@"encrypted"]; [scope setExtraValue:@([PearlDeviceUtils isJailbroken]) forKey:@"jailbroken"]; [scope setExtraValue:[PearlDeviceUtils platform] forKey:@"platform"]; #ifdef APPSTORE [scope setExtraValue:@([PearlDeviceUtils isAppEncrypted]) forKey:@"reviewedVersion"]; #else [scope setExtraValue:@(NO) forKey:@"reviewedVersion"]; #endif [Countly.sharedInstance giveConsentForFeatures:countlyFeatures]; }]; } else { [Countly.sharedInstance cancelConsentForFeatures:countlyFeatures]; [SentrySDK.currentHub getClient].options.enabled = @NO; } } @end