Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7e72e98ac | ||
|
|
a0b1e63c74 | ||
|
|
42704ab504 | ||
|
|
6cc74c8898 | ||
|
|
44911f1d9e | ||
|
|
16fc32ee30 | ||
|
|
ac9534c680 | ||
|
|
2020637f0e | ||
|
|
ada6d6b36a | ||
|
|
b0875beb36 | ||
|
|
94818bb242 | ||
|
|
1f68a4cb09 | ||
|
|
f7e64fe4b8 | ||
|
|
301366f1f1 | ||
|
|
941b428cfc | ||
|
|
0d638a4c3f | ||
|
|
cdfafa55cf | ||
|
|
82b8de5e23 | ||
|
|
6bbd183ac9 | ||
|
|
7d9131cdca | ||
|
|
12fb7243d6 | ||
|
|
eb4e25c0ba | ||
|
|
587461144b | ||
|
|
9968491e3b | ||
|
|
db7d68a091 | ||
|
|
c8f9f79bb2 | ||
|
|
21c0565619 | ||
|
|
04bc7a497c | ||
|
|
7c5cea9c8d | ||
|
|
1b90c9bfa3 | ||
|
|
f622b2c7d4 | ||
|
|
98080ceb51 | ||
|
|
f5d9334b06 | ||
|
|
f882d0fb53 | ||
|
|
dfc62fa8a8 | ||
|
|
f5551d4823 | ||
|
|
424479dada | ||
|
|
376953ae56 | ||
|
|
26f8e086bb | ||
|
|
950ce888e2 | ||
|
|
657fef6249 | ||
|
|
0d19202ca7 | ||
|
|
1755bd1607 | ||
|
|
dc8b6aa86c | ||
|
|
0c536ef640 | ||
|
|
fa3680f9c4 | ||
|
|
c8e03ff016 | ||
|
|
3fcac696bb | ||
|
|
d0ae954dbf | ||
|
|
f3c24fd96f | ||
|
|
02ffa9611a | ||
|
|
6bda70920b | ||
|
|
2761355180 | ||
|
|
cbf624b3da | ||
|
|
9899104891 | ||
|
|
d34ec96b94 | ||
|
|
ac735f3ccb | ||
|
|
c508ed8c46 | ||
|
|
bad54f2c0c | ||
|
|
eae8e9b7c4 | ||
|
|
2ff3f0804b | ||
|
|
50da5c0cb1 | ||
|
|
f3196841f3 | ||
|
|
039ec9b082 | ||
|
|
ad9c52896d |
5
.gitmodules
vendored
@@ -1,6 +1,9 @@
|
||||
[submodule "External/Pearl"]
|
||||
path = External/Pearl
|
||||
url = git@github.com:Lyndir/Pearl.git
|
||||
url = git://github.com/Lyndir/Pearl.git
|
||||
[submodule "External/InAppSettingsKit"]
|
||||
path = External/InAppSettingsKit
|
||||
url = git://github.com/futuretap/InAppSettingsKit.git
|
||||
[submodule "External/iCloudStoreManager"]
|
||||
path = External/iCloudStoreManager
|
||||
url = git://github.com/alekseyn/iCloudStoreManager.git
|
||||
|
||||
1
Crashlytics/Crashlytics.framework/Crashlytics
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
Versions/Current/Crashlytics
|
||||
1
Crashlytics/Crashlytics.framework/Headers
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
Versions/Current/Headers
|
||||
1
Crashlytics/Crashlytics.framework/Resources
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
Versions/Current/Resources
|
||||
BIN
Crashlytics/Crashlytics.framework/Versions/A/Crashlytics
vendored
Normal file
89
Crashlytics/Crashlytics.framework/Versions/A/Headers/Crashlytics.h
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Crashlytics.h
|
||||
// Crashlytics
|
||||
//
|
||||
// Copyright 2012 Crashlytics, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol CrashlyticsDelegate;
|
||||
|
||||
@interface Crashlytics : NSObject {
|
||||
@private
|
||||
NSString *_apiKey;
|
||||
NSString *_dataDirectory;
|
||||
NSString *_bundleIdentifier;
|
||||
BOOL _installed;
|
||||
NSMutableDictionary *_customAttributes;
|
||||
id _user;
|
||||
NSInteger _sendButtonIndex;
|
||||
NSInteger _alwaysSendButtonIndex;
|
||||
NSObject <CrashlyticsDelegate> *_delegate;
|
||||
}
|
||||
|
||||
@property (nonatomic, readonly, copy) NSString *apiKey;
|
||||
@property (nonatomic, readonly, copy) NSString *version;
|
||||
@property (nonatomic, assign) BOOL debugMode;
|
||||
|
||||
@property (nonatomic, assign) NSObject <CrashlyticsDelegate> *delegate;
|
||||
|
||||
/**
|
||||
*
|
||||
* The recommended way to install Crashlytics into your application is to place a call
|
||||
* to +startWithAPIKey: in your -application:didFinishLaunchingWithOptions: method.
|
||||
*
|
||||
* This delay defaults to 1 second in order to generally give the application time to
|
||||
* fully finish launching.
|
||||
*
|
||||
**/
|
||||
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey;
|
||||
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey afterDelay:(NSTimeInterval)delay;
|
||||
|
||||
/**
|
||||
*
|
||||
* If you need the functionality provided by the CrashlyticsDelegate protocol, you can use
|
||||
* these convenience methods to activate the framework and set the delegate in one call.
|
||||
*
|
||||
**/
|
||||
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(NSObject <CrashlyticsDelegate> *)delegate;
|
||||
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(NSObject <CrashlyticsDelegate> *)delegate afterDelay:(NSTimeInterval)delay;
|
||||
|
||||
/**
|
||||
*
|
||||
* Access the singleton Crashlytics instance.
|
||||
*
|
||||
**/
|
||||
+ (Crashlytics *)sharedInstance;
|
||||
|
||||
/**
|
||||
*
|
||||
* The easiest way to cause a crash - great for testing!
|
||||
*
|
||||
**/
|
||||
- (void)crash;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
*
|
||||
* The CrashlyticsDelegate protocol provides a mechanism for your application to take
|
||||
* action on events that occur in the Crashlytics crash reporting system. You can make
|
||||
* use of these calls by assigning an object to the Crashlytics' delegate property directly,
|
||||
* or through the convenience startWithAPIKey:delegate:... methods.
|
||||
*
|
||||
**/
|
||||
@protocol CrashlyticsDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
/**
|
||||
*
|
||||
* Called once a Crashlytics instance has determined that the last execution of the
|
||||
* application ended in a crash. This is called some time after the crash reporting
|
||||
* process has begun. If you have specififed a delay in one of the
|
||||
* startWithAPIKey:... calls, this will take at least that long to be invoked.
|
||||
*
|
||||
**/
|
||||
- (void)crashlyticsDidDetectCrashDuringPreviousExecution:(Crashlytics *)crashlytics;
|
||||
|
||||
@end
|
||||
54
Crashlytics/Crashlytics.framework/Versions/A/Resources/Info.plist
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildMachineOSBuild</key>
|
||||
<string>11C74</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Crashlytics</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.crashlytics.ios</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Crashlytics</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.1.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0100.01.03</string>
|
||||
<key>CrashlyticsAPIKey</key>
|
||||
<string>0d10c90776f5ef5acd01ddbeaca9a6cba4814560</string>
|
||||
<key>DTCompiler</key>
|
||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||
<key>DTPlatformBuild</key>
|
||||
<string>8H7</string>
|
||||
<key>DTPlatformName</key>
|
||||
<string>iphoneos</string>
|
||||
<key>DTPlatformVersion</key>
|
||||
<string>4.3</string>
|
||||
<key>DTSDKBuild</key>
|
||||
<string>8H7</string>
|
||||
<key>DTSDKName</key>
|
||||
<string>iphoneos4.3</string>
|
||||
<key>DTXcode</key>
|
||||
<string>0410</string>
|
||||
<key>DTXcodeBuild</key>
|
||||
<string>4B110</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>3.1</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
1
Crashlytics/Crashlytics.framework/Versions/A/Resources/Runner
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../run
|
||||
BIN
Crashlytics/Crashlytics.framework/Versions/A/Resources/en.lproj/InfoPlist.strings
vendored
Normal file
15
Crashlytics/Crashlytics.framework/Versions/A/Resources/runner.rb
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/ruby
|
||||
|
||||
#
|
||||
# WARNING: DO NOT MODIFY THIS FILE.
|
||||
#
|
||||
# Crashlytics
|
||||
# Crashlytics Version: 1.0.0.1
|
||||
#
|
||||
# Copyright Crashlytics, Inc. 2012. All rights reserved.
|
||||
#
|
||||
|
||||
require 'pathname'
|
||||
|
||||
path = Pathname.new(__FILE__).parent
|
||||
`#{path}/../../../run`
|
||||
1
Crashlytics/Crashlytics.framework/Versions/Current
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
A
|
||||
BIN
Crashlytics/Crashlytics.framework/run
vendored
Executable file
8
Crashlytics/Crashlytics.plist
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>API Key</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Default.png
|
Before Width: | Height: | Size: 130 KiB |
BIN
Default@2x.png
|
Before Width: | Height: | Size: 502 KiB |
2
External/InAppSettingsKit
vendored
2
External/Pearl
vendored
1
External/iCloudStoreManager
vendored
Submodule
10
Localytics/Localytics.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Key.development</key>
|
||||
<string>e6238ceba8ec92832e77b1b-9ccd60bc-c39b-11e0-06e4-007f58cb3154</string>
|
||||
<key>Key.distribution</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
57
Localytics/LocalyticsDatabase.h
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// LocalyticsDatabase.h
|
||||
// LocalyticsDemo
|
||||
//
|
||||
// Created by jkaufman on 5/26/11.
|
||||
// Copyright 2011 Localytics. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <sqlite3.h>
|
||||
|
||||
#define MAX_DATABASE_SIZE 500000 // The maximum allowed disk size of the primary database file at open, in bytes
|
||||
#define VACUUM_THRESHOLD 0.8 // The database is vacuumed after its size exceeds this proportion of the maximum.
|
||||
|
||||
@interface LocalyticsDatabase : NSObject {
|
||||
sqlite3 *_databaseConnection;
|
||||
}
|
||||
|
||||
+ (LocalyticsDatabase *)sharedLocalyticsDatabase;
|
||||
|
||||
- (NSUInteger)databaseSize;
|
||||
- (int)eventCount;
|
||||
- (NSTimeInterval)createdTimestamp;
|
||||
|
||||
- (BOOL)beginTransaction:(NSString *)name;
|
||||
- (BOOL)releaseTransaction:(NSString *)name;
|
||||
- (BOOL)rollbackTransaction:(NSString *)name;
|
||||
|
||||
- (BOOL)incrementLastUploadNumber:(int *)uploadNumber;
|
||||
- (BOOL)incrementLastSessionNumber:(int *)sessionNumber;
|
||||
|
||||
- (BOOL)addEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)addCloseEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)addFlowEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)removeLastCloseAndFlowEvents;
|
||||
|
||||
- (BOOL)addHeaderWithSequenceNumber:(int)number blobString:(NSString *)blob rowId:(sqlite3_int64 *)insertedRowId;
|
||||
- (int)unstagedEventCount;
|
||||
- (BOOL)stageEventsForUpload:(sqlite3_int64)headerId;
|
||||
- (BOOL)updateAppKey:(NSString *)appKey;
|
||||
- (NSString *)uploadBlobString;
|
||||
- (BOOL)deleteUploadedData;
|
||||
- (BOOL)resetAnalyticsData;
|
||||
- (BOOL)vacuumIfRequired;
|
||||
|
||||
- (NSTimeInterval)lastSessionStartTimestamp;
|
||||
- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp;
|
||||
|
||||
- (BOOL)isOptedOut;
|
||||
- (BOOL)setOptedOut:(BOOL)optOut;
|
||||
- (NSString *)installId;
|
||||
- (NSString *)appKey; // Most recent app key-- may not be that used to open the session.
|
||||
|
||||
- (NSString *)customDimension:(int)dimension;
|
||||
- (BOOL)setCustomDimension:(int)dimension value:(NSString *)value;
|
||||
|
||||
@end
|
||||
743
Localytics/LocalyticsDatabase.m
Normal file
@@ -0,0 +1,743 @@
|
||||
//
|
||||
// LocalyticsDatabase.m
|
||||
// LocalyticsDemo
|
||||
//
|
||||
// Created by jkaufman on 5/26/11.
|
||||
// Copyright 2011 Localytics. All rights reserved.
|
||||
//
|
||||
|
||||
#import "LocalyticsDatabase.h"
|
||||
|
||||
#define LOCALYTICS_DIR @".localytics" // Name for the directory in which Localytics database is stored
|
||||
#define LOCALYTICS_DB @"localytics" // File name for the database (without extension)
|
||||
#define BUSY_TIMEOUT 30 // Maximum time SQlite will busy-wait for the database to unlock before returning SQLITE_BUSY
|
||||
|
||||
@interface LocalyticsDatabase ()
|
||||
- (int)schemaVersion;
|
||||
- (void)createSchema;
|
||||
- (void)upgradeToSchemaV2;
|
||||
- (void)upgradeToSchemaV3;
|
||||
- (void)moveDbToCaches;
|
||||
- (NSString *)randomUUID;
|
||||
@end
|
||||
|
||||
@implementation LocalyticsDatabase
|
||||
|
||||
// The singleton database object.
|
||||
static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
|
||||
|
||||
+ (NSString *)localyticsDirectoryPath {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
return [[paths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
}
|
||||
|
||||
+ (NSString *)localyticsDatabasePath {
|
||||
NSString *path = [[LocalyticsDatabase localyticsDirectoryPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.sqlite", LOCALYTICS_DB]];
|
||||
return path;
|
||||
}
|
||||
|
||||
#pragma mark Singleton Class
|
||||
+ (LocalyticsDatabase *)sharedLocalyticsDatabase {
|
||||
@synchronized(self) {
|
||||
if (_sharedLocalyticsDatabase == nil) {
|
||||
_sharedLocalyticsDatabase = [[self alloc] init];
|
||||
}
|
||||
}
|
||||
return _sharedLocalyticsDatabase;
|
||||
}
|
||||
|
||||
- (LocalyticsDatabase *)init {
|
||||
if((self = [super init])) {
|
||||
|
||||
// Mover any data that a previous library may have left in the documents directory
|
||||
[self moveDbToCaches];
|
||||
|
||||
// Create directory structure for Localytics.
|
||||
NSString *directoryPath = [LocalyticsDatabase localyticsDirectoryPath];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:directoryPath]) {
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
}
|
||||
|
||||
// Attempt to open database. It will be created if it does not exist, already.
|
||||
NSString *dbPath = [LocalyticsDatabase localyticsDatabasePath];
|
||||
int code = sqlite3_open([dbPath UTF8String], &_databaseConnection);
|
||||
|
||||
// If we were unable to open the database, it is likely corrupted. Clobber it and move on.
|
||||
if (code != SQLITE_OK) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:dbPath error:nil];
|
||||
code = sqlite3_open([dbPath UTF8String], &_databaseConnection);
|
||||
}
|
||||
|
||||
// Check db connection, creating schema if necessary.
|
||||
if (code == SQLITE_OK) {
|
||||
sqlite3_busy_timeout(_databaseConnection, BUSY_TIMEOUT); // Defaults to 0, otherwise.
|
||||
if ([self schemaVersion] == 0) {
|
||||
[self createSchema];
|
||||
}
|
||||
}
|
||||
|
||||
// Perform any Migrations if necessary
|
||||
if ([self schemaVersion] < 2) {
|
||||
[self upgradeToSchemaV2];
|
||||
}
|
||||
if ([self schemaVersion] < 3) {
|
||||
[self upgradeToSchemaV3];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Database
|
||||
|
||||
- (BOOL)beginTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)releaseTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"RELEASE SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)rollbackTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"ROLLBACK SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (int)schemaVersion {
|
||||
int version = 0;
|
||||
const char *sql = "SELECT MAX(schema_version) FROM localytics_info";
|
||||
sqlite3_stmt *selectSchemaVersion;
|
||||
if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectSchemaVersion, NULL) == SQLITE_OK) {
|
||||
if(sqlite3_step(selectSchemaVersion) == SQLITE_ROW) {
|
||||
version = sqlite3_column_int(selectSchemaVersion, 0);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectSchemaVersion);
|
||||
return version;
|
||||
}
|
||||
|
||||
- (NSString *)installId {
|
||||
NSString *installId = nil;
|
||||
|
||||
sqlite3_stmt *selectInstallId;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT install_id FROM localytics_info", -1, &selectInstallId, NULL);
|
||||
int code = sqlite3_step(selectInstallId);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectInstallId, 0)) {
|
||||
installId = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectInstallId, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectInstallId);
|
||||
|
||||
return installId;
|
||||
}
|
||||
|
||||
- (NSString *)appKey {
|
||||
NSString *appKey = nil;
|
||||
|
||||
sqlite3_stmt *selectAppKey;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT app_key FROM localytics_info", -1, &selectAppKey, NULL);
|
||||
int code = sqlite3_step(selectAppKey);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectAppKey, 0)) {
|
||||
appKey = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectAppKey, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectAppKey);
|
||||
|
||||
return appKey;
|
||||
}
|
||||
|
||||
// Due to the new iOS storage guidelines it is necessary to move the database out of the documents directory
|
||||
// and into the /library/caches directory
|
||||
- (void)moveDbToCaches {
|
||||
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *localyticsDocumentsDirectory = [[documentPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
NSArray *cachesPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
NSString *localyticsCachesDirectory = [[cachesPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
|
||||
// If the old directory doesn't exist, there is nothing else to do here
|
||||
if([[NSFileManager defaultManager] fileExistsAtPath:localyticsDocumentsDirectory] == NO)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to move the directory
|
||||
if(NO == [[NSFileManager defaultManager] moveItemAtPath:localyticsDocumentsDirectory
|
||||
toPath:localyticsCachesDirectory
|
||||
error:nil])
|
||||
{
|
||||
// If the move failed try and, delete the old directory
|
||||
[ [NSFileManager defaultManager] removeItemAtPath:localyticsDocumentsDirectory error:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)createSchema {
|
||||
int code = SQLITE_OK;
|
||||
|
||||
// Execute schema creation within a single transaction.
|
||||
code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE upload_headers ("
|
||||
"sequence_number INTEGER PRIMARY KEY, "
|
||||
"blob_string TEXT)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE events ("
|
||||
"event_id INTEGER PRIMARY KEY AUTOINCREMENT, " // In case foreign key constraints are reintroduced.
|
||||
"upload_header INTEGER, "
|
||||
"blob_string TEXT NOT NULL)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE localytics_info ("
|
||||
"schema_version INTEGER PRIMARY KEY, "
|
||||
"last_upload_number INTEGER, "
|
||||
"last_session_number INTEGER, "
|
||||
"opt_out BOOLEAN, "
|
||||
"last_close_event INTEGER, "
|
||||
"last_flow_event INTEGER, "
|
||||
"last_session_start REAL, "
|
||||
"app_key CHAR(64), "
|
||||
"custom_d0 CHAR(64), "
|
||||
"custom_d1 CHAR(64), "
|
||||
"custom_d2 CHAR(64), "
|
||||
"custom_d3 CHAR(64) "
|
||||
")",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"INSERT INTO localytics_info (schema_version, last_upload_number, last_session_number, opt_out) "
|
||||
"VALUES (3, 0, 0, 0)", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
// Commit transaction.
|
||||
if (code == SQLITE_OK || code == SQLITE_DONE) {
|
||||
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
|
||||
} else {
|
||||
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// V2 adds a unique identifier for each installation
|
||||
// This identifier has been moved to user preferences so the database an live in the caches directory
|
||||
// Also adds storage for custom dimensions
|
||||
- (void)upgradeToSchemaV2 {
|
||||
int code = SQLITE_OK;
|
||||
|
||||
code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD install_id CHAR(40)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d0 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d1 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d2 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d3 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
// Attempt to set schema version and install_id regardless of the result code following the ALTER statements above.
|
||||
// This is necessary because a previous version of the library performed the migration without setting these values.
|
||||
// The transaction will succeed even if the individual statements fail with errors (eg. "duplicate column name").
|
||||
sqlite3_stmt *updateLocalyticsInfo;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set install_id = ?, schema_version = 2 ", -1, &updateLocalyticsInfo, NULL);
|
||||
sqlite3_bind_text (updateLocalyticsInfo, 1, [[self randomUUID] UTF8String], -1, SQLITE_TRANSIENT);
|
||||
code = sqlite3_step(updateLocalyticsInfo);
|
||||
sqlite3_finalize(updateLocalyticsInfo);
|
||||
|
||||
// Commit transaction.
|
||||
if (code == SQLITE_OK || code == SQLITE_DONE) {
|
||||
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
|
||||
} else {
|
||||
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// V3 adds a field for the last app key and patches a V2 migration issue.
|
||||
- (void)upgradeToSchemaV3 {
|
||||
sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD app_key CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
- (NSUInteger)databaseSize {
|
||||
NSUInteger size = 0;
|
||||
NSDictionary *fileAttributes = [[NSFileManager defaultManager]
|
||||
attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath]
|
||||
error:nil];
|
||||
size = [fileAttributes fileSize];
|
||||
return size;
|
||||
}
|
||||
|
||||
- (int) eventCount {
|
||||
int count = 0;
|
||||
const char *sql = "SELECT count(*) FROM events";
|
||||
sqlite3_stmt *selectEventCount;
|
||||
|
||||
if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectEventCount, NULL) == SQLITE_OK)
|
||||
{
|
||||
if(sqlite3_step(selectEventCount) == SQLITE_ROW) {
|
||||
count = sqlite3_column_int(selectEventCount, 0);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectEventCount);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)createdTimestamp {
|
||||
NSTimeInterval timestamp = 0;
|
||||
NSDictionary *fileAttributes = [[NSFileManager defaultManager]
|
||||
attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath]
|
||||
error:nil];
|
||||
timestamp = [[fileAttributes fileCreationDate] timeIntervalSince1970];
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)lastSessionStartTimestamp {
|
||||
|
||||
NSTimeInterval lastSessionStart = 0;
|
||||
|
||||
sqlite3_stmt *selectLastSessionStart;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT last_session_start FROM localytics_info", -1, &selectLastSessionStart, NULL);
|
||||
int code = sqlite3_step(selectLastSessionStart);
|
||||
if (code == SQLITE_ROW) {
|
||||
lastSessionStart = sqlite3_column_double(selectLastSessionStart, 0) == 1;
|
||||
}
|
||||
sqlite3_finalize(selectLastSessionStart);
|
||||
|
||||
return lastSessionStart;
|
||||
}
|
||||
|
||||
- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp {
|
||||
sqlite3_stmt *updateLastSessionStart;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_session_start = ?", -1, &updateLastSessionStart, NULL);
|
||||
sqlite3_bind_double(updateLastSessionStart, 1, timestamp);
|
||||
int code = sqlite3_step(updateLastSessionStart);
|
||||
sqlite3_finalize(updateLastSessionStart);
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (BOOL)isOptedOut {
|
||||
BOOL optedOut = NO;
|
||||
|
||||
sqlite3_stmt *selectOptOut;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT opt_out FROM localytics_info", -1, &selectOptOut, NULL);
|
||||
int code = sqlite3_step(selectOptOut);
|
||||
if (code == SQLITE_ROW) {
|
||||
optedOut = sqlite3_column_int(selectOptOut, 0) == 1;
|
||||
}
|
||||
sqlite3_finalize(selectOptOut);
|
||||
|
||||
return optedOut;
|
||||
}
|
||||
|
||||
- (BOOL)setOptedOut:(BOOL)optOut {
|
||||
sqlite3_stmt *updateOptedOut;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET opt_out = ?", -1, &updateOptedOut, NULL);
|
||||
sqlite3_bind_int(updateOptedOut, 1, optOut);
|
||||
int code = sqlite3_step(updateOptedOut);
|
||||
sqlite3_finalize(updateOptedOut);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (NSString *)customDimension:(int)dimension {
|
||||
if(dimension < 0 || dimension > 3) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *value = nil;
|
||||
NSString *query = [NSString stringWithFormat:@"select custom_d%i from localytics_info", dimension];
|
||||
|
||||
sqlite3_stmt *selectCustomDim;
|
||||
sqlite3_prepare_v2(_databaseConnection, [query UTF8String], -1, &selectCustomDim, NULL);
|
||||
int code = sqlite3_step(selectCustomDim);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectCustomDim, 0)) {
|
||||
value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectCustomDim, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectCustomDim);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
- (BOOL)setCustomDimension:(int)dimension value:(NSString *)value {
|
||||
if(dimension < 0 || dimension > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NSString *query = [NSString stringWithFormat:@"update localytics_info SET custom_d%i = %@",
|
||||
dimension,
|
||||
(value == nil) ? @"null" : [NSString stringWithFormat:@"\"%@\"", value]];
|
||||
|
||||
int code = sqlite3_exec(_databaseConnection, [query UTF8String], NULL, NULL, NULL);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)incrementLastUploadNumber:(int *)uploadNumber {
|
||||
NSString *t = @"increment_upload_number";
|
||||
int code = SQLITE_OK;
|
||||
|
||||
code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Increment value
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"UPDATE localytics_info "
|
||||
"SET last_upload_number = (last_upload_number + 1)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Retrieve new value
|
||||
sqlite3_stmt *selectUploadNumber;
|
||||
sqlite3_prepare_v2(_databaseConnection,
|
||||
"SELECT last_upload_number FROM localytics_info",
|
||||
-1, &selectUploadNumber, NULL);
|
||||
code = sqlite3_step(selectUploadNumber);
|
||||
if (code == SQLITE_ROW) {
|
||||
*uploadNumber = sqlite3_column_int(selectUploadNumber, 0);
|
||||
}
|
||||
sqlite3_finalize(selectUploadNumber);
|
||||
}
|
||||
|
||||
if(code == SQLITE_ROW) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_ROW;
|
||||
}
|
||||
|
||||
- (BOOL)incrementLastSessionNumber:(int *)sessionNumber {
|
||||
NSString *t = @"increment_session_number";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Increment value
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"UPDATE localytics_info "
|
||||
"SET last_session_number = (last_session_number + 1)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Retrieve new value
|
||||
sqlite3_stmt *selectSessionNumber;
|
||||
sqlite3_prepare_v2(_databaseConnection,
|
||||
"SELECT last_session_number FROM localytics_info",
|
||||
-1, &selectSessionNumber, NULL);
|
||||
code = sqlite3_step(selectSessionNumber);
|
||||
if (code == SQLITE_ROW && sessionNumber != NULL) {
|
||||
*sessionNumber = sqlite3_column_int(selectSessionNumber, 0);
|
||||
}
|
||||
sqlite3_finalize(selectSessionNumber);
|
||||
}
|
||||
|
||||
if(code == SQLITE_ROW) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_ROW;
|
||||
}
|
||||
|
||||
- (BOOL)addEventWithBlobString:(NSString *)blob {
|
||||
|
||||
int code = SQLITE_OK;
|
||||
sqlite3_stmt *insertEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "INSERT INTO events (blob_string) VALUES (?)", -1, &insertEvent, NULL);
|
||||
sqlite3_bind_text(insertEvent, 1, [blob UTF8String], -1, SQLITE_TRANSIENT);
|
||||
code = sqlite3_step(insertEvent);
|
||||
sqlite3_finalize(insertEvent);
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (BOOL)addCloseEventWithBlobString:(NSString *)blob {
|
||||
NSString *t = @"add_close_event";
|
||||
BOOL success = [self beginTransaction:t];
|
||||
|
||||
// Add close event.
|
||||
if (success) {
|
||||
success = [self addEventWithBlobString:blob];
|
||||
}
|
||||
|
||||
// Record row id to localytics_info so that it can be removed if the session resumes.
|
||||
if (success) {
|
||||
sqlite3_stmt *updateCloseEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_close_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateCloseEvent, NULL);
|
||||
sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
sqlite3_bind_int64(updateCloseEvent, 1, lastRow);
|
||||
int code = sqlite3_step(updateCloseEvent);
|
||||
sqlite3_finalize(updateCloseEvent);
|
||||
success = code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)addFlowEventWithBlobString:(NSString *)blob {
|
||||
NSString *t = @"add_flow_event";
|
||||
BOOL success = [self beginTransaction:t];
|
||||
|
||||
// Add flow event.
|
||||
if (success) {
|
||||
success = [self addEventWithBlobString:blob];
|
||||
}
|
||||
|
||||
// Record row id to localytics_info so that it can be removed if the session resumes.
|
||||
if (success) {
|
||||
sqlite3_stmt *updateFlowEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_flow_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateFlowEvent, NULL);
|
||||
sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
sqlite3_bind_int64(updateFlowEvent, 1, lastRow);
|
||||
int code = sqlite3_step(updateFlowEvent);
|
||||
sqlite3_finalize(updateFlowEvent);
|
||||
success = code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)removeLastCloseAndFlowEvents {
|
||||
// Attempt to remove the last recorded close event.
|
||||
// Fail quietly if none was saved or it was previously removed.
|
||||
int code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE event_id = (SELECT last_close_event FROM localytics_info) OR event_id = (SELECT last_flow_event FROM localytics_info)", NULL, NULL, NULL);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)addHeaderWithSequenceNumber:(int)number blobString:(NSString *)blob rowId:(sqlite3_int64 *)insertedRowId {
|
||||
sqlite3_stmt *insertHeader;
|
||||
sqlite3_prepare_v2(_databaseConnection, "INSERT INTO upload_headers (sequence_number, blob_string) VALUES (?, ?)", -1, &insertHeader, NULL);
|
||||
sqlite3_bind_int(insertHeader, 1, number);
|
||||
sqlite3_bind_text(insertHeader, 2, [blob UTF8String], -1, SQLITE_TRANSIENT);
|
||||
int code = sqlite3_step(insertHeader);
|
||||
sqlite3_finalize(insertHeader);
|
||||
|
||||
if (code == SQLITE_DONE && insertedRowId != NULL) {
|
||||
*insertedRowId = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
}
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (int)unstagedEventCount {
|
||||
int rowCount = 0;
|
||||
sqlite3_stmt *selectEventCount;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT COUNT(*) FROM events WHERE UPLOAD_HEADER IS NULL", -1, &selectEventCount, NULL);
|
||||
int code = sqlite3_step(selectEventCount);
|
||||
if (code == SQLITE_ROW) {
|
||||
rowCount = sqlite3_column_int(selectEventCount, 0);
|
||||
}
|
||||
sqlite3_finalize(selectEventCount);
|
||||
|
||||
return rowCount;
|
||||
}
|
||||
|
||||
- (BOOL)stageEventsForUpload:(sqlite3_int64)headerId {
|
||||
|
||||
// Associate all outstanding events with the given upload header ID.
|
||||
NSString *stageEvents = [NSString stringWithFormat:@"UPDATE events SET upload_header = ? WHERE upload_header IS NULL"];
|
||||
sqlite3_stmt *updateEvents;
|
||||
sqlite3_prepare_v2(_databaseConnection, [stageEvents UTF8String], -1, &updateEvents, NULL);
|
||||
sqlite3_bind_int(updateEvents, 1, headerId);
|
||||
int code = sqlite3_step(updateEvents);
|
||||
sqlite3_finalize(updateEvents);
|
||||
BOOL success = (code == SQLITE_DONE);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)updateAppKey:(NSString *)appKey {
|
||||
sqlite3_stmt *updateAppKey;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set app_key = ?", -1, &updateAppKey, NULL);
|
||||
sqlite3_bind_text (updateAppKey, 1, [appKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
int code = sqlite3_step(updateAppKey);
|
||||
sqlite3_finalize(updateAppKey);
|
||||
BOOL success = (code == SQLITE_DONE);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (NSString *)uploadBlobString {
|
||||
|
||||
// Retrieve the blob strings of each upload header and its child events, in order.
|
||||
const char *sql = "SELECT * FROM ( "
|
||||
" SELECT h.blob_string AS 'blob', h.sequence_number as 'seq', 0 FROM upload_headers h"
|
||||
" UNION ALL "
|
||||
" SELECT e.blob_string AS 'blob', e.upload_header as 'seq', 1 FROM events e"
|
||||
") "
|
||||
"ORDER BY 2, 3";
|
||||
sqlite3_stmt *selectBlobs;
|
||||
sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectBlobs, NULL);
|
||||
NSMutableString *uploadBlobString = [NSMutableString string];
|
||||
while (sqlite3_step(selectBlobs) == SQLITE_ROW) {
|
||||
const char *blob = (const char *)sqlite3_column_text(selectBlobs, 0);
|
||||
if (blob != NULL) {
|
||||
NSString *blobString = [[NSString alloc] initWithCString:blob encoding:NSUTF8StringEncoding];
|
||||
[uploadBlobString appendString:blobString];
|
||||
[blobString release];
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectBlobs);
|
||||
|
||||
return [[uploadBlobString copy] autorelease];
|
||||
}
|
||||
|
||||
- (BOOL)deleteUploadedData {
|
||||
// Delete all headers and staged events.
|
||||
NSString *t = @"delete_upload_data";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE upload_header IS NOT NULL", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)resetAnalyticsData {
|
||||
// Delete or zero all analytics data.
|
||||
// Reset: headers, events, session number, upload number, last session start, last close event, and last flow event.
|
||||
// Unaffected: schema version, opt out status, install ID (deprecated), and app key.
|
||||
|
||||
NSString *t = @"reset_analytics_data";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM events", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,"UPDATE localytics_info SET last_session_number = 0, last_upload_number = 0,"
|
||||
"last_close_event = null, last_flow_event = null, last_session_start = null, "
|
||||
"custom_d0 = null, custom_d1 = null, custom_d2 = null, custom_d3 = null",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)vacuumIfRequired {
|
||||
int code = SQLITE_OK;
|
||||
if ([self databaseSize] > MAX_DATABASE_SIZE * VACUUM_THRESHOLD) {
|
||||
code = sqlite3_exec(_databaseConnection, "VACUUM", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (NSString *)randomUUID {
|
||||
CFUUIDRef theUUID = CFUUIDCreate(NULL);
|
||||
CFStringRef stringUUID = CFUUIDCreateString(NULL, theUUID);
|
||||
CFRelease(theUUID);
|
||||
return [(NSString *)stringUUID autorelease];
|
||||
}
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
+ (id)allocWithZone:(NSZone *)zone {
|
||||
@synchronized(self) {
|
||||
if (_sharedLocalyticsDatabase == nil) {
|
||||
_sharedLocalyticsDatabase = [super allocWithZone:zone];
|
||||
return _sharedLocalyticsDatabase;
|
||||
}
|
||||
}
|
||||
// returns nil on subsequent allocations
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)retain {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (unsigned)retainCount {
|
||||
// maximum value of an unsigned int - prevents additional retains for the class
|
||||
return UINT_MAX;
|
||||
}
|
||||
|
||||
- (oneway void)release {
|
||||
// ignore release commands
|
||||
}
|
||||
|
||||
- (id)autorelease {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
sqlite3_close(_databaseConnection);
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
216
Localytics/LocalyticsSession.h
Normal file
@@ -0,0 +1,216 @@
|
||||
// LocalyticsSession.h
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// Set this to true to enable localytics traces (useful for debugging)
|
||||
#define DO_LOCALYTICS_LOGGING false
|
||||
|
||||
/*!
|
||||
@class LocalyticsSession
|
||||
@discussion The class which manages creating, collecting, & uploading a Localytics session.
|
||||
Please see the following guides for information on how to best use this
|
||||
library, sample code, and other useful information:
|
||||
<ul>
|
||||
<li><a href="http://wiki.localytics.com/index.php?title=Developer's_Integration_Guide">Main Developer's Integration Guide</a></li>
|
||||
</ul>
|
||||
|
||||
<strong>Best Practices</strong>
|
||||
<ul>
|
||||
<li>Instantiate the LocalyticsSession object in applicationDidFinishLaunching.</li>
|
||||
<li>Open your session and begin your uploads in applicationDidFinishLaunching. This way the
|
||||
upload has time to complete and it all happens before your users have a
|
||||
chance to begin any data intensive actions of their own.</li>
|
||||
<li>Close the session in applicationWillTerminate, and in applicationDidEnterBackground.</li>
|
||||
<li>Resume the session in applicationWillEnterForeground.</li>
|
||||
<li>Do not call any Localytics functions inside a loop. Instead, calls
|
||||
such as <code>tagEvent</code> should follow user actions. This limits the
|
||||
amount of data which is stored and uploaded.</li>
|
||||
<li>Do not use multiple LocalticsSession objects to upload data with
|
||||
multiple application keys. This can cause invalid state.</li>
|
||||
</ul>
|
||||
|
||||
@author Localytics
|
||||
*/
|
||||
@interface LocalyticsSession : NSObject {
|
||||
|
||||
BOOL _hasInitialized; // Whether or not the session object has been initialized.
|
||||
BOOL _isSessionOpen; // Whether or not this session has been opened.
|
||||
float _backgroundSessionTimeout; // If an App stays in the background for more
|
||||
// than this many seconds, start a new session
|
||||
// when it returns to foreground.
|
||||
@private
|
||||
#pragma mark Member Variables
|
||||
dispatch_queue_t _queue; // Queue of Localytics block objects.
|
||||
dispatch_group_t _criticalGroup; // Group of blocks the must complete before backgrounding.
|
||||
NSString *_sessionUUID; // Unique identifier for this session.
|
||||
NSString *_applicationKey; // Unique identifier for the instrumented application
|
||||
NSTimeInterval _lastSessionStartTimestamp; // The start time of the most recent session.
|
||||
NSDate *_sessionResumeTime; // Time session was started or resumed.
|
||||
NSDate *_sessionCloseTime; // Time session was closed.
|
||||
NSMutableString *_unstagedFlowEvents; // Comma-delimited list of app screens and events tagged during this
|
||||
// session that have NOT been staged for upload.
|
||||
NSMutableString *_stagedFlowEvents; // App screens and events tagged during this session that HAVE been staged
|
||||
// for upload.
|
||||
NSMutableString *_screens; // Comma-delimited list of screens tagged during this session.
|
||||
NSTimeInterval _sessionActiveDuration; // Duration that session open.
|
||||
BOOL _sessionHasBeenOpen; // Whether or not this session has ever been open.
|
||||
}
|
||||
|
||||
@property dispatch_queue_t queue;
|
||||
@property dispatch_group_t criticalGroup;
|
||||
@property BOOL isSessionOpen;
|
||||
@property BOOL hasInitialized;
|
||||
@property float backgroundSessionTimeout;
|
||||
|
||||
#pragma mark Public Methods
|
||||
/*!
|
||||
@method sharedLocalyticsSession
|
||||
@abstract Accesses the Session object. This is a Singleton class which maintains
|
||||
a single session throughout your application. It is possible to manage your own
|
||||
session, but this is the easiest way to access the Localytics object throughout your code.
|
||||
The class is accessed within the code using the following syntax:
|
||||
[[LocalyticsSession sharedLocalyticsSession] functionHere]
|
||||
So, to tag an event, all that is necessary, anywhere in the code is:
|
||||
[[LocalyticsSession sharedLocalyticsSession] tagEvent:@"MY_EVENT"];
|
||||
*/
|
||||
+ (LocalyticsSession *)sharedLocalyticsSession;
|
||||
|
||||
/*!
|
||||
@method LocalyticsSession
|
||||
@abstract Initializes the Localytics Object. Not necessary if you choose to use startSession.
|
||||
@param applicationKey The key unique for each application generated at www.localytics.com
|
||||
*/
|
||||
- (void)LocalyticsSession:(NSString *)appKey;
|
||||
|
||||
/*!
|
||||
@method startSession
|
||||
@abstract An optional convenience initialize method that also calls the LocalyticsSession, open &
|
||||
upload methods. Best Practice is to call open & upload immediately after Localytics Session when loading an app,
|
||||
this method fascilitates that behavior.
|
||||
It is recommended that this call be placed in <code>applicationDidFinishLaunching</code>.
|
||||
@param applicationKey The key unique for each application generated
|
||||
at www.localytics.com
|
||||
*/
|
||||
- (void)startSession:(NSString *)appKey;
|
||||
|
||||
/*!
|
||||
@method setOptIn
|
||||
@abstract (OPTIONAL) Allows the application to control whether or not it will collect user data.
|
||||
Even if this call is used, it is necessary to continue calling upload(). No new data will be
|
||||
collected, so nothing new will be uploaded but it is necessary to upload an event telling the
|
||||
server this user has opted out.
|
||||
@param optedIn True if the user is opted in, false otherwise.
|
||||
*/
|
||||
- (void)setOptIn:(BOOL)optedIn;
|
||||
|
||||
/*!
|
||||
@method isOptedIn
|
||||
@abstract (OPTIONAL) Whether or not this user has is opted in or out. The only way they can be
|
||||
opted out is if setOptIn(false) has been called before this. This function should only be
|
||||
used to pre-populate a checkbox in an options menu. It is not recommended that an application
|
||||
branch based on Localytics instrumentation because this creates an additional test case. If
|
||||
the app is opted out, all subsequent Localytics calls will return immediately.
|
||||
@result true if the user is opted in, false otherwise.
|
||||
*/
|
||||
- (BOOL)isOptedIn;
|
||||
|
||||
/*!
|
||||
@method open
|
||||
@abstract Opens the Localytics session. Not necessary if you choose to use startSession.
|
||||
The session time as presented on the website is the time between <code>open</code> and the
|
||||
final <code>close</code> so it is recommended to open the session as early as possible, and close
|
||||
it at the last moment. The session must be opened before any tags can
|
||||
be written. It is recommended that this call be placed in <code>applicationDidFinishLaunching</code>.
|
||||
<br>
|
||||
If for any reason this is called more than once every subsequent open call
|
||||
will be ignored.
|
||||
*/
|
||||
- (void)open;
|
||||
|
||||
/*!
|
||||
@method resume
|
||||
@abstract Resumes the Localytics session. When the App enters the background, the session is
|
||||
closed and the time of closing is recorded. When the app returns to the foreground, the session
|
||||
is resumed. If the time since closing is greater than BACKGROUND_SESSION_TIMEOUT, (15 seconds
|
||||
by default) a new session is created, and uploading is triggered. Otherwise, the previous session
|
||||
is reopened.
|
||||
*/
|
||||
- (void)resume;
|
||||
|
||||
/*!
|
||||
@method close
|
||||
@abstract Closes the Localytics session. This should be called in
|
||||
<code>applicationWillTerminate</code>.
|
||||
<br>
|
||||
If close is not called, the session will still be uploaded but no
|
||||
events will be processed and the session time will not appear. This is
|
||||
because the session is not yet closed so it should not be used in
|
||||
comparison with sessions which are closed.
|
||||
*/
|
||||
- (void)close;
|
||||
|
||||
/*!
|
||||
@method tagEvent
|
||||
@abstract Allows a session to tag a particular event as having occurred. For
|
||||
example, if a view has three buttons, it might make sense to tag
|
||||
each button click with the name of the button which was clicked.
|
||||
For another example, in a game with many levels it might be valuable
|
||||
to create a new tag every time the user gets to a new level in order
|
||||
to determine how far the average user is progressing in the game.
|
||||
<br>
|
||||
<strong>Tagging Best Practices</strong>
|
||||
<ul>
|
||||
<li>DO NOT use tags to record personally identifiable information.</li>
|
||||
<li>The best way to use tags is to create all the tag strings as predefined
|
||||
constants and only use those. This is more efficient and removes the risk of
|
||||
collecting personal information.</li>
|
||||
<li>Do not set tags inside loops or any other place which gets called
|
||||
frequently. This can cause a lot of data to be stored and uploaded.</li>
|
||||
</ul>
|
||||
<br>
|
||||
See the tagging guide at: http://wiki.localytics.com/
|
||||
@param event The name of the event which occurred.
|
||||
*/
|
||||
- (void)tagEvent:(NSString *)event;
|
||||
|
||||
- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes;
|
||||
|
||||
- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes reportAttributes:(NSDictionary *)reportAttributes;
|
||||
|
||||
/*!
|
||||
@method tagScreen
|
||||
@abstract Allows tagging the flow of screens encountered during the session.
|
||||
@param screen The name of the screen
|
||||
*/
|
||||
- (void)tagScreen:(NSString *)screen;
|
||||
|
||||
/*!
|
||||
@method upload
|
||||
@abstract Creates a low priority thread which uploads any Localytics data already stored
|
||||
on the device. This should be done early in the process life in order to
|
||||
guarantee as much time as possible for slow connections to complete. It is also reasonable
|
||||
to upload again when the application is exiting because if the upload is cancelled the data
|
||||
will just get uploaded the next time the app comes up.
|
||||
*/
|
||||
- (void)upload;
|
||||
|
||||
/*!
|
||||
@method setCustomDimension
|
||||
@abstract (ENTERPRISE ONLY) Sets the value of a custom dimension. Custom dimensions are dimensions
|
||||
which contain user defined data unlike the predefined dimensions such as carrier, model, and country.
|
||||
Once a value for a custom dimension is set, the device it was set on will continue to upload that value
|
||||
until the value is changed. To clear a value pass nil as the value.
|
||||
The proper use of custom dimensions involves defining a dimension with less than ten distinct possible
|
||||
values and assigning it to one of the four available custom dimensions. Once assigned this definition should
|
||||
never be changed without changing the App Key otherwise old installs of the application will pollute new data.
|
||||
*/
|
||||
- (void)setCustomDimension:(int)dimension value:(NSString *)value;
|
||||
|
||||
@end
|
||||
1148
Localytics/LocalyticsSession.m
Normal file
42
Localytics/LocalyticsUploader.h
Normal file
@@ -0,0 +1,42 @@
|
||||
// LocalyticsUploader.h
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/*!
|
||||
@class LocalyticsUploader
|
||||
@discussion Singleton class to handle data uploads
|
||||
*/
|
||||
|
||||
@interface LocalyticsUploader : NSObject {
|
||||
}
|
||||
|
||||
@property (readonly) BOOL isUploading;
|
||||
|
||||
/*!
|
||||
@method sharedLocalyticsUploader
|
||||
@abstract Establishes this as a Singleton Class allowing for data persistence.
|
||||
The class is accessed within the code using the following syntax:
|
||||
[[LocalyticsUploader sharedLocalyticsUploader] functionHere]
|
||||
*/
|
||||
+ (LocalyticsUploader *)sharedLocalyticsUploader;
|
||||
|
||||
/*!
|
||||
@method LocalyticsUploader
|
||||
@abstract Creates a thread which uploads all queued header and event data.
|
||||
All files starting with sessionFilePrefix are renamed,
|
||||
uploaded and deleted on upload. This way the sessions can continue
|
||||
writing data regardless of whether or not the upload succeeds. Files
|
||||
which have been renamed still count towards the total number of Localytics
|
||||
files which can be stored on the disk.
|
||||
@param localyticsApplicationKey the Localytics application ID
|
||||
*/
|
||||
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey;
|
||||
|
||||
@end
|
||||
236
Localytics/LocalyticsUploader.m
Normal file
@@ -0,0 +1,236 @@
|
||||
// LocalyticsUploader.m
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import "LocalyticsUploader.h"
|
||||
#import "LocalyticsSession.h"
|
||||
#import "LocalyticsDatabase.h"
|
||||
#import <zlib.h>
|
||||
|
||||
#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads"
|
||||
|
||||
static LocalyticsUploader *_sharedUploader = nil;
|
||||
|
||||
@interface LocalyticsUploader ()
|
||||
- (void)finishUpload;
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data;
|
||||
- (void)logMessage:(NSString *)message;
|
||||
|
||||
@property (readwrite) BOOL isUploading;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LocalyticsUploader
|
||||
@synthesize isUploading = _isUploading;
|
||||
|
||||
#pragma mark - Singleton Class
|
||||
+ (LocalyticsUploader *)sharedLocalyticsUploader {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploader == nil) {
|
||||
_sharedUploader = [[self alloc] init];
|
||||
}
|
||||
}
|
||||
return _sharedUploader;
|
||||
}
|
||||
|
||||
#pragma mark - Class Methods
|
||||
|
||||
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey {
|
||||
|
||||
// Do nothing if already uploading.
|
||||
if (self.isUploading == true)
|
||||
{
|
||||
[self logMessage:@"Upload already in progress. Aborting."];
|
||||
return;
|
||||
}
|
||||
|
||||
[self logMessage:@"Beginning upload process"];
|
||||
self.isUploading = true;
|
||||
|
||||
// Prepare the data for upload. The upload could take a long time, so some effort has to be made to be sure that events
|
||||
// which get written while the upload is taking place don't get lost or duplicated. To achieve this, the logic is:
|
||||
// 1) Append every header row blob string and and those of its associated events to the upload string.
|
||||
// 2) Deflate and upload the data.
|
||||
// 3) On success, delete all blob headers and staged events. Events added while an upload is in process are not
|
||||
// deleted because they are not associated a header (and cannot be until the upload completes).
|
||||
|
||||
// Step 1
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
|
||||
LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase];
|
||||
NSString *blobString = [db uploadBlobString];
|
||||
|
||||
if ([blobString length] == 0) {
|
||||
// There is nothing outstanding to upload.
|
||||
[self logMessage:@"Abandoning upload. There are no new events."];
|
||||
[pool drain];
|
||||
[self finishUpload];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease];
|
||||
[self logMessage:[NSString stringWithFormat:@"Uploading data (length: %u)", [myString length]]];
|
||||
|
||||
// Step 2
|
||||
NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain];
|
||||
|
||||
[pool drain];
|
||||
|
||||
NSString *apiUrlString = [NSString stringWithFormat:LOCALYTICS_URL, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
|
||||
NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString]
|
||||
cachePolicy:NSURLRequestReloadIgnoringCacheData
|
||||
timeoutInterval:60.0];
|
||||
[submitRequest setHTTPMethod:@"POST"];
|
||||
[submitRequest setValue:@"application/x-gzip" forHTTPHeaderField:@"Content-Type"];
|
||||
[submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
|
||||
[submitRequest setValue:[NSString stringWithFormat:@"%d", [deflatedRequestData length]] forHTTPHeaderField:@"Content-Length"];
|
||||
[submitRequest setHTTPBody:deflatedRequestData];
|
||||
[deflatedRequestData release];
|
||||
|
||||
// Perform synchronous upload in an async dispatch. This is necessary because the calling block will not persist to
|
||||
// receive the response data.
|
||||
dispatch_group_async([[LocalyticsSession sharedLocalyticsSession] criticalGroup], [[LocalyticsSession sharedLocalyticsSession] queue], ^{
|
||||
@try {
|
||||
NSURLResponse *response = nil;
|
||||
NSError *responseError = nil;
|
||||
[NSURLConnection sendSynchronousRequest:submitRequest returningResponse:&response error:&responseError];
|
||||
NSInteger responseStatusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
|
||||
if (responseError) {
|
||||
// On error, simply print the error and close the uploader. We have to assume the data was not transmited
|
||||
// so it is not deleted. In the event that we accidently store data which was succesfully uploaded, the
|
||||
// duplicate data will be ignored by the server when it is next uploaded.
|
||||
[self logMessage:[NSString stringWithFormat:
|
||||
@"Error Uploading. Code: %d, Description: %@",
|
||||
[responseError code],
|
||||
[responseError localizedDescription]]];
|
||||
} else {
|
||||
// Step 3
|
||||
// While response status codes in the 5xx range leave upload rows intact, the default case is to delete.
|
||||
if (responseStatusCode >= 500 && responseStatusCode < 600) {
|
||||
[self logMessage:[NSString stringWithFormat:@"Upload failed with response status code %d", responseStatusCode]];
|
||||
} else {
|
||||
// Because only one instance of the uploader can be running at a time it should not be possible for
|
||||
// new upload rows to appear so there is no fear of deleting data which has not yet been uploaded.
|
||||
[self logMessage:[NSString stringWithFormat:@"Upload completed successfully. Response code %d", responseStatusCode]];
|
||||
[[LocalyticsDatabase sharedLocalyticsDatabase] deleteUploadedData];
|
||||
}
|
||||
}
|
||||
}
|
||||
@catch (NSException * e) {}
|
||||
|
||||
[self finishUpload];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)finishUpload
|
||||
{
|
||||
self.isUploading = false;
|
||||
|
||||
// Upload data has been deleted. Recover the disk space if necessary.
|
||||
[[LocalyticsDatabase sharedLocalyticsDatabase] vacuumIfRequired];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method gzipDeflatedDataWithData
|
||||
@abstract Deflates the provided data using gzip at the default compression level (6). Complete NSData gzip category available on CocoaDev. http://www.cocoadev.com/index.pl?NSDataCategory.
|
||||
@return the deflated data
|
||||
*/
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data
|
||||
{
|
||||
if ([data length] == 0) return data;
|
||||
|
||||
z_stream strm;
|
||||
|
||||
strm.zalloc = Z_NULL;
|
||||
strm.zfree = Z_NULL;
|
||||
strm.opaque = Z_NULL;
|
||||
strm.total_out = 0;
|
||||
strm.next_in=(Bytef *)[data bytes];
|
||||
strm.avail_in = [data length];
|
||||
|
||||
// Compresssion Levels:
|
||||
// Z_NO_COMPRESSION
|
||||
// Z_BEST_SPEED
|
||||
// Z_BEST_COMPRESSION
|
||||
// Z_DEFAULT_COMPRESSION
|
||||
|
||||
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil;
|
||||
|
||||
NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion
|
||||
|
||||
do {
|
||||
|
||||
if (strm.total_out >= [compressed length])
|
||||
[compressed increaseLengthBy: 16384];
|
||||
|
||||
strm.next_out = [compressed mutableBytes] + strm.total_out;
|
||||
strm.avail_out = [compressed length] - strm.total_out;
|
||||
|
||||
deflate(&strm, Z_FINISH);
|
||||
|
||||
} while (strm.avail_out == 0);
|
||||
|
||||
deflateEnd(&strm);
|
||||
|
||||
[compressed setLength: strm.total_out];
|
||||
return [NSData dataWithData:compressed];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method logMessage
|
||||
@abstract Logs a message with (localytics uploader) prepended to it
|
||||
@param message The message to log
|
||||
*/
|
||||
- (void) logMessage:(NSString *)message {
|
||||
if(DO_LOCALYTICS_LOGGING) {
|
||||
NSLog(@"(localytics uploader) %s\n", [message UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - System Functions
|
||||
+ (id)allocWithZone:(NSZone *)zone {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploader == nil) {
|
||||
_sharedUploader = [super allocWithZone:zone];
|
||||
return _sharedUploader;
|
||||
}
|
||||
}
|
||||
// returns nil on subsequent allocations
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)retain {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (unsigned)retainCount {
|
||||
// maximum value of an unsigned int - prevents additional retains for the class
|
||||
return UINT_MAX;
|
||||
}
|
||||
|
||||
- (oneway void)release {
|
||||
// ignore release commands
|
||||
}
|
||||
|
||||
- (id)autorelease {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_sharedUploader release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
48
Localytics/UploaderThread.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// UploaderThread.h
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/*!
|
||||
@class UploaderThread
|
||||
@discussion Singleton class to handle data uploads
|
||||
*/
|
||||
|
||||
@interface UploaderThread : NSObject {
|
||||
NSURLConnection *_uploadConnection; // The connection which uploads the bits
|
||||
NSInteger _responseStatusCode; // The HTTP response status code for the current connection
|
||||
|
||||
BOOL _isUploading; // A flag to gaurantee only one uploader instance can happen at once
|
||||
}
|
||||
|
||||
@property (nonatomic, retain) NSURLConnection *uploadConnection;
|
||||
|
||||
@property BOOL isUploading;
|
||||
|
||||
/*!
|
||||
@method sharedUploaderThread
|
||||
@abstract Establishes this as a Singleton Class allowing for data persistence.
|
||||
The class is accessed within the code using the following syntax:
|
||||
[[UploaderThread sharedUploaderThread] functionHere]
|
||||
*/
|
||||
+ (UploaderThread *)sharedUploaderThread;
|
||||
|
||||
/*!
|
||||
@method UploaderThread
|
||||
@abstract Creates a thread which uploads all queued header and event data.
|
||||
All files starting with sessionFilePrefix are renamed,
|
||||
uploaded and deleted on upload. This way the sessions can continue
|
||||
writing data regardless of whether or not the upload succeeds. Files
|
||||
which have been renamed still count towards the total number of Localytics
|
||||
files which can be stored on the disk.
|
||||
@param localyticsApplicationKey the Localytics application ID
|
||||
*/
|
||||
- (void)uploaderThreadwithApplicationKey:(NSString *)localyticsApplicationKey;
|
||||
|
||||
@end
|
||||
260
Localytics/UploaderThread.m
Normal file
@@ -0,0 +1,260 @@
|
||||
// UploaderThread.m
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import "UploaderThread.h"
|
||||
#import "LocalyticsSession.h"
|
||||
#import "LocalyticsDatabase.h"
|
||||
#import <zlib.h>
|
||||
|
||||
#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads" // url to send the
|
||||
|
||||
static UploaderThread *_sharedUploaderThread = nil;
|
||||
|
||||
@interface UploaderThread ()
|
||||
- (void)complete;
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data;
|
||||
- (void)logMessage:(NSString *)message;
|
||||
@end
|
||||
|
||||
@implementation UploaderThread
|
||||
|
||||
@synthesize uploadConnection = _uploadConnection;
|
||||
@synthesize isUploading = _isUploading;
|
||||
|
||||
#pragma mark Singleton Class
|
||||
+ (UploaderThread *)sharedUploaderThread {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploaderThread == nil)
|
||||
{
|
||||
_sharedUploaderThread = [[self alloc] init];
|
||||
}
|
||||
}
|
||||
return _sharedUploaderThread;
|
||||
}
|
||||
|
||||
#pragma mark Class Methods
|
||||
- (void)uploaderThreadwithApplicationKey:(NSString *)localyticsApplicationKey {
|
||||
|
||||
// Do nothing if already uploading.
|
||||
if (self.uploadConnection != nil || self.isUploading == true)
|
||||
{
|
||||
[self logMessage:@"Upload already in progress. Aborting."];
|
||||
return;
|
||||
}
|
||||
|
||||
[self logMessage:@"Beginning upload process"];
|
||||
self.isUploading = true;
|
||||
|
||||
// Prepare the data for upload. The upload could take a long time, so some effort has to be made to be sure that events
|
||||
// which get written while the upload is taking place don't get lost or duplicated. To achieve this, the logic is:
|
||||
// 1) Append every header row blob string and and those of its associated events to the upload string.
|
||||
// 2) Deflate and upload the data.
|
||||
// 3) On success, delete all blob headers and staged events. Events added while an upload is in process are not
|
||||
// deleted because they are not associated a header (and cannot be until the upload completes).
|
||||
|
||||
// Step 1
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
|
||||
LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase];
|
||||
NSString *blobString = [db uploadBlobString];
|
||||
|
||||
if ([blobString length] == 0) {
|
||||
// There is nothing outstanding to upload.
|
||||
[self logMessage:@"Abandoning upload. There are no new events."];
|
||||
|
||||
[pool drain];
|
||||
[self complete];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease];
|
||||
[self logMessage:@"Upload data:"];
|
||||
[self logMessage:myString];
|
||||
|
||||
// Step 2
|
||||
NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain];
|
||||
|
||||
[pool drain];
|
||||
|
||||
NSString *apiUrlString = [NSString stringWithFormat:LOCALYTICS_URL, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
|
||||
NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString]
|
||||
cachePolicy:NSURLRequestReloadIgnoringCacheData
|
||||
timeoutInterval:60.0];
|
||||
[submitRequest setHTTPMethod:@"POST"];
|
||||
[submitRequest setValue:@"application/x-gzip" forHTTPHeaderField:@"Content-Type"];
|
||||
[submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
|
||||
[submitRequest setValue:[NSString stringWithFormat:@"%d", [deflatedRequestData length]] forHTTPHeaderField:@"Content-Length"];
|
||||
[submitRequest setHTTPBody:deflatedRequestData];
|
||||
[deflatedRequestData release];
|
||||
|
||||
// The NSURLConnection Object automatically spawns its own thread as a default behavior.
|
||||
@try
|
||||
{
|
||||
[self logMessage:@"Spawning new thread for upload"];
|
||||
self.uploadConnection = [NSURLConnection connectionWithRequest:submitRequest delegate:self];
|
||||
|
||||
// Step 3 is handled by connectionDidFinishLoading.
|
||||
}
|
||||
@catch (NSException * e)
|
||||
{
|
||||
[self complete];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark **** NSURLConnection FUNCTIONS ****
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
|
||||
// Used to gather response data from server - Not utilized in this version
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
|
||||
// Could receive multiple response callbacks, likely due to redirection.
|
||||
// Record status and act only when connection completes load.
|
||||
_responseStatusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
}
|
||||
|
||||
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
|
||||
// If the connection finished loading, the files should be deleted. While response status codes in the 5xx range
|
||||
// leave upload rows intact, the default case is to delete.
|
||||
if (_responseStatusCode >= 500 && _responseStatusCode < 600)
|
||||
{
|
||||
[self logMessage:[NSString stringWithFormat:@"Upload failed with response status code %d", _responseStatusCode]];
|
||||
} else
|
||||
{
|
||||
// The connection finished loading and uploaded data should be deleted. Because only one instance of the
|
||||
// uploader can be running at a time it should not be possible for new upload rows to appear so there is no
|
||||
// fear of deleting data which has not yet been uploaded.
|
||||
[self logMessage:[NSString stringWithFormat:@"Upload completed successfully. Response code %d", _responseStatusCode]];
|
||||
[[LocalyticsDatabase sharedLocalyticsDatabase] deleteUploadData];
|
||||
}
|
||||
|
||||
// Close upload session
|
||||
[self complete];
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
|
||||
// On error, simply print the error and close the uploader. We have to assume the data was not transmited
|
||||
// so it is not deleted. In the event that we accidently store data which was succesfully uploaded, the
|
||||
// duplicate data will be ignored by the server when it is next uploaded.
|
||||
[self logMessage:[NSString stringWithFormat:
|
||||
@"Error Uploading. Code: %d, Description: %s",
|
||||
[error code],
|
||||
[error localizedDescription]]];
|
||||
|
||||
[self complete];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method complete
|
||||
@abstract closes the upload connection and reports back to the session that the upload is complete
|
||||
*/
|
||||
- (void)complete {
|
||||
_responseStatusCode = 0;
|
||||
self.uploadConnection = nil;
|
||||
self.isUploading = false;
|
||||
}
|
||||
|
||||
/*!
|
||||
@method gzipDeflatedDataWithData
|
||||
@abstract Deflates the provided data using gzip at the default compression level (6). Complete NSData gzip category available on CocoaDev. http://www.cocoadev.com/index.pl?NSDataCategory.
|
||||
@return the deflated data
|
||||
*/
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data
|
||||
{
|
||||
if ([data length] == 0) return data;
|
||||
|
||||
z_stream strm;
|
||||
|
||||
strm.zalloc = Z_NULL;
|
||||
strm.zfree = Z_NULL;
|
||||
strm.opaque = Z_NULL;
|
||||
strm.total_out = 0;
|
||||
strm.next_in=(Bytef *)[data bytes];
|
||||
strm.avail_in = [data length];
|
||||
|
||||
// Compresssion Levels:
|
||||
// Z_NO_COMPRESSION
|
||||
// Z_BEST_SPEED
|
||||
// Z_BEST_COMPRESSION
|
||||
// Z_DEFAULT_COMPRESSION
|
||||
|
||||
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil;
|
||||
|
||||
NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion
|
||||
|
||||
do {
|
||||
|
||||
if (strm.total_out >= [compressed length])
|
||||
[compressed increaseLengthBy: 16384];
|
||||
|
||||
strm.next_out = [compressed mutableBytes] + strm.total_out;
|
||||
strm.avail_out = [compressed length] - strm.total_out;
|
||||
|
||||
deflate(&strm, Z_FINISH);
|
||||
|
||||
} while (strm.avail_out == 0);
|
||||
|
||||
deflateEnd(&strm);
|
||||
|
||||
[compressed setLength: strm.total_out];
|
||||
return [NSData dataWithData:compressed];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method logMessage
|
||||
@abstract Logs a message with (localytics uploader) prepended to it
|
||||
@param message The message to log
|
||||
*/
|
||||
- (void) logMessage:(NSString *)message {
|
||||
if(DO_LOCALYTICS_LOGGING) {
|
||||
NSLog(@"(localytics uploader) %s\n", [message UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark System Functions
|
||||
+ (id)allocWithZone:(NSZone *)zone {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploaderThread == nil) {
|
||||
_sharedUploaderThread = [super allocWithZone:zone];
|
||||
return _sharedUploaderThread;
|
||||
}
|
||||
}
|
||||
// returns nil on subsequent allocations
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)retain {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (unsigned)retainCount {
|
||||
// maximum value of an unsigned int - prevents additional retains for the class
|
||||
return UINT_MAX;
|
||||
}
|
||||
|
||||
- (oneway void)release {
|
||||
// ignore release commands
|
||||
}
|
||||
|
||||
- (id)autorelease {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_uploadConnection release];
|
||||
[_sharedUploaderThread release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
111
Localytics/WebserviceConstants.h
Normal file
@@ -0,0 +1,111 @@
|
||||
// WebserviceConstants.h
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
// The constants which are used to make up the JSON blob
|
||||
// To save disk space and network bandwidth all the keywords have been
|
||||
// abbreviated and are exploded by the server.
|
||||
|
||||
/*********************
|
||||
* Shared Attributes *
|
||||
*********************/
|
||||
#define PARAM_UUID @"u" // UUID for JSON document
|
||||
#define PARAM_DATA_TYPE @"dt" // Data Type
|
||||
#define PARAM_CLIENT_TIME @"ct" // Client Time, seconds from Unix epoch (int)
|
||||
#define PARAM_LATITUDE @"lat" // Latitude - if available
|
||||
#define PARAM_LONGITUDE @"lon" // Longitude - if available
|
||||
#define PARAM_SESSION_UUID @"su" // UUID for an existing session
|
||||
#define PARAM_NEW_SESSION_UUID @"u" // UUID for a new session
|
||||
#define PARAM_ATTRIBUTES @"attrs" // Attributes (dictionary)
|
||||
|
||||
/***************
|
||||
* Blob Header *
|
||||
***************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "h" for Header
|
||||
// PARAM_ATTRIBUTES => dictionary containing Header Common Attributes
|
||||
#define PARAM_PERSISTED_AT @"pa" // Persistent Storage Created At. A timestamp created when the app was
|
||||
// first launched and the persistent storage was created. Stores as
|
||||
// seconds from Unix epoch. (int)
|
||||
#define PARAM_SEQUENCE_NUMBER @"seq" // Sequence number - an increasing count for each blob, stored in the
|
||||
// persistent store Consistent across app starts. (int)
|
||||
|
||||
/****************************
|
||||
* Header Common Attributes *
|
||||
****************************/
|
||||
|
||||
// PARAM_DATA_TYPE
|
||||
#define PARAM_APP_KEY @"au" // Localytics Application ID
|
||||
#define PARAM_DEVICE_UUID @"du" // Device UUID
|
||||
#define PARAM_DEVICE_UUID_HASHED @"udid" // Hashed version of the UUID
|
||||
#define PARAM_DEVICE_MAC @"wmac" // Hashed version of the device Mac
|
||||
#define PARAM_INSTALL_ID @"iu" // Install ID
|
||||
#define PARAM_JAILBROKEN @"j" // Jailbroken (boolean)
|
||||
#define PARAM_LIBRARY_VERSION @"lv" // Client Version
|
||||
#define PARAM_APP_VERSION @"av" // Application Version
|
||||
#define PARAM_DEVICE_PLATFORM @"dp" // Device Platform
|
||||
#define PARAM_LOCALE_LANGUAGE @"dll" // Locale Language
|
||||
#define PARAM_LOCALE_COUNTRY @"dlc" // Locale Country
|
||||
#define PARAM_NETWORK_COUNTRY @"nc" // Network Country (iso code) // ???: Never used on iPhone.
|
||||
#define PARAM_DEVICE_COUNTRY @"dc" // Device Country (iso code)
|
||||
#define PARAM_DEVICE_MANUFACTURER @"dma" // Device Manufacturer // ???: Never used on iPhone. Used to be "Device Make".
|
||||
#define PARAM_DEVICE_MODEL @"dmo" // Device Model
|
||||
#define PARAM_DEVICE_OS_VERSION @"dov" // Device OS Version
|
||||
#define PARAM_NETWORK_CARRIER @"nca" // Network Carrier
|
||||
#define PARAM_DATA_CONNECTION @"dac" // Data Connection Type // ???: Never used on iPhone.
|
||||
#define PARAM_OPT_VALUE @"optin" // Opt In (boolean)
|
||||
#define PARAM_DEVICE_MEMORY @"dmem" // Device Memory
|
||||
|
||||
/*****************
|
||||
* Session Start *
|
||||
*****************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "s" for Start
|
||||
// PARAM_CLIENT_TIME
|
||||
#define PARAM_SESSION_NUMBER @"nth" // This is the nth session on the device, 1-indexed (int)
|
||||
|
||||
/****************
|
||||
* Session Stop *
|
||||
****************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "c" for Close
|
||||
// PARAM_CLIENT_TIME
|
||||
// PARAM_LATITUDE
|
||||
// PARAM_LONGITUDE
|
||||
// PARAM_SESSION_UUID => UUID of session being closed
|
||||
#define PARAM_SESSION_ACTIVE @"cta" // Active time in seconds (time app was active)
|
||||
#define PARAM_SESSION_TOTAL @"ctl" // Total session length
|
||||
#define PARAM_SESSION_SCREENFLOW @"fl" // Screens encountered during this session, in order
|
||||
|
||||
/*********************
|
||||
* Application Event *
|
||||
*********************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "e" for Event
|
||||
// PARAM_CLIENT_TIME
|
||||
// PARAM_LATITUDE
|
||||
// PARAM_LONGITUDE
|
||||
// PARAM_SESSION_UUID => UUID of session event occured in
|
||||
// PARAM_ATTRIBUTES => dictionary containing attributes for this event as key-value string pairs
|
||||
#define PARAM_EVENT_NAME @"n" // Event Name, (eg. 'Button Click')
|
||||
#define PARAM_REPORT_ATTRIBUTES @"rattrs" // Attributes used in custom reports
|
||||
|
||||
/********************
|
||||
* Application flow *
|
||||
********************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "f" for Flow
|
||||
// PARAM_CLIENT_TIME
|
||||
#define PARAM_SESSION_START @"ss" // Start time for the current session.
|
||||
#define PARAM_NEW_FLOW_EVENTS @"nw" // Events and screens encountered during this session that have NOT been staged for upload.
|
||||
#define PARAM_OLD_FLOW_EVENTS @"od" // Events and screens encountered during this session that HAVE been staged for upload.
|
||||
6312
MasterPassword-Mac.xcodeproj/project.pbxproj
Normal file
@@ -2,6 +2,6 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:MasterPassword.xcodeproj">
|
||||
location = "self:MasterPassword-Mac.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.coredata.ubiquity.logLevel 3"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
4638
MasterPassword-iOS.xcodeproj/project.pbxproj
Normal file
7
MasterPassword-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:MasterPassword-iOS.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -6,7 +6,7 @@
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
@@ -16,16 +16,16 @@
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.GDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Production">
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
@@ -34,16 +34,17 @@
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.GDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Production"
|
||||
buildConfiguration = "AppStore"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable>
|
||||
@@ -52,7 +53,7 @@
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
@@ -64,18 +65,9 @@
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Production"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Production">
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Production"
|
||||
@@ -1,12 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
@@ -16,14 +17,14 @@
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.GDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
@@ -34,7 +35,7 @@
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
@@ -44,6 +45,7 @@
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable>
|
||||
@@ -52,17 +54,28 @@
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.coredata.ubiquity.logLevel 3"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<AdditionalOptions>
|
||||
<AdditionalOption
|
||||
key = "NSZombieEnabled"
|
||||
value = "YES"
|
||||
isEnabled = "YES">
|
||||
</AdditionalOption>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "AdHoc"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
@@ -70,7 +83,7 @@
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
@@ -78,7 +91,7 @@
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "AdHoc"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// MPAppDelegate.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface MPAppDelegate : AbstractAppDelegate
|
||||
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
|
||||
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
@property (strong, nonatomic) NSString *keyPhrase;
|
||||
@property (strong, nonatomic) NSData *keyPhraseHash;
|
||||
@property (strong, nonatomic) NSString *keyPhraseHashHex;
|
||||
|
||||
+ (MPAppDelegate *)get;
|
||||
+ (NSManagedObjectModel *)managedObjectModel;
|
||||
+ (NSManagedObjectContext *)managedObjectContext;
|
||||
|
||||
- (void)saveContext;
|
||||
- (NSURL *)applicationDocumentsDirectory;
|
||||
|
||||
- (void)showGuide;
|
||||
- (void)loadKeyPhrase;
|
||||
|
||||
@end
|
||||
@@ -1,492 +0,0 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
#import "MPMainViewController.h"
|
||||
#import "IASKSettingsReader.h"
|
||||
|
||||
@interface MPAppDelegate ()
|
||||
|
||||
+ (NSDictionary *)keyPhraseQuery;
|
||||
+ (NSDictionary *)keyPhraseHashQuery;
|
||||
|
||||
- (void)forgetKeyPhrase;
|
||||
- (void)loadStoredKeyPhrase;
|
||||
- (void)askKeyPhrase;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPAppDelegate
|
||||
|
||||
@synthesize managedObjectModel = __managedObjectModel;
|
||||
@synthesize managedObjectContext = __managedObjectContext;
|
||||
@synthesize persistentStoreCoordinator = __persistentStoreCoordinator;
|
||||
|
||||
@synthesize keyPhrase = _keyPhrase;
|
||||
@synthesize keyPhraseHash = _keyPhraseHash;
|
||||
@synthesize keyPhraseHashHex = _keyPhraseHashHex;
|
||||
|
||||
+ (void)initialize {
|
||||
|
||||
#ifdef DEBUG
|
||||
[Logger get].autoprintLevel = LogLevelTrace;
|
||||
[NSClassFromString(@"WebView") performSelector:@selector(_enableRemoteInspector)];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (NSDictionary *)keyPhraseQuery {
|
||||
|
||||
static NSDictionary *MPKeyPhraseQuery = nil;
|
||||
if (!MPKeyPhraseQuery)
|
||||
MPKeyPhraseQuery = [KeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObject:@"MasterPassword"
|
||||
forKey:(__bridge id)kSecAttrService]
|
||||
matches:nil];
|
||||
|
||||
return MPKeyPhraseQuery;
|
||||
}
|
||||
|
||||
+ (NSDictionary *)keyPhraseHashQuery {
|
||||
|
||||
static NSDictionary *MPKeyPhraseHashQuery = nil;
|
||||
if (!MPKeyPhraseHashQuery)
|
||||
MPKeyPhraseHashQuery = [KeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObject:@"MasterPasswordHash"
|
||||
forKey:(__bridge id)kSecAttrService]
|
||||
matches:nil];
|
||||
|
||||
return MPKeyPhraseHashQuery;
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight takeOff:@"bd44885deee7adce0645ce8e5498d80a_NDQ5NDQyMDExLTEyLTAyIDExOjM1OjQ4LjQ2NjM4NA"];
|
||||
[TestFlight setOptions:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:YES], @"logToConsole",
|
||||
nil]];
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointLaunched];
|
||||
#endif
|
||||
|
||||
UIImage *navBarImage = [[UIImage imageNamed:@"ui_navbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
|
||||
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsDefault];
|
||||
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsLandscapePhone];
|
||||
[[UINavigationBar appearance] setTitleTextAttributes:
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:1.0], UITextAttributeTextColor,
|
||||
[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8], UITextAttributeTextShadowColor,
|
||||
[NSValue valueWithUIOffset:UIOffsetMake(0, -1)], UITextAttributeTextShadowOffset,
|
||||
[UIFont fontWithName:@"Helvetica-Neue" size:0.0], UITextAttributeFont,
|
||||
nil]];
|
||||
|
||||
UIImage *navBarButton = [[UIImage imageNamed:@"ui_navbar_button"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
|
||||
UIImage *navBarBack = [[UIImage imageNamed:@"ui_navbar_back"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
|
||||
[[UIBarButtonItem appearance] setBackgroundImage:navBarButton forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UIBarButtonItem appearance] setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
||||
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:navBarBack forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
||||
[[UIBarButtonItem appearance] setTitleTextAttributes:
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0], UITextAttributeTextColor,
|
||||
[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5], UITextAttributeTextShadowColor,
|
||||
[NSValue valueWithUIOffset:UIOffsetMake(0, 1)], UITextAttributeTextShadowOffset,
|
||||
[UIFont fontWithName:@"Helvetica-Neue" size:0.0], UITextAttributeFont,
|
||||
nil]
|
||||
forState:UIControlStateNormal];
|
||||
|
||||
UIImage *toolBarImage = [[UIImage imageNamed:@"ui_toolbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(25, 5, 5, 5)];
|
||||
[[UISearchBar appearance] setBackgroundImage:toolBarImage];
|
||||
[[UIToolbar appearance] setBackgroundImage:toolBarImage forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsDefault];
|
||||
|
||||
/*
|
||||
UIImage *minImage = [[UIImage imageNamed:@"slider-minimum.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 0)];
|
||||
UIImage *maxImage = [[UIImage imageNamed:@"slider-maximum.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 0)];
|
||||
UIImage *thumbImage = [UIImage imageNamed:@"slider-handle.png"];
|
||||
|
||||
[[UISlider appearance] setMaximumTrackImage:maxImage forState:UIControlStateNormal];
|
||||
[[UISlider appearance] setMinimumTrackImage:minImage forState:UIControlStateNormal];
|
||||
[[UISlider appearance] setThumbImage:thumbImage forState:UIControlStateNormal];
|
||||
|
||||
UIImage *segmentSelected = [[UIImage imageNamed:@"segcontrol_sel.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
|
||||
UIImage *segmentUnselected = [[UIImage imageNamed:@"segcontrol_uns.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 15, 0, 15)];
|
||||
UIImage *segmentSelectedUnselected = [UIImage imageNamed:@"segcontrol_sel-uns.png"];
|
||||
UIImage *segUnselectedSelected = [UIImage imageNamed:@"segcontrol_uns-sel.png"];
|
||||
UIImage *segmentUnselectedUnselected = [UIImage imageNamed:@"segcontrol_uns-uns.png"];
|
||||
|
||||
[[UISegmentedControl appearance] setBackgroundImage:segmentUnselected forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UISegmentedControl appearance] setBackgroundImage:segmentSelected forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
|
||||
|
||||
[[UISegmentedControl appearance] setDividerImage:segmentUnselectedUnselected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UISegmentedControl appearance] setDividerImage:segmentSelectedUnselected forLeftSegmentState:UIControlStateSelected rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UISegmentedControl appearance] setDividerImage:segUnselectedSelected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateSelected barMetrics:UIBarMetricsDefault];*/
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
if ([NSStringFromSelector(@selector(storeKeyPhrase))
|
||||
isEqualToString:[note.object description]]) {
|
||||
self.keyPhrase = self.keyPhrase;
|
||||
[self loadKeyPhrase];
|
||||
}
|
||||
if ([NSStringFromSelector(@selector(forgetKeyPhrase))
|
||||
isEqualToString:[note.object description]])
|
||||
[self loadKeyPhrase];
|
||||
}];
|
||||
|
||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
}
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||
|
||||
if ([[MPConfig get].showQuickStart boolValue])
|
||||
[self showGuide];
|
||||
else
|
||||
[self loadKeyPhrase];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointActivated];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)showGuide {
|
||||
|
||||
[self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointShowGuide];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)loadKeyPhrase {
|
||||
|
||||
if ([[MPConfig get].forgetKeyPhrase boolValue]) {
|
||||
[self forgetKeyPhrase];
|
||||
return;
|
||||
}
|
||||
|
||||
[self loadStoredKeyPhrase];
|
||||
if (!self.keyPhrase) {
|
||||
// Key phrase is not known. Ask user to set/specify it.
|
||||
dbg(@"Key phrase not known. Will ask user.");
|
||||
[self askKeyPhrase];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)forgetKeyPhrase {
|
||||
|
||||
dbg(@"Forgetting key phrase.");
|
||||
[AlertViewController showAlertWithTitle:@"Changing Master Password"
|
||||
message:
|
||||
@"You've requested to change your master password.\n\n"
|
||||
@"If you continue, your current sites and passwords will become unavailable.\n\n"
|
||||
@"You can always change back to the old master password later.\n"
|
||||
@"Your old sites and passwords will then become available again."
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex != [alert cancelButtonIndex]) {
|
||||
// Key phrase reset. Delete it.
|
||||
dbg(@"Deleting master key phrase and hash from key chain.");
|
||||
[KeyChain deleteItemForQuery:[MPAppDelegate keyPhraseQuery]];
|
||||
[KeyChain deleteItemForQuery:[MPAppDelegate keyPhraseHashQuery]];
|
||||
}
|
||||
|
||||
[self loadKeyPhrase];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPChanged];
|
||||
#endif
|
||||
}
|
||||
cancelTitle:[PearlStrings get].commonButtonAbort
|
||||
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
||||
[MPConfig get].forgetKeyPhrase = [NSNumber numberWithBool:NO];
|
||||
}
|
||||
|
||||
- (void)loadStoredKeyPhrase {
|
||||
|
||||
if ([[MPConfig get].storeKeyPhrase boolValue]) {
|
||||
// Key phrase is stored in keychain. Load it.
|
||||
dbg(@"Loading master key phrase from key chain.");
|
||||
NSData *keyPhraseData = [KeyChain dataOfItemForQuery:[MPAppDelegate keyPhraseQuery]];
|
||||
dbg(@" -> Master key phrase %@.", keyPhraseData? @"found": @"NOT found");
|
||||
|
||||
self.keyPhrase = keyPhraseData? [[NSString alloc] initWithBytes:keyPhraseData.bytes length:keyPhraseData.length
|
||||
encoding:NSUTF8StringEncoding]: nil;
|
||||
} else {
|
||||
// Key phrase should not be stored in keychain. Delete it.
|
||||
dbg(@"Deleting master key phrase from key chain.");
|
||||
[KeyChain deleteItemForQuery:[MPAppDelegate keyPhraseQuery]];
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void)askKeyPhrase {
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSData *keyPhraseHash = [KeyChain dataOfItemForQuery:[MPAppDelegate keyPhraseHashQuery]];
|
||||
dbg(@"Key phrase hash %@.", keyPhraseHash? @"known": @"NOT known");
|
||||
|
||||
[AlertViewController showAlertWithTitle:@"Master Password"
|
||||
message:keyPhraseHash? @"Unlock with your master password:": @"Choose your master password:"
|
||||
viewStyle:UIAlertViewStyleSecureTextInput
|
||||
tappedButtonBlock:
|
||||
^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
exit(0);
|
||||
|
||||
NSString *answer = [alert textFieldAtIndex:0].text;
|
||||
if (![answer length]) {
|
||||
// User didn't enter a key phrase.
|
||||
[AlertViewController showAlertWithTitle:[PearlStrings get].commonTitleError
|
||||
message:@"No master password entered."
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
tappedButtonBlock:
|
||||
^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
exit(0);
|
||||
} cancelTitle:@"Quit" otherTitles:nil];
|
||||
}
|
||||
|
||||
NSData *answerHash = [answer hashWith:PearlDigestSHA512];
|
||||
if (keyPhraseHash)
|
||||
// A key phrase hash is known -> a key phrase is set.
|
||||
// Make sure the user's entered key phrase matches it.
|
||||
if (![keyPhraseHash isEqual:answerHash]) {
|
||||
dbg(@"Key phrase hash mismatch. Expected: %@, answer: %@.", keyPhraseHash, answerHash);
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch];
|
||||
#endif
|
||||
[AlertViewController showAlertWithTitle:[PearlStrings get].commonTitleError
|
||||
message:
|
||||
@"Incorrect master password.\n\n"
|
||||
@"If you are trying to use the app with a different master password, "
|
||||
@"flip the 'Change my password' option in Settings."
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
tappedButtonBlock:
|
||||
^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
exit(0);
|
||||
} cancelTitle:@"Quit" otherTitles:nil];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPAsked];
|
||||
#endif
|
||||
|
||||
self.keyPhrase = answer;
|
||||
} cancelTitle:@"Quit" otherTitles:@"Unlock", nil];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||
|
||||
[self saveContext];
|
||||
|
||||
if (![[MPConfig get].rememberKeyPhrase boolValue])
|
||||
self.keyPhrase = nil;
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointDeactivated];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application {
|
||||
|
||||
[self saveContext];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointTerminated];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (MPAppDelegate *)get {
|
||||
|
||||
return (MPAppDelegate *)[super get];
|
||||
}
|
||||
|
||||
+ (NSManagedObjectContext *)managedObjectContext {
|
||||
|
||||
return [(MPAppDelegate *)[UIApplication sharedApplication].delegate managedObjectContext];
|
||||
}
|
||||
|
||||
+ (NSManagedObjectModel *)managedObjectModel {
|
||||
|
||||
return [(MPAppDelegate *)[UIApplication sharedApplication].delegate managedObjectModel];
|
||||
}
|
||||
|
||||
- (void)saveContext {
|
||||
|
||||
[self.managedObjectContext performBlock:^{
|
||||
NSError *error = nil;
|
||||
if ([self.managedObjectContext hasChanges] && ![self.managedObjectContext save:&error])
|
||||
err(@"Unresolved error %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setKeyPhrase:(NSString *)keyPhrase {
|
||||
|
||||
_keyPhrase = keyPhrase;
|
||||
|
||||
if (keyPhrase) {
|
||||
self.keyPhraseHash = [keyPhrase hashWith:PearlDigestSHA512];
|
||||
self.keyPhraseHashHex = [self.keyPhraseHash encodeHex];
|
||||
|
||||
dbg(@"Updating master key phrase hash to: %@.", self.keyPhraseHashHex);
|
||||
[KeyChain addOrUpdateItemForQuery:[MPAppDelegate keyPhraseHashQuery]
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
self.keyPhraseHash, (__bridge id)kSecValueData,
|
||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
||||
nil]];
|
||||
if ([[MPConfig get].storeKeyPhrase boolValue]) {
|
||||
dbg(@"Storing master key phrase in key chain.");
|
||||
[KeyChain addOrUpdateItemForQuery:[MPAppDelegate keyPhraseQuery]
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[keyPhrase dataUsingEncoding:NSUTF8StringEncoding], (__bridge id)kSecValueData,
|
||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
||||
nil]];
|
||||
}
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSetKeyphraseLength, _keyPhrase.length]];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Core Data stack
|
||||
|
||||
/**
|
||||
Returns the managed object context for the application.
|
||||
If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
|
||||
*/
|
||||
- (NSManagedObjectContext *)managedObjectContext
|
||||
{
|
||||
if (__managedObjectContext)
|
||||
return __managedObjectContext;
|
||||
|
||||
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
|
||||
if (coordinator) {
|
||||
__managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
|
||||
__managedObjectContext.persistentStoreCoordinator = coordinator;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSPersistentStoreDidImportUbiquitousContentChangesNotification
|
||||
object:coordinator
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
dbg(@"Ubiquitous content change: %@", note);
|
||||
|
||||
[__managedObjectContext performBlock:^{
|
||||
[__managedObjectContext mergeChangesFromContextDidSaveNotification:note];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotification:
|
||||
[NSNotification notificationWithName:UIScreenModeDidChangeNotification
|
||||
object:self userInfo:[note userInfo]]];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
return __managedObjectContext;
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the managed object model for the application.
|
||||
If the model doesn't already exist, it is created from the application's model.
|
||||
*/
|
||||
- (NSManagedObjectModel *)managedObjectModel
|
||||
{
|
||||
if (__managedObjectModel)
|
||||
return __managedObjectModel;
|
||||
|
||||
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"MasterPassword" withExtension:@"momd"];
|
||||
return __managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the persistent store coordinator for the application.
|
||||
If the coordinator doesn't already exist, it is created and the application's store added to it.
|
||||
*/
|
||||
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
|
||||
{
|
||||
if (__persistentStoreCoordinator)
|
||||
return __persistentStoreCoordinator;
|
||||
|
||||
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"MasterPassword.sqlite"];
|
||||
|
||||
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
|
||||
[__persistentStoreCoordinator lock];
|
||||
NSError *error = nil;
|
||||
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL
|
||||
options:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
|
||||
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
|
||||
@"MasterPassword.store", NSPersistentStoreUbiquitousContentNameKey,
|
||||
[[[NSFileManager defaultManager]
|
||||
URLForUbiquityContainerIdentifier:nil]
|
||||
URLByAppendingPathComponent:@"store"
|
||||
isDirectory:YES], NSPersistentStoreUbiquitousContentURLKey,
|
||||
nil]
|
||||
error:&error])
|
||||
{
|
||||
/*
|
||||
Replace this implementation with code to handle the error appropriately.
|
||||
|
||||
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
||||
Typical reasons for an error here include:
|
||||
* The persistent store is not accessible;
|
||||
* The schema for the persistent store is incompatible with current managed object model.
|
||||
Check the error message to determine what the actual problem was.
|
||||
|
||||
|
||||
If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.
|
||||
|
||||
If you encounter schema incompatibility errors during development, you can reduce their frequency by:
|
||||
* Simply deleting the existing store:
|
||||
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]
|
||||
|
||||
* Performing automatic lightweight migration by passing the following dictionary as the options parameter:
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
|
||||
|
||||
Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.
|
||||
|
||||
*/
|
||||
err(@"Unresolved error %@, %@", error, [error userInfo]);
|
||||
#if DEBUG
|
||||
wrn(@"Deleted datastore: %@", storeURL);
|
||||
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
|
||||
#endif
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointStoreIncompatible];
|
||||
#endif
|
||||
|
||||
@throw [NSException exceptionWithName:error.domain reason:error.localizedDescription
|
||||
userInfo:[NSDictionary dictionaryWithObject:error forKey:@"cause"]];
|
||||
}
|
||||
|
||||
if (![[NSFileManager defaultManager] setAttributes:[NSDictionary dictionaryWithObject:NSFileProtectionComplete
|
||||
forKey:NSFileProtectionKey]
|
||||
ofItemAtPath:storeURL.path error:&error])
|
||||
err(@"Unresolved error %@, %@", error, [error userInfo]);
|
||||
[__persistentStoreCoordinator unlock];
|
||||
|
||||
return __persistentStoreCoordinator;
|
||||
}
|
||||
|
||||
#pragma mark - Application's Documents directory
|
||||
|
||||
/**
|
||||
Returns the URL to the application's Documents directory.
|
||||
*/
|
||||
- (NSURL *)applicationDocumentsDirectory
|
||||
{
|
||||
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
}
|
||||
|
||||
@end
|
||||
22
MasterPassword/MPAppDelegate_Key.h
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// MPAppDelegate_Key.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
@interface MPAppDelegate_Shared (Key)
|
||||
|
||||
- (void)loadStoredKey;
|
||||
- (IBAction)signOut:(id)sender;
|
||||
|
||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword;
|
||||
- (void)updateKey:(NSData *)key;
|
||||
- (void)forgetKey;
|
||||
|
||||
- (NSData *)keyWithLength:(NSUInteger)keyLength;
|
||||
|
||||
@end
|
||||
158
MasterPassword/MPAppDelegate_Key.m
Normal file
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPConfig.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
#import "MPElementEntity.h"
|
||||
|
||||
@implementation MPAppDelegate_Shared (Key)
|
||||
|
||||
static NSDictionary *keyQuery() {
|
||||
|
||||
static NSDictionary *MPKeyQuery = nil;
|
||||
if (!MPKeyQuery)
|
||||
MPKeyQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"Saved Master Password", (__bridge id)kSecAttrService,
|
||||
@"default", (__bridge id)kSecAttrAccount,
|
||||
nil]
|
||||
matches:nil];
|
||||
|
||||
return MPKeyQuery;
|
||||
}
|
||||
|
||||
static NSDictionary *keyIDQuery() {
|
||||
|
||||
static NSDictionary *MPKeyIDQuery = nil;
|
||||
if (!MPKeyIDQuery)
|
||||
MPKeyIDQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"Master Password Check", (__bridge id)kSecAttrService,
|
||||
@"default", (__bridge id)kSecAttrAccount,
|
||||
nil]
|
||||
matches:nil];
|
||||
|
||||
return MPKeyIDQuery;
|
||||
}
|
||||
|
||||
- (void)forgetKey {
|
||||
|
||||
inf(@"Deleting key and ID from keychain.");
|
||||
if ([PearlKeyChain deleteItemForQuery:keyQuery()] != errSecItemNotFound)
|
||||
inf(@"Removed key from keychain.");
|
||||
if ([PearlKeyChain deleteItemForQuery:keyIDQuery()] != errSecItemNotFound)
|
||||
inf(@"Removed key ID from keychain.");
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (IBAction)signOut:(id)sender {
|
||||
|
||||
[MPConfig get].saveKey = [NSNumber numberWithBool:NO];
|
||||
[self updateKey:nil];
|
||||
}
|
||||
|
||||
- (void)loadStoredKey {
|
||||
|
||||
if ([[MPConfig get].saveKey boolValue]) {
|
||||
// Key is stored in keychain. Load it.
|
||||
[self updateKey:[PearlKeyChain dataOfItemForQuery:keyQuery()]];
|
||||
inf(@"Looking for key in keychain: %@.", self.key? @"found": @"missing");
|
||||
} else {
|
||||
// Key should not be stored in keychain. Delete it.
|
||||
if ([PearlKeyChain deleteItemForQuery:keyQuery()] != errSecItemNotFound)
|
||||
inf(@"Removed key from keychain.");
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword {
|
||||
|
||||
if (![tryPassword length])
|
||||
return NO;
|
||||
|
||||
NSData *tryKey = keyForPassword(tryPassword);
|
||||
NSData *tryKeyID = keyIDForKey(tryKey);
|
||||
NSData *keyID = [PearlKeyChain dataOfItemForQuery:keyIDQuery()];
|
||||
inf(@"Key ID known? %@.", keyID? @"YES": @"NO");
|
||||
if (keyID)
|
||||
// A key ID is known -> a password is set.
|
||||
// Make sure the user's entered password matches it.
|
||||
if (![keyID isEqual:tryKeyID]) {
|
||||
wrn(@"Key ID mismatch. Expected: %@, answer: %@.", [keyID encodeHex], [tryKeyID encodeHex]);
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch];
|
||||
#endif
|
||||
return NO;
|
||||
}
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPEntered];
|
||||
#endif
|
||||
|
||||
[self updateKey:tryKey];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)updateKey:(NSData *)key {
|
||||
|
||||
if (self.key != key) {
|
||||
self.key = key;
|
||||
|
||||
if (key)
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
|
||||
else
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
|
||||
}
|
||||
|
||||
if (self.key) {
|
||||
self.keyID = keyIDForKey(self.key);
|
||||
|
||||
NSData *existingKeyID = [PearlKeyChain dataOfItemForQuery:keyIDQuery()];
|
||||
if (![existingKeyID isEqualToData:self.keyID]) {
|
||||
inf(@"Updating key ID in keychain.");
|
||||
[PearlKeyChain addOrUpdateItemForQuery:keyIDQuery()
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
self.keyID, (__bridge id)kSecValueData,
|
||||
#if TARGET_OS_IPHONE
|
||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
||||
#endif
|
||||
nil]];
|
||||
}
|
||||
if ([[MPConfig get].saveKey boolValue]) {
|
||||
NSData *existingKey = [PearlKeyChain dataOfItemForQuery:keyQuery()];
|
||||
if (![existingKey isEqualToData:self.key]) {
|
||||
inf(@"Updating key in keychain.");
|
||||
[PearlKeyChain addOrUpdateItemForQuery:keyQuery()
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
self.key, (__bridge id)kSecValueData,
|
||||
#if TARGET_OS_IPHONE
|
||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
||||
#endif
|
||||
nil]];
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSetKey];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (NSData *)keyWithLength:(NSUInteger)keyLength {
|
||||
|
||||
return [self.key subdataWithRange:NSMakeRange(0, MIN(keyLength, self.key.length))];
|
||||
}
|
||||
|
||||
@end
|
||||
22
MasterPassword/MPAppDelegate_Shared.h
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// MPAppDelegate_Shared.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
@interface MPAppDelegate_Shared : PearlAppDelegate
|
||||
#else
|
||||
@interface MPAppDelegate_Shared : NSObject <PearlConfigDelegate>
|
||||
#endif
|
||||
|
||||
@property (strong, nonatomic) NSData *key;
|
||||
@property (strong, nonatomic) NSData *keyID;
|
||||
|
||||
+ (MPAppDelegate_Shared *)get;
|
||||
|
||||
- (NSURL *)applicationFilesDirectory;
|
||||
|
||||
@end
|
||||
44
MasterPassword/MPAppDelegate_Shared.m
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
@implementation MPAppDelegate_Shared
|
||||
|
||||
@synthesize key;
|
||||
@synthesize keyID;
|
||||
|
||||
+ (MPAppDelegate_Shared *)get {
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return (MPAppDelegate_Shared *)[UIApplication sharedApplication].delegate;
|
||||
#elif defined (__MAC_OS_X_VERSION_MIN_REQUIRED)
|
||||
return (MPAppDelegate_Shared *)[NSApplication sharedApplication].delegate;
|
||||
#else
|
||||
#error Unsupported OS.
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSURL *)applicationFilesDirectory {
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
#else
|
||||
NSURL *appSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
NSURL *applicationFilesDirectory = [appSupportURL URLByAppendingPathComponent:@"com.lyndir.lhunath.MasterPassword"];
|
||||
|
||||
NSError *error = nil;
|
||||
[[NSFileManager defaultManager] createDirectoryAtURL:applicationFilesDirectory withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
if (error)
|
||||
err(@"Couldn't create application directory: %@, error occurred: %@", applicationFilesDirectory, error);
|
||||
|
||||
return applicationFilesDirectory;
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
36
MasterPassword/MPAppDelegate_Store.h
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// MPAppDelegate_Key.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
#import "UbiquityStoreManager.h"
|
||||
|
||||
typedef enum {
|
||||
MPImportResultSuccess,
|
||||
MPImportResultCancelled,
|
||||
MPImportResultInvalidPassword,
|
||||
MPImportResultMalformedInput,
|
||||
MPImportResultInternalError,
|
||||
} MPImportResult;
|
||||
|
||||
@interface MPAppDelegate_Shared (Store) <UbiquityStoreManagerDelegate>
|
||||
|
||||
+ (NSManagedObjectContext *)managedObjectContext;
|
||||
+ (NSManagedObjectModel *)managedObjectModel;
|
||||
- (NSManagedObjectContext *)managedObjectContext;
|
||||
- (NSManagedObjectModel *)managedObjectModel;
|
||||
|
||||
- (UbiquityStoreManager *)storeManager;
|
||||
- (void)saveContext;
|
||||
- (void)printStore;
|
||||
|
||||
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation;
|
||||
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords;
|
||||
|
||||
@end
|
||||
439
MasterPassword/MPAppDelegate_Store.m
Normal file
@@ -0,0 +1,439 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Store.h"
|
||||
#import "MPElementEntity.h"
|
||||
#import "MPConfig.h"
|
||||
|
||||
@implementation MPAppDelegate_Shared (Store)
|
||||
|
||||
static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
|
||||
#pragma mark - Core Data setup
|
||||
|
||||
+ (NSManagedObjectContext *)managedObjectContext {
|
||||
|
||||
return [[self get] managedObjectContext];
|
||||
}
|
||||
|
||||
+ (NSManagedObjectModel *)managedObjectModel {
|
||||
|
||||
return [[self get] managedObjectModel];
|
||||
}
|
||||
|
||||
- (NSManagedObjectModel *)managedObjectModel {
|
||||
|
||||
static NSManagedObjectModel *managedObjectModel = nil;
|
||||
if (managedObjectModel)
|
||||
return managedObjectModel;
|
||||
|
||||
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"MasterPassword" withExtension:@"momd"];
|
||||
return managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
|
||||
}
|
||||
|
||||
- (NSManagedObjectContext *)managedObjectContext {
|
||||
|
||||
static NSManagedObjectContext *managedObjectContext = nil;
|
||||
if (managedObjectContext)
|
||||
return managedObjectContext;
|
||||
|
||||
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
|
||||
assert(coordinator);
|
||||
|
||||
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
||||
[managedObjectContext performBlockAndWait:^{
|
||||
managedObjectContext.persistentStoreCoordinator = coordinator;
|
||||
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
|
||||
}];
|
||||
|
||||
return managedObjectContext;
|
||||
}
|
||||
|
||||
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
|
||||
|
||||
// Wait until the storeManager is ready.
|
||||
for(__block BOOL isReady = [self storeManager].isReady; !isReady;)
|
||||
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
isReady = [self storeManager].isReady;
|
||||
});
|
||||
|
||||
assert([self storeManager].isReady);
|
||||
return [self storeManager].persistentStoreCoordinator;
|
||||
}
|
||||
|
||||
- (UbiquityStoreManager *)storeManager {
|
||||
|
||||
static UbiquityStoreManager *storeManager = nil;
|
||||
if (storeManager)
|
||||
return storeManager;
|
||||
|
||||
storeManager = [[UbiquityStoreManager alloc] initWithManagedObjectModel:[self managedObjectModel]
|
||||
localStoreURL:[[self applicationFilesDirectory] URLByAppendingPathComponent:@"MasterPassword.sqlite"]
|
||||
containerIdentifier:@"HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.shared"
|
||||
#if TARGET_OS_IPHONE
|
||||
additionalStoreOptions:[NSDictionary dictionaryWithObject:NSFileProtectionComplete forKey:NSPersistentStoreFileProtectionKey]
|
||||
#else
|
||||
additionalStoreOptions:nil
|
||||
#endif
|
||||
];
|
||||
storeManager.delegate = self;
|
||||
#ifdef DEBUG
|
||||
storeManager.hardResetEnabled = YES;
|
||||
#endif
|
||||
#if TARGET_OS_IPHONE
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification
|
||||
object:[UIApplication sharedApplication] queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[storeManager checkiCloudStatus];
|
||||
}];
|
||||
#else
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillBecomeActiveNotification
|
||||
object:[NSApplication sharedApplication] queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[storeManager checkiCloudStatus];
|
||||
}];
|
||||
#endif
|
||||
#if TARGET_OS_IPHONE
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification
|
||||
object:[UIApplication sharedApplication] queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[self saveContext];
|
||||
}];
|
||||
#else
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification
|
||||
object:[NSApplication sharedApplication] queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[self saveContext];
|
||||
}];
|
||||
#endif
|
||||
|
||||
return storeManager;
|
||||
}
|
||||
|
||||
- (void)saveContext {
|
||||
|
||||
[self.managedObjectContext performBlock:^{
|
||||
NSError *error = nil;
|
||||
if ([self.managedObjectContext hasChanges])
|
||||
if (![self.managedObjectContext save:&error])
|
||||
err(@"While saving context: %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)printStore {
|
||||
|
||||
if (![self managedObjectModel] || ![self managedObjectContext]) {
|
||||
trc(@"Not printing store: store not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
[self.managedObjectContext performBlock:^{
|
||||
trc(@"=== All entities ===");
|
||||
for(NSEntityDescription *entity in [[self managedObjectModel] entities]) {
|
||||
NSFetchRequest *request = [NSFetchRequest new];
|
||||
[request setEntity:entity];
|
||||
NSError *error;
|
||||
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
|
||||
for(NSManagedObject *o in results) {
|
||||
if ([o isKindOfClass:[MPElementEntity class]]) {
|
||||
MPElementEntity *e = (MPElementEntity *)o;
|
||||
trc(@"For descriptor: %@, found: %@: %@ (%@)", entity.name, [o class], e.name, e.keyID);
|
||||
} else {
|
||||
trc(@"For descriptor: %@, found: %@", entity.name, [o class]);
|
||||
}
|
||||
}
|
||||
}
|
||||
trc(@"---");
|
||||
if ([MPAppDelegate_Shared get].keyID) {
|
||||
trc(@"=== Known sites ===");
|
||||
NSFetchRequest *fetchRequest = [[self managedObjectModel]
|
||||
fetchRequestFromTemplateWithName:@"MPElements"
|
||||
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"", @"query",
|
||||
[MPAppDelegate_Shared get].keyID, @"keyID",
|
||||
nil]];
|
||||
[fetchRequest setSortDescriptors:
|
||||
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
|
||||
|
||||
NSError *error = nil;
|
||||
for (MPElementEntity *e in [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]) {
|
||||
trc(@"Found site: %@ (%@): %@", e.name, e.keyID, e);
|
||||
}
|
||||
trc(@"---");
|
||||
} else
|
||||
trc(@"Not printing sites: master password not set.");
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - UbiquityStoreManagerDelegate
|
||||
|
||||
- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm {
|
||||
|
||||
return self.managedObjectContext;
|
||||
}
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {
|
||||
|
||||
dbg(@"[StoreManager] %@", message);
|
||||
}
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)iCloudEnabled {
|
||||
|
||||
// manager.iCloudEnabled is more reliable (eg. iOS tampers with didSwitch a bit)
|
||||
iCloudEnabled = manager.iCloudEnabled;
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:iCloudEnabled? MPTestFlightCheckpointCloudEnabled: MPTestFlightCheckpointCloudDisabled];
|
||||
#endif
|
||||
|
||||
inf(@"Using iCloud? %@", iCloudEnabled? @"YES": @"NO");
|
||||
[MPConfig get].iCloud = [NSNumber numberWithBool:iCloudEnabled];
|
||||
}
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context {
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:PearlString(@"MPTestFlightCheckpointMPErrorUbiquity_%d", cause)];
|
||||
#endif
|
||||
err(@"StoreManager: cause=%d, context=%@, error=%@", cause, context, error);
|
||||
|
||||
switch (cause) {
|
||||
case UbiquityStoreManagerErrorCauseDeleteStore:
|
||||
case UbiquityStoreManagerErrorCauseDeleteLogs:
|
||||
case UbiquityStoreManagerErrorCauseCreateStorePath:
|
||||
case UbiquityStoreManagerErrorCauseClearStore:
|
||||
break;
|
||||
case UbiquityStoreManagerErrorCauseOpenLocalStore: {
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointLocalStoreIncompatible];
|
||||
#endif
|
||||
wrn(@"Local store could not be opened, resetting it.");
|
||||
manager.hardResetEnabled = YES;
|
||||
[manager hardResetLocalStorage];
|
||||
|
||||
[NSException raise:NSGenericException format:@"Local store was reset, application must be restarted to use it."];
|
||||
return;
|
||||
}
|
||||
case UbiquityStoreManagerErrorCauseOpenCloudStore: {
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointCloudStoreIncompatible];
|
||||
#endif
|
||||
wrn(@"iCloud store could not be opened, resetting it.");
|
||||
manager.hardResetEnabled = YES;
|
||||
[manager hardResetCloudStorage];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Import / Export
|
||||
|
||||
- (void)loadRFC3339DateFormatter {
|
||||
|
||||
if (rfc3339DateFormatter)
|
||||
return;
|
||||
|
||||
rfc3339DateFormatter = [NSDateFormatter new];
|
||||
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
|
||||
[rfc3339DateFormatter setLocale:enUSPOSIXLocale];
|
||||
[rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
|
||||
[rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
|
||||
}
|
||||
|
||||
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation {
|
||||
|
||||
[self loadRFC3339DateFormatter];
|
||||
|
||||
static NSRegularExpression *headerPattern, *sitePattern;
|
||||
__autoreleasing NSError *error;
|
||||
if (!headerPattern) {
|
||||
headerPattern = [[NSRegularExpression alloc]
|
||||
initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
|
||||
options:0 error:&error];
|
||||
if (error)
|
||||
err(@"Error loading the header pattern: %@", error);
|
||||
}
|
||||
if (!sitePattern) {
|
||||
sitePattern = [[NSRegularExpression alloc]
|
||||
initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([^\t]+)\t(.*)"
|
||||
options:0 error:&error];
|
||||
if (error)
|
||||
err(@"Error loading the site pattern: %@", error);
|
||||
}
|
||||
if (!headerPattern || !sitePattern)
|
||||
return MPImportResultInternalError;
|
||||
|
||||
NSString *keyIDHex = nil;
|
||||
BOOL headerStarted = NO, headerEnded = NO;
|
||||
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
||||
NSMutableSet *elementsToDelete = [NSMutableSet set];
|
||||
NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||
for(NSString *importedSiteLine in importedSiteLines) {
|
||||
if ([importedSiteLine hasPrefix:@"#"]) {
|
||||
// Comment or header
|
||||
if (!headerStarted) {
|
||||
if ([importedSiteLine isEqualToString:@"##"])
|
||||
headerStarted = YES;
|
||||
continue;
|
||||
}
|
||||
if (headerEnded)
|
||||
continue;
|
||||
if ([importedSiteLine isEqualToString:@"##"]) {
|
||||
headerEnded = YES;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Header
|
||||
if ([headerPattern numberOfMatchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] != 1) {
|
||||
err(@"Invalid header format in line: %@", importedSiteLine);
|
||||
return MPImportResultMalformedInput;
|
||||
}
|
||||
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
||||
NSString *key = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
|
||||
NSString *value = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
|
||||
if ([key isEqualToString:@"Key ID"]) {
|
||||
if (![(keyIDHex = value) isEqualToString:[keyIDForPassword(password) encodeHex]])
|
||||
return MPImportResultInvalidPassword;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!headerEnded)
|
||||
continue;
|
||||
if (!keyIDHex)
|
||||
return MPImportResultMalformedInput;
|
||||
if (![importedSiteLine length])
|
||||
continue;
|
||||
|
||||
// Site
|
||||
if ([sitePattern numberOfMatchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] != 1) {
|
||||
err(@"Invalid site format in line: %@", importedSiteLine);
|
||||
return MPImportResultMalformedInput;
|
||||
}
|
||||
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
||||
NSString *lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
|
||||
NSString *uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
|
||||
NSString *type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
|
||||
NSString *name = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
|
||||
NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
|
||||
|
||||
// Find existing site.
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND keyID == %@", name, keyIDHex];
|
||||
NSArray *existingSites = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
||||
if (error)
|
||||
err(@"Couldn't search existing sites: %@", error);
|
||||
if (!existingSites)
|
||||
return MPImportResultInternalError;
|
||||
|
||||
[elementsToDelete addObjectsFromArray:existingSites];
|
||||
[importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, name, exportContent, nil]];
|
||||
}
|
||||
|
||||
// Ask for confirmation to import these sites.
|
||||
if (!confirmation([importedSiteElements count], [elementsToDelete count]))
|
||||
return MPImportResultCancelled;
|
||||
|
||||
// Delete existing sites.
|
||||
[elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
|
||||
inf(@"Deleting site: %@, it will be replaced by an imported site.", [obj name]);
|
||||
[self.managedObjectContext deleteObject:obj];
|
||||
}];
|
||||
[self saveContext];
|
||||
|
||||
// Import new sites.
|
||||
for (NSArray *siteElements in importedSiteElements) {
|
||||
NSDate *lastUsed = [rfc3339DateFormatter dateFromString:[siteElements objectAtIndex:0]];
|
||||
NSInteger uses = [[siteElements objectAtIndex:1] integerValue];
|
||||
MPElementType type = (unsigned)[[siteElements objectAtIndex:2] integerValue];
|
||||
NSString *name = [siteElements objectAtIndex:3];
|
||||
NSString *exportContent = [siteElements objectAtIndex:4];
|
||||
|
||||
// Create new site.
|
||||
inf(@"Importing site: name=%@, lastUsed=%@, uses=%d, type=%u, keyID=%@", name, lastUsed, uses, type, keyIDHex);
|
||||
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
|
||||
inManagedObjectContext:self.managedObjectContext];
|
||||
element.name = name;
|
||||
element.keyID = [keyIDHex decodeHex];
|
||||
element.type = type;
|
||||
element.uses = uses;
|
||||
element.lastUsed = [lastUsed timeIntervalSinceReferenceDate];
|
||||
if ([exportContent length])
|
||||
[element importContent:exportContent];
|
||||
}
|
||||
[self saveContext];
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSitesImported];
|
||||
#endif
|
||||
|
||||
return MPImportResultSuccess;
|
||||
}
|
||||
|
||||
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {
|
||||
|
||||
[self loadRFC3339DateFormatter];
|
||||
|
||||
// Header.
|
||||
NSMutableString *export = [NSMutableString new];
|
||||
[export appendFormat:@"# Master Password site export\n"];
|
||||
if (showPasswords)
|
||||
[export appendFormat:@"# Export of site names and passwords in clear-text.\n"];
|
||||
else
|
||||
[export appendFormat:@"# Export of site names and stored passwords (unless device-private) encrypted with the master key.\n"];
|
||||
[export appendFormat:@"# \n"];
|
||||
[export appendFormat:@"##\n"];
|
||||
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
|
||||
[export appendFormat:@"# Key ID: %@\n", [self.keyID encodeHex]];
|
||||
[export appendFormat:@"# Date: %@\n", [rfc3339DateFormatter stringFromDate:[NSDate date]]];
|
||||
if (showPasswords)
|
||||
[export appendFormat:@"# Passwords: VISIBLE\n"];
|
||||
else
|
||||
[export appendFormat:@"# Passwords: PROTECTED\n"];
|
||||
[export appendFormat:@"##\n"];
|
||||
[export appendFormat:@"#\n"];
|
||||
[export appendFormat:@"# Last Times Password Site\tSite\n"];
|
||||
[export appendFormat:@"# used used type name\tpassword\n"];
|
||||
|
||||
// Sites.
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", self.keyID];
|
||||
__autoreleasing NSError *error = nil;
|
||||
NSArray *elements = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
||||
if (error)
|
||||
err(@"Error fetching sites for export: %@", error);
|
||||
|
||||
for (MPElementEntity *element in elements) {
|
||||
NSTimeInterval lastUsed = element.lastUsed;
|
||||
int16_t uses = element.uses;
|
||||
MPElementType type = (unsigned)element.type;
|
||||
NSString *name = element.name;
|
||||
NSString *content = nil;
|
||||
|
||||
// Determine the content to export.
|
||||
if (!(type & MPElementFeatureDevicePrivate)) {
|
||||
if (showPasswords)
|
||||
content = element.content;
|
||||
else if (type & MPElementFeatureExportContent)
|
||||
content = element.exportContent;
|
||||
}
|
||||
|
||||
[export appendFormat:@"%@ %8d %8d %20s\t%@\n",
|
||||
[rfc3339DateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUsed]], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content? content: @""];
|
||||
}
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSitesExported];
|
||||
#endif
|
||||
|
||||
return export;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,14 +6,13 @@
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
@interface MPConfig : Config
|
||||
@interface MPConfig : PearlConfig
|
||||
|
||||
@property (nonatomic, retain) NSNumber *dataStoreError;
|
||||
@property (nonatomic, retain) NSNumber *storeKeyPhrase;
|
||||
@property (nonatomic, retain) NSNumber *rememberKeyPhrase;
|
||||
@property (nonatomic, retain) NSNumber *forgetKeyPhrase;
|
||||
@property (nonatomic, retain) NSNumber *helpHidden;
|
||||
@property (nonatomic, retain) NSNumber *showQuickStart;
|
||||
@property (nonatomic, retain) NSNumber *saveKey;
|
||||
@property (nonatomic, retain) NSNumber *rememberKey;
|
||||
|
||||
@property (nonatomic, retain) NSNumber *iCloud;
|
||||
@property (nonatomic, retain) NSNumber *iCloudDecided;
|
||||
|
||||
+ (MPConfig *)get;
|
||||
|
||||
|
||||
@@ -7,26 +7,27 @@
|
||||
//
|
||||
|
||||
#import "MPConfig.h"
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
@implementation MPConfig
|
||||
|
||||
@dynamic dataStoreError, storeKeyPhrase, rememberKeyPhrase, forgetKeyPhrase, helpHidden, showQuickStart;
|
||||
|
||||
@dynamic saveKey, rememberKey, iCloud, iCloudDecided;
|
||||
|
||||
- (id)init {
|
||||
|
||||
if(!(self = [super init]))
|
||||
return self;
|
||||
return nil;
|
||||
|
||||
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(dataStoreError)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(storeKeyPhrase)),
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKeyPhrase)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(forgetKeyPhrase)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(helpHidden)),
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(showQuickStart)),
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(askForReviews)),
|
||||
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(saveKey)),
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKey)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloud)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloudDecided)),
|
||||
nil]];
|
||||
|
||||
self.delegate = [MPAppDelegate get];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
@interface MPElementEntity : NSManagedObject
|
||||
|
||||
@property (nonatomic, retain) NSString *name;
|
||||
@property (nonatomic, retain) NSString *mpHashHex;
|
||||
@property (nonatomic, retain) NSData *keyID;
|
||||
@property (nonatomic, assign) int16_t type;
|
||||
@property (nonatomic, assign) int16_t uses;
|
||||
@property (nonatomic, assign) NSTimeInterval lastUsed;
|
||||
|
||||
@property (nonatomic, retain, readonly) id content;
|
||||
|
||||
- (void)use;
|
||||
- (int16_t)use;
|
||||
- (NSString *)exportContent;
|
||||
- (void)importContent:(NSString *)content;
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
@implementation MPElementEntity
|
||||
|
||||
@dynamic name;
|
||||
@dynamic mpHashHex;
|
||||
@dynamic keyID;
|
||||
@dynamic type;
|
||||
@dynamic uses;
|
||||
@dynamic lastUsed;
|
||||
|
||||
- (void)use {
|
||||
- (int16_t)use {
|
||||
|
||||
++self.uses;
|
||||
self.lastUsed = [[NSDate date] timeIntervalSinceReferenceDate];
|
||||
return ++self.uses;
|
||||
}
|
||||
|
||||
- (id)content {
|
||||
@@ -28,15 +28,24 @@
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
||||
}
|
||||
|
||||
- (NSString *)exportContent {
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)importContent:(NSString *)content {
|
||||
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
|
||||
return [[self content] description];
|
||||
return PearlString(@"%@:%@", [self class], [self name]);
|
||||
}
|
||||
|
||||
- (NSString *)debugDescription {
|
||||
|
||||
return [NSString stringWithFormat:@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}",
|
||||
NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed];
|
||||
return PearlString(@"{%@: name=%@, keyID=%@, type=%d, uses=%d, lastUsed=%@}",
|
||||
NSStringFromClass([self class]), self.name, self.keyID, self.type, self.uses, self.lastUsed);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
|
||||
@interface MPElementGeneratedEntity : MPElementEntity
|
||||
|
||||
@property (nonatomic, assign) int16_t counter;
|
||||
@property (nonatomic, assign) int32_t counter;
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
|
||||
|
||||
@implementation MPElementGeneratedEntity
|
||||
@@ -16,16 +17,15 @@
|
||||
|
||||
- (id)content {
|
||||
|
||||
assert(self.type & MPElementTypeClassCalculated);
|
||||
if (!(self.type & MPElementTypeClassGenerated)) {
|
||||
err(@"Corrupt element: %@, type: %d is not in MPElementTypeClassGenerated", self.name, self.type);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self.name length])
|
||||
return nil;
|
||||
|
||||
if (self.type & MPElementTypeClassCalculated)
|
||||
return MPCalculateContent(self.type, self.name, [MPAppDelegate get].keyPhrase, self.counter);
|
||||
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException
|
||||
reason:[NSString stringWithFormat:@"Unsupported type: %d", self.type] userInfo:nil];
|
||||
return MPCalculateContent((unsigned)self.type, self.name, [MPAppDelegate get].key, self.counter);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#import "MPElementStoredEntity.h"
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
|
||||
@interface MPElementStoredEntity ()
|
||||
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
|
||||
|
||||
return [KeyChain createQueryForClass:kSecClassGenericPassword
|
||||
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"DevicePrivate", (__bridge id)kSecAttrService,
|
||||
name, (__bridge id)kSecAttrAccount,
|
||||
@@ -34,32 +35,42 @@
|
||||
assert(self.type & MPElementTypeClassStored);
|
||||
|
||||
NSData *encryptedContent;
|
||||
if (self.type == MPElementTypeStoredDevicePrivate)
|
||||
encryptedContent = [KeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
||||
if (self.type & MPElementFeatureDevicePrivate)
|
||||
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
||||
else
|
||||
encryptedContent = self.contentObject;
|
||||
|
||||
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get].keyPhrase
|
||||
dataUsingEncoding:NSUTF8StringEncoding]
|
||||
usePadding:YES];
|
||||
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
||||
padding:YES];
|
||||
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (void)setContent:(id)content {
|
||||
|
||||
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get].keyPhrase
|
||||
dataUsingEncoding:NSUTF8StringEncoding]
|
||||
usePadding:YES];
|
||||
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
||||
padding:YES];
|
||||
|
||||
if (self.type == MPElementTypeStoredDevicePrivate) {
|
||||
[KeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
||||
if (self.type & MPElementFeatureDevicePrivate) {
|
||||
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
encryptedContent, (__bridge id)kSecValueData,
|
||||
#if TARGET_OS_IPHONE
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
||||
#endif
|
||||
nil]];
|
||||
self.contentObject = nil;
|
||||
} else
|
||||
self.contentObject = encryptedContent;
|
||||
}
|
||||
|
||||
- (NSString *)exportContent {
|
||||
|
||||
return [self.contentObject encodeBase64];
|
||||
}
|
||||
|
||||
- (void)importContent:(NSString *)content {
|
||||
|
||||
self.contentObject = [content decodeBase64];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
//
|
||||
// MPMainViewController.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPMainViewController.h"
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
#import "MPElementStoredEntity.h"
|
||||
#import "IASKAppSettingsViewController.h"
|
||||
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
|
||||
|
||||
@interface MPMainViewController (Private)
|
||||
|
||||
- (void)updateAnimated:(BOOL)animated;
|
||||
- (void)updateWasAnimated:(BOOL)animated;
|
||||
- (void)showContentTip:(NSString *)message withIcon:(UIImageView *)icon;
|
||||
- (void)showAlertWithTitle:(NSString *)title message:(NSString *)message;
|
||||
- (void)updateElement:(void (^)(void))updateElement;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPMainViewController
|
||||
@synthesize activeElement = _activeElement;
|
||||
@synthesize searchResultsController = _searchResultsController;
|
||||
@synthesize typeButton = _typeButton;
|
||||
@synthesize helpView = _helpView;
|
||||
@synthesize siteName = _siteName;
|
||||
@synthesize passwordCounter = _passwordCounter;
|
||||
@synthesize passwordIncrementer = _passwordIncrementer;
|
||||
@synthesize passwordEdit = _passwordEdit;
|
||||
@synthesize contentContainer = _contentContainer;
|
||||
@synthesize helpContainer = _helpContainer;
|
||||
@synthesize contentTipContainer = _copiedContainer;
|
||||
@synthesize alertContainer = _alertContainer;
|
||||
@synthesize alertTitle = _alertTitle;
|
||||
@synthesize alertBody = _alertBody;
|
||||
@synthesize contentTipBody = _contentTipBody;
|
||||
@synthesize contentTipEditIcon = _contentTipEditIcon;
|
||||
@synthesize searchTipContainer = _searchTip;
|
||||
@synthesize contentField = _contentField;
|
||||
|
||||
#pragma mark - View lifecycle
|
||||
|
||||
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
||||
|
||||
return [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad || interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown;
|
||||
}
|
||||
|
||||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
||||
|
||||
if ([[segue identifier] isEqualToString:@"MP_Main_ChooseType"])
|
||||
[[segue destinationViewController] setDelegate:self];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
self.searchTipContainer.hidden = NO;
|
||||
|
||||
if (!self.activeElement.name)
|
||||
[UIView animateWithDuration:animated? 0.2f: 0 animations:^{
|
||||
self.searchTipContainer.alpha = 1;
|
||||
}];
|
||||
|
||||
[self setHelpHidden:[[MPConfig get].helpHidden boolValue] animated:animated];
|
||||
[self updateAnimated:animated];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
self.searchTipContainer.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
|
||||
[super viewDidAppear:animated];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
|
||||
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"ui_background"]];
|
||||
|
||||
// Put the search tip on the window so it's above the nav bar.
|
||||
if (![self.searchTipContainer.superview isEqual:self.navigationController.navigationBar.superview]) {
|
||||
CGRect frameInWindow = [self.searchTipContainer.window convertRect:self.searchTipContainer.frame
|
||||
fromView:self.searchTipContainer.superview];
|
||||
[self.searchTipContainer removeFromSuperview];
|
||||
[self.navigationController.navigationBar.superview addSubview:self.searchTipContainer];
|
||||
self.searchTipContainer.frame = [self.searchTipContainer.window convertRect:frameInWindow
|
||||
toView:self.searchTipContainer.superview];
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(NSNotification *note) {
|
||||
if (![MPAppDelegate get].keyPhrase) {
|
||||
self.activeElement = nil;
|
||||
[self updateAnimated:NO];
|
||||
}
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(NSNotification *note) {
|
||||
if (![MPAppDelegate get].keyPhrase) {
|
||||
self.activeElement = nil;
|
||||
[self updateAnimated:NO];
|
||||
}
|
||||
}];
|
||||
|
||||
[self closeAlert];
|
||||
|
||||
[super viewDidLoad];
|
||||
}
|
||||
|
||||
- (void)viewDidUnload {
|
||||
|
||||
[self setContentField:nil];
|
||||
[self setTypeButton:nil];
|
||||
[self setSearchResultsController:nil];
|
||||
[self setHelpView:nil];
|
||||
[self setSiteName:nil];
|
||||
[self setPasswordCounter:nil];
|
||||
[self setPasswordIncrementer:nil];
|
||||
[self setPasswordEdit:nil];
|
||||
[self setContentContainer:nil];
|
||||
[self setHelpContainer:nil];
|
||||
[self setContentTipContainer:nil];
|
||||
[self setAlertContainer:nil];
|
||||
[self setAlertTitle:nil];
|
||||
[self setAlertBody:nil];
|
||||
[self setContentTipBody:nil];
|
||||
[self setContentTipEditIcon:nil];
|
||||
[self setSearchTipContainer:nil];
|
||||
[super viewDidUnload];
|
||||
}
|
||||
|
||||
- (void)updateAnimated:(BOOL)animated {
|
||||
|
||||
[[MPAppDelegate get] saveContext];
|
||||
|
||||
if (animated)
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
[self updateWasAnimated:YES];
|
||||
}];
|
||||
else
|
||||
[self updateWasAnimated:NO];
|
||||
}
|
||||
|
||||
- (void)updateWasAnimated:(BOOL)animated {
|
||||
|
||||
[self setHelpChapter:self.activeElement? @"2": @"1"];
|
||||
self.siteName.text = self.activeElement.name;
|
||||
|
||||
self.passwordCounter.alpha = self.activeElement.type & MPElementTypeClassCalculated? 0.5f: 0;
|
||||
self.passwordIncrementer.alpha = self.activeElement.type & MPElementTypeClassCalculated? 0.5f: 0;
|
||||
self.passwordEdit.alpha = self.activeElement.type & MPElementTypeClassStored? 0.5f: 0;
|
||||
|
||||
[self.typeButton setTitle:NSStringFromMPElementType(self.activeElement.type)
|
||||
forState:UIControlStateNormal];
|
||||
self.typeButton.alpha = NSStringFromMPElementType(self.activeElement.type).length? 1: 0;
|
||||
|
||||
self.contentField.enabled = NO;
|
||||
|
||||
if ([self.activeElement isKindOfClass:[MPElementGeneratedEntity class]])
|
||||
self.passwordCounter.text = [NSString stringWithFormat:@"%d", ((MPElementGeneratedEntity *) self.activeElement).counter];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
||||
NSString *description = self.activeElement.description;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.contentField.text = description;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)isHelpVisible {
|
||||
|
||||
return self.helpContainer.frame.origin.y < 400;
|
||||
}
|
||||
|
||||
- (void)toggleHelpAnimated:(BOOL)animated {
|
||||
|
||||
[self setHelpHidden:[self isHelpVisible] animated:animated];
|
||||
}
|
||||
|
||||
- (void)setHelpHidden:(BOOL)hidden animated:(BOOL)animated {
|
||||
|
||||
[UIView animateWithDuration:animated? 0.3f: 0 animations:^{
|
||||
|
||||
if (hidden) {
|
||||
self.contentContainer.frame = CGRectSetHeight(self.contentContainer.frame, 373);
|
||||
self.helpContainer.frame = CGRectSetY(self.helpContainer.frame, 415);
|
||||
[MPConfig get].helpHidden = [NSNumber numberWithBool:YES];
|
||||
} else {
|
||||
self.contentContainer.frame = CGRectSetHeight(self.contentContainer.frame, 175);
|
||||
self.helpContainer.frame = CGRectSetY(self.helpContainer.frame, 216);
|
||||
[MPConfig get].helpHidden = [NSNumber numberWithBool:NO];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setHelpChapter:(NSString *)chapter {
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointHelpChapter, chapter]];
|
||||
#endif
|
||||
|
||||
[self.helpView loadRequest:
|
||||
[NSURLRequest requestWithURL:
|
||||
[NSURL URLWithString:[NSString stringWithFormat:@"#%@", chapter] relativeToURL:
|
||||
[[NSBundle mainBundle] URLForResource:@"help" withExtension:@"html"]]]];
|
||||
[self.helpView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"setClass('%@');",
|
||||
ClassNameFromMPElementType(self.activeElement.type)]];
|
||||
}
|
||||
|
||||
- (void)showContentTip:(NSString *)message withIcon:(UIImageView *)icon {
|
||||
|
||||
self.contentTipBody.text = message;
|
||||
|
||||
icon.hidden = NO;
|
||||
[UIView animateWithDuration:0.2f animations:^{
|
||||
self.contentTipContainer.alpha = 1;
|
||||
} completion:^(BOOL finished) {
|
||||
if (!finished) {
|
||||
icon.hidden = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 5.0f * NSEC_PER_SEC);
|
||||
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
|
||||
[UIView animateWithDuration:0.2f animations:^{
|
||||
self.contentTipContainer.alpha = 0;
|
||||
} completion:^(BOOL finished) {
|
||||
icon.hidden = YES;
|
||||
}];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)showAlertWithTitle:(NSString *)title message:(NSString *)message {
|
||||
|
||||
self.alertTitle.text = title;
|
||||
NSRange scrollRange = NSMakeRange(self.alertBody.text.length, message.length);
|
||||
if ([self.alertBody.text length])
|
||||
self.alertBody.text = [NSString stringWithFormat:@"%@\n\n---\n\n%@", self.alertBody.text, message];
|
||||
else
|
||||
self.alertBody.text = message;
|
||||
[self.alertBody scrollRangeToVisible:scrollRange];
|
||||
|
||||
[UIView animateWithDuration:0.2f animations:^{
|
||||
self.alertContainer.alpha = 1;
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Protocols
|
||||
|
||||
- (IBAction)copyContent {
|
||||
|
||||
if (!self.activeElement)
|
||||
return;
|
||||
|
||||
[[UIPasteboard generalPasteboard] setValue:self.activeElement.content
|
||||
forPasteboardType:(id)kUTTypeUTF8PlainText];
|
||||
|
||||
[self showContentTip:@"Copied!" withIcon:nil];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointCopyToPasteboard];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (IBAction)incrementPasswordCounter {
|
||||
|
||||
if (![self.activeElement isKindOfClass:[MPElementGeneratedEntity class]])
|
||||
// Not of a type that supports a password counter;
|
||||
return;
|
||||
|
||||
[self updateElement:^{
|
||||
++((MPElementGeneratedEntity *) self.activeElement).counter;
|
||||
}];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointIncrementPasswordCounter];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)updateElement:(void (^)(void))updateElement {
|
||||
|
||||
// Update password counter.
|
||||
NSString *oldPassword = self.activeElement.description;
|
||||
updateElement();
|
||||
NSString *newPassword = self.activeElement.description;
|
||||
[self updateAnimated:YES];
|
||||
|
||||
// Show new and old password.
|
||||
if (oldPassword && ![oldPassword isEqualToString:newPassword])
|
||||
[self showAlertWithTitle:@"Password Changed!" message:l(@"The password for %@ has changed.\n\n"
|
||||
@"Don't forget to update the site with your new password! "
|
||||
@"Your old password was:\n"
|
||||
@"%@", self.activeElement.name, oldPassword)];
|
||||
}
|
||||
|
||||
- (IBAction)editPassword {
|
||||
|
||||
if (self.activeElement.type & MPElementTypeClassStored) {
|
||||
self.contentField.enabled = YES;
|
||||
[self.contentField becomeFirstResponder];
|
||||
}
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointEditPassword];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (IBAction)closeAlert {
|
||||
|
||||
[UIView animateWithDuration:0.3f animations:^{
|
||||
self.alertContainer.alpha = 0;
|
||||
} completion:^(BOOL finished) {
|
||||
self.alertBody.text = nil;
|
||||
}];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointCloseAlert];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (IBAction)action:(id)sender {
|
||||
|
||||
[SheetViewController showSheetWithTitle:nil message:nil viewStyle:UIActionSheetStyleAutomatic
|
||||
tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [sheet cancelButtonIndex])
|
||||
return;
|
||||
|
||||
switch (buttonIndex - [sheet firstOtherButtonIndex]) {
|
||||
case 0:
|
||||
[self toggleHelpAnimated:YES];
|
||||
break;
|
||||
case 1:
|
||||
[self setHelpChapter:@"faq"];
|
||||
[self setHelpHidden:NO animated:YES];
|
||||
break;
|
||||
case 2:
|
||||
[[MPAppDelegate get] showGuide];
|
||||
break;
|
||||
case 3: {
|
||||
IASKAppSettingsViewController *settingsVC = [IASKAppSettingsViewController new];
|
||||
settingsVC.delegate = self;
|
||||
[self.navigationController pushViewController:settingsVC animated:YES];
|
||||
break;
|
||||
}
|
||||
#ifndef PRODUCTION
|
||||
case 4:
|
||||
[TestFlight openFeedbackView];
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointAction];
|
||||
#endif
|
||||
} cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil
|
||||
otherTitles:
|
||||
[self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings",
|
||||
#ifndef PRODUCTION
|
||||
@"Feedback",
|
||||
#endif
|
||||
nil];
|
||||
}
|
||||
|
||||
- (void)didSelectType:(MPElementType)type {
|
||||
|
||||
[self updateElement:^{
|
||||
// Update password type.
|
||||
if (ClassFromMPElementType(type) != ClassFromMPElementType(self.activeElement.type))
|
||||
// Type requires a different class of element. Recreate the element.
|
||||
[[MPAppDelegate managedObjectContext] performBlockAndWait:^{
|
||||
MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
|
||||
inManagedObjectContext:[MPAppDelegate managedObjectContext]];
|
||||
newElement.name = self.activeElement.name;
|
||||
newElement.mpHashHex = self.activeElement.mpHashHex;
|
||||
newElement.uses = self.activeElement.uses;
|
||||
newElement.lastUsed = self.activeElement.lastUsed;
|
||||
|
||||
[[MPAppDelegate managedObjectContext] deleteObject:self.activeElement];
|
||||
self.activeElement = newElement;
|
||||
}];
|
||||
|
||||
self.activeElement.type = type;
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSelectType, NSStringFromMPElementType(type)]];
|
||||
#endif
|
||||
|
||||
if (type & MPElementTypeClassStored && ![self.activeElement.description length])
|
||||
[self showContentTip:@"Tap to set a password." withIcon:self.contentTipEditIcon];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)didSelectElement:(MPElementEntity *)element {
|
||||
|
||||
self.activeElement = element;
|
||||
[self.activeElement use];
|
||||
|
||||
[self.searchDisplayController setActive:NO animated:YES];
|
||||
self.searchDisplayController.searchBar.text = self.activeElement.name;
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSelectElement];
|
||||
#endif
|
||||
|
||||
[self updateAnimated:YES];
|
||||
}
|
||||
|
||||
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointCancelSearch];
|
||||
#endif
|
||||
|
||||
[self updateAnimated:YES];
|
||||
}
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||
|
||||
if (textField == self.contentField)
|
||||
[self.contentField resignFirstResponder];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)textFieldDidEndEditing:(UITextField *)textField {
|
||||
|
||||
if (textField == self.contentField) {
|
||||
self.contentField.enabled = NO;
|
||||
if (![self.activeElement isKindOfClass:[MPElementStoredEntity class]])
|
||||
// Not of a type whose content can be edited.
|
||||
return;
|
||||
|
||||
if ([((MPElementStoredEntity *) self.activeElement).content isEqual:self.contentField.text])
|
||||
// Content hasn't changed.
|
||||
return;
|
||||
|
||||
[self updateElement:^{
|
||||
((MPElementStoredEntity *) self.activeElement).content = self.contentField.text;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
|
||||
navigationType:(UIWebViewNavigationType)navigationType {
|
||||
|
||||
if (navigationType == UIWebViewNavigationTypeLinkClicked) {
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointExternalLink];
|
||||
#endif
|
||||
|
||||
[[UIApplication sharedApplication] openURL:[request URL]];
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)settingsViewControllerDidEnd:(IASKAppSettingsViewController *)sender {
|
||||
|
||||
while ([self.navigationController.viewControllers containsObject:sender])
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,263 +0,0 @@
|
||||
//
|
||||
// MPSearchDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 04/01/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPSearchDelegate.h"
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
|
||||
@interface MPSearchDelegate (Private)
|
||||
|
||||
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath;
|
||||
- (void)update;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPSearchDelegate
|
||||
@synthesize fetchedResultsController;
|
||||
@synthesize delegate;
|
||||
@synthesize searchDisplayController;
|
||||
@synthesize searchTipContainer;
|
||||
|
||||
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
|
||||
|
||||
self.searchDisplayController.searchBar.text = @"";
|
||||
self.searchDisplayController.searchBar.prompt = @"Enter the site's domain name (eg. apple.com):";
|
||||
|
||||
[UIView animateWithDuration:0.2f animations:^{
|
||||
self.searchTipContainer.alpha = 0;
|
||||
}];
|
||||
|
||||
[self update];
|
||||
}
|
||||
|
||||
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller {
|
||||
|
||||
self.searchDisplayController.searchBar.prompt = nil;
|
||||
}
|
||||
|
||||
- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView {
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIScreenModeDidChangeNotification
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
NSError *error;
|
||||
if (![self.fetchedResultsController performFetch:&error])
|
||||
err(@"Couldn't fetch elements: %@", error);
|
||||
}];
|
||||
|
||||
tableView.backgroundColor = [UIColor blackColor];
|
||||
tableView.rowHeight = 34.0f;
|
||||
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
}
|
||||
|
||||
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
|
||||
|
||||
[self update];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)update {
|
||||
|
||||
NSString *query = self.searchDisplayController.searchBar.text;
|
||||
if (!query)
|
||||
query = @"";
|
||||
|
||||
NSFetchRequest *fetchRequest = [[MPAppDelegate get].managedObjectModel
|
||||
fetchRequestFromTemplateWithName:@"MPSearchElement"
|
||||
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
query, @"query",
|
||||
[MPAppDelegate get].keyPhraseHashHex, @"mpHashHex",
|
||||
nil]];
|
||||
[fetchRequest setSortDescriptors:
|
||||
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
|
||||
|
||||
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
|
||||
managedObjectContext:[MPAppDelegate managedObjectContext]
|
||||
sectionNameKeyPath:nil cacheName:nil];
|
||||
self.fetchedResultsController.delegate = self;
|
||||
|
||||
NSError *error;
|
||||
if (![self.fetchedResultsController performFetch:&error])
|
||||
err(@"Couldn't fetch elements: %@", error);
|
||||
}
|
||||
|
||||
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
|
||||
|
||||
[self.searchDisplayController.searchResultsTableView beginUpdates];
|
||||
}
|
||||
|
||||
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
|
||||
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
|
||||
|
||||
UITableView *tableView = self.searchDisplayController.searchResultsTableView;
|
||||
switch(type) {
|
||||
|
||||
case NSFetchedResultsChangeInsert:
|
||||
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
|
||||
break;
|
||||
|
||||
case NSFetchedResultsChangeDelete:
|
||||
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
|
||||
break;
|
||||
|
||||
case NSFetchedResultsChangeUpdate:
|
||||
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
|
||||
inTableView:tableView
|
||||
atIndexPath:indexPath];
|
||||
break;
|
||||
|
||||
case NSFetchedResultsChangeMove:
|
||||
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
|
||||
withRowAnimation:UITableViewRowAnimationFade];
|
||||
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
|
||||
withRowAnimation:UITableViewRowAnimationFade];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
|
||||
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
|
||||
|
||||
UITableView *tableView = self.searchDisplayController.searchResultsTableView;
|
||||
switch(type) {
|
||||
|
||||
case NSFetchedResultsChangeInsert:
|
||||
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
|
||||
withRowAnimation:UITableViewRowAnimationFade];
|
||||
break;
|
||||
|
||||
case NSFetchedResultsChangeDelete:
|
||||
[tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
|
||||
withRowAnimation:UITableViewRowAnimationFade];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
|
||||
|
||||
[self.searchDisplayController.searchResultsTableView endUpdates];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
|
||||
return [[self.fetchedResultsController sections] count] + 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
|
||||
if (section == [self numberOfSectionsInTableView:tableView] - 1)
|
||||
return 1;
|
||||
|
||||
return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MPElementSearch"];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"MPElementSearch"];
|
||||
UIImageView *backgroundImageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"ui_list_middle"] resizableImageWithCapInsets:UIEdgeInsetsMake(5, 5, 5, 5)]];
|
||||
backgroundImageView.frame = CGRectMake(-5, 0, 330, 34);
|
||||
UIView *backgroundView = [[UIView alloc] initWithFrame:cell.frame];
|
||||
[backgroundView addSubview:backgroundImageView];
|
||||
|
||||
cell.backgroundView = backgroundView;
|
||||
cell.textLabel.backgroundColor = [UIColor clearColor];
|
||||
cell.textLabel.textColor = [UIColor whiteColor];
|
||||
cell.detailTextLabel.backgroundColor = [UIColor clearColor];
|
||||
cell.detailTextLabel.textColor = [UIColor lightGrayColor];
|
||||
}
|
||||
|
||||
[self configureCell:cell inTableView:tableView atIndexPath:indexPath];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {
|
||||
|
||||
if (indexPath.section < [[self.fetchedResultsController sections] count]) {
|
||||
MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath];
|
||||
|
||||
cell.textLabel.text = element.name;
|
||||
cell.detailTextLabel.text = [NSString stringWithFormat:@"%d", element.uses];
|
||||
} else {
|
||||
// "New" section
|
||||
cell.textLabel.text = self.searchDisplayController.searchBar.text;
|
||||
cell.detailTextLabel.text = @"New";
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
||||
if (indexPath.section < [[self.fetchedResultsController sections] count])
|
||||
[self.delegate didSelectElement:[self.fetchedResultsController objectAtIndexPath:indexPath]];
|
||||
|
||||
else {
|
||||
// "New" section.
|
||||
NSString *siteName = self.searchDisplayController.searchBar.text;
|
||||
[AlertViewController showAlertWithTitle:@"New Site"
|
||||
message:l(@"Do you want to create a new site named:\n%@", siteName)
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
return;
|
||||
|
||||
[self.fetchedResultsController.managedObjectContext performBlock:^{
|
||||
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
|
||||
inManagedObjectContext:self.fetchedResultsController.managedObjectContext];
|
||||
assert([element isKindOfClass:ClassFromMPElementType(element.type)]);
|
||||
|
||||
element.name = siteName;
|
||||
element.mpHashHex = [MPAppDelegate get].keyPhraseHashHex;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.delegate didSelectElement:element];
|
||||
});
|
||||
}];
|
||||
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonYes, nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
|
||||
if (--section == -1)
|
||||
return @"";
|
||||
|
||||
return [[[self.fetchedResultsController sections] objectAtIndex:section] name];
|
||||
}
|
||||
|
||||
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
|
||||
|
||||
return [self.fetchedResultsController sectionIndexTitles];
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
|
||||
|
||||
return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
||||
if (indexPath.section == [[self.fetchedResultsController sections] count])
|
||||
// "New" section.
|
||||
return;
|
||||
|
||||
if (editingStyle == UITableViewCellEditingStyleDelete)
|
||||
[self.fetchedResultsController.managedObjectContext performBlock:^{
|
||||
MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath];
|
||||
[self.fetchedResultsController.managedObjectContext deleteObject:element];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointDeleteElement];
|
||||
#endif
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -17,25 +17,34 @@ typedef enum {
|
||||
} MPElementContentType;
|
||||
|
||||
typedef enum {
|
||||
MPElementTypeClassCalculated = 2 << 7,
|
||||
MPElementTypeClassStored = 2 << 8,
|
||||
/** Generate the password. */
|
||||
MPElementTypeClassGenerated = 1 << 4,
|
||||
/** Store the password. */
|
||||
MPElementTypeClassStored = 1 << 5,
|
||||
} MPElementTypeClass;
|
||||
|
||||
typedef enum {
|
||||
MPElementTypeCalculatedLong = MPElementTypeClassCalculated | 0x01,
|
||||
MPElementTypeCalculatedMedium = MPElementTypeClassCalculated | 0x02,
|
||||
MPElementTypeCalculatedShort = MPElementTypeClassCalculated | 0x03,
|
||||
MPElementTypeCalculatedBasic = MPElementTypeClassCalculated | 0x04,
|
||||
MPElementTypeCalculatedPIN = MPElementTypeClassCalculated | 0x05,
|
||||
/** Export the key-protected content data. */
|
||||
MPElementFeatureExportContent = 1 << 10,
|
||||
/** Never export content. */
|
||||
MPElementFeatureDevicePrivate = 1 << 11,
|
||||
} MPElementFeature;
|
||||
|
||||
MPElementTypeStoredPersonal = MPElementTypeClassStored | 0x01,
|
||||
MPElementTypeStoredDevicePrivate = MPElementTypeClassStored | 0x02,
|
||||
typedef enum {
|
||||
MPElementTypeGeneratedLong = 0x0 | MPElementTypeClassGenerated | 0x0,
|
||||
MPElementTypeGeneratedMedium = 0x1 | MPElementTypeClassGenerated | 0x0,
|
||||
MPElementTypeGeneratedShort = 0x2 | MPElementTypeClassGenerated | 0x0,
|
||||
MPElementTypeGeneratedBasic = 0x3 | MPElementTypeClassGenerated | 0x0,
|
||||
MPElementTypeGeneratedPIN = 0x4 | MPElementTypeClassGenerated | 0x0,
|
||||
|
||||
MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent,
|
||||
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate,
|
||||
} MPElementType;
|
||||
|
||||
#ifndef PRODUCTION
|
||||
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
||||
#define MPTestFlightCheckpointHelpChapter @"MPTestFlightCheckpointHelpChapter_%@"
|
||||
#define MPTestFlightCheckpointCopyToPasteboard @"MPTestFlightCheckpointCopyToPasteboard"
|
||||
#define MPTestFlightCheckpointResetPasswordCounter @"MPTestFlightCheckpointResetPasswordCounter"
|
||||
#define MPTestFlightCheckpointIncrementPasswordCounter @"MPTestFlightCheckpointIncrementPasswordCounter"
|
||||
#define MPTestFlightCheckpointEditPassword @"MPTestFlightCheckpointEditPassword"
|
||||
#define MPTestFlightCheckpointCloseAlert @"MPTestFlightCheckpointCloseAlert"
|
||||
@@ -49,15 +58,28 @@ typedef enum {
|
||||
#define MPTestFlightCheckpointDeactivated @"MPTestFlightCheckpointDeactivated"
|
||||
#define MPTestFlightCheckpointTerminated @"MPTestFlightCheckpointTerminated"
|
||||
#define MPTestFlightCheckpointShowGuide @"MPTestFlightCheckpointShowGuide"
|
||||
#define MPTestFlightCheckpointMPForgotten @"MPTestFlightCheckpointMPForgotten"
|
||||
#define MPTestFlightCheckpointMPChanged @"MPTestFlightCheckpointMPChanged"
|
||||
#define MPTestFlightCheckpointMPUnstored @"MPTestFlightCheckpointMPUnstored"
|
||||
#define MPTestFlightCheckpointMPMismatch @"MPTestFlightCheckpointMPMismatch"
|
||||
#define MPTestFlightCheckpointMPAsked @"MPTestFlightCheckpointMPAsked"
|
||||
#define MPTestFlightCheckpointStoreIncompatible @"MPTestFlightCheckpointStoreIncompatible"
|
||||
#define MPTestFlightCheckpointSetKeyphraseLength @"MPTestFlightCheckpointSetKeyphraseLength_%d"
|
||||
#endif
|
||||
#define MPTestFlightCheckpointMPEntered @"MPTestFlightCheckpointMPEntered"
|
||||
#define MPTestFlightCheckpointLocalStoreIncompatible @"MPTestFlightCheckpointLocalStoreIncompatible"
|
||||
#define MPTestFlightCheckpointCloudStoreIncompatible @"MPTestFlightCheckpointCloudStoreIncompatible"
|
||||
#define MPTestFlightCheckpointSetKey @"MPTestFlightCheckpointSetKey"
|
||||
#define MPTestFlightCheckpointCloudEnabled @"MPTestFlightCheckpointCloudEnabled"
|
||||
#define MPTestFlightCheckpointCloudDisabled @"MPTestFlightCheckpointCloudDisabled"
|
||||
#define MPTestFlightCheckpointSitesImported @"MPTestFlightCheckpointSitesImported"
|
||||
#define MPTestFlightCheckpointSitesExported @"MPTestFlightCheckpointSitesExported"
|
||||
|
||||
#define MPNotificationStoreUpdated @"MPNotificationStoreUpdated"
|
||||
#define MPNotificationKeySet @"MPNotificationKeySet"
|
||||
#define MPNotificationKeyUnset @"MPNotificationKeyUnset"
|
||||
#define MPNotificationKeyForgotten @"MPNotificationKeyForgotten"
|
||||
|
||||
NSData *keyForPassword(NSString *password);
|
||||
NSData *keyIDForPassword(NSString *password);
|
||||
NSData *keyIDForKey(NSData *key);
|
||||
NSString *NSStringFromMPElementType(MPElementType type);
|
||||
NSString *ClassNameFromMPElementType(MPElementType type);
|
||||
Class ClassFromMPElementType(MPElementType type);
|
||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSString *keyPhrase, int counter);
|
||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int32_t counter);
|
||||
|
||||
@@ -11,25 +11,48 @@
|
||||
#import "MPElementStoredEntity.h"
|
||||
|
||||
|
||||
#define MP_salt nil
|
||||
#define MP_N 16384
|
||||
#define MP_r 8
|
||||
#define MP_p 1
|
||||
#define MP_dkLen 64
|
||||
#define MP_hash PearlDigestSHA256
|
||||
|
||||
NSData *keyForPassword(NSString *password) {
|
||||
|
||||
NSData *key = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding]
|
||||
usingSalt:MP_salt N:MP_N r:MP_r p:MP_p];
|
||||
|
||||
trc(@"Password: %@ derives to key ID: %@", password, [keyIDForKey(key) encodeHex]);
|
||||
return key;
|
||||
}
|
||||
NSData *keyIDForPassword(NSString *password) {
|
||||
|
||||
return keyIDForKey(keyForPassword(password));
|
||||
}
|
||||
NSData *keyIDForKey(NSData *key) {
|
||||
|
||||
return [key hashWith:MP_hash];
|
||||
}
|
||||
NSString *NSStringFromMPElementType(MPElementType type) {
|
||||
|
||||
if (!type)
|
||||
return nil;
|
||||
|
||||
switch (type) {
|
||||
case MPElementTypeCalculatedLong:
|
||||
case MPElementTypeGeneratedLong:
|
||||
return @"Long Password";
|
||||
|
||||
case MPElementTypeCalculatedMedium:
|
||||
case MPElementTypeGeneratedMedium:
|
||||
return @"Medium Password";
|
||||
|
||||
case MPElementTypeCalculatedShort:
|
||||
case MPElementTypeGeneratedShort:
|
||||
return @"Short Password";
|
||||
|
||||
case MPElementTypeCalculatedBasic:
|
||||
case MPElementTypeGeneratedBasic:
|
||||
return @"Basic Password";
|
||||
|
||||
case MPElementTypeCalculatedPIN:
|
||||
case MPElementTypeGeneratedPIN:
|
||||
return @"PIN";
|
||||
|
||||
case MPElementTypeStoredPersonal:
|
||||
@@ -49,19 +72,19 @@ Class ClassFromMPElementType(MPElementType type) {
|
||||
return nil;
|
||||
|
||||
switch (type) {
|
||||
case MPElementTypeCalculatedLong:
|
||||
case MPElementTypeGeneratedLong:
|
||||
return [MPElementGeneratedEntity class];
|
||||
|
||||
case MPElementTypeCalculatedMedium:
|
||||
case MPElementTypeGeneratedMedium:
|
||||
return [MPElementGeneratedEntity class];
|
||||
|
||||
case MPElementTypeCalculatedShort:
|
||||
case MPElementTypeGeneratedShort:
|
||||
return [MPElementGeneratedEntity class];
|
||||
|
||||
case MPElementTypeCalculatedBasic:
|
||||
case MPElementTypeGeneratedBasic:
|
||||
return [MPElementGeneratedEntity class];
|
||||
|
||||
case MPElementTypeCalculatedPIN:
|
||||
case MPElementTypeGeneratedPIN:
|
||||
return [MPElementGeneratedEntity class];
|
||||
|
||||
case MPElementTypeStoredPersonal:
|
||||
@@ -81,34 +104,59 @@ NSString *ClassNameFromMPElementType(MPElementType type) {
|
||||
}
|
||||
|
||||
static NSDictionary *MPTypes_ciphers = nil;
|
||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSString *keyPhrase, int counter) {
|
||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int32_t counter) {
|
||||
|
||||
assert(type & MPElementTypeClassCalculated);
|
||||
if (!(type & MPElementTypeClassGenerated)) {
|
||||
err(@"Incorrect type (is not MPElementTypeClassGenerated): %d, for: %@", type, name);
|
||||
return nil;
|
||||
}
|
||||
if (!name) {
|
||||
err(@"Missing name.");
|
||||
return nil;
|
||||
}
|
||||
if (!key) {
|
||||
err(@"Key not set.");
|
||||
return nil;
|
||||
}
|
||||
uint32_t salt = (unsigned)counter;
|
||||
if (!counter)
|
||||
// Counter unset, go into OTP mode.
|
||||
// Get the UNIX timestamp of the start of the interval of 5 minutes that the current time is in.
|
||||
salt = ((uint32_t)([[NSDate date] timeIntervalSince1970] / 300)) * 300;
|
||||
|
||||
if (MPTypes_ciphers == nil)
|
||||
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
|
||||
withExtension:@"plist"]];
|
||||
|
||||
// Determine the hash whose bytes will be used for calculating a password: md4(name-keyPhrase)
|
||||
assert(name && keyPhrase);
|
||||
NSData *keyHash = [[NSString stringWithFormat:@"%@-%@-%d", name, keyPhrase, counter] hashWith:PearlDigestSHA1];
|
||||
const char *keyBytes = keyHash.bytes;
|
||||
// Determine the seed whose bytes will be used for calculating a password: sha1(name . '\0' . key . '\0' . salt)
|
||||
uint32_t nsalt = htonl(salt);
|
||||
trc(@"seed from: sha1(%@, %@, %u)", name, key, nsalt);
|
||||
NSData *seed = [[NSData dataByConcatenatingWithDelimitor:'\0' datas:
|
||||
[name dataUsingEncoding:NSUTF8StringEncoding],
|
||||
key,
|
||||
[NSData dataWithBytes:&nsalt length:sizeof(nsalt)],
|
||||
nil] hashWith:PearlDigestSHA1];
|
||||
trc(@"seed is: %@", seed);
|
||||
const char *seedBytes = seed.bytes;
|
||||
|
||||
// Determine the cipher from the first hash byte.
|
||||
assert([keyHash length]);
|
||||
// Determine the cipher from the first seed byte.
|
||||
assert([seed length]);
|
||||
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:ClassNameFromMPElementType(type)]
|
||||
valueForKey:NSStringFromMPElementType(type)];
|
||||
NSString *cipher = [typeCiphers objectAtIndex:keyBytes[0] % [typeCiphers count]];
|
||||
NSString *cipher = [typeCiphers objectAtIndex:htons(seedBytes[0]) % [typeCiphers count]];
|
||||
trc(@"type %d, ciphers: %@, selected: %@", type, typeCiphers, cipher);
|
||||
|
||||
// Encode the content, character by character, using subsequent hash bytes and the cipher.
|
||||
assert([keyHash length] >= [cipher length] + 1);
|
||||
// Encode the content, character by character, using subsequent seed bytes and the cipher.
|
||||
assert([seed length] >= [cipher length] + 1);
|
||||
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
|
||||
for (NSUInteger c = 0; c < [cipher length]; ++c) {
|
||||
const char keyByte = keyBytes[c + 1];
|
||||
uint16_t keyByte = htons(seedBytes[c + 1]);
|
||||
NSString *cipherClass = [cipher substringWithRange:NSMakeRange(c, 1)];
|
||||
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
|
||||
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange(keyByte % [cipherClassCharacters length], 1)];
|
||||
|
||||
[content appendString:[cipherClassCharacters substringWithRange:NSMakeRange(keyByte % [cipherClassCharacters length], 1)]];
|
||||
trc(@"class %@ has characters: %@, selected: %@", cipherClass, cipherClassCharacters, character);
|
||||
[content appendString:character];
|
||||
}
|
||||
|
||||
return content;
|
||||
|
||||
29
MasterPassword/Mac/MPAppDelegate.h
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// MPAppDelegate.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 04/03/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
#import "MPPasswordWindowController.h"
|
||||
|
||||
@interface MPAppDelegate : MPAppDelegate_Shared <NSApplicationDelegate>
|
||||
|
||||
@property (strong) NSStatusItem *statusItem;
|
||||
@property (strong) MPPasswordWindowController *passwordWindow;
|
||||
@property (weak) IBOutlet NSMenuItem *lockItem;
|
||||
@property (weak) IBOutlet NSMenuItem *showItem;
|
||||
@property (strong) IBOutlet NSMenu *statusMenu;
|
||||
@property (weak) IBOutlet NSMenuItem *useICloudItem;
|
||||
@property (weak) IBOutlet NSMenuItem *rememberPasswordItem;
|
||||
@property (weak) IBOutlet NSMenuItem *savePasswordItem;
|
||||
|
||||
+ (MPAppDelegate *)get;
|
||||
|
||||
- (IBAction)activate:(id)sender;
|
||||
- (IBAction)togglePreference:(NSMenuItem *)sender;
|
||||
|
||||
@end
|
||||
221
MasterPassword/Mac/MPAppDelegate.m
Normal file
@@ -0,0 +1,221 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 04/03/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
#import "MPAppDelegate_Store.h"
|
||||
#import "MPConfig.h"
|
||||
#import "MPElementEntity.h"
|
||||
#import <Carbon/Carbon.h>
|
||||
|
||||
|
||||
@implementation MPAppDelegate
|
||||
@synthesize statusItem;
|
||||
@synthesize lockItem;
|
||||
@synthesize showItem;
|
||||
@synthesize statusMenu;
|
||||
@synthesize useICloudItem;
|
||||
@synthesize rememberPasswordItem;
|
||||
@synthesize savePasswordItem;
|
||||
@synthesize passwordWindow;
|
||||
|
||||
@synthesize key;
|
||||
@synthesize keyID;
|
||||
|
||||
#pragma GCC diagnostic ignored "-Wfour-char-constants"
|
||||
static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 };
|
||||
|
||||
+ (void)initialize {
|
||||
|
||||
[MPConfig get];
|
||||
|
||||
#ifdef DEBUG
|
||||
[PearlLogger get].autoprintLevel = PearlLogLevelTrace;
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (MPAppDelegate *)get {
|
||||
|
||||
return (MPAppDelegate *)[super get];
|
||||
}
|
||||
|
||||
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 MPAppDelegate *)userData) activate:nil];
|
||||
return noErr;
|
||||
}
|
||||
|
||||
return eventNotHandledErr;
|
||||
}
|
||||
|
||||
- (void)showMenu {
|
||||
|
||||
self.rememberPasswordItem.state = [[MPConfig get].rememberKey boolValue]? NSOnState: NSOffState;
|
||||
self.savePasswordItem.state = [[MPConfig get].saveKey boolValue]? NSOnState: NSOffState;
|
||||
self.showItem.enabled = ![self.passwordWindow.window isVisible];
|
||||
|
||||
[self.statusItem popUpStatusItemMenu:self.statusMenu];
|
||||
}
|
||||
|
||||
- (IBAction)activate:(id)sender {
|
||||
|
||||
if ([[NSApplication sharedApplication] isActive])
|
||||
[self applicationDidBecomeActive:nil];
|
||||
else
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||
}
|
||||
|
||||
- (IBAction)togglePreference:(NSMenuItem *)sender {
|
||||
|
||||
if (sender == useICloudItem)
|
||||
[self.storeManager useiCloudStore:sender.state == NSOffState alertUser:YES];
|
||||
if (sender == rememberPasswordItem)
|
||||
[MPConfig get].rememberKey = [NSNumber numberWithBool:![[MPConfig get].rememberKey boolValue]];
|
||||
if (sender == savePasswordItem)
|
||||
[MPConfig get].saveKey = [NSNumber numberWithBool:![[MPConfig get].saveKey boolValue]];
|
||||
}
|
||||
|
||||
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
|
||||
|
||||
if (configKey == @selector(rememberKey))
|
||||
self.rememberPasswordItem.state = [[MPConfig get].rememberKey boolValue]? NSOnState: NSOffState;
|
||||
if (configKey == @selector(saveKey))
|
||||
self.savePasswordItem.state = [[MPConfig get].saveKey boolValue]? NSOnState: NSOffState;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
||||
|
||||
if ([keyPath isEqualToString:@"key"]) {
|
||||
if (self.key)
|
||||
[self.lockItem setEnabled:YES];
|
||||
else {
|
||||
[self.lockItem setEnabled:NO];
|
||||
[self.passwordWindow close];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window {
|
||||
|
||||
return [[self managedObjectContext] undoManager];
|
||||
}
|
||||
|
||||
#pragma mark - NSApplicationDelegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
|
||||
// Setup delegates and listeners.
|
||||
[MPConfig get].delegate = self;
|
||||
[self addObserver:self forKeyPath:@"key" options:0 context:nil];
|
||||
|
||||
// Initially, use iCloud.
|
||||
if ([[MPConfig get].firstRun boolValue])
|
||||
[[self storeManager] useiCloudStore:YES alertUser:YES];
|
||||
|
||||
// Status item.
|
||||
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
||||
self.statusItem.title = @"•••";
|
||||
self.statusItem.highlightMode = YES;
|
||||
self.statusItem.target = self;
|
||||
self.statusItem.action = @selector(showMenu);
|
||||
|
||||
// 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: %d", status);
|
||||
status = RegisterEventHotKey(35 /* p */, controlKey + cmdKey, MPShowHotKey, GetApplicationEventTarget(), 0, &hotKeyRef);
|
||||
if(status != noErr)
|
||||
err(@"Error registering hotkey: %d", status);
|
||||
}
|
||||
|
||||
- (void)applicationWillBecomeActive:(NSNotification *)notification {
|
||||
|
||||
if (!self.passwordWindow)
|
||||
self.passwordWindow = [[MPPasswordWindowController alloc] initWithWindowNibName:@"MPPasswordWindowController"];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||
|
||||
static BOOL firstTime = YES;
|
||||
if (firstTime)
|
||||
firstTime = NO;
|
||||
else
|
||||
[self.passwordWindow showWindow:self];
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
|
||||
if (![[MPConfig get].rememberKey boolValue])
|
||||
self.key = nil;
|
||||
}
|
||||
|
||||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
|
||||
{
|
||||
// Save changes in the application's managed object context before the application terminates.
|
||||
|
||||
if (![self managedObjectContext]) {
|
||||
return NSTerminateNow;
|
||||
}
|
||||
|
||||
if (![[self managedObjectContext] commitEditing]) {
|
||||
NSLog(@"%@:%@ unable to commit editing to terminate", [self class], NSStringFromSelector(_cmd));
|
||||
return NSTerminateCancel;
|
||||
}
|
||||
|
||||
if (![[self managedObjectContext] hasChanges]) {
|
||||
return NSTerminateNow;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
if (![[self managedObjectContext] save:&error]) {
|
||||
|
||||
// Customize this code block to include application-specific recovery steps.
|
||||
BOOL result = [sender presentError:error];
|
||||
if (result) {
|
||||
return NSTerminateCancel;
|
||||
}
|
||||
|
||||
NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message");
|
||||
NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info");
|
||||
NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title");
|
||||
NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title");
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:question];
|
||||
[alert setInformativeText:info];
|
||||
[alert addButtonWithTitle:quitButton];
|
||||
[alert addButtonWithTitle:cancelButton];
|
||||
|
||||
NSInteger answer = [alert runModal];
|
||||
|
||||
if (answer == NSAlertAlternateReturn) {
|
||||
return NSTerminateCancel;
|
||||
}
|
||||
}
|
||||
|
||||
return NSTerminateNow;
|
||||
}
|
||||
|
||||
#pragma mark - UbiquityStoreManagerDelegate
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)iCloudEnabled {
|
||||
|
||||
self.useICloudItem.state = iCloudEnabled? NSOnState: NSOffState;
|
||||
self.useICloudItem.enabled = !iCloudEnabled;
|
||||
}
|
||||
|
||||
@end
|
||||
24
MasterPassword/Mac/MPPasswordWindowController.h
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// MPPasswordWindowController.h
|
||||
// MasterPassword-Mac
|
||||
//
|
||||
// Created by Maarten Billemont on 04/03/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface MPPasswordWindowController : NSWindowController <NSTextFieldDelegate> {
|
||||
|
||||
NSString *_content;
|
||||
}
|
||||
|
||||
@property (strong) NSString *content;
|
||||
|
||||
@property (weak) IBOutlet NSTextField *siteField;
|
||||
@property (weak) IBOutlet NSTextField *contentField;
|
||||
@property (weak) IBOutlet NSTextField *tipField;
|
||||
|
||||
- (void)unlock;
|
||||
|
||||
@end
|
||||
247
MasterPassword/Mac/MPPasswordWindowController.m
Normal file
@@ -0,0 +1,247 @@
|
||||
//
|
||||
// MPPasswordWindowController.m
|
||||
// MasterPassword-Mac
|
||||
//
|
||||
// Created by Maarten Billemont on 04/03/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPPasswordWindowController.h"
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
#import "MPAppDelegate_Store.h"
|
||||
#import "MPElementEntity.h"
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
|
||||
@interface MPPasswordWindowController ()
|
||||
|
||||
@property (nonatomic, strong) NSString *oldSiteName;
|
||||
@property (nonatomic, strong) NSArray /* MPElementEntity */ *siteResults;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPPasswordWindowController
|
||||
@synthesize oldSiteName, siteResults;
|
||||
@synthesize siteField;
|
||||
@synthesize contentField;
|
||||
@synthesize tipField;
|
||||
|
||||
- (void)windowDidLoad {
|
||||
|
||||
[self setContent:@""];
|
||||
[self.tipField setStringValue:@""];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification object:self.window queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[self unlock];
|
||||
[self.siteField selectText:self];
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[[NSApplication sharedApplication] hide:self];
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSControlTextDidChangeNotification object:self.siteField queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
NSString *newSiteName = [self.siteField stringValue];
|
||||
BOOL shouldComplete = [self.oldSiteName length] < [newSiteName length];
|
||||
self.oldSiteName = newSiteName;
|
||||
|
||||
if ([self trySite])
|
||||
shouldComplete = NO;
|
||||
|
||||
if (shouldComplete)
|
||||
[[[note userInfo] objectForKey:@"NSFieldEditor"] complete:nil];
|
||||
}];
|
||||
|
||||
[super windowDidLoad];
|
||||
}
|
||||
|
||||
- (void)unlock {
|
||||
|
||||
if (![MPAppDelegate get].key)
|
||||
// Try and load the key from the keychain.
|
||||
[[MPAppDelegate get] loadStoredKey];
|
||||
|
||||
if (![MPAppDelegate get].key)
|
||||
// Ask the user to set the key through his master password.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([MPAppDelegate get].key)
|
||||
return;
|
||||
|
||||
NSAlert *alert = [NSAlert alertWithMessageText:@"Master Password is locked."
|
||||
defaultButton:@"Unlock" alternateButton:@"Change" otherButton:@"Quit"
|
||||
informativeTextWithFormat:@"Your master password is required to unlock the application."];
|
||||
NSSecureTextField *passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 22)];
|
||||
[alert setAccessoryView:passwordField];
|
||||
[alert layout];
|
||||
[passwordField becomeFirstResponder];
|
||||
[alert beginSheetModalForWindow:self.window modalDelegate:self
|
||||
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:NULL];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
|
||||
|
||||
switch (returnCode) {
|
||||
case NSAlertAlternateReturn:
|
||||
// "Change" button.
|
||||
if ([[NSAlert alertWithMessageText:@"Changing Master Password"
|
||||
defaultButton:nil alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil
|
||||
informativeTextWithFormat:
|
||||
@"This will allow you to log in with a different master password.\n\n"
|
||||
@"Note that you will only see the sites and passwords for the master password you log in with.\n"
|
||||
@"If you log in with a different master password, your current sites will be unavailable.\n\n"
|
||||
@"You can always change back to your current master password later.\n"
|
||||
@"Your current sites and passwords will then become available again."] runModal] == 1)
|
||||
[[MPAppDelegate get] forgetKey];
|
||||
break;
|
||||
|
||||
case NSAlertOtherReturn:
|
||||
// "Quit" button.
|
||||
[[NSApplication sharedApplication] terminate:self];
|
||||
return;
|
||||
|
||||
case NSAlertDefaultReturn:
|
||||
// "Unlock" button.
|
||||
[[MPAppDelegate get] tryMasterPassword:[(NSSecureTextField *)alert.accessoryView stringValue]];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index {
|
||||
|
||||
NSString *query = [[control stringValue] substringWithRange:charRange];
|
||||
if (![query length] || ![MPAppDelegate get].keyID)
|
||||
return nil;
|
||||
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@",
|
||||
query, query, [MPAppDelegate get].keyID];
|
||||
|
||||
NSError *error = nil;
|
||||
self.siteResults = [[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:&error];
|
||||
if (error)
|
||||
err(@"Couldn't fetch elements: %@", error);
|
||||
|
||||
NSMutableArray *mutableResults = [NSMutableArray arrayWithCapacity:[self.siteResults count] + 1];
|
||||
if (self.siteResults)
|
||||
for (MPElementEntity *element in self.siteResults)
|
||||
[mutableResults addObject:element.name];
|
||||
// [mutableResults addObject:query]; // For when the app should be able to create new sites.
|
||||
return mutableResults;
|
||||
}
|
||||
|
||||
- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector {
|
||||
|
||||
if (commandSelector == @selector(cancel:)) {
|
||||
[self.window close];
|
||||
return YES;
|
||||
}
|
||||
if (commandSelector == @selector(insertNewline:) && [self.content length]) {
|
||||
[[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
|
||||
if ([[NSPasteboard generalPasteboard] setString:self.content forType:NSPasteboardTypeString]) {
|
||||
self.tipField.alphaValue = 1;
|
||||
[self.tipField setStringValue:@"Copied!"];
|
||||
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0f * NSEC_PER_SEC));
|
||||
dispatch_after(popTime, dispatch_get_main_queue(), ^{
|
||||
[NSAnimationContext beginGrouping];
|
||||
[[NSAnimationContext currentContext] setDuration:0.2f];
|
||||
[self.tipField.animator setAlphaValue:0];
|
||||
[NSAnimationContext endGrouping];
|
||||
});
|
||||
|
||||
[[self findElement] use];
|
||||
return YES;
|
||||
} else
|
||||
wrn(@"Couldn't copy password to pasteboard.");
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)controlTextDidEndEditing:(NSNotification *)obj {
|
||||
|
||||
if (obj.object == self.siteField)
|
||||
[self trySite];
|
||||
}
|
||||
|
||||
- (NSString *)content {
|
||||
|
||||
return _content;
|
||||
}
|
||||
|
||||
- (void)setContent:(NSString *)content {
|
||||
|
||||
_content = content;
|
||||
|
||||
NSShadow *shadow = [NSShadow new];
|
||||
shadow.shadowColor = [NSColor colorWithDeviceWhite:0.0f alpha:0.6f];
|
||||
shadow.shadowOffset = NSMakeSize(1.0f, -1.0f);
|
||||
shadow.shadowBlurRadius = 1.2f;
|
||||
|
||||
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
|
||||
paragraph.alignment = NSCenterTextAlignment;
|
||||
|
||||
[self.contentField setAttributedStringValue:
|
||||
[[NSAttributedString alloc] initWithString:_content
|
||||
attributes:[[NSMutableDictionary alloc] initWithObjectsAndKeys:
|
||||
shadow, NSShadowAttributeName,
|
||||
paragraph, NSParagraphStyleAttributeName,
|
||||
nil]]];
|
||||
}
|
||||
|
||||
- (BOOL)trySite {
|
||||
|
||||
MPElementEntity *result = [self findElement];
|
||||
if (!result) {
|
||||
[self setContent:@""];
|
||||
[self.tipField setStringValue:@""];
|
||||
return NO;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
||||
NSString *description = [result.content description];
|
||||
if (!description)
|
||||
description = @"";
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self setContent:description];
|
||||
[self.tipField setStringValue:@"Hit enter to copy the password."];
|
||||
self.tipField.alphaValue = 1;
|
||||
});
|
||||
});
|
||||
|
||||
// For when the app should be able to create new sites.
|
||||
/*
|
||||
else
|
||||
[[MPAppDelegate get].managedObjectContext performBlock:^{
|
||||
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
|
||||
inManagedObjectContext:[MPAppDelegate get].managedObjectContext];
|
||||
assert([element isKindOfClass:ClassFromMPElementType(element.type)]);
|
||||
assert([MPAppDelegate get].keyID);
|
||||
|
||||
element.name = siteName;
|
||||
element.keyID = [MPAppDelegate get].keyID;
|
||||
|
||||
NSString *description = [element.content description];
|
||||
[element use];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self setContent:description];
|
||||
});
|
||||
}];
|
||||
*/
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (MPElementEntity *)findElement {
|
||||
|
||||
for (MPElementEntity *element in self.siteResults)
|
||||
if ([element.name isEqualToString:[self.siteField stringValue]])
|
||||
return element;
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
691
MasterPassword/Mac/MPPasswordWindowController.xib
Normal file
@@ -0,0 +1,691 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00">
|
||||
<data>
|
||||
<int key="IBDocument.SystemTarget">1070</int>
|
||||
<string key="IBDocument.SystemVersion">11D50</string>
|
||||
<string key="IBDocument.InterfaceBuilderVersion">2182</string>
|
||||
<string key="IBDocument.AppKitVersion">1138.32</string>
|
||||
<string key="IBDocument.HIToolboxVersion">568.00</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="NS.object.0">2182</string>
|
||||
</object>
|
||||
<array key="IBDocument.IntegratedClassDependencies">
|
||||
<string>NSTextField</string>
|
||||
<string>NSTextFieldCell</string>
|
||||
<string>NSWindowTemplate</string>
|
||||
<string>NSView</string>
|
||||
<string>IBNSLayoutConstraint</string>
|
||||
<string>NSCustomObject</string>
|
||||
</array>
|
||||
<array key="IBDocument.PluginDependencies">
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
</array>
|
||||
<object class="NSMutableDictionary" key="IBDocument.Metadata">
|
||||
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
|
||||
<integer value="1" key="NS.object.0"/>
|
||||
</object>
|
||||
<array class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
|
||||
<object class="NSCustomObject" id="1001">
|
||||
<string key="NSClassName">MPPasswordWindowController</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1003">
|
||||
<string key="NSClassName">FirstResponder</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1004">
|
||||
<string key="NSClassName">NSApplication</string>
|
||||
</object>
|
||||
<object class="NSWindowTemplate" id="45434518">
|
||||
<int key="NSWindowStyleMask">277</int>
|
||||
<int key="NSWindowBacking">2</int>
|
||||
<string key="NSWindowRect">{{600, 530}, {480, 159}}</string>
|
||||
<int key="NSWTFlags">611845120</int>
|
||||
<string key="NSWindowTitle">Master Password</string>
|
||||
<string key="NSWindowClass">NSPanel</string>
|
||||
<nil key="NSViewClass"/>
|
||||
<nil key="NSUserInterfaceItemIdentifier"/>
|
||||
<string key="NSWindowContentMaxSize">{480, 320}</string>
|
||||
<string key="NSWindowContentMinSize">{480, 134}</string>
|
||||
<object class="NSView" key="NSWindowView" id="258451033">
|
||||
<reference key="NSNextResponder"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<array class="NSMutableArray" key="NSSubviews">
|
||||
<object class="NSTextField" id="291791585">
|
||||
<reference key="NSNextResponder" ref="258451033"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{140, 117}, {200, 22}}</string>
|
||||
<reference key="NSSuperview" ref="258451033"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="226215720"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<string key="NSReuseIdentifierKey">_NS:9</string>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSTextFieldCell" key="NSCell" id="213149334">
|
||||
<int key="NSCellFlags">-1804468671</int>
|
||||
<int key="NSCellFlags2">138413120</int>
|
||||
<string key="NSContents"/>
|
||||
<object class="NSFont" key="NSSupport" id="590895625">
|
||||
<string key="NSName">LucidaGrande</string>
|
||||
<double key="NSSize">13</double>
|
||||
<int key="NSfFlags">1044</int>
|
||||
</object>
|
||||
<string key="NSPlaceholderString">Site name</string>
|
||||
<string key="NSCellIdentifier">_NS:9</string>
|
||||
<reference key="NSControlView" ref="291791585"/>
|
||||
<int key="NSTextBezelStyle">1</int>
|
||||
<object class="NSColor" key="NSBackgroundColor">
|
||||
<int key="NSColorSpace">6</int>
|
||||
<string key="NSCatalogName">System</string>
|
||||
<string key="NSColorName">textBackgroundColor</string>
|
||||
<object class="NSColor" key="NSColor" id="370404594">
|
||||
<int key="NSColorSpace">3</int>
|
||||
<bytes key="NSWhite">MQA</bytes>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSColor" key="NSTextColor">
|
||||
<int key="NSColorSpace">6</int>
|
||||
<string key="NSCatalogName">System</string>
|
||||
<string key="NSColorName">textColor</string>
|
||||
<object class="NSColor" key="NSColor">
|
||||
<int key="NSColorSpace">3</int>
|
||||
<bytes key="NSWhite">MAA</bytes>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSTextField" id="226215720">
|
||||
<reference key="NSNextResponder" ref="258451033"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{17, 45}, {446, 64}}</string>
|
||||
<reference key="NSSuperview" ref="258451033"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="1020047619"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<string key="NSReuseIdentifierKey">_NS:9</string>
|
||||
<string key="NSAntiCompressionPriority">{250, 750}</string>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSTextFieldCell" key="NSCell" id="254722226">
|
||||
<int key="NSCellFlags">67239424</int>
|
||||
<int key="NSCellFlags2">138412032</int>
|
||||
<string key="NSContents">S3cretP4s$w0rD</string>
|
||||
<object class="NSFont" key="NSSupport">
|
||||
<string key="NSName">Exo-Black</string>
|
||||
<double key="NSSize">48</double>
|
||||
<int key="NSfFlags">16</int>
|
||||
</object>
|
||||
<string key="NSCellIdentifier">_NS:9</string>
|
||||
<reference key="NSControlView" ref="226215720"/>
|
||||
<bool key="NSDrawsBackground">YES</bool>
|
||||
<object class="NSColor" key="NSBackgroundColor" id="245864165">
|
||||
<int key="NSColorSpace">6</int>
|
||||
<string key="NSCatalogName">System</string>
|
||||
<string key="NSColorName">controlColor</string>
|
||||
<object class="NSColor" key="NSColor">
|
||||
<int key="NSColorSpace">3</int>
|
||||
<bytes key="NSWhite">MC42NjY2NjY2NjY3AA</bytes>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSColor" key="NSTextColor">
|
||||
<int key="NSColorSpace">1</int>
|
||||
<bytes key="NSRGB">MC40NzQ1MDk4MDM5IDAuODY2NjY2NjY2NyAwLjk4NDMxMzcyNTUAA</bytes>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSTextField" id="1020047619">
|
||||
<reference key="NSNextResponder" ref="258451033"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{17, 20}, {446, 17}}</string>
|
||||
<reference key="NSSuperview" ref="258451033"/>
|
||||
<reference key="NSWindow"/>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<string key="NSReuseIdentifierKey">_NS:1505</string>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSTextFieldCell" key="NSCell" id="691402180">
|
||||
<int key="NSCellFlags">68288064</int>
|
||||
<int key="NSCellFlags2">138413056</int>
|
||||
<string key="NSContents">Hit enter to copy the password.</string>
|
||||
<reference key="NSSupport" ref="590895625"/>
|
||||
<string key="NSCellIdentifier">_NS:1505</string>
|
||||
<reference key="NSControlView" ref="1020047619"/>
|
||||
<reference key="NSBackgroundColor" ref="245864165"/>
|
||||
<object class="NSColor" key="NSTextColor">
|
||||
<int key="NSColorSpace">6</int>
|
||||
<string key="NSCatalogName">System</string>
|
||||
<string key="NSColorName">controlLightHighlightColor</string>
|
||||
<reference key="NSColor" ref="370404594"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
</array>
|
||||
<string key="NSFrameSize">{480, 159}</string>
|
||||
<reference key="NSSuperview"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="291791585"/>
|
||||
<bool key="NSViewIsLayerTreeHost">YES</bool>
|
||||
<int key="NSViewLayerContentsRedrawPolicy">2</int>
|
||||
<string key="NSReuseIdentifierKey">_NS:21</string>
|
||||
</object>
|
||||
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
|
||||
<string key="NSMinSize">{480, 150}</string>
|
||||
<string key="NSMaxSize">{480, 336}</string>
|
||||
<bool key="NSWindowIsRestorable">YES</bool>
|
||||
</object>
|
||||
</array>
|
||||
<object class="IBObjectContainer" key="IBDocument.Objects">
|
||||
<array class="NSMutableArray" key="connectionRecords">
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">window</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="45434518"/>
|
||||
</object>
|
||||
<int key="connectionID">40</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">siteField</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="291791585"/>
|
||||
</object>
|
||||
<int key="connectionID">41</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">contentField</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="226215720"/>
|
||||
</object>
|
||||
<int key="connectionID">42</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">tipField</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="1020047619"/>
|
||||
</object>
|
||||
<int key="connectionID">95</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">delegate</string>
|
||||
<reference key="source" ref="45434518"/>
|
||||
<reference key="destination" ref="1001"/>
|
||||
</object>
|
||||
<int key="connectionID">39</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">initialFirstResponder</string>
|
||||
<reference key="source" ref="45434518"/>
|
||||
<reference key="destination" ref="291791585"/>
|
||||
</object>
|
||||
<int key="connectionID">49</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">delegate</string>
|
||||
<reference key="source" ref="291791585"/>
|
||||
<reference key="destination" ref="1001"/>
|
||||
</object>
|
||||
<int key="connectionID">43</int>
|
||||
</object>
|
||||
</array>
|
||||
<object class="IBMutableOrderedSet" key="objectRecords">
|
||||
<array key="orderedObjects">
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">0</int>
|
||||
<array key="object" id="0"/>
|
||||
<reference key="children" ref="1000"/>
|
||||
<nil key="parent"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-2</int>
|
||||
<reference key="object" ref="1001"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">File's Owner</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-1</int>
|
||||
<reference key="object" ref="1003"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">First Responder</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-3</int>
|
||||
<reference key="object" ref="1004"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">Application</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">22</int>
|
||||
<reference key="object" ref="45434518"/>
|
||||
<array class="NSMutableArray" key="children">
|
||||
<reference ref="258451033"/>
|
||||
</array>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">23</int>
|
||||
<reference key="object" ref="258451033"/>
|
||||
<array class="NSMutableArray" key="children">
|
||||
<object class="IBNSLayoutConstraint" id="490796257">
|
||||
<reference key="firstItem" ref="226215720"/>
|
||||
<int key="firstAttribute">3</int>
|
||||
<int key="relation">0</int>
|
||||
<reference key="secondItem" ref="258451033"/>
|
||||
<int key="secondAttribute">3</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBLayoutConstant" key="constant">
|
||||
<double key="value">50</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">9</int>
|
||||
<float key="scoringTypeFloat">40</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="751044970">
|
||||
<reference key="firstItem" ref="291791585"/>
|
||||
<int key="firstAttribute">3</int>
|
||||
<int key="relation">1</int>
|
||||
<reference key="secondItem" ref="258451033"/>
|
||||
<int key="secondAttribute">3</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBLayoutConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">9</int>
|
||||
<float key="scoringTypeFloat">40</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="1014829211">
|
||||
<reference key="firstItem" ref="258451033"/>
|
||||
<int key="firstAttribute">4</int>
|
||||
<int key="relation">1</int>
|
||||
<reference key="secondItem" ref="226215720"/>
|
||||
<int key="secondAttribute">4</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBLayoutConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">9</int>
|
||||
<float key="scoringTypeFloat">40</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="582018795">
|
||||
<reference key="firstItem" ref="258451033"/>
|
||||
<int key="firstAttribute">6</int>
|
||||
<int key="relation">1</int>
|
||||
<reference key="secondItem" ref="226215720"/>
|
||||
<int key="secondAttribute">6</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBLayoutConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">9</int>
|
||||
<float key="scoringTypeFloat">40</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="137127436">
|
||||
<reference key="firstItem" ref="226215720"/>
|
||||
<int key="firstAttribute">5</int>
|
||||
<int key="relation">1</int>
|
||||
<reference key="secondItem" ref="258451033"/>
|
||||
<int key="secondAttribute">5</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBLayoutConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">9</int>
|
||||
<float key="scoringTypeFloat">40</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<reference ref="226215720"/>
|
||||
<reference ref="291791585"/>
|
||||
<object class="IBNSLayoutConstraint" id="456428551">
|
||||
<reference key="firstItem" ref="226215720"/>
|
||||
<int key="firstAttribute">5</int>
|
||||
<int key="relation">0</int>
|
||||
<reference key="secondItem" ref="258451033"/>
|
||||
<int key="secondAttribute">5</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBNSLayoutSymbolicConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">8</int>
|
||||
<float key="scoringTypeFloat">29</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<reference ref="1020047619"/>
|
||||
<object class="IBNSLayoutConstraint" id="51508433">
|
||||
<reference key="firstItem" ref="291791585"/>
|
||||
<int key="firstAttribute">9</int>
|
||||
<int key="relation">0</int>
|
||||
<reference key="secondItem" ref="226215720"/>
|
||||
<int key="secondAttribute">9</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBLayoutConstant" key="constant">
|
||||
<double key="value">0.0</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">6</int>
|
||||
<float key="scoringTypeFloat">24</float>
|
||||
<int key="contentType">2</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="99093936">
|
||||
<reference key="firstItem" ref="258451033"/>
|
||||
<int key="firstAttribute">6</int>
|
||||
<int key="relation">0</int>
|
||||
<reference key="secondItem" ref="1020047619"/>
|
||||
<int key="secondAttribute">6</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBNSLayoutSymbolicConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">8</int>
|
||||
<float key="scoringTypeFloat">29</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="536265876">
|
||||
<reference key="firstItem" ref="258451033"/>
|
||||
<int key="firstAttribute">4</int>
|
||||
<int key="relation">0</int>
|
||||
<reference key="secondItem" ref="1020047619"/>
|
||||
<int key="secondAttribute">4</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBNSLayoutSymbolicConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">8</int>
|
||||
<float key="scoringTypeFloat">29</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="1695776">
|
||||
<reference key="firstItem" ref="258451033"/>
|
||||
<int key="firstAttribute">6</int>
|
||||
<int key="relation">0</int>
|
||||
<reference key="secondItem" ref="226215720"/>
|
||||
<int key="secondAttribute">6</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBNSLayoutSymbolicConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">8</int>
|
||||
<float key="scoringTypeFloat">29</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="400319127">
|
||||
<reference key="firstItem" ref="291791585"/>
|
||||
<int key="firstAttribute">3</int>
|
||||
<int key="relation">0</int>
|
||||
<reference key="secondItem" ref="258451033"/>
|
||||
<int key="secondAttribute">3</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBNSLayoutSymbolicConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">8</int>
|
||||
<float key="scoringTypeFloat">29</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBNSLayoutConstraint" id="105480775">
|
||||
<reference key="firstItem" ref="1020047619"/>
|
||||
<int key="firstAttribute">5</int>
|
||||
<int key="relation">0</int>
|
||||
<reference key="secondItem" ref="258451033"/>
|
||||
<int key="secondAttribute">5</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBNSLayoutSymbolicConstant" key="constant">
|
||||
<double key="value">20</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">8</int>
|
||||
<float key="scoringTypeFloat">29</float>
|
||||
<int key="contentType">3</int>
|
||||
<reference key="containingView" ref="258451033"/>
|
||||
</object>
|
||||
</array>
|
||||
<reference key="parent" ref="45434518"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">26</int>
|
||||
<reference key="object" ref="490796257"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">27</int>
|
||||
<reference key="object" ref="751044970"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">28</int>
|
||||
<reference key="object" ref="1014829211"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">32</int>
|
||||
<reference key="object" ref="582018795"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">33</int>
|
||||
<reference key="object" ref="137127436"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">34</int>
|
||||
<reference key="object" ref="226215720"/>
|
||||
<array class="NSMutableArray" key="children">
|
||||
<reference ref="254722226"/>
|
||||
</array>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">35</int>
|
||||
<reference key="object" ref="291791585"/>
|
||||
<array class="NSMutableArray" key="children">
|
||||
<reference ref="213149334"/>
|
||||
<object class="IBNSLayoutConstraint" id="302161926">
|
||||
<reference key="firstItem" ref="291791585"/>
|
||||
<int key="firstAttribute">7</int>
|
||||
<int key="relation">0</int>
|
||||
<nil key="secondItem"/>
|
||||
<int key="secondAttribute">0</int>
|
||||
<float key="multiplier">1</float>
|
||||
<object class="IBLayoutConstant" key="constant">
|
||||
<double key="value">200</double>
|
||||
</object>
|
||||
<float key="priority">1000</float>
|
||||
<int key="scoringType">3</int>
|
||||
<float key="scoringTypeFloat">9</float>
|
||||
<int key="contentType">1</int>
|
||||
<reference key="containingView" ref="291791585"/>
|
||||
</object>
|
||||
</array>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">37</int>
|
||||
<reference key="object" ref="213149334"/>
|
||||
<reference key="parent" ref="291791585"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">38</int>
|
||||
<reference key="object" ref="254722226"/>
|
||||
<reference key="parent" ref="226215720"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">50</int>
|
||||
<reference key="object" ref="1020047619"/>
|
||||
<array class="NSMutableArray" key="children">
|
||||
<reference ref="691402180"/>
|
||||
</array>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">51</int>
|
||||
<reference key="object" ref="691402180"/>
|
||||
<reference key="parent" ref="1020047619"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">74</int>
|
||||
<reference key="object" ref="456428551"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">85</int>
|
||||
<reference key="object" ref="51508433"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">87</int>
|
||||
<reference key="object" ref="99093936"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">90</int>
|
||||
<reference key="object" ref="302161926"/>
|
||||
<reference key="parent" ref="291791585"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">91</int>
|
||||
<reference key="object" ref="536265876"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">92</int>
|
||||
<reference key="object" ref="1695776"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">93</int>
|
||||
<reference key="object" ref="400319127"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">94</int>
|
||||
<reference key="object" ref="105480775"/>
|
||||
<reference key="parent" ref="258451033"/>
|
||||
</object>
|
||||
</array>
|
||||
</object>
|
||||
<dictionary class="NSMutableDictionary" key="flattenedProperties">
|
||||
<string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<boolean value="YES" key="22.IBNSWindowAutoPositionCentersHorizontal"/>
|
||||
<boolean value="YES" key="22.IBNSWindowAutoPositionCentersVertical"/>
|
||||
<string key="22.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<boolean value="NO" key="22.NSWindowTemplate.visibleAtLaunch"/>
|
||||
<array class="NSMutableArray" key="23.IBNSViewMetadataConstraints">
|
||||
<reference ref="137127436"/>
|
||||
<reference ref="582018795"/>
|
||||
<reference ref="1014829211"/>
|
||||
<reference ref="751044970"/>
|
||||
<reference ref="490796257"/>
|
||||
<reference ref="456428551"/>
|
||||
<reference ref="51508433"/>
|
||||
<reference ref="99093936"/>
|
||||
<reference ref="536265876"/>
|
||||
<reference ref="1695776"/>
|
||||
<reference ref="400319127"/>
|
||||
<reference ref="105480775"/>
|
||||
</array>
|
||||
<string key="23.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="26.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="27.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="28.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="32.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="33.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<boolean value="NO" key="34.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
|
||||
<string key="34.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<array class="NSMutableArray" key="35.IBNSViewMetadataConstraints">
|
||||
<reference ref="302161926"/>
|
||||
</array>
|
||||
<boolean value="NO" key="35.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
|
||||
<string key="35.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="37.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="38.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<boolean value="NO" key="50.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
|
||||
<string key="50.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="51.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="74.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="85.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="87.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="90.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="91.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="92.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="93.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="94.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
</dictionary>
|
||||
<dictionary class="NSMutableDictionary" key="unlocalizedProperties"/>
|
||||
<nil key="activeLocalization"/>
|
||||
<dictionary class="NSMutableDictionary" key="localizations"/>
|
||||
<nil key="sourceID"/>
|
||||
<int key="maxID">95</int>
|
||||
</object>
|
||||
<object class="IBClassDescriber" key="IBDocument.Classes">
|
||||
<array class="NSMutableArray" key="referencedPartialClassDescriptions">
|
||||
<object class="IBPartialClassDescription">
|
||||
<string key="className">MPPasswordWindowController</string>
|
||||
<string key="superclassName">NSWindowController</string>
|
||||
<dictionary class="NSMutableDictionary" key="outlets">
|
||||
<string key="contentField">NSTextField</string>
|
||||
<string key="siteField">NSTextField</string>
|
||||
<string key="tipField">NSTextField</string>
|
||||
</dictionary>
|
||||
<dictionary class="NSMutableDictionary" key="toOneOutletInfosByName">
|
||||
<object class="IBToOneOutletInfo" key="contentField">
|
||||
<string key="name">contentField</string>
|
||||
<string key="candidateClassName">NSTextField</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo" key="siteField">
|
||||
<string key="name">siteField</string>
|
||||
<string key="candidateClassName">NSTextField</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo" key="tipField">
|
||||
<string key="name">tipField</string>
|
||||
<string key="candidateClassName">NSTextField</string>
|
||||
</object>
|
||||
</dictionary>
|
||||
<object class="IBClassDescriptionSource" key="sourceIdentifier">
|
||||
<string key="majorKey">IBProjectSource</string>
|
||||
<string key="minorKey">./Classes/MPPasswordWindowController.h</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBPartialClassDescription">
|
||||
<string key="className">NSLayoutConstraint</string>
|
||||
<string key="superclassName">NSObject</string>
|
||||
<object class="IBClassDescriptionSource" key="sourceIdentifier">
|
||||
<string key="majorKey">IBProjectSource</string>
|
||||
<string key="minorKey">./Classes/NSLayoutConstraint.h</string>
|
||||
</object>
|
||||
</object>
|
||||
</array>
|
||||
</object>
|
||||
<int key="IBDocument.localizationMode">0</int>
|
||||
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
|
||||
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
|
||||
<int key="IBDocument.defaultPropertyAccessControl">3</int>
|
||||
<bool key="IBDocument.UseAutolayout">YES</bool>
|
||||
</data>
|
||||
</archive>
|
||||
38
MasterPassword/Mac/MasterPassword-Info.plist
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Icon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.lyndir.lhunath.${PRODUCT_NAME:rfc1034identifier}.Mac</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>[auto]</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>[auto]</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.productivity</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2012 Lyndir. All rights reserved.</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
11
MasterPassword/Mac/MasterPassword-Prefix.pch
Normal file
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Prefix header for all source files of the 'MasterPassword' target in the 'MasterPassword' project
|
||||
//
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#endif
|
||||
|
||||
#import "Pearl-Prefix.pch"
|
||||
|
||||
#import "MPTypes.h"
|
||||
29
MasterPassword/Mac/en.lproj/Credits.rtf
Normal file
@@ -0,0 +1,29 @@
|
||||
{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\paperw9840\paperh8400
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural
|
||||
|
||||
\f0\b\fs24 \cf0 Engineering:
|
||||
\b0 \
|
||||
Some people\
|
||||
\
|
||||
|
||||
\b Human Interface Design:
|
||||
\b0 \
|
||||
Some other people\
|
||||
\
|
||||
|
||||
\b Testing:
|
||||
\b0 \
|
||||
Hopefully not nobody\
|
||||
\
|
||||
|
||||
\b Documentation:
|
||||
\b0 \
|
||||
Whoever\
|
||||
\
|
||||
|
||||
\b With special thanks to:
|
||||
\b0 \
|
||||
Mom\
|
||||
}
|
||||
523
MasterPassword/Mac/en.lproj/MainMenu.xib
Normal file
@@ -0,0 +1,523 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00">
|
||||
<data>
|
||||
<int key="IBDocument.SystemTarget">1070</int>
|
||||
<string key="IBDocument.SystemVersion">11D50</string>
|
||||
<string key="IBDocument.InterfaceBuilderVersion">2182</string>
|
||||
<string key="IBDocument.AppKitVersion">1138.32</string>
|
||||
<string key="IBDocument.HIToolboxVersion">568.00</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="NS.object.0">2182</string>
|
||||
</object>
|
||||
<array key="IBDocument.IntegratedClassDependencies">
|
||||
<string>NSMenuItem</string>
|
||||
<string>NSMenu</string>
|
||||
<string>NSCustomObject</string>
|
||||
<string>NSUserDefaultsController</string>
|
||||
</array>
|
||||
<array key="IBDocument.PluginDependencies">
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
</array>
|
||||
<object class="NSMutableDictionary" key="IBDocument.Metadata">
|
||||
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
|
||||
<integer value="1" key="NS.object.0"/>
|
||||
</object>
|
||||
<array class="NSMutableArray" key="IBDocument.RootObjects" id="1048">
|
||||
<object class="NSCustomObject" id="1021">
|
||||
<string key="NSClassName">NSApplication</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1014">
|
||||
<string key="NSClassName">FirstResponder</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1050">
|
||||
<string key="NSClassName">NSApplication</string>
|
||||
</object>
|
||||
<object class="NSMenu" id="649796088">
|
||||
<string key="NSTitle">AMainMenu</string>
|
||||
<array class="NSMutableArray" key="NSMenuItems"/>
|
||||
<string key="NSName">_NSMainMenu</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="976324537">
|
||||
<string key="NSClassName">MPAppDelegate</string>
|
||||
</object>
|
||||
<object class="NSUserDefaultsController" id="705910970">
|
||||
<bool key="NSSharedInstance">YES</bool>
|
||||
</object>
|
||||
<object class="NSMenu" id="764588027">
|
||||
<string key="NSTitle"/>
|
||||
<array class="NSMutableArray" key="NSMenuItems">
|
||||
<object class="NSMenuItem" id="846612332">
|
||||
<reference key="NSMenu" ref="764588027"/>
|
||||
<string key="NSTitle">Show</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<object class="NSCustomResource" key="NSOnImage" id="552246001">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSMenuCheckmark</string>
|
||||
</object>
|
||||
<object class="NSCustomResource" key="NSMixedImage" id="752047669">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSMenuMixedState</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="851296005">
|
||||
<reference key="NSMenu" ref="764588027"/>
|
||||
<string key="NSTitle">Preferences</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
<string key="NSAction">submenuAction:</string>
|
||||
<object class="NSMenu" key="NSSubmenu" id="800575174">
|
||||
<string key="NSTitle">Preferences</string>
|
||||
<array class="NSMutableArray" key="NSMenuItems">
|
||||
<object class="NSMenuItem" id="14397049">
|
||||
<reference key="NSMenu" ref="800575174"/>
|
||||
<string key="NSTitle">Use iCloud</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="461686112">
|
||||
<reference key="NSMenu" ref="800575174"/>
|
||||
<bool key="NSIsDisabled">YES</bool>
|
||||
<string key="NSTitle">Synchronize available sites from your iCloud account.</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
<object class="NSAttributedString" key="NSAttributedTitle">
|
||||
<string key="NSString">Synchronize available sites from your iCloud account.</string>
|
||||
<dictionary key="NSAttributes" id="583461090">
|
||||
<object class="NSFont" key="NSFont">
|
||||
<string key="NSName">Helvetica</string>
|
||||
<double key="NSSize">12</double>
|
||||
<int key="NSfFlags">16</int>
|
||||
</object>
|
||||
<object class="NSParagraphStyle" key="NSParagraphStyle">
|
||||
<int key="NSAlignment">4</int>
|
||||
<nil key="NSTabStops"/>
|
||||
</object>
|
||||
</dictionary>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="290760748">
|
||||
<reference key="NSMenu" ref="800575174"/>
|
||||
<string key="NSTitle">Remember Password</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="907921953">
|
||||
<reference key="NSMenu" ref="800575174"/>
|
||||
<bool key="NSIsDisabled">YES</bool>
|
||||
<string key="NSTitle">Remember the password while the application is running.</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
<object class="NSAttributedString" key="NSAttributedTitle">
|
||||
<string key="NSString">Remember the password while the application is running.</string>
|
||||
<reference key="NSAttributes" ref="583461090"/>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="110488020">
|
||||
<reference key="NSMenu" ref="800575174"/>
|
||||
<string key="NSTitle">Save Password</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="123831322">
|
||||
<reference key="NSMenu" ref="800575174"/>
|
||||
<bool key="NSIsDisabled">YES</bool>
|
||||
<string key="NSTitle">Save the password in your keychain so you don't need to enter it again.</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
<object class="NSAttributedString" key="NSAttributedTitle">
|
||||
<string key="NSString">Save the password in your keychain so you don't need to enter it again.</string>
|
||||
<reference key="NSAttributes" ref="583461090"/>
|
||||
</object>
|
||||
</object>
|
||||
</array>
|
||||
<bool key="NSNoAutoenable">YES</bool>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="466252869">
|
||||
<reference key="NSMenu" ref="764588027"/>
|
||||
<bool key="NSIsDisabled">YES</bool>
|
||||
<bool key="NSIsSeparator">YES</bool>
|
||||
<string key="NSTitle"/>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="229948989">
|
||||
<reference key="NSMenu" ref="764588027"/>
|
||||
<bool key="NSIsDisabled">YES</bool>
|
||||
<string key="NSTitle">Lock</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
</object>
|
||||
<object class="NSMenuItem" id="291035877">
|
||||
<reference key="NSMenu" ref="764588027"/>
|
||||
<string key="NSTitle">Quit</string>
|
||||
<string key="NSKeyEquiv"/>
|
||||
<int key="NSMnemonicLoc">2147483647</int>
|
||||
<reference key="NSOnImage" ref="552246001"/>
|
||||
<reference key="NSMixedImage" ref="752047669"/>
|
||||
</object>
|
||||
</array>
|
||||
<bool key="NSNoAutoenable">YES</bool>
|
||||
</object>
|
||||
</array>
|
||||
<object class="IBObjectContainer" key="IBDocument.Objects">
|
||||
<array class="NSMutableArray" key="connectionRecords">
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">terminate:</string>
|
||||
<reference key="source" ref="1050"/>
|
||||
<reference key="destination" ref="291035877"/>
|
||||
</object>
|
||||
<int key="connectionID">734</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">delegate</string>
|
||||
<reference key="source" ref="1021"/>
|
||||
<reference key="destination" ref="976324537"/>
|
||||
</object>
|
||||
<int key="connectionID">495</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">lockItem</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="229948989"/>
|
||||
</object>
|
||||
<int key="connectionID">726</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">signOut:</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="229948989"/>
|
||||
</object>
|
||||
<int key="connectionID">727</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">showItem</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="846612332"/>
|
||||
</object>
|
||||
<int key="connectionID">730</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">statusMenu</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="764588027"/>
|
||||
</object>
|
||||
<int key="connectionID">731</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">activate:</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="846612332"/>
|
||||
</object>
|
||||
<int key="connectionID">736</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">useICloudItem</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="14397049"/>
|
||||
</object>
|
||||
<int key="connectionID">749</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">rememberPasswordItem</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="290760748"/>
|
||||
</object>
|
||||
<int key="connectionID">750</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">savePasswordItem</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="110488020"/>
|
||||
</object>
|
||||
<int key="connectionID">751</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">togglePreference:</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="14397049"/>
|
||||
</object>
|
||||
<int key="connectionID">752</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">togglePreference:</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="290760748"/>
|
||||
</object>
|
||||
<int key="connectionID">753</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">togglePreference:</string>
|
||||
<reference key="source" ref="976324537"/>
|
||||
<reference key="destination" ref="110488020"/>
|
||||
</object>
|
||||
<int key="connectionID">754</int>
|
||||
</object>
|
||||
</array>
|
||||
<object class="IBMutableOrderedSet" key="objectRecords">
|
||||
<array key="orderedObjects">
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">0</int>
|
||||
<array key="object" id="0"/>
|
||||
<reference key="children" ref="1048"/>
|
||||
<nil key="parent"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-2</int>
|
||||
<reference key="object" ref="1021"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">File's Owner</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-1</int>
|
||||
<reference key="object" ref="1014"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">First Responder</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-3</int>
|
||||
<reference key="object" ref="1050"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">Application</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">29</int>
|
||||
<reference key="object" ref="649796088"/>
|
||||
<array class="NSMutableArray" key="children"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">494</int>
|
||||
<reference key="object" ref="976324537"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">548</int>
|
||||
<reference key="object" ref="705910970"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">716</int>
|
||||
<reference key="object" ref="764588027"/>
|
||||
<array class="NSMutableArray" key="children">
|
||||
<reference ref="291035877"/>
|
||||
<reference ref="466252869"/>
|
||||
<reference ref="846612332"/>
|
||||
<reference ref="229948989"/>
|
||||
<reference ref="851296005"/>
|
||||
</array>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">717</int>
|
||||
<reference key="object" ref="291035877"/>
|
||||
<reference key="parent" ref="764588027"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">718</int>
|
||||
<reference key="object" ref="466252869"/>
|
||||
<reference key="parent" ref="764588027"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">719</int>
|
||||
<reference key="object" ref="846612332"/>
|
||||
<reference key="parent" ref="764588027"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">720</int>
|
||||
<reference key="object" ref="229948989"/>
|
||||
<reference key="parent" ref="764588027"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">739</int>
|
||||
<reference key="object" ref="851296005"/>
|
||||
<array class="NSMutableArray" key="children">
|
||||
<reference ref="800575174"/>
|
||||
</array>
|
||||
<reference key="parent" ref="764588027"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">742</int>
|
||||
<reference key="object" ref="800575174"/>
|
||||
<array class="NSMutableArray" key="children">
|
||||
<reference ref="14397049"/>
|
||||
<reference ref="290760748"/>
|
||||
<reference ref="907921953"/>
|
||||
<reference ref="461686112"/>
|
||||
<reference ref="110488020"/>
|
||||
<reference ref="123831322"/>
|
||||
</array>
|
||||
<reference key="parent" ref="851296005"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">743</int>
|
||||
<reference key="object" ref="14397049"/>
|
||||
<reference key="parent" ref="800575174"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">745</int>
|
||||
<reference key="object" ref="907921953"/>
|
||||
<reference key="parent" ref="800575174"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">744</int>
|
||||
<reference key="object" ref="290760748"/>
|
||||
<reference key="parent" ref="800575174"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">746</int>
|
||||
<reference key="object" ref="461686112"/>
|
||||
<reference key="parent" ref="800575174"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">747</int>
|
||||
<reference key="object" ref="110488020"/>
|
||||
<reference key="parent" ref="800575174"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">748</int>
|
||||
<reference key="object" ref="123831322"/>
|
||||
<reference key="parent" ref="800575174"/>
|
||||
</object>
|
||||
</array>
|
||||
</object>
|
||||
<dictionary class="NSMutableDictionary" key="flattenedProperties">
|
||||
<string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="29.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="494.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="548.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="716.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="717.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="718.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="719.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="720.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="739.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="742.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="743.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="744.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="745.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="746.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="747.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="748.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
</dictionary>
|
||||
<dictionary class="NSMutableDictionary" key="unlocalizedProperties"/>
|
||||
<nil key="activeLocalization"/>
|
||||
<dictionary class="NSMutableDictionary" key="localizations"/>
|
||||
<nil key="sourceID"/>
|
||||
<int key="maxID">754</int>
|
||||
</object>
|
||||
<object class="IBClassDescriber" key="IBDocument.Classes">
|
||||
<array class="NSMutableArray" key="referencedPartialClassDescriptions">
|
||||
<object class="IBPartialClassDescription">
|
||||
<string key="className">MPAppDelegate</string>
|
||||
<string key="superclassName">NSObject</string>
|
||||
<dictionary class="NSMutableDictionary" key="actions">
|
||||
<string key="activate:">id</string>
|
||||
<string key="signOut:">id</string>
|
||||
<string key="togglePreference:">NSMenuItem</string>
|
||||
</dictionary>
|
||||
<dictionary class="NSMutableDictionary" key="actionInfosByName">
|
||||
<object class="IBActionInfo" key="activate:">
|
||||
<string key="name">activate:</string>
|
||||
<string key="candidateClassName">id</string>
|
||||
</object>
|
||||
<object class="IBActionInfo" key="signOut:">
|
||||
<string key="name">signOut:</string>
|
||||
<string key="candidateClassName">id</string>
|
||||
</object>
|
||||
<object class="IBActionInfo" key="togglePreference:">
|
||||
<string key="name">togglePreference:</string>
|
||||
<string key="candidateClassName">NSMenuItem</string>
|
||||
</object>
|
||||
</dictionary>
|
||||
<dictionary class="NSMutableDictionary" key="outlets">
|
||||
<string key="lockItem">NSMenuItem</string>
|
||||
<string key="rememberPasswordItem">NSMenuItem</string>
|
||||
<string key="savePasswordItem">NSMenuItem</string>
|
||||
<string key="showItem">NSMenuItem</string>
|
||||
<string key="statusMenu">NSMenu</string>
|
||||
<string key="useICloudItem">NSMenuItem</string>
|
||||
</dictionary>
|
||||
<dictionary class="NSMutableDictionary" key="toOneOutletInfosByName">
|
||||
<object class="IBToOneOutletInfo" key="lockItem">
|
||||
<string key="name">lockItem</string>
|
||||
<string key="candidateClassName">NSMenuItem</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo" key="rememberPasswordItem">
|
||||
<string key="name">rememberPasswordItem</string>
|
||||
<string key="candidateClassName">NSMenuItem</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo" key="savePasswordItem">
|
||||
<string key="name">savePasswordItem</string>
|
||||
<string key="candidateClassName">NSMenuItem</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo" key="showItem">
|
||||
<string key="name">showItem</string>
|
||||
<string key="candidateClassName">NSMenuItem</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo" key="statusMenu">
|
||||
<string key="name">statusMenu</string>
|
||||
<string key="candidateClassName">NSMenu</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo" key="useICloudItem">
|
||||
<string key="name">useICloudItem</string>
|
||||
<string key="candidateClassName">NSMenuItem</string>
|
||||
</object>
|
||||
</dictionary>
|
||||
<object class="IBClassDescriptionSource" key="sourceIdentifier">
|
||||
<string key="majorKey">IBProjectSource</string>
|
||||
<string key="minorKey">./Classes/MPAppDelegate.h</string>
|
||||
</object>
|
||||
</object>
|
||||
</array>
|
||||
</object>
|
||||
<int key="IBDocument.localizationMode">0</int>
|
||||
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
|
||||
<integer value="1070" key="NS.object.0"/>
|
||||
</object>
|
||||
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
|
||||
<int key="IBDocument.defaultPropertyAccessControl">3</int>
|
||||
<dictionary class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
|
||||
<string key="NSMenuCheckmark">{11, 11}</string>
|
||||
<string key="NSMenuMixedState">{10, 3}</string>
|
||||
</dictionary>
|
||||
<bool key="IBDocument.UseAutolayout">YES</bool>
|
||||
</data>
|
||||
</archive>
|
||||
14
MasterPassword/Mac/main.m
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// main.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 04/03/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
return NSApplicationMain(argc, (const char **)argv);
|
||||
}
|
||||
@@ -4,13 +4,12 @@
|
||||
<dict>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword</string>
|
||||
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.Mac</string>
|
||||
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.shared</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword</string>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.lyndir.lhunath.MasterPassword</string>
|
||||
</array>
|
||||
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.shared</string>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="872" systemVersion="11C74" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1171" systemVersion="11E53" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" syncable="YES">
|
||||
<attribute name="keyID" attributeType="Binary" indexed="YES" syncable="YES" isSyncIdentityProperty="YES"/>
|
||||
<attribute name="lastUsed" attributeType="Date" syncable="YES"/>
|
||||
<attribute name="mpHashHex" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="257" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES" isSyncIdentityProperty="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="16" syncable="YES"/>
|
||||
<attribute name="uses" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||
<attribute name="counter" optional="YES" attributeType="Integer 16" defaultValueString="1" syncable="YES"/>
|
||||
<attribute name="counter" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
||||
</entity>
|
||||
<fetchRequest name="MPSearchElement" entity="MPElementEntity" predicateString="name BEGINSWITH[cd] $query AND mpHashHex == $mpHashHex"/>
|
||||
<elements>
|
||||
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="135"/>
|
||||
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="120"/>
|
||||
<element name="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||
</elements>
|
||||
|
||||
|
Before Width: | Height: | Size: 1023 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |