From 8bedcedfaf7d84593e3467ce5e29a394b1e1d1bc Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 25 Apr 2020 17:10:20 -0400 Subject: [PATCH] Enable support for internal actions from URLs. --- platform-darwin/Source/MPAppDelegate_Store.h | 7 +- platform-darwin/Source/MPAppDelegate_Store.m | 35 ++- .../Source/iOS/MPNavigationController.m | 11 + .../Source/iOS/MPSitesViewController.m | 67 +--- .../Source/iOS/MPWebViewController.m | 17 +- platform-darwin/Source/iOS/MPiOSAppDelegate.h | 9 +- platform-darwin/Source/iOS/MPiOSAppDelegate.m | 289 +++++++++++++----- .../Source/iOS/MasterPassword-Info.plist | 16 + 8 files changed, 292 insertions(+), 159 deletions(-) diff --git a/platform-darwin/Source/MPAppDelegate_Store.h b/platform-darwin/Source/MPAppDelegate_Store.h index a2147596..2023738a 100644 --- a/platform-darwin/Source/MPAppDelegate_Store.h +++ b/platform-darwin/Source/MPAppDelegate_Store.h @@ -39,8 +39,9 @@ askImportPassword:(NSString *( ^ )(NSString *userName))importPassword askUserPassword:(NSString *( ^ )(NSString *userName))userPassword result:(void ( ^ )(NSError *error))resultBlock; -- (void)exportSitesRevealPasswords:(BOOL)revealPasswords - askExportPassword:(NSString *( ^ )(NSString *userName))askImportPassword - result:(void ( ^ )(NSString *exportedUser, NSError *error))resultBlock; +- (NSString *)exportSitesFor:(MPUserEntity *)user + revealPasswords:(BOOL)revealPasswords + askExportPassword:(NSString *( ^ )(NSString *userName))askExportPassword + error:(__autoreleasing NSError **)error; @end diff --git a/platform-darwin/Source/MPAppDelegate_Store.m b/platform-darwin/Source/MPAppDelegate_Store.m index 7c1696d0..226d0744 100644 --- a/platform-darwin/Source/MPAppDelegate_Store.m +++ b/platform-darwin/Source/MPAppDelegate_Store.m @@ -704,16 +704,17 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); site.lastUsed = [NSDate dateWithTimeIntervalSince1970:importSite->lastUsed]; } -- (void)exportSitesRevealPasswords:(BOOL)revealPasswords - askExportPassword:(NSString *( ^ )(NSString *userName))askImportPassword - result:(void ( ^ )(NSString *exportedUser, NSError *error))resultBlock { - - [MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - MPUserEntity *user = [self activeUserInContext:context]; +- (NSString *)exportSitesFor:(MPUserEntity *)user + revealPasswords:(BOOL)revealPasswords + askExportPassword:(NSString *( ^ )(NSString *userName))askExportPassword + error:(__autoreleasing NSError **)error { + MPMarshalledUser *exportUser = NULL; + MPMarshalledFile *exportFile = NULL; + @try { inf( @"Exporting sites, %@, for user: %@", revealPasswords? @"revealing passwords": @"omitting passwords", user.userID ); - MPMarshalledUser *exportUser = mpw_marshal_user( user.name.UTF8String, - mpw_masterKeyProvider_str( askImportPassword( user.name ).UTF8String ), user.algorithm.version ); + exportUser = mpw_marshal_user( user.name.UTF8String, + mpw_masterKeyProvider_str( askExportPassword( user.name ).UTF8String ), user.algorithm.version ); exportUser->redacted = !revealPasswords; exportUser->avatar = (unsigned int)user.avatar; exportUser->keyID = mpw_strdup( [user.keyID encodeHex].UTF8String ); @@ -737,22 +738,26 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); mpw_marshal_question( exportSite, siteQuestion.keyword.UTF8String ); } - MPMarshalledFile *exportFile = NULL; const char *export = mpw_marshal_write( MPMarshalFormatDefault, &exportFile, exportUser ); NSString *exportedUser = nil; if (export && exportFile && exportFile->error.type == MPMarshalSuccess) exportedUser = [NSString stringWithCString:export encoding:NSUTF8StringEncoding]; mpw_free_string( &export ); - resultBlock( exportedUser, exportFile && exportFile->error.type == MPMarshalSuccess? nil: - [NSError errorWithDomain:MPErrorDomain code:MPErrorMarshalCode userInfo:@{ - @"type" : @(exportFile? exportFile->error.type: MPMarshalErrorInternal), - NSLocalizedDescriptionKey: @(exportFile? exportFile->error.message: nil), - }] ); + if (error) + *error = exportFile && exportFile->error.type == MPMarshalSuccess? nil: + [NSError errorWithDomain:MPErrorDomain code:MPErrorMarshalCode userInfo:@{ + @"type" : @(exportFile? exportFile->error.type: MPMarshalErrorInternal), + NSLocalizedDescriptionKey: @(exportFile? exportFile->error.message: nil), + }]; + + return exportedUser; + } + @finally { mpw_marshal_file_free( &exportFile ); mpw_marshal_user_free( &exportUser ); mpw_masterKeyProvider_free(); - }]; + } } @end diff --git a/platform-darwin/Source/iOS/MPNavigationController.m b/platform-darwin/Source/iOS/MPNavigationController.m index bf252eab..a7c0438c 100644 --- a/platform-darwin/Source/iOS/MPNavigationController.m +++ b/platform-darwin/Source/iOS/MPNavigationController.m @@ -18,6 +18,7 @@ #import "MPNavigationController.h" #import "MPWebViewController.h" +#import "MPiOSAppDelegate.h" @implementation MPNavigationController @@ -29,6 +30,16 @@ [self performSegueWithIdentifier:@"setup" sender:self]; } +- (void)performSegueWithIdentifier:(NSString *)identifier sender:(id)sender { + + if ([identifier isEqualToString:@"web"] && [[(NSURL *)sender scheme] isEqualToString:@"masterpassword"]) { + [[MPiOSAppDelegate get] openURL:sender]; + return; + } + + [super performSegueWithIdentifier:identifier sender:sender]; +} + - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"web"]) diff --git a/platform-darwin/Source/iOS/MPSitesViewController.m b/platform-darwin/Source/iOS/MPSitesViewController.m index c165086b..de8183ee 100644 --- a/platform-darwin/Source/iOS/MPSitesViewController.m +++ b/platform-darwin/Source/iOS/MPSitesViewController.m @@ -33,9 +33,8 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { MPPasswordsBadNameTip = 1 << 0, }; -@interface MPSitesViewController() +@interface MPSitesViewController() -@property(nonatomic, strong) SKStoreProductViewController *voltoViewController; @property(nonatomic, strong) NSFetchedResultsController *fetchedResultsController; @property(nonatomic, strong) NSArray *fuzzyGroups; @property(nonatomic, strong) NSCharacterSet *siteNameAcceptableCharactersSet; @@ -435,13 +434,6 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { } completion:completion]; } -#pragma mark - SKStoreProductViewControllerDelegate - -- (void)productViewControllerDidFinish:(SKStoreProductViewController *)viewController { - - [viewController dismissViewControllerAnimated:YES completion:nil]; -} - #pragma mark - Actions - (IBAction)dismissPopdown:(id)sender { @@ -454,45 +446,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { - (IBAction)upgradeVolto:(UIButton *)sender { - if ([UIApp canOpenURL:[[NSURL alloc] initWithString:@"volto:"]]) { - [[MPiOSAppDelegate get] exportSitesRevealPasswords:NO askExportPassword:^NSString *(NSString *userName) { - return PearlAwait( ^(void (^setResult)(id)) { - PearlMainQueue( ^{ - UIAlertController *alert = [UIAlertController alertControllerWithTitle:strf( @"Master Password For:\n%@", userName ) - message:@"Enter your master password to export the user." - preferredStyle:UIAlertControllerStyleAlert]; - [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { - textField.secureTextEntry = YES; - }]; - [alert addAction:[UIAlertAction actionWithTitle:@"Export" 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:^(NSString *exportedUser, NSError *error) { - if (!exportedUser || error) { - MPError( error, @"Failed to export user." ); - PearlMainQueue( ^{ - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Export Error" - message:[error localizedDescription] - preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleCancel handler:nil]]; - [self.navigationController presentViewController:alert animated:YES completion:nil]; - } ); - return; - } - - NSURLComponents *components = [NSURLComponents new]; - components.scheme = @"volto"; - components.path = @"import"; - components.queryItems = @[ [[NSURLQueryItem alloc] initWithName:@"data" value:exportedUser] ]; - [UIApp openURL:components.URL]; - }]; - } - else if (self.voltoViewController) - [self presentViewController:self.voltoViewController animated:YES completion:nil]; + [[MPiOSAppDelegate get] migrateFor:[MPiOSAppDelegate get].activeUserForMainThread]; } #pragma mark - Private @@ -505,23 +459,8 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { self.voltoMigrateAlert.visible = YES; } else { - self.voltoInstallAlert.visible = NO; + self.voltoInstallAlert.visible = [MPiOSAppDelegate get].voltoViewController != nil; self.voltoMigrateAlert.visible = NO; - self.voltoViewController = [SKStoreProductViewController new]; - self.voltoViewController.delegate = self; - [self.voltoViewController 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 ); - - [UIView animateWithDuration:0.3f animations:^{ - self.voltoInstallAlert.visible = result; - }]; - }]; } } diff --git a/platform-darwin/Source/iOS/MPWebViewController.m b/platform-darwin/Source/iOS/MPWebViewController.m index 0e92d8a7..8c2095fa 100644 --- a/platform-darwin/Source/iOS/MPWebViewController.m +++ b/platform-darwin/Source/iOS/MPWebViewController.m @@ -17,6 +17,7 @@ //============================================================================== #import "MPWebViewController.h" +#import "MPiOSAppDelegate.h" @implementation MPWebViewController @@ -57,6 +58,18 @@ #pragma mark - WKNavigationDelegate +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction +decisionHandler:(void ( ^ )(WKNavigationActionPolicy))decisionHandler { + + if ([navigationAction.request.mainDocumentURL.scheme isEqualToString:@"masterpassword"]) { + [[MPiOSAppDelegate get] openURL:navigationAction.request.mainDocumentURL]; + decisionHandler( WKNavigationActionPolicyCancel ); + return; + } + + decisionHandler( WKNavigationActionPolicyAllow ); +} + - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation { self.webNavigationItem.title = webView.URL.host; @@ -77,7 +90,7 @@ } [self.webNavigationItem setLeftBarButtonItem:[[UIBarButtonItem alloc] - initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(openURL:)]]; + initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector( action: )]]; [webView evaluateJavaScript:@"document.title" completionHandler:^(id o, NSError *error) { self.webNavigationItem.prompt = [o description]; }]; @@ -85,7 +98,7 @@ #pragma mark - Actions -- (IBAction)openURL:(id)sender { +- (IBAction)action:(id)sender { UIAlertController *controller = [UIAlertController new]; controller.title = self.webView.URL.host; diff --git a/platform-darwin/Source/iOS/MPiOSAppDelegate.h b/platform-darwin/Source/iOS/MPiOSAppDelegate.h index 9108cebd..270ceb34 100644 --- a/platform-darwin/Source/iOS/MPiOSAppDelegate.h +++ b/platform-darwin/Source/iOS/MPiOSAppDelegate.h @@ -17,15 +17,22 @@ //============================================================================== #import +#import #import "MPAppDelegate_Shared.h" -@interface MPiOSAppDelegate : MPAppDelegate_Shared +@interface MPiOSAppDelegate : MPAppDelegate_Shared + +@property(nonatomic, strong) SKStoreProductViewController *voltoViewController; + +- (void)openURL:(NSURL *)url; - (void)showFeedbackWithLogs:(BOOL)logs forVC:(UIViewController *)viewController; - (void)openFeedbackWithLogs:(BOOL)logs forVC:(UIViewController *)viewController; - (void)showExportForVC:(UIViewController *)viewController; +- (void)migrateFor:(MPUserEntity *)user; + - (void)changeMasterPasswordFor:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc didResetBlock:(void ( ^ )(void))didReset; @end diff --git a/platform-darwin/Source/iOS/MPiOSAppDelegate.m b/platform-darwin/Source/iOS/MPiOSAppDelegate.m index 82ccef1b..c9eca5f1 100644 --- a/platform-darwin/Source/iOS/MPiOSAppDelegate.m +++ b/platform-darwin/Source/iOS/MPiOSAppDelegate.m @@ -35,7 +35,7 @@ @implementation CountlyPushNotifications(MPNotifications) - (void)openURL:(NSString *)URLString { - [UIApp.keyWindow.rootViewController performSegueWithIdentifier:@"web" sender:[NSURL URLWithString:URLString]]; + [[MPiOSAppDelegate get].navigationController performSegueWithIdentifier:@"web" sender:[NSURL URLWithString:URLString]]; } @end @@ -186,7 +186,26 @@ } } ); + 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]; @@ -207,6 +226,12 @@ 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) { @@ -444,6 +469,42 @@ #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]) { @@ -564,89 +625,162 @@ return; } - [self exportSitesRevealPasswords:revealPasswords askExportPassword:^NSString *(NSString *userName) { - return PearlAwait( ^(void (^setResult)(id)) { - PearlMainQueue( ^{ - UIAlertController *alert = [UIAlertController alertControllerWithTitle:strf( @"Master Password For:\n%@", userName ) - message:@"Enter your master password to export the user." - preferredStyle:UIAlertControllerStyleAlert]; - [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { - textField.secureTextEntry = YES; - }]; - [alert addAction:[UIAlertAction actionWithTitle:@"Export" 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]; + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + NSError *error = nil; + NSString *exportedUser = [self exportSitesFor:[self activeUserInContext:context] revealPasswords:revealPasswords askExportPassword:^NSString *(NSString *userName) { + return PearlAwait( ^(void (^setResult)(id)) { + PearlMainQueue( ^{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:strf( @"Master Password For:\n%@", userName ) + message:@"Enter your master password to export the user." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.secureTextEntry = YES; + }]; + [alert addAction:[UIAlertAction actionWithTitle:@"Export" 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:^(NSString *exportedUser, NSError *error) { - if (!exportedUser || error) { - MPError( error, @"Failed to export mpsites." ); - PearlMainQueue( ^{ + } error:&error]; + + PearlMainQueue( ^{ + if (!exportedUser || error) { + MPError( error, @"Failed to export mpsites." ); UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Export Error" message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleCancel handler:nil]]; [self.navigationController presentViewController:alert animated:YES completion:nil]; - } ); + return; + } + + NSDateFormatter *exportDateFormatter = [NSDateFormatter new]; + [exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"]; + NSString *exportFileName = strf( @"%@ (%@).mpsites", + [self activeUserForMainThread].name, [exportDateFormatter stringFromDate:[NSDate date]] ); + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Export Destination" message:nil + preferredStyle:UIAlertControllerStyleActionSheet]; + [alert addAction:[UIAlertAction actionWithTitle:@"Send As E-Mail" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + NSString *message; + if (revealPasswords) + message = strf( @"Export of Master Password sites with passwords included.\n\n" + @"REMINDER: Make sure nobody else sees this file! Passwords are visible!\n\n\n" + @"--\n" + @"%@\n" + @"Master Password %@, build %@", + [self activeUserForMainThread].name, + [PearlInfoPlist get].CFBundleShortVersionString, + [PearlInfoPlist get].CFBundleVersion ); + else + message = strf( @"Backup of Master Password sites.\n\n\n" + @"--\n" + @"%@\n" + @"Master Password %@, build %@", + [self activeUserForMainThread].name, + [PearlInfoPlist get].CFBundleShortVersionString, + [PearlInfoPlist get].CFBundleVersion ); + + [PearlEMail sendEMailTo:nil fromVC:viewController subject:@"Master Password Export" body:message + attachments:[[PearlEMailAttachment alloc] initWithContent:[exportedUser dataUsingEncoding:NSUTF8StringEncoding] + mimeType:@"text/plain" + fileName:exportFileName], nil]; + return; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Share / Export" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + NSURL *applicationSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory + inDomains:NSUserDomainMask] lastObject]; + NSURL *exportURL = [[applicationSupportURL + URLByAppendingPathComponent:[NSBundle mainBundle].bundleIdentifier isDirectory:YES] + URLByAppendingPathComponent:exportFileName isDirectory:NO]; + NSError *writeError = nil; + if (![[exportedUser dataUsingEncoding:NSUTF8StringEncoding] + writeToURL:exportURL options:NSDataWritingFileProtectionComplete error:&writeError]) + MPError( writeError, @"Failed to write export data to URL %@.", exportURL ); + else { + self.interactionController = [UIDocumentInteractionController interactionControllerWithURL:exportURL]; + self.interactionController.UTI = @"com.lyndir.masterpassword.sites"; + self.interactionController.delegate = self; + [self.interactionController presentOpenInMenuFromRect:CGRectZero inView:viewController.view animated:YES]; + } + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Continue" style:UIAlertActionStyleCancel handler:nil]]; + [self.navigationController presentViewController:alert animated:YES completion:nil]; + } ); + }]; +} + +- (void)migrateFor:(MPUserEntity *)user { + + if ([UIApp canOpenURL:[[NSURL alloc] initWithString:@"volto:"]]) { + if (!user) { + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )]; + NSArray *users = [context executeFetchRequest:fetchRequest error:nil]; + if (![users count]) + return; + + UIAlertController *usersSheet = [UIAlertController alertControllerWithTitle:@"Migrate User" + message:@"Choose a user to migrate out to Volto." + preferredStyle:UIAlertControllerStyleActionSheet]; + [usersSheet addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; + for (MPUserEntity *user_ in users) + [usersSheet addAction:[UIAlertAction actionWithTitle:user_.name style:UIAlertActionStyleDefault handler: + ^(UIAlertAction *action) { [self migrateFor:user_]; }]]; + + PearlMainQueue( ^{ + [self.navigationController presentViewController:usersSheet animated:YES completion:nil]; + } ); + }]; return; } - NSDateFormatter *exportDateFormatter = [NSDateFormatter new]; - [exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"]; - NSString *exportFileName = strf( @"%@ (%@).mpsites", - [self activeUserForMainThread].name, [exportDateFormatter stringFromDate:[NSDate date]] ); + [MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + NSError *error = nil; + NSString *exportedUser = [[MPAppDelegate_Shared get] exportSitesFor:[MPUserEntity existingObjectWithID:user.objectID inContext:context] + revealPasswords:NO askExportPassword:^NSString *(NSString *userName) { + return PearlAwait( ^(void (^setResult)(id)) { + PearlMainQueue( ^{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:strf( @"Master Password For:\n%@", userName ) + message:@"Enter your master password to export the user." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.secureTextEntry = YES; + }]; + [alert addAction:[UIAlertAction actionWithTitle:@"Export" 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]; + } ); + } ); + } error:&error]; - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Export Destination" message:nil - preferredStyle:UIAlertControllerStyleActionSheet]; - [alert addAction:[UIAlertAction actionWithTitle:@"Send As E-Mail" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - NSString *message; - if (revealPasswords) - message = strf( @"Export of Master Password sites with passwords included.\n\n" - @"REMINDER: Make sure nobody else sees this file! Passwords are visible!\n\n\n" - @"--\n" - @"%@\n" - @"Master Password %@, build %@", - [self activeUserForMainThread].name, - [PearlInfoPlist get].CFBundleShortVersionString, - [PearlInfoPlist get].CFBundleVersion ); - else - message = strf( @"Backup of Master Password sites.\n\n\n" - @"--\n" - @"%@\n" - @"Master Password %@, build %@", - [self activeUserForMainThread].name, - [PearlInfoPlist get].CFBundleShortVersionString, - [PearlInfoPlist get].CFBundleVersion ); + PearlMainQueue( ^{ + if (!exportedUser || error) { + MPError( error, @"Failed to export user." ); + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Export Error" + message:[error localizedDescription] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleCancel handler:nil]]; + [self.navigationController presentViewController:alert animated:YES completion:nil]; + return; + } - [PearlEMail sendEMailTo:nil fromVC:viewController subject:@"Master Password Export" body:message - attachments:[[PearlEMailAttachment alloc] initWithContent:[exportedUser dataUsingEncoding:NSUTF8StringEncoding] - mimeType:@"text/plain" - fileName:exportFileName], nil]; - return; - }]]; - [alert addAction:[UIAlertAction actionWithTitle:@"Share / Export" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - NSURL *applicationSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory - inDomains:NSUserDomainMask] lastObject]; - NSURL *exportURL = [[applicationSupportURL - URLByAppendingPathComponent:[NSBundle mainBundle].bundleIdentifier isDirectory:YES] - URLByAppendingPathComponent:exportFileName isDirectory:NO]; - NSError *writeError = nil; - if (![[exportedUser dataUsingEncoding:NSUTF8StringEncoding] - writeToURL:exportURL options:NSDataWritingFileProtectionComplete error:&writeError]) - MPError( writeError, @"Failed to write export data to URL %@.", exportURL ); - else { - self.interactionController = [UIDocumentInteractionController interactionControllerWithURL:exportURL]; - self.interactionController.UTI = @"com.lyndir.masterpassword.sites"; - self.interactionController.delegate = self; - [self.interactionController presentOpenInMenuFromRect:CGRectZero inView:viewController.view animated:YES]; - } - }]]; - [alert addAction:[UIAlertAction actionWithTitle:@"Continue" style:UIAlertActionStyleCancel handler:nil]]; - [self.navigationController presentViewController:alert animated:YES completion:nil]; - }]; + NSURLComponents *components = [NSURLComponents new]; + components.scheme = @"volto"; + components.path = @"import"; + components.queryItems = @[ [[NSURLQueryItem alloc] initWithName:@"data" value:exportedUser] ]; + [UIApp openURL:components.URL]; + } ); + }]; + } + + else if (self.voltoViewController) + [self.navigationController presentViewController:self.voltoViewController animated:YES completion:nil]; } - (void)changeMasterPasswordFor:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc didResetBlock:(void ( ^ )(void))didReset { @@ -674,6 +808,13 @@ } ); } +#pragma mark - SKStoreProductViewControllerDelegate + +- (void)productViewControllerDidFinish:(SKStoreProductViewController *)viewController { + + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + #pragma mark - UIDocumentInteractionControllerDelegate - (void)documentInteractionController:(UIDocumentInteractionController *)controller didEndSendingToApplication:(NSString *)application { diff --git a/platform-darwin/Source/iOS/MasterPassword-Info.plist b/platform-darwin/Source/iOS/MasterPassword-Info.plist index e63bf3c4..4d5af2af 100644 --- a/platform-darwin/Source/iOS/MasterPassword-Info.plist +++ b/platform-darwin/Source/iOS/MasterPassword-Info.plist @@ -37,6 +37,21 @@ APPL CFBundleShortVersionString [auto] + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLIconFile + Icon-320 + CFBundleURLName + com.lyndir.masterpassword + CFBundleURLSchemes + + masterpassword + + + CFBundleVersion [auto] LSApplicationQueriesSchemes @@ -44,6 +59,7 @@ firefox googlechrome opera-http + volto LSRequiresIPhoneOS