2017-04-05 20:56:22 +00:00
|
|
|
//==============================================================================
|
|
|
|
// This file is part of Master Password.
|
|
|
|
// Copyright (c) 2011-2017, Maarten Billemont.
|
2012-03-04 14:31:26 +00:00
|
|
|
//
|
2017-04-05 20:56:22 +00:00
|
|
|
// 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.
|
2012-03-04 14:31:26 +00:00
|
|
|
//
|
2017-04-05 20:56:22 +00:00
|
|
|
// 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.
|
2012-03-04 14:31:26 +00:00
|
|
|
//
|
2017-04-05 20:56:22 +00:00
|
|
|
// You can find a copy of the GNU General Public License in the
|
|
|
|
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
|
|
|
//==============================================================================
|
2012-03-04 14:31:26 +00:00
|
|
|
|
2013-04-25 01:23:53 +00:00
|
|
|
#import "MPMacAppDelegate.h"
|
2012-05-13 08:24:19 +00:00
|
|
|
#import "MPAppDelegate_Key.h"
|
|
|
|
#import "MPAppDelegate_Store.h"
|
2012-05-04 22:15:51 +00:00
|
|
|
#import <Carbon/Carbon.h>
|
2013-06-08 22:12:49 +00:00
|
|
|
#import <ServiceManagement/ServiceManagement.h>
|
|
|
|
|
|
|
|
#define LOGIN_HELPER_BUNDLE_ID @"com.lyndir.lhunath.MasterPassword.Mac.LoginHelper"
|
2012-03-04 14:31:26 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
@implementation MPMacAppDelegate
|
2013-04-20 18:11:19 +00:00
|
|
|
|
2012-06-24 14:29:51 +00:00
|
|
|
#pragma clang diagnostic push
|
|
|
|
#pragma clang diagnostic ignored "-Wfour-char-constants"
|
2013-04-20 18:11:19 +00:00
|
|
|
static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 };
|
|
|
|
static EventHotKeyID MPLockHotKey = { .signature = 'lock', .id = 1 };
|
2012-06-24 14:29:51 +00:00
|
|
|
#pragma clang diagnostic pop
|
2012-05-04 22:15:51 +00:00
|
|
|
|
2012-03-06 00:04:19 +00:00
|
|
|
+ (void)initialize {
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2014-07-26 05:26:33 +00:00
|
|
|
static dispatch_once_t once = 0;
|
|
|
|
dispatch_once( &once, ^{
|
2013-01-17 05:37:20 +00:00
|
|
|
[MPMacConfig get];
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2013-04-20 18:11:19 +00:00
|
|
|
#ifdef DEBUG
|
2014-06-08 00:13:53 +00:00
|
|
|
[PearlLogger get].printLevel = PearlLogLevelDebug; //Trace;
|
2013-04-20 18:11:19 +00:00
|
|
|
#endif
|
|
|
|
} );
|
2012-03-06 00:04:19 +00:00
|
|
|
}
|
|
|
|
|
2012-06-08 21:46:13 +00:00
|
|
|
static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
|
|
|
|
|
2012-05-04 22:15:51 +00:00
|
|
|
// Extract the hotkey ID.
|
2012-06-08 21:46:13 +00:00
|
|
|
EventHotKeyID hotKeyID;
|
2013-04-20 18:11:19 +00:00
|
|
|
GetEventParameter( theEvent, kEventParamDirectObject, typeEventHotKeyID,
|
2014-06-22 01:56:28 +00:00
|
|
|
NULL, sizeof( hotKeyID ), NULL, &hotKeyID );
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2012-05-04 22:15:51 +00:00
|
|
|
// Check which hotkey this was.
|
|
|
|
if (hotKeyID.signature == MPShowHotKey.signature && hotKeyID.id == MPShowHotKey.id) {
|
2013-05-16 04:19:50 +00:00
|
|
|
[((__bridge MPMacAppDelegate *)userData) showPasswordWindow:nil];
|
2012-05-04 22:15:51 +00:00
|
|
|
return noErr;
|
|
|
|
}
|
2013-01-17 05:37:20 +00:00
|
|
|
if (hotKeyID.signature == MPLockHotKey.signature && hotKeyID.id == MPLockHotKey.id) {
|
2013-04-25 01:23:53 +00:00
|
|
|
[((__bridge MPMacAppDelegate *)userData) lock:nil];
|
2013-01-17 05:37:20 +00:00
|
|
|
return noErr;
|
|
|
|
}
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2012-05-04 22:15:51 +00:00
|
|
|
return eventNotHandledErr;
|
|
|
|
}
|
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
#pragma mark - Life
|
2013-04-20 18:11:19 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
|
|
|
|
2014-12-31 16:28:46 +00:00
|
|
|
#ifdef CRASHLYTICS
|
|
|
|
NSString *crashlyticsAPIKey = [self crashlyticsAPIKey];
|
|
|
|
if ([crashlyticsAPIKey length]) {
|
|
|
|
inf(@"Initializing Crashlytics");
|
|
|
|
#if defined (DEBUG) || defined (ADHOC)
|
|
|
|
[Crashlytics sharedInstance].debugMode = YES;
|
|
|
|
#endif
|
2015-10-29 01:12:47 +00:00
|
|
|
[[Crashlytics sharedInstance] setUserIdentifier:[PearlKeyChain deviceIdentifier]];
|
|
|
|
[[Crashlytics sharedInstance] setObjectValue:[PearlKeyChain deviceIdentifier] forKey:@"deviceIdentifier"];
|
|
|
|
[[Crashlytics sharedInstance] setUserName:@"Anonymous"];
|
|
|
|
[[Crashlytics sharedInstance] setObjectValue:@"Anonymous" forKey:@"username"];
|
2014-12-31 16:28:46 +00:00
|
|
|
[Crashlytics startWithAPIKey:crashlyticsAPIKey];
|
|
|
|
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
|
|
|
|
PearlLogLevel level = PearlLogLevelInfo;
|
|
|
|
if ([[MPConfig get].sendInfo boolValue])
|
|
|
|
level = PearlLogLevelDebug;
|
|
|
|
|
|
|
|
if (message.level >= level)
|
|
|
|
CLSLog( @"%@", [message messageDescription] );
|
|
|
|
|
|
|
|
return YES;
|
|
|
|
}];
|
|
|
|
CLSLog( @"Crashlytics (%@) initialized for: %@ v%@.", //
|
|
|
|
[Crashlytics sharedInstance].version, [PearlInfoPlist get].CFBundleName, [PearlInfoPlist get].CFBundleVersion );
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
// 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];
|
|
|
|
|
|
|
|
// Status item.
|
2014-10-31 01:05:13 +00:00
|
|
|
self.statusView = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
2014-06-25 00:30:15 +00:00
|
|
|
self.statusView.image = [NSImage imageNamed:@"menu-icon"];
|
2014-10-31 01:05:13 +00:00
|
|
|
self.statusView.image.template = YES;
|
2014-06-25 00:30:15 +00:00
|
|
|
self.statusView.menu = self.statusMenu;
|
|
|
|
self.statusView.target = self;
|
|
|
|
self.statusView.action = @selector( showMenu );
|
|
|
|
|
2014-10-28 04:53:16 +00:00
|
|
|
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresWillChangeNotification, self.storeCoordinator, nil,
|
|
|
|
^(id self, NSNotification *note) {
|
|
|
|
PearlMainQueue( ^{
|
|
|
|
[self updateUsers];
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresDidChangeNotification, self.storeCoordinator, nil,
|
|
|
|
^(id self, NSNotification *note) {
|
|
|
|
PearlMainQueue( ^{
|
|
|
|
[self updateUsers];
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
PearlAddNotificationObserver( MPCheckConfigNotification, nil, nil,
|
|
|
|
^(MPMacAppDelegate *self, NSNotification *note) {
|
|
|
|
PearlMainQueue( ^{
|
|
|
|
NSString *key = note.object;
|
2017-04-01 04:30:25 +00:00
|
|
|
if (!key || [key isEqualToString:NSStringFromSelector( @selector( hidePasswords ) )])
|
|
|
|
self.hidePasswordsItem.state = [[MPConfig get].hidePasswords boolValue]? NSOnState: NSOffState;
|
|
|
|
if (!key || [key isEqualToString:NSStringFromSelector( @selector( rememberLogin ) )])
|
|
|
|
self.rememberPasswordItem.state = [[MPConfig get].rememberLogin boolValue]? NSOnState: NSOffState;
|
2014-10-28 04:53:16 +00:00
|
|
|
} );
|
|
|
|
} );
|
2014-06-25 00:30:15 +00:00
|
|
|
[self updateUsers];
|
2012-10-31 02:54:34 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
// Global hotkey.
|
|
|
|
EventHotKeyRef hotKeyRef;
|
|
|
|
EventTypeSpec hotKeyEvents[1] = { { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed } };
|
2017-04-01 04:30:25 +00:00
|
|
|
OSStatus status = InstallApplicationEventHandler( NewEventHandlerUPP( MPHotKeyHander ), GetEventTypeCount( hotKeyEvents ), hotKeyEvents,
|
|
|
|
(__bridge void *)self, NULL );
|
2014-06-25 00:30:15 +00:00
|
|
|
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 );
|
2013-01-17 05:37:20 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
// Initial display.
|
2014-07-19 02:04:10 +00:00
|
|
|
if ([[MPMacConfig get].firstRun boolValue]) {
|
|
|
|
[(self.initialWindowController = [[MPInitialWindowController alloc] initWithWindowNibName:@"MPInitialWindow"])
|
|
|
|
.window makeKeyAndOrderFront:self];
|
2014-06-30 03:18:25 +00:00
|
|
|
[NSApp activateIgnoringOtherApps:YES];
|
2012-10-31 02:54:34 +00:00
|
|
|
}
|
2014-06-25 00:30:15 +00:00
|
|
|
}
|
2012-10-31 02:54:34 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
- (void)applicationWillResignActive:(NSNotification *)notification {
|
2014-06-08 00:13:53 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
if (![[MPConfig get].rememberLogin boolValue])
|
|
|
|
[self lock:nil];
|
|
|
|
}
|
2013-04-20 18:11:19 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
|
|
|
|
// Save changes in the application's managed object context before the application terminates.
|
2014-06-08 00:13:53 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
NSManagedObjectContext *context = [MPMacAppDelegate managedObjectContextForMainThreadIfReady];
|
|
|
|
if (!context)
|
|
|
|
return NSTerminateNow;
|
2013-04-20 18:11:19 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
if (![context commitEditing])
|
|
|
|
return NSTerminateCancel;
|
2013-04-20 18:11:19 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
if (![context hasChanges])
|
|
|
|
return NSTerminateNow;
|
2013-04-20 18:11:19 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
[context saveToStore];
|
|
|
|
return NSTerminateNow;
|
|
|
|
}
|
2014-06-08 00:13:53 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
#pragma mark - State
|
|
|
|
|
|
|
|
- (void)setActiveUser:(MPUserEntity *)activeUser {
|
|
|
|
|
|
|
|
[super setActiveUser:activeUser];
|
|
|
|
|
2016-07-22 00:03:47 +00:00
|
|
|
if (activeUser)
|
|
|
|
[MPMacConfig get].usedUserName = activeUser.name;
|
2014-06-25 00:30:15 +00:00
|
|
|
|
|
|
|
PearlMainQueue( ^{
|
|
|
|
[self updateUsers];
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setLoginItemEnabled:(BOOL)enabled {
|
|
|
|
|
|
|
|
BOOL loginItemEnabled = [self loginItemEnabled];
|
|
|
|
if (loginItemEnabled != enabled) {
|
|
|
|
if (SMLoginItemSetEnabled( (__bridge CFStringRef)LOGIN_HELPER_BUNDLE_ID, (Boolean)enabled ) == true)
|
|
|
|
loginItemEnabled = enabled;
|
2014-06-08 00:13:53 +00:00
|
|
|
else
|
2014-06-25 00:30:15 +00:00
|
|
|
wrn( @"Failed to set login item." );
|
2013-01-31 05:42:32 +00:00
|
|
|
}
|
2013-06-08 22:12:49 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
self.openAtLoginItem.state = loginItemEnabled? NSOnState: NSOffState;
|
2014-06-30 03:18:25 +00:00
|
|
|
self.initialWindowController.openAtLoginButton.state = loginItemEnabled? NSOnState: NSOffState;
|
2014-06-25 00:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (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)
|
2014-10-28 04:53:16 +00:00
|
|
|
if ([LOGIN_HELPER_BUNDLE_ID isEqualToString:job[@"Label"]])
|
2014-10-26 14:39:59 +00:00
|
|
|
return [job[@"OnDemand"] boolValue];
|
2014-06-25 00:30:15 +00:00
|
|
|
|
|
|
|
return NO;
|
2012-10-31 02:54:34 +00:00
|
|
|
}
|
|
|
|
|
2016-05-16 04:50:17 +00:00
|
|
|
- (BOOL)isFeatureUnlocked:(NSString *)productIdentifier {
|
|
|
|
|
|
|
|
// All features are unlocked for mac versions.
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
#pragma mark - Actions
|
|
|
|
|
2012-10-31 02:54:34 +00:00
|
|
|
- (void)selectUser:(NSMenuItem *)item {
|
2013-04-20 18:11:19 +00:00
|
|
|
|
2013-05-07 04:45:06 +00:00
|
|
|
[self signOutAnimated:NO];
|
|
|
|
|
2014-06-29 03:45:06 +00:00
|
|
|
NSManagedObjectContext *mainContext = [MPMacAppDelegate managedObjectContextForMainThreadIfReady];
|
|
|
|
self.activeUser = [MPUserEntity existingObjectWithID:[item representedObject] inContext:mainContext];
|
|
|
|
}
|
2013-04-24 00:38:56 +00:00
|
|
|
|
2014-06-29 03:45:06 +00:00
|
|
|
- (IBAction)exportSitesSecure:(id)sender {
|
|
|
|
|
|
|
|
[self exportSitesAndRevealPasswords:NO];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)exportSitesReveal:(id)sender {
|
|
|
|
|
|
|
|
[self exportSitesAndRevealPasswords:YES];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)importSites:(id)sender {
|
|
|
|
|
|
|
|
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
|
|
|
|
openPanel.allowsMultipleSelection = NO;
|
|
|
|
openPanel.canChooseDirectories = NO;
|
|
|
|
openPanel.title = @"Master Password";
|
|
|
|
openPanel.message = @"Locate the Master Password export file to import.";
|
|
|
|
openPanel.prompt = @"Import";
|
|
|
|
openPanel.directoryURL = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
|
|
|
|
openPanel.allowedFileTypes = @[ @"mpsites" ];
|
|
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
|
|
if ([openPanel runModal] == NSFileHandlingPanelCancelButton)
|
|
|
|
return;
|
|
|
|
|
|
|
|
NSURL *url = openPanel.URL;
|
2014-12-29 21:35:23 +00:00
|
|
|
[openPanel close];
|
2014-06-29 03:45:06 +00:00
|
|
|
|
2017-04-14 04:24:34 +00:00
|
|
|
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:
|
|
|
|
^(NSData *importedSitesData, NSURLResponse *response, NSError *error) {
|
|
|
|
if (error)
|
|
|
|
err( @"While reading imported sites from %@: %@", url, [error fullDescription] );
|
|
|
|
if (!importedSitesData)
|
|
|
|
return;
|
|
|
|
|
|
|
|
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
|
|
|
|
MPImportResult result = [self importSites:importedSitesString askImportPassword:^NSString *(NSString *userName) {
|
|
|
|
__block NSString *masterPassword = nil;
|
|
|
|
|
|
|
|
PearlMainQueueWait( ^{
|
|
|
|
NSAlert *alert = [NSAlert new];
|
|
|
|
[alert addButtonWithTitle:@"Unlock"];
|
|
|
|
[alert addButtonWithTitle:@"Cancel"];
|
|
|
|
alert.messageText = @"Import File's Master Password";
|
|
|
|
alert.informativeText = strf( @"%@'s export was done using a different master password.\n"
|
|
|
|
@"Enter that master password to unlock the exported data.", userName );
|
|
|
|
alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
|
|
|
|
[alert layout];
|
|
|
|
if ([alert runModal] == NSAlertFirstButtonReturn)
|
|
|
|
masterPassword = ((NSTextField *)alert.accessoryView).stringValue;
|
|
|
|
} );
|
|
|
|
|
|
|
|
return masterPassword;
|
|
|
|
} askUserPassword:^NSString *(NSString *userName, NSUInteger importCount, NSUInteger deleteCount) {
|
|
|
|
__block NSString *masterPassword = nil;
|
|
|
|
|
|
|
|
PearlMainQueueWait( ^{
|
|
|
|
NSAlert *alert = [NSAlert new];
|
|
|
|
[alert addButtonWithTitle:@"Import"];
|
|
|
|
[alert addButtonWithTitle:@"Cancel"];
|
|
|
|
alert.messageText = strf( @"Master Password for\n%@", userName );
|
|
|
|
alert.informativeText = strf( @"Imports %lu sites, overwriting %lu.",
|
|
|
|
(unsigned long)importCount, (unsigned long)deleteCount );
|
|
|
|
alert.accessoryView = [[NSSecureTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
|
|
|
|
[alert layout];
|
|
|
|
if ([alert runModal] == NSAlertFirstButtonReturn)
|
|
|
|
masterPassword = ((NSTextField *)alert.accessoryView).stringValue;
|
|
|
|
} );
|
|
|
|
|
|
|
|
return masterPassword;
|
|
|
|
}];
|
2014-06-29 03:45:06 +00:00
|
|
|
|
2017-04-14 04:24:34 +00:00
|
|
|
PearlMainQueue( ^{
|
|
|
|
switch (result) {
|
|
|
|
case MPImportResultSuccess: {
|
|
|
|
[self updateUsers];
|
|
|
|
|
|
|
|
NSAlert *alert = [NSAlert new];
|
|
|
|
alert.messageText = @"Successfully imported sites.";
|
|
|
|
[alert runModal];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case MPImportResultCancelled:
|
|
|
|
break;
|
|
|
|
case MPImportResultInternalError:
|
|
|
|
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
|
|
|
|
NSLocalizedDescriptionKey: @"Import failed because of an internal error."
|
|
|
|
}]] runModal];
|
|
|
|
break;
|
|
|
|
case MPImportResultMalformedInput:
|
|
|
|
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
|
|
|
|
NSLocalizedDescriptionKey: @"The import doesn't look like a Master Password export."
|
|
|
|
}]] runModal];
|
|
|
|
break;
|
|
|
|
case MPImportResultInvalidPassword:
|
|
|
|
[[NSAlert alertWithError:[NSError errorWithDomain:MPErrorDomain code:0 userInfo:@{
|
|
|
|
NSLocalizedDescriptionKey: @"Incorrect master password for the import sites."
|
|
|
|
}]] runModal];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
}] resume];
|
2012-10-31 02:54:34 +00:00
|
|
|
}
|
|
|
|
|
2013-06-08 22:12:49 +00:00
|
|
|
- (IBAction)togglePreference:(id)sender {
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2014-06-27 03:13:21 +00:00
|
|
|
if (sender == self.hidePasswordsItem)
|
2016-04-17 23:30:06 +00:00
|
|
|
[MPConfig get].hidePasswords = @(self.hidePasswordsItem.state != NSOnState);
|
2013-04-24 04:25:51 +00:00
|
|
|
if (sender == self.rememberPasswordItem)
|
2016-04-17 23:30:06 +00:00
|
|
|
[MPConfig get].rememberLogin = @(self.rememberPasswordItem.state != NSOnState);
|
2013-06-08 22:12:49 +00:00
|
|
|
if (sender == self.openAtLoginItem)
|
2014-02-22 23:27:14 +00:00
|
|
|
[self setLoginItemEnabled:self.openAtLoginItem.state != NSOnState];
|
2016-04-17 23:30:06 +00:00
|
|
|
if (sender == self.showFullScreenItem) {
|
|
|
|
[MPMacConfig get].fullScreen = @(self.showFullScreenItem.state != NSOnState);
|
|
|
|
[NSApp updateWindows];
|
|
|
|
}
|
2013-04-24 04:25:51 +00:00
|
|
|
if (sender == self.savePasswordItem) {
|
2013-05-16 02:42:21 +00:00
|
|
|
[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];
|
|
|
|
}];
|
2012-11-01 14:55:11 +00:00
|
|
|
}
|
2014-06-27 03:13:21 +00:00
|
|
|
|
|
|
|
[MPMacConfig flush];
|
2016-05-15 04:14:41 +00:00
|
|
|
[self updateMenuItems];
|
2012-10-31 02:54:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)newUser:(NSMenuItem *)sender {
|
2013-05-03 00:40:12 +00:00
|
|
|
|
2016-07-02 13:04:31 +00:00
|
|
|
NSAlert *alert = [NSAlert new];
|
|
|
|
[alert setMessageText:@"New User"];
|
|
|
|
[alert setInformativeText:@"To begin, enter your full name.\n\n"
|
|
|
|
@"IMPORTANT: Enter your name correctly, including the right capitalization, "
|
|
|
|
@"as you would on an official document."];
|
|
|
|
[alert addButtonWithTitle:@"Create User"];
|
|
|
|
[alert addButtonWithTitle:@"Cancel"];
|
2013-05-03 00:40:12 +00:00
|
|
|
NSTextField *nameField = [[NSTextField alloc] initWithFrame:NSMakeRect( 0, 0, 200, 22 )];
|
|
|
|
[alert setAccessoryView:nameField];
|
|
|
|
[alert layout];
|
|
|
|
[nameField becomeFirstResponder];
|
2016-07-02 13:04:31 +00:00
|
|
|
if ([alert runModal] != NSAlertFirstButtonReturn)
|
2013-05-03 00:40:12 +00:00
|
|
|
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])
|
2014-09-22 03:28:50 +00:00
|
|
|
err( @"Failed to obtain permanent object ID for new user: %@", [error fullDescription] );
|
2013-05-03 00:40:12 +00:00
|
|
|
|
|
|
|
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
|
|
|
|
[self updateUsers];
|
|
|
|
[self setActiveUser:newUser];
|
2013-05-16 04:19:50 +00:00
|
|
|
[self showPasswordWindow:nil];
|
2013-05-03 00:40:12 +00:00
|
|
|
}];
|
|
|
|
}];
|
2012-10-31 02:54:34 +00:00
|
|
|
}
|
|
|
|
|
2014-06-08 00:13:53 +00:00
|
|
|
- (IBAction)deleteUser:(NSMenuItem *)sender {
|
|
|
|
|
2016-07-02 13:04:31 +00:00
|
|
|
NSAlert *alert = [NSAlert new];
|
|
|
|
[alert setMessageText:@"Delete User"];
|
|
|
|
[alert setInformativeText:strf( @"This will delete %@ and all their sites.", self.activeUserForMainThread.name )];
|
|
|
|
[alert addButtonWithTitle:@"Delete"];
|
|
|
|
[alert addButtonWithTitle:@"Cancel"];
|
|
|
|
if ([alert runModal] != NSAlertFirstButtonReturn)
|
2014-06-08 00:13:53 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) {
|
|
|
|
[moc deleteObject:[self activeUserInContext:moc]];
|
|
|
|
[self setActiveUser:nil];
|
|
|
|
[moc saveToStore];
|
|
|
|
|
|
|
|
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
|
|
|
|
[self updateUsers];
|
|
|
|
[self showPasswordWindow:nil];
|
|
|
|
}];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
2013-01-27 03:05:57 +00:00
|
|
|
- (IBAction)lock:(id)sender {
|
|
|
|
|
2015-10-29 01:12:47 +00:00
|
|
|
[self signOutAnimated:YES];
|
2013-01-27 03:05:57 +00:00
|
|
|
}
|
|
|
|
|
2013-05-19 20:55:43 +00:00
|
|
|
- (IBAction)terminate:(id)sender {
|
|
|
|
|
2014-06-30 03:18:25 +00:00
|
|
|
[self.passwordWindowController close];
|
|
|
|
self.passwordWindowController = nil;
|
2013-06-08 22:12:49 +00:00
|
|
|
|
2013-05-19 20:55:43 +00:00
|
|
|
[NSApp terminate:nil];
|
|
|
|
}
|
|
|
|
|
2014-06-27 03:13:21 +00:00
|
|
|
- (IBAction)showPopup:(id)sender {
|
|
|
|
|
2014-10-31 01:05:13 +00:00
|
|
|
[self.statusView popUpStatusItemMenu:self.statusView.menu];
|
2014-06-27 03:13:21 +00:00
|
|
|
}
|
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
- (IBAction)showPasswordWindow:(id)sender {
|
2012-05-09 23:02:55 +00:00
|
|
|
|
2015-11-01 04:19:47 +00:00
|
|
|
prof_new( @"showPasswordWindow" );
|
2014-06-25 00:30:15 +00:00
|
|
|
[NSApp activateIgnoringOtherApps:YES];
|
2017-04-01 04:30:25 +00:00
|
|
|
prof_rewind( @"activateIgnoringOtherApps" );
|
2012-05-09 23:02:55 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
// If no user, can't activate.
|
|
|
|
if (![self activeUserForMainThread]) {
|
2014-06-29 03:45:06 +00:00
|
|
|
NSAlert *alert = [NSAlert new];
|
|
|
|
alert.messageText = @"No User Selected";
|
|
|
|
alert.informativeText = @"Begin by selecting or creating your user from the status menu (●●●|) next to the clock.";
|
|
|
|
[alert runModal];
|
2014-06-27 03:13:21 +00:00
|
|
|
[self showPopup:nil];
|
2015-11-01 04:19:47 +00:00
|
|
|
prof_finish( @"activeUserForMainThread" );
|
2014-06-25 00:30:15 +00:00
|
|
|
return;
|
|
|
|
}
|
2015-11-01 04:19:47 +00:00
|
|
|
prof_rewind( @"activeUserForMainThread" );
|
2012-05-09 23:02:55 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
// Don't show window if we weren't already running (ie. if we haven't been activated before).
|
2014-06-30 03:18:25 +00:00
|
|
|
if (!self.passwordWindowController)
|
|
|
|
self.passwordWindowController = [[MPPasswordWindowController alloc] initWithWindowNibName:@"MPPasswordWindowController"];
|
2015-11-01 04:19:47 +00:00
|
|
|
prof_rewind( @"initWithWindow" );
|
2013-04-24 04:25:51 +00:00
|
|
|
|
2014-06-30 03:18:25 +00:00
|
|
|
[self.passwordWindowController showWindow:self];
|
2015-11-01 04:19:47 +00:00
|
|
|
prof_finish( @"showWindow" );
|
2014-06-25 00:30:15 +00:00
|
|
|
}
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
#pragma mark - Private
|
2012-11-01 14:55:11 +00:00
|
|
|
|
2014-06-29 03:45:06 +00:00
|
|
|
- (void)exportSitesAndRevealPasswords:(BOOL)revealPasswords {
|
|
|
|
|
|
|
|
MPUserEntity *mainActiveUser = [self activeUserForMainThread];
|
|
|
|
if (!mainActiveUser) {
|
|
|
|
NSAlert *alert = [NSAlert new];
|
|
|
|
alert.messageText = @"No User Selected";
|
|
|
|
alert.informativeText = @"To export your sites, first select the user whose sites to export.";
|
|
|
|
[alert runModal];
|
|
|
|
[self showPopup:nil];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!self.key) {
|
|
|
|
NSAlert *alert = [NSAlert new];
|
|
|
|
alert.messageText = @"User Locked";
|
|
|
|
alert.informativeText = @"To export your sites, first unlock your user by opening Master Password.";
|
|
|
|
[alert runModal];
|
|
|
|
[self showPopup:nil];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
|
|
|
|
[exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
|
|
|
|
|
|
|
|
NSSavePanel *savePanel = [NSSavePanel savePanel];
|
|
|
|
savePanel.title = @"Master Password";
|
|
|
|
savePanel.message = @"Pick a location for the export Master Password's sites.";
|
|
|
|
if (revealPasswords)
|
|
|
|
savePanel.message = strf( @"%@\nWARNING: Your passwords will be visible. Make sure to always keep the file in a secure location.",
|
|
|
|
savePanel.message );
|
|
|
|
savePanel.prompt = @"Export";
|
|
|
|
savePanel.directoryURL = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
|
|
|
|
savePanel.nameFieldStringValue = strf( @"%@ (%@).mpsites", mainActiveUser.name,
|
|
|
|
[exportDateFormatter stringFromDate:[NSDate date]] );
|
|
|
|
savePanel.allowedFileTypes = @[ @"mpsites" ];
|
|
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
|
|
if ([savePanel runModal] == NSFileHandlingPanelCancelButton)
|
|
|
|
return;
|
|
|
|
|
|
|
|
NSError *coordinateError = nil;
|
|
|
|
NSString *exportedSites = [self exportSitesRevealPasswords:revealPasswords];
|
|
|
|
[[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:savePanel.URL options:0 error:&coordinateError
|
|
|
|
byAccessor:^(NSURL *newURL) {
|
2014-10-28 04:53:16 +00:00
|
|
|
NSError *writeError = nil;
|
|
|
|
if (![exportedSites writeToURL:newURL atomically:NO
|
|
|
|
encoding:NSUTF8StringEncoding
|
|
|
|
error:&writeError])
|
|
|
|
PearlMainQueue( ^{
|
|
|
|
[[NSAlert alertWithError:writeError] runModal];
|
|
|
|
} );
|
|
|
|
}];
|
2014-06-29 03:45:06 +00:00
|
|
|
if (coordinateError)
|
|
|
|
PearlMainQueue( ^{
|
|
|
|
[[NSAlert alertWithError:coordinateError] runModal];
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
- (void)updateUsers {
|
|
|
|
|
|
|
|
[[[self.usersItem submenu] itemArray] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
|
|
|
if (idx > 2)
|
|
|
|
[[self.usersItem submenu] removeItem:obj];
|
2014-06-06 01:43:06 +00:00
|
|
|
}];
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2014-06-29 03:45:06 +00:00
|
|
|
NSManagedObjectContext *mainContext = [MPMacAppDelegate managedObjectContextForMainThreadIfReady];
|
|
|
|
if (!mainContext) {
|
2014-06-25 00:30:15 +00:00
|
|
|
self.createUserItem.title = @"New User (Not ready)";
|
|
|
|
self.createUserItem.enabled = NO;
|
|
|
|
self.createUserItem.toolTip = @"Please wait until the app is fully loaded.";
|
|
|
|
self.deleteUserItem.title = @"Delete User (Not ready)";
|
|
|
|
self.deleteUserItem.enabled = NO;
|
|
|
|
self.deleteUserItem.toolTip = @"Please wait until the app is fully loaded.";
|
|
|
|
[self.usersItem.submenu addItemWithTitle:@"Loading..." action:NULL keyEquivalent:@""].enabled = NO;
|
2013-06-08 22:12:49 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
return;
|
2014-02-22 23:27:14 +00:00
|
|
|
}
|
2013-01-17 05:37:20 +00:00
|
|
|
|
2014-06-29 03:45:06 +00:00
|
|
|
MPUserEntity *mainActiveUser = [self activeUserInContext:mainContext];
|
2013-04-24 00:38:56 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
self.createUserItem.title = @"New User";
|
|
|
|
self.createUserItem.enabled = YES;
|
|
|
|
self.createUserItem.toolTip = nil;
|
2013-04-24 00:38:56 +00:00
|
|
|
|
2014-06-29 03:45:06 +00:00
|
|
|
self.deleteUserItem.title = mainActiveUser? @"Delete User": @"Delete User (None Selected)";
|
|
|
|
self.deleteUserItem.enabled = mainActiveUser != nil;
|
|
|
|
self.deleteUserItem.toolTip = mainActiveUser? nil: @"First select the user to delete.";
|
2014-06-08 00:13:53 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
NSError *error = nil;
|
|
|
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
|
|
|
|
fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ];
|
2014-06-29 03:45:06 +00:00
|
|
|
NSArray *users = [mainContext executeFetchRequest:fetchRequest error:&error];
|
2014-06-25 00:30:15 +00:00
|
|
|
if (!users)
|
2014-09-22 03:28:50 +00:00
|
|
|
err( @"Failed to load users: %@", [error fullDescription] );
|
2014-06-25 00:30:15 +00:00
|
|
|
|
|
|
|
if (![users count]) {
|
|
|
|
NSMenuItem *noUsersItem = [self.usersItem.submenu addItemWithTitle:@"No users" action:NULL keyEquivalent:@""];
|
|
|
|
noUsersItem.enabled = NO;
|
2014-07-19 02:04:10 +00:00
|
|
|
noUsersItem.toolTip = @"Begin by creating a user.";
|
2014-06-25 00:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
self.usersItem.state = NSMixedState;
|
|
|
|
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];
|
|
|
|
|
2014-06-29 03:45:06 +00:00
|
|
|
if (!mainActiveUser && [user.name isEqualToString:[MPMacConfig get].usedUserName])
|
|
|
|
[super setActiveUser:mainActiveUser = user];
|
2014-06-25 00:30:15 +00:00
|
|
|
|
2014-06-29 03:45:06 +00:00
|
|
|
if ([mainActiveUser isEqual:user]) {
|
2014-06-25 00:30:15 +00:00
|
|
|
userItem.state = NSOnState;
|
|
|
|
self.usersItem.state = NSOffState;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
userItem.state = NSOffState;
|
|
|
|
}
|
|
|
|
|
|
|
|
[self updateMenuItems];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)showMenu {
|
|
|
|
|
|
|
|
[self updateMenuItems];
|
|
|
|
|
2014-10-31 01:05:13 +00:00
|
|
|
[self.statusView popUpStatusItemMenu:self.statusView.menu];
|
2013-04-24 00:38:56 +00:00
|
|
|
}
|
|
|
|
|
2013-01-17 05:37:20 +00:00
|
|
|
- (void)updateMenuItems {
|
|
|
|
|
2013-06-16 16:39:52 +00:00
|
|
|
MPUserEntity *activeUser = [self activeUserForMainThread];
|
2014-06-30 03:18:25 +00:00
|
|
|
// if (!(self.showItem.enabled = ![self.passwordWindowController.window isVisible])) {
|
2013-05-16 04:19:50 +00:00
|
|
|
// 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;
|
|
|
|
// }
|
2013-01-17 05:37:20 +00:00
|
|
|
|
|
|
|
if (self.key) {
|
2013-04-20 18:11:19 +00:00
|
|
|
self.lockItem.title = @"Lock";
|
2013-01-17 05:37:20 +00:00
|
|
|
self.lockItem.enabled = YES;
|
|
|
|
self.lockItem.toolTip = nil;
|
2013-04-20 18:11:19 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
self.lockItem.title = @"Lock (Locked)";
|
2013-01-17 05:37:20 +00:00
|
|
|
self.lockItem.enabled = NO;
|
|
|
|
self.lockItem.toolTip = @"Master Password is currently locked.";
|
|
|
|
}
|
|
|
|
|
2013-06-08 22:12:49 +00:00
|
|
|
BOOL loginItemEnabled = [self loginItemEnabled];
|
|
|
|
self.openAtLoginItem.state = loginItemEnabled? NSOnState: NSOffState;
|
2016-04-17 23:30:06 +00:00
|
|
|
self.showFullScreenItem.state = [[MPMacConfig get].fullScreen boolValue]? NSOnState: NSOffState;
|
2014-06-30 03:18:25 +00:00
|
|
|
self.initialWindowController.openAtLoginButton.state = loginItemEnabled? NSOnState: NSOffState;
|
2013-01-17 05:37:20 +00:00
|
|
|
self.rememberPasswordItem.state = [[MPConfig get].rememberLogin boolValue]? NSOnState: NSOffState;
|
|
|
|
|
2013-04-21 21:05:59 +00:00
|
|
|
self.savePasswordItem.state = activeUser.saveKey? NSOnState: NSOffState;
|
|
|
|
if (!activeUser) {
|
2013-04-20 18:11:19 +00:00
|
|
|
self.savePasswordItem.title = @"Save Password (No user)";
|
2013-01-17 05:37:20 +00:00
|
|
|
self.savePasswordItem.enabled = NO;
|
|
|
|
self.savePasswordItem.toolTip = @"First select your user and unlock by showing the Master Password window.";
|
2013-04-20 18:11:19 +00:00
|
|
|
}
|
|
|
|
else if (!self.key) {
|
|
|
|
self.savePasswordItem.title = @"Save Password (Locked)";
|
2013-01-17 05:37:20 +00:00
|
|
|
self.savePasswordItem.enabled = NO;
|
|
|
|
self.savePasswordItem.toolTip = @"First unlock by showing the Master Password window.";
|
2013-04-20 18:11:19 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
self.savePasswordItem.title = @"Save Password";
|
2013-01-17 05:37:20 +00:00
|
|
|
self.savePasswordItem.enabled = YES;
|
|
|
|
self.savePasswordItem.toolTip = nil;
|
|
|
|
}
|
2014-07-19 02:04:10 +00:00
|
|
|
}
|
2013-01-17 05:37:20 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
#pragma mark - PearlConfigDelegate
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
|
2012-06-08 21:46:13 +00:00
|
|
|
|
2014-06-25 00:30:15 +00:00
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey )];
|
2012-03-04 14:31:26 +00:00
|
|
|
}
|
|
|
|
|
2014-12-31 16:28:46 +00:00
|
|
|
#pragma mark - Crashlytics
|
|
|
|
|
|
|
|
- (NSDictionary *)crashlyticsInfo {
|
|
|
|
|
|
|
|
static NSDictionary *crashlyticsInfo = nil;
|
|
|
|
if (crashlyticsInfo == nil)
|
|
|
|
crashlyticsInfo = [[NSDictionary alloc] initWithContentsOfURL:
|
|
|
|
[[NSBundle mainBundle] URLForResource:@"Crashlytics" withExtension:@"plist"]];
|
|
|
|
|
|
|
|
return crashlyticsInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)crashlyticsAPIKey {
|
|
|
|
|
|
|
|
NSString *crashlyticsAPIKey = NSNullToNil( [[self crashlyticsInfo] valueForKeyPath:@"API Key"] );
|
|
|
|
if (![crashlyticsAPIKey length])
|
|
|
|
wrn( @"Crashlytics API key not set. Crash logs won't be recorded." );
|
|
|
|
|
|
|
|
return crashlyticsAPIKey;
|
|
|
|
}
|
|
|
|
|
2012-03-04 14:31:26 +00:00
|
|
|
@end
|