// // MPMacAppDelegate.m // MasterPassword // // Created by Maarten Billemont on 04/03/12. // Copyright (c) 2012 Lyndir. All rights reserved. // #import "MPMacAppDelegate.h" #import "MPAppDelegate_Key.h" #import "MPAppDelegate_Store.h" #import #import #define LOGIN_HELPER_BUNDLE_ID @"com.lyndir.lhunath.MasterPassword.Mac.LoginHelper" @interface MPMacAppDelegate() @property(nonatomic, strong) NSWindowController *initialWindow; @end @implementation MPMacAppDelegate #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wfour-char-constants" static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 }; static EventHotKeyID MPLockHotKey = { .signature = 'lock', .id = 1 }; #pragma clang diagnostic pop + (void)initialize { static dispatch_once_t initialize; dispatch_once( &initialize, ^{ [MPMacConfig get]; #ifdef DEBUG [PearlLogger get].printLevel = PearlLogLevelDebug;//Trace; #endif } ); } static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) { // Extract the hotkey ID. EventHotKeyID hotKeyID; GetEventParameter( theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID ); // Check which hotkey this was. if (hotKeyID.signature == MPShowHotKey.signature && hotKeyID.id == MPShowHotKey.id) { [((__bridge MPMacAppDelegate *)userData) showPasswordWindow:nil]; return noErr; } if (hotKeyID.signature == MPLockHotKey.signature && hotKeyID.id == MPLockHotKey.id) { [((__bridge MPMacAppDelegate *)userData) lock:nil]; return noErr; } return eventNotHandledErr; } - (void)updateUsers { [[[self.usersItem submenu] itemArray] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if (idx > 1) [[self.usersItem submenu] removeItem:obj]; }]; NSManagedObjectContext *context = [MPMacAppDelegate managedObjectContextForMainThreadIfReady]; if (!context) { self.createUserItem.title = @"New User (Not ready)"; self.createUserItem.enabled = NO; self.createUserItem.toolTip = @"Please wait until the app is fully loaded."; [self.usersItem.submenu addItemWithTitle:@"Loading..." action:NULL keyEquivalent:@""].enabled = NO; return; } self.createUserItem.title = @"New User"; self.createUserItem.enabled = YES; self.createUserItem.toolTip = nil; NSError *error = nil; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )]; fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ]; NSArray *users = [context executeFetchRequest:fetchRequest error:&error]; if (!users) err(@"Failed to load users: %@", error); if (![users count]) { NSMenuItem *noUsersItem = [self.usersItem.submenu addItemWithTitle:@"No users" action:NULL keyEquivalent:@""]; noUsersItem.enabled = NO; noUsersItem.toolTip = @"Use the iOS app to create users and make sure iCloud is enabled in its preferences as well. " @"Then give iCloud some time to sync the new user to your Mac."; } MPUserEntity *activeUser = [self activeUserInContext:context]; for (MPUserEntity *user in users) { NSMenuItem *userItem = [[NSMenuItem alloc] initWithTitle:user.name action:@selector(selectUser:) keyEquivalent:@""]; [userItem setTarget:self]; [userItem setRepresentedObject:[user objectID]]; [[self.usersItem submenu] addItem:userItem]; if (!activeUser && [user.name isEqualToString:[MPMacConfig get].usedUserName]) [self selectUser:userItem]; } [self updateMenuItems]; } - (void)selectUser:(NSMenuItem *)item { [self signOutAnimated:NO]; NSError *error = nil; NSManagedObjectContext *context = [MPMacAppDelegate managedObjectContextForMainThreadIfReady]; self.activeUser = (MPUserEntity *)[context existingObjectWithID:[item representedObject] error:&error]; if (error) err(@"While looking up selected user: %@", error); } - (void)showMenu { [self updateMenuItems]; [self.statusView popUpMenu]; } - (IBAction)togglePreference:(id)sender { if (sender == self.enableCloudButton) [self storeManager].cloudEnabled = (self.enableCloudButton.state == NSOnState); if (sender == self.useCloudItem) [self storeManager].cloudEnabled = !(self.useCloudItem.state == NSOnState); if (sender == self.rememberPasswordItem) [MPConfig get].rememberLogin = [NSNumber numberWithBool:![[MPConfig get].rememberLogin boolValue]]; if (sender == self.openAtLoginButton) [self setLoginItemEnabled:(self.openAtLoginButton.state == NSOnState)]; if (sender == self.openAtLoginItem) [self setLoginItemEnabled:!(self.openAtLoginItem.state == NSOnState)]; if (sender == self.savePasswordItem) { [MPMacAppDelegate managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) { MPUserEntity *activeUser = [[MPMacAppDelegate get] activeUserInContext:context]; if ((activeUser.saveKey = !activeUser.saveKey)) [[MPMacAppDelegate get] storeSavedKeyFor:activeUser]; else [[MPMacAppDelegate get] forgetSavedKeyFor:activeUser]; [context saveToStore]; }]; } if (sender == self.dialogStyleRegular) [MPMacConfig get].dialogStyleHUD = @NO; if (sender == self.dialogStyleHUD) [MPMacConfig get].dialogStyleHUD = @YES; } - (IBAction)newUser:(NSMenuItem *)sender { NSAlert *alert = [NSAlert alertWithMessageText:@"New User" defaultButton:@"Create User" alternateButton:nil otherButton:@"Cancel" informativeTextWithFormat:@"To begin, enter your full name.\n\n" @"IMPORTANT: Enter your name correctly, including the right capitalization, " @"as you would on an official document."]; NSTextField *nameField = [[NSTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )]; [alert setAccessoryView:nameField]; [alert layout]; [nameField becomeFirstResponder]; if ([alert runModal] != NSAlertDefaultReturn) return; NSString *name = [(NSSecureTextField *)alert.accessoryView stringValue]; [MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) { MPUserEntity *newUser = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass( [MPUserEntity class] ) inManagedObjectContext:moc]; newUser.name = name; [moc saveToStore]; NSError *error = nil; if (![moc obtainPermanentIDsForObjects:@[ newUser ] error:&error]) err(@"Failed to obtain permanent object ID for new user: %@", error); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self updateUsers]; [self setActiveUser:newUser]; [self showPasswordWindow:nil]; }]; }]; } - (IBAction)lock:(id)sender { self.key = nil; } - (IBAction)rebuildCloud:(id)sender { if ([[NSAlert alertWithMessageText:@"iCloud Truth Sync" defaultButton:@"Continue" alternateButton:nil otherButton:@"Cancel" informativeTextWithFormat:@"This action will force all your iCloud enabled devices to revert to this device's version of the truth." @"\n\nThis is only necessary if you notice that your devices aren't syncing properly anymore. " "Any data on other devices not available from here will be lost."] runModal] == NSAlertDefaultReturn) [self.storeManager rebuildCloudContentFromCloudStoreOrLocalStore:NO]; } - (IBAction)terminate:(id)sender { [self.passwordWindow close]; self.passwordWindow = nil; [NSApp terminate:nil]; } - (IBAction)iphoneAppStore:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://itunes.apple.com/app/id510296984"]]; [self.initialWindow close]; self.initialWindow = nil; } - (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue { [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey ) userInfo:nil]; } #pragma mark - NSApplicationDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Setup delegates and listeners. [MPConfig get].delegate = self; __weak id weakSelf = self; [self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) { dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf updateMenuItems]; }); } forKeyPath:@"key" options:0 context:nil]; [self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) { dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf updateMenuItems]; }); } forKeyPath:@"activeUser" options:0 context:nil]; [self addObserverBlock:^(NSString *keyPath, id object, NSDictionary *change, void *context) { dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf updateMenuItems]; }); } forKeyPath:@"storeManager.cloudAvailable" options:0 context:nil]; // Status item. self.statusView = [[RHStatusItemView alloc] initWithStatusBarItem: [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]]; self.statusView.image = [NSImage imageNamed:@"menu-icon"]; self.statusView.menu = self.statusMenu; self.statusView.target = self; self.statusView.action = @selector(showMenu); [[NSNotificationCenter defaultCenter] addObserverForName:USMStoreDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) { [self updateUsers]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:USMStoreDidImportChangesNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) { [self updateUsers]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:MPCheckConfigNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) { self.rememberPasswordItem.state = [[MPConfig get].rememberLogin boolValue]? NSOnState: NSOffState; self.savePasswordItem.state = [[MPMacAppDelegate get] activeUserForMainThread].saveKey? NSOnState: NSOffState; self.dialogStyleRegular.state = ![[MPMacConfig get].dialogStyleHUD boolValue]? NSOnState: NSOffState; self.dialogStyleHUD.state = [[MPMacConfig get].dialogStyleHUD boolValue]? NSOnState: NSOffState; if ([note.object isEqual:NSStringFromSelector( @selector(dialogStyleHUD) )]) { if (![self.passwordWindow.window isVisible]) self.passwordWindow = nil; else { [self.passwordWindow close]; self.passwordWindow = nil; [self showPasswordWindow:nil]; } } }]; [self updateUsers]; // Global hotkey. EventHotKeyRef hotKeyRef; EventTypeSpec hotKeyEvents[1] = { { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed } }; OSStatus status = InstallApplicationEventHandler(NewEventHandlerUPP( MPHotKeyHander ), GetEventTypeCount( hotKeyEvents ), hotKeyEvents, (__bridge void *)self, NULL); if (status != noErr) err(@"Error installing application event handler: %i", (int)status); status = RegisterEventHotKey( 35 /* p */, controlKey + cmdKey, MPShowHotKey, GetApplicationEventTarget(), 0, &hotKeyRef ); if (status != noErr) err(@"Error registering 'show' hotkey: %i", (int)status); status = RegisterEventHotKey( 35 /* p */, controlKey + optionKey + cmdKey, MPLockHotKey, GetApplicationEventTarget(), 0, &hotKeyRef ); if (status != noErr) err(@"Error registering 'lock' hotkey: %i", (int)status); // Initial display. [NSApp activateIgnoringOtherApps:YES]; if ([[MPMacConfig get].firstRun boolValue]) [self.initialWindow = [[NSWindowController alloc] initWithWindowNibName:@"MPInitialWindow" owner:self] showWindow:self]; } - (void)setActiveUser:(MPUserEntity *)activeUser { [super setActiveUser:activeUser]; self.usersItem.state = NSMixedState; [[[self.usersItem submenu] itemArray] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if ([[obj representedObject] isEqual:[activeUser objectID]]) { [obj setState:NSOnState]; self.usersItem.state = NSOffState; } else [obj setState:NSOffState]; }]; [MPMacConfig get].usedUserName = activeUser.name; } - (void)updateMenuItems { MPUserEntity *activeUser = [self activeUserForMainThread]; // if (!(self.showItem.enabled = ![self.passwordWindow.window isVisible])) { // self.showItem.title = @"Show (Showing)"; // self.showItem.toolTip = @"Master Password is already showing."; // } // else if (!(self.showItem.enabled = (activeUser != nil))) { // self.showItem.title = @"Show (No user)"; // self.showItem.toolTip = @"First select the user to show passwords for."; // } // else { // self.showItem.title = @"Show"; // self.showItem.toolTip = nil; // } if (self.key) { self.lockItem.title = @"Lock"; self.lockItem.enabled = YES; self.lockItem.toolTip = nil; } else { self.lockItem.title = @"Lock (Locked)"; self.lockItem.enabled = NO; self.lockItem.toolTip = @"Master Password is currently locked."; } BOOL loginItemEnabled = [self loginItemEnabled]; self.openAtLoginItem.state = loginItemEnabled? NSOnState: NSOffState; self.openAtLoginButton.state = loginItemEnabled? NSOnState: NSOffState; self.rememberPasswordItem.state = [[MPConfig get].rememberLogin boolValue]? NSOnState: NSOffState; self.savePasswordItem.state = activeUser.saveKey? NSOnState: NSOffState; if (!activeUser) { self.savePasswordItem.title = @"Save Password (No user)"; self.savePasswordItem.enabled = NO; self.savePasswordItem.toolTip = @"First select your user and unlock by showing the Master Password window."; } else if (!self.key) { self.savePasswordItem.title = @"Save Password (Locked)"; self.savePasswordItem.enabled = NO; self.savePasswordItem.toolTip = @"First unlock by showing the Master Password window."; } else { self.savePasswordItem.title = @"Save Password"; self.savePasswordItem.enabled = YES; self.savePasswordItem.toolTip = nil; } self.useCloudItem.state = self.storeManager.cloudEnabled? NSOnState: NSOffState; self.enableCloudButton.state = self.storeManager.cloudEnabled? NSOnState: NSOffState; self.useCloudItem.enabled = self.storeManager.cloudAvailable; if (self.storeManager.cloudAvailable) { self.useCloudItem.title = @"Use iCloud"; self.useCloudItem.toolTip = nil; } else { self.useCloudItem.title = @"Use iCloud (Unavailable)"; self.useCloudItem.toolTip = @"iCloud is not set up for your Mac user."; } } - (IBAction)showPasswordWindow:(id)sender { [NSApp activateIgnoringOtherApps:YES]; // If no user, can't activate. if (![self activeUserForMainThread]) { [[NSAlert alertWithMessageText:@"No User Selected" defaultButton:[PearlStrings get].commonButtonOkay alternateButton:nil otherButton:nil informativeTextWithFormat: @"Begin by selecting or creating your user from the status menu (●●●|) next to the clock."] runModal]; [self.statusView popUpMenu]; return; } // Don't show window if we weren't already running (ie. if we haven't been activated before). if (!self.passwordWindow) self.passwordWindow = [[MPPasswordWindowController alloc] initWithWindowNibName:@"MPPasswordWindowController"]; [self.passwordWindow showWindow:self]; } - (void)setLoginItemEnabled:(BOOL)enabled { BOOL loginItemEnabled = [self loginItemEnabled]; if (loginItemEnabled != enabled) { if (SMLoginItemSetEnabled( (__bridge CFStringRef)LOGIN_HELPER_BUNDLE_ID, (Boolean)enabled ) == true) loginItemEnabled = enabled; else wrn(@"Failed to set login item."); } self.openAtLoginItem.state = loginItemEnabled? NSOnState: NSOffState; self.openAtLoginButton.state = loginItemEnabled? NSOnState: NSOffState; } - (BOOL)loginItemEnabled { // The easy and sane method (SMJobCopyDictionary) can pose problems when the app is sandboxed. -_- NSArray *jobs = (__bridge_transfer NSArray *)SMCopyAllJobDictionaries( kSMDomainUserLaunchd ); for (NSDictionary *job in jobs) if ([LOGIN_HELPER_BUNDLE_ID isEqualToString:[job objectForKey:@"Label"]]) { dbg(@"loginItemEnabled: %@", @([[job objectForKey:@"OnDemand"] boolValue])); return [[job objectForKey:@"OnDemand"] boolValue]; } dbg(@"loginItemEnabled: not found"); return NO; } - (void)applicationWillResignActive:(NSNotification *)notification { if (![[MPConfig get].rememberLogin boolValue]) [self lock:nil]; } - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { // Save changes in the application's managed object context before the application terminates. NSManagedObjectContext *context = [MPMacAppDelegate managedObjectContextForMainThreadIfReady]; if (!context) return NSTerminateNow; if (![context commitEditing]) return NSTerminateCancel; if (![context hasChanges]) return NSTerminateNow; [context saveToStore]; return NSTerminateNow; } @end