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"]
|
[submodule "External/Pearl"]
|
||||||
path = External/Pearl
|
path = External/Pearl
|
||||||
url = git@github.com:Lyndir/Pearl.git
|
url = git://github.com/Lyndir/Pearl.git
|
||||||
[submodule "External/InAppSettingsKit"]
|
[submodule "External/InAppSettingsKit"]
|
||||||
path = External/InAppSettingsKit
|
path = External/InAppSettingsKit
|
||||||
url = git://github.com/futuretap/InAppSettingsKit.git
|
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
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "self:MasterPassword.xcodeproj">
|
location = "self:MasterPassword-Mac.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
</Workspace>
|
</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">
|
buildImplicitDependencies = "YES">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "NO"
|
||||||
buildForRunning = "YES"
|
buildForRunning = "YES"
|
||||||
buildForProfiling = "YES"
|
buildForProfiling = "YES"
|
||||||
buildForArchiving = "YES"
|
buildForArchiving = "YES"
|
||||||
@@ -16,16 +16,16 @@
|
|||||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||||
BuildableName = "MasterPassword.app"
|
BuildableName = "MasterPassword.app"
|
||||||
BlueprintName = "MasterPassword"
|
BlueprintName = "MasterPassword"
|
||||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.GDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
buildConfiguration = "Production">
|
buildConfiguration = "Debug">
|
||||||
<Testables>
|
<Testables>
|
||||||
</Testables>
|
</Testables>
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
@@ -34,16 +34,17 @@
|
|||||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||||
BuildableName = "MasterPassword.app"
|
BuildableName = "MasterPassword.app"
|
||||||
BlueprintName = "MasterPassword"
|
BlueprintName = "MasterPassword"
|
||||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.GDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
buildConfiguration = "Production"
|
buildConfiguration = "AppStore"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable>
|
<BuildableProductRunnable>
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||||
BuildableName = "MasterPassword.app"
|
BuildableName = "MasterPassword.app"
|
||||||
BlueprintName = "MasterPassword"
|
BlueprintName = "MasterPassword"
|
||||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
<AdditionalOptions>
|
<AdditionalOptions>
|
||||||
@@ -64,18 +65,9 @@
|
|||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
buildConfiguration = "Production"
|
buildConfiguration = "Production"
|
||||||
debugDocumentVersioning = "YES">
|
debugDocumentVersioning = "YES">
|
||||||
<BuildableProductRunnable>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
|
||||||
BuildableName = "MasterPassword.app"
|
|
||||||
BlueprintName = "MasterPassword"
|
|
||||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
<AnalyzeAction
|
<AnalyzeAction
|
||||||
buildConfiguration = "Production">
|
buildConfiguration = "Debug">
|
||||||
</AnalyzeAction>
|
</AnalyzeAction>
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
buildConfiguration = "Production"
|
buildConfiguration = "Production"
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
|
LastUpgradeVersion = "0430"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "NO"
|
||||||
buildForRunning = "YES"
|
buildForRunning = "YES"
|
||||||
buildForProfiling = "YES"
|
buildForProfiling = "YES"
|
||||||
buildForArchiving = "YES"
|
buildForArchiving = "YES"
|
||||||
@@ -16,14 +17,14 @@
|
|||||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||||
BuildableName = "MasterPassword.app"
|
BuildableName = "MasterPassword.app"
|
||||||
BlueprintName = "MasterPassword"
|
BlueprintName = "MasterPassword"
|
||||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.GDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
<Testables>
|
<Testables>
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||||
BuildableName = "MasterPassword.app"
|
BuildableName = "MasterPassword.app"
|
||||||
BlueprintName = "MasterPassword"
|
BlueprintName = "MasterPassword"
|
||||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable>
|
<BuildableProductRunnable>
|
||||||
@@ -52,17 +54,28 @@
|
|||||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||||
BuildableName = "MasterPassword.app"
|
BuildableName = "MasterPassword.app"
|
||||||
BlueprintName = "MasterPassword"
|
BlueprintName = "MasterPassword"
|
||||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.coredata.ubiquity.logLevel 3"
|
||||||
|
isEnabled = "NO">
|
||||||
|
</CommandLineArgument>
|
||||||
|
</CommandLineArguments>
|
||||||
<AdditionalOptions>
|
<AdditionalOptions>
|
||||||
|
<AdditionalOption
|
||||||
|
key = "NSZombieEnabled"
|
||||||
|
value = "YES"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</AdditionalOption>
|
||||||
</AdditionalOptions>
|
</AdditionalOptions>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
savedToolIdentifier = ""
|
savedToolIdentifier = ""
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "AdHoc"
|
||||||
debugDocumentVersioning = "YES">
|
debugDocumentVersioning = "YES">
|
||||||
<BuildableProductRunnable>
|
<BuildableProductRunnable>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
@@ -70,7 +83,7 @@
|
|||||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||||
BuildableName = "MasterPassword.app"
|
BuildableName = "MasterPassword.app"
|
||||||
BlueprintName = "MasterPassword"
|
BlueprintName = "MasterPassword"
|
||||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
@@ -78,7 +91,7 @@
|
|||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
</AnalyzeAction>
|
</AnalyzeAction>
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "AdHoc"
|
||||||
revealArchiveInOrganizer = "YES">
|
revealArchiveInOrganizer = "YES">
|
||||||
</ArchiveAction>
|
</ArchiveAction>
|
||||||
</Scheme>
|
</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.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
@interface MPConfig : Config
|
@interface MPConfig : PearlConfig
|
||||||
|
|
||||||
@property (nonatomic, retain) NSNumber *dataStoreError;
|
@property (nonatomic, retain) NSNumber *saveKey;
|
||||||
@property (nonatomic, retain) NSNumber *storeKeyPhrase;
|
@property (nonatomic, retain) NSNumber *rememberKey;
|
||||||
@property (nonatomic, retain) NSNumber *rememberKeyPhrase;
|
|
||||||
@property (nonatomic, retain) NSNumber *forgetKeyPhrase;
|
@property (nonatomic, retain) NSNumber *iCloud;
|
||||||
@property (nonatomic, retain) NSNumber *helpHidden;
|
@property (nonatomic, retain) NSNumber *iCloudDecided;
|
||||||
@property (nonatomic, retain) NSNumber *showQuickStart;
|
|
||||||
|
|
||||||
+ (MPConfig *)get;
|
+ (MPConfig *)get;
|
||||||
|
|
||||||
|
|||||||
@@ -7,26 +7,27 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "MPConfig.h"
|
#import "MPConfig.h"
|
||||||
|
#import "MPAppDelegate.h"
|
||||||
|
|
||||||
@implementation MPConfig
|
@implementation MPConfig
|
||||||
|
@dynamic saveKey, rememberKey, iCloud, iCloudDecided;
|
||||||
@dynamic dataStoreError, storeKeyPhrase, rememberKeyPhrase, forgetKeyPhrase, helpHidden, showQuickStart;
|
|
||||||
|
|
||||||
|
|
||||||
- (id)init {
|
- (id)init {
|
||||||
|
|
||||||
if(!(self = [super init]))
|
if(!(self = [super init]))
|
||||||
return self;
|
return nil;
|
||||||
|
|
||||||
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(dataStoreError)),
|
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(askForReviews)),
|
||||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(storeKeyPhrase)),
|
|
||||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKeyPhrase)),
|
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(saveKey)),
|
||||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(forgetKeyPhrase)),
|
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKey)),
|
||||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(helpHidden)),
|
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloud)),
|
||||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(showQuickStart)),
|
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloudDecided)),
|
||||||
nil]];
|
nil]];
|
||||||
|
|
||||||
|
self.delegate = [MPAppDelegate get];
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,15 @@
|
|||||||
@interface MPElementEntity : NSManagedObject
|
@interface MPElementEntity : NSManagedObject
|
||||||
|
|
||||||
@property (nonatomic, retain) NSString *name;
|
@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 type;
|
||||||
@property (nonatomic, assign) int16_t uses;
|
@property (nonatomic, assign) int16_t uses;
|
||||||
@property (nonatomic, assign) NSTimeInterval lastUsed;
|
@property (nonatomic, assign) NSTimeInterval lastUsed;
|
||||||
|
|
||||||
@property (nonatomic, retain, readonly) id content;
|
@property (nonatomic, retain, readonly) id content;
|
||||||
|
|
||||||
- (void)use;
|
- (int16_t)use;
|
||||||
|
- (NSString *)exportContent;
|
||||||
|
- (void)importContent:(NSString *)content;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -12,15 +12,15 @@
|
|||||||
@implementation MPElementEntity
|
@implementation MPElementEntity
|
||||||
|
|
||||||
@dynamic name;
|
@dynamic name;
|
||||||
@dynamic mpHashHex;
|
@dynamic keyID;
|
||||||
@dynamic type;
|
@dynamic type;
|
||||||
@dynamic uses;
|
@dynamic uses;
|
||||||
@dynamic lastUsed;
|
@dynamic lastUsed;
|
||||||
|
|
||||||
- (void)use {
|
- (int16_t)use {
|
||||||
|
|
||||||
++self.uses;
|
|
||||||
self.lastUsed = [[NSDate date] timeIntervalSinceReferenceDate];
|
self.lastUsed = [[NSDate date] timeIntervalSinceReferenceDate];
|
||||||
|
return ++self.uses;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)content {
|
- (id)content {
|
||||||
@@ -28,15 +28,24 @@
|
|||||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSString *)exportContent {
|
||||||
|
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)importContent:(NSString *)content {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
- (NSString *)description {
|
- (NSString *)description {
|
||||||
|
|
||||||
return [[self content] description];
|
return PearlString(@"%@:%@", [self class], [self name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)debugDescription {
|
- (NSString *)debugDescription {
|
||||||
|
|
||||||
return [NSString stringWithFormat:@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}",
|
return PearlString(@"{%@: name=%@, keyID=%@, type=%d, uses=%d, lastUsed=%@}",
|
||||||
NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed];
|
NSStringFromClass([self class]), self.name, self.keyID, self.type, self.uses, self.lastUsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -13,6 +13,6 @@
|
|||||||
|
|
||||||
@interface MPElementGeneratedEntity : MPElementEntity
|
@interface MPElementGeneratedEntity : MPElementEntity
|
||||||
|
|
||||||
@property (nonatomic, assign) int16_t counter;
|
@property (nonatomic, assign) int32_t counter;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#import "MPElementGeneratedEntity.h"
|
#import "MPElementGeneratedEntity.h"
|
||||||
#import "MPAppDelegate.h"
|
#import "MPAppDelegate.h"
|
||||||
|
#import "MPAppDelegate_Key.h"
|
||||||
|
|
||||||
|
|
||||||
@implementation MPElementGeneratedEntity
|
@implementation MPElementGeneratedEntity
|
||||||
@@ -16,16 +17,15 @@
|
|||||||
|
|
||||||
- (id)content {
|
- (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])
|
if (![self.name length])
|
||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
if (self.type & MPElementTypeClassCalculated)
|
return MPCalculateContent((unsigned)self.type, self.name, [MPAppDelegate get].key, self.counter);
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#import "MPElementStoredEntity.h"
|
#import "MPElementStoredEntity.h"
|
||||||
#import "MPAppDelegate.h"
|
#import "MPAppDelegate.h"
|
||||||
|
#import "MPAppDelegate_Key.h"
|
||||||
|
|
||||||
@interface MPElementStoredEntity ()
|
@interface MPElementStoredEntity ()
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
|
|
||||||
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
|
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
|
||||||
|
|
||||||
return [KeyChain createQueryForClass:kSecClassGenericPassword
|
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
@"DevicePrivate", (__bridge id)kSecAttrService,
|
@"DevicePrivate", (__bridge id)kSecAttrService,
|
||||||
name, (__bridge id)kSecAttrAccount,
|
name, (__bridge id)kSecAttrAccount,
|
||||||
@@ -34,32 +35,42 @@
|
|||||||
assert(self.type & MPElementTypeClassStored);
|
assert(self.type & MPElementTypeClassStored);
|
||||||
|
|
||||||
NSData *encryptedContent;
|
NSData *encryptedContent;
|
||||||
if (self.type == MPElementTypeStoredDevicePrivate)
|
if (self.type & MPElementFeatureDevicePrivate)
|
||||||
encryptedContent = [KeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
||||||
else
|
else
|
||||||
encryptedContent = self.contentObject;
|
encryptedContent = self.contentObject;
|
||||||
|
|
||||||
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get].keyPhrase
|
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
||||||
dataUsingEncoding:NSUTF8StringEncoding]
|
padding:YES];
|
||||||
usePadding:YES];
|
|
||||||
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setContent:(id)content {
|
- (void)setContent:(id)content {
|
||||||
|
|
||||||
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get].keyPhrase
|
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
||||||
dataUsingEncoding:NSUTF8StringEncoding]
|
padding:YES];
|
||||||
usePadding:YES];
|
|
||||||
|
|
||||||
if (self.type == MPElementTypeStoredDevicePrivate) {
|
if (self.type & MPElementFeatureDevicePrivate) {
|
||||||
[KeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
||||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
encryptedContent, (__bridge id)kSecValueData,
|
encryptedContent, (__bridge id)kSecValueData,
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
||||||
|
#endif
|
||||||
nil]];
|
nil]];
|
||||||
self.contentObject = nil;
|
self.contentObject = nil;
|
||||||
} else
|
} else
|
||||||
self.contentObject = encryptedContent;
|
self.contentObject = encryptedContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSString *)exportContent {
|
||||||
|
|
||||||
|
return [self.contentObject encodeBase64];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)importContent:(NSString *)content {
|
||||||
|
|
||||||
|
self.contentObject = [content decodeBase64];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@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;
|
} MPElementContentType;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
MPElementTypeClassCalculated = 2 << 7,
|
/** Generate the password. */
|
||||||
MPElementTypeClassStored = 2 << 8,
|
MPElementTypeClassGenerated = 1 << 4,
|
||||||
|
/** Store the password. */
|
||||||
|
MPElementTypeClassStored = 1 << 5,
|
||||||
} MPElementTypeClass;
|
} MPElementTypeClass;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
MPElementTypeCalculatedLong = MPElementTypeClassCalculated | 0x01,
|
/** Export the key-protected content data. */
|
||||||
MPElementTypeCalculatedMedium = MPElementTypeClassCalculated | 0x02,
|
MPElementFeatureExportContent = 1 << 10,
|
||||||
MPElementTypeCalculatedShort = MPElementTypeClassCalculated | 0x03,
|
/** Never export content. */
|
||||||
MPElementTypeCalculatedBasic = MPElementTypeClassCalculated | 0x04,
|
MPElementFeatureDevicePrivate = 1 << 11,
|
||||||
MPElementTypeCalculatedPIN = MPElementTypeClassCalculated | 0x05,
|
} MPElementFeature;
|
||||||
|
|
||||||
MPElementTypeStoredPersonal = MPElementTypeClassStored | 0x01,
|
typedef enum {
|
||||||
MPElementTypeStoredDevicePrivate = MPElementTypeClassStored | 0x02,
|
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;
|
} MPElementType;
|
||||||
|
|
||||||
#ifndef PRODUCTION
|
|
||||||
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
||||||
#define MPTestFlightCheckpointHelpChapter @"MPTestFlightCheckpointHelpChapter_%@"
|
#define MPTestFlightCheckpointHelpChapter @"MPTestFlightCheckpointHelpChapter_%@"
|
||||||
#define MPTestFlightCheckpointCopyToPasteboard @"MPTestFlightCheckpointCopyToPasteboard"
|
#define MPTestFlightCheckpointCopyToPasteboard @"MPTestFlightCheckpointCopyToPasteboard"
|
||||||
|
#define MPTestFlightCheckpointResetPasswordCounter @"MPTestFlightCheckpointResetPasswordCounter"
|
||||||
#define MPTestFlightCheckpointIncrementPasswordCounter @"MPTestFlightCheckpointIncrementPasswordCounter"
|
#define MPTestFlightCheckpointIncrementPasswordCounter @"MPTestFlightCheckpointIncrementPasswordCounter"
|
||||||
#define MPTestFlightCheckpointEditPassword @"MPTestFlightCheckpointEditPassword"
|
#define MPTestFlightCheckpointEditPassword @"MPTestFlightCheckpointEditPassword"
|
||||||
#define MPTestFlightCheckpointCloseAlert @"MPTestFlightCheckpointCloseAlert"
|
#define MPTestFlightCheckpointCloseAlert @"MPTestFlightCheckpointCloseAlert"
|
||||||
@@ -49,15 +58,28 @@ typedef enum {
|
|||||||
#define MPTestFlightCheckpointDeactivated @"MPTestFlightCheckpointDeactivated"
|
#define MPTestFlightCheckpointDeactivated @"MPTestFlightCheckpointDeactivated"
|
||||||
#define MPTestFlightCheckpointTerminated @"MPTestFlightCheckpointTerminated"
|
#define MPTestFlightCheckpointTerminated @"MPTestFlightCheckpointTerminated"
|
||||||
#define MPTestFlightCheckpointShowGuide @"MPTestFlightCheckpointShowGuide"
|
#define MPTestFlightCheckpointShowGuide @"MPTestFlightCheckpointShowGuide"
|
||||||
|
#define MPTestFlightCheckpointMPForgotten @"MPTestFlightCheckpointMPForgotten"
|
||||||
#define MPTestFlightCheckpointMPChanged @"MPTestFlightCheckpointMPChanged"
|
#define MPTestFlightCheckpointMPChanged @"MPTestFlightCheckpointMPChanged"
|
||||||
#define MPTestFlightCheckpointMPUnstored @"MPTestFlightCheckpointMPUnstored"
|
#define MPTestFlightCheckpointMPUnstored @"MPTestFlightCheckpointMPUnstored"
|
||||||
#define MPTestFlightCheckpointMPMismatch @"MPTestFlightCheckpointMPMismatch"
|
#define MPTestFlightCheckpointMPMismatch @"MPTestFlightCheckpointMPMismatch"
|
||||||
#define MPTestFlightCheckpointMPAsked @"MPTestFlightCheckpointMPAsked"
|
#define MPTestFlightCheckpointMPEntered @"MPTestFlightCheckpointMPEntered"
|
||||||
#define MPTestFlightCheckpointStoreIncompatible @"MPTestFlightCheckpointStoreIncompatible"
|
#define MPTestFlightCheckpointLocalStoreIncompatible @"MPTestFlightCheckpointLocalStoreIncompatible"
|
||||||
#define MPTestFlightCheckpointSetKeyphraseLength @"MPTestFlightCheckpointSetKeyphraseLength_%d"
|
#define MPTestFlightCheckpointCloudStoreIncompatible @"MPTestFlightCheckpointCloudStoreIncompatible"
|
||||||
#endif
|
#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 *NSStringFromMPElementType(MPElementType type);
|
||||||
NSString *ClassNameFromMPElementType(MPElementType type);
|
NSString *ClassNameFromMPElementType(MPElementType type);
|
||||||
Class ClassFromMPElementType(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"
|
#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) {
|
NSString *NSStringFromMPElementType(MPElementType type) {
|
||||||
|
|
||||||
if (!type)
|
if (!type)
|
||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MPElementTypeCalculatedLong:
|
case MPElementTypeGeneratedLong:
|
||||||
return @"Long Password";
|
return @"Long Password";
|
||||||
|
|
||||||
case MPElementTypeCalculatedMedium:
|
case MPElementTypeGeneratedMedium:
|
||||||
return @"Medium Password";
|
return @"Medium Password";
|
||||||
|
|
||||||
case MPElementTypeCalculatedShort:
|
case MPElementTypeGeneratedShort:
|
||||||
return @"Short Password";
|
return @"Short Password";
|
||||||
|
|
||||||
case MPElementTypeCalculatedBasic:
|
case MPElementTypeGeneratedBasic:
|
||||||
return @"Basic Password";
|
return @"Basic Password";
|
||||||
|
|
||||||
case MPElementTypeCalculatedPIN:
|
case MPElementTypeGeneratedPIN:
|
||||||
return @"PIN";
|
return @"PIN";
|
||||||
|
|
||||||
case MPElementTypeStoredPersonal:
|
case MPElementTypeStoredPersonal:
|
||||||
@@ -49,19 +72,19 @@ Class ClassFromMPElementType(MPElementType type) {
|
|||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MPElementTypeCalculatedLong:
|
case MPElementTypeGeneratedLong:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeCalculatedMedium:
|
case MPElementTypeGeneratedMedium:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeCalculatedShort:
|
case MPElementTypeGeneratedShort:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeCalculatedBasic:
|
case MPElementTypeGeneratedBasic:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeCalculatedPIN:
|
case MPElementTypeGeneratedPIN:
|
||||||
return [MPElementGeneratedEntity class];
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
case MPElementTypeStoredPersonal:
|
case MPElementTypeStoredPersonal:
|
||||||
@@ -81,34 +104,59 @@ NSString *ClassNameFromMPElementType(MPElementType type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static NSDictionary *MPTypes_ciphers = nil;
|
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)
|
if (MPTypes_ciphers == nil)
|
||||||
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
|
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
|
||||||
withExtension:@"plist"]];
|
withExtension:@"plist"]];
|
||||||
|
|
||||||
// Determine the hash whose bytes will be used for calculating a password: md4(name-keyPhrase)
|
// Determine the seed whose bytes will be used for calculating a password: sha1(name . '\0' . key . '\0' . salt)
|
||||||
assert(name && keyPhrase);
|
uint32_t nsalt = htonl(salt);
|
||||||
NSData *keyHash = [[NSString stringWithFormat:@"%@-%@-%d", name, keyPhrase, counter] hashWith:PearlDigestSHA1];
|
trc(@"seed from: sha1(%@, %@, %u)", name, key, nsalt);
|
||||||
const char *keyBytes = keyHash.bytes;
|
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.
|
// Determine the cipher from the first seed byte.
|
||||||
assert([keyHash length]);
|
assert([seed length]);
|
||||||
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:ClassNameFromMPElementType(type)]
|
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:ClassNameFromMPElementType(type)]
|
||||||
valueForKey:NSStringFromMPElementType(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.
|
// Encode the content, character by character, using subsequent seed bytes and the cipher.
|
||||||
assert([keyHash length] >= [cipher length] + 1);
|
assert([seed length] >= [cipher length] + 1);
|
||||||
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
|
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
|
||||||
for (NSUInteger c = 0; c < [cipher length]; ++c) {
|
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 *cipherClass = [cipher substringWithRange:NSMakeRange(c, 1)];
|
||||||
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
|
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;
|
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>
|
<dict>
|
||||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||||
<array>
|
<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>
|
</array>
|
||||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||||
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword</string>
|
<string>$(TeamIdentifierPrefix)com.lyndir.lhunath.MasterPassword.shared</string>
|
||||||
<key>keychain-access-groups</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<array>
|
<true/>
|
||||||
<string>$(AppIdentifierPrefix)com.lyndir.lhunath.MasterPassword</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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">
|
<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="lastUsed" attributeType="Date" syncable="YES"/>
|
||||||
<attribute name="mpHashHex" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES" isSyncIdentityProperty="YES"/>
|
||||||
<attribute name="name" attributeType="String" indexed="YES" syncable="YES"/>
|
<attribute name="type" attributeType="Integer 16" defaultValueString="16" syncable="YES"/>
|
||||||
<attribute name="type" attributeType="Integer 16" defaultValueString="257" syncable="YES"/>
|
|
||||||
<attribute name="uses" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
<attribute name="uses" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
|
<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>
|
||||||
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||||
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<fetchRequest name="MPSearchElement" entity="MPElementEntity" predicateString="name BEGINSWITH[cd] $query AND mpHashHex == $mpHashHex"/>
|
|
||||||
<elements>
|
<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="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||||
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||||
</elements>
|
</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 |