mirror of
https://github.com/MacPass/MacPass.git
synced 2025-12-14 08:12:28 +00:00
Fixed errors that would occur if undo was mixed with emptying the trash Updated localizations Added localized descriptions Fixed #31 Database name is displayed, if none is given fallback is used
483 lines
14 KiB
Objective-C
483 lines
14 KiB
Objective-C
//
|
|
// MPDocument.m
|
|
// MacPass
|
|
//
|
|
// Created by Michael Starke on 08.05.13.
|
|
// Copyright (c) 2013 HicknHack Software GmbH. All rights reserved.
|
|
//
|
|
|
|
#import "MPDocument.h"
|
|
#import "MPDocumentWindowController.h"
|
|
|
|
#import "MPDatabaseVersion.h"
|
|
#import "MPRootAdapter.h"
|
|
#import "MPIconHelper.h"
|
|
#import "MPActionHelper.h"
|
|
|
|
#import "KdbLib.h"
|
|
#import "Kdb3Node.h"
|
|
#import "Kdb4Node.h"
|
|
#import "KdbPassword.h"
|
|
#import "KdbGroup+Undo.h"
|
|
#import "KdbGroup+KVOAdditions.h"
|
|
#import "Kdb4Entry+KVOAdditions.h"
|
|
#import "KdbGroup+MPTreeTools.h"
|
|
#import "KdbGroup+MPAdditions.h"
|
|
#import "KdbEntry+Undo.h"
|
|
#import "Kdb3Tree+NewTree.h"
|
|
#import "Kdb4Tree+NewTree.h"
|
|
|
|
NSString *const MPDocumentDidAddGroupNotification = @"com.hicknhack.macpass.MPDocumentDidAddGroupNotification";
|
|
NSString *const MPDocumentWillDelteGroupNotification = @"com.hicknhack.macpass.MPDocumentDidDelteGroupNotification";
|
|
NSString *const MPDocumentDidAddEntryNotification = @"com.hicknhack.macpass.MPDocumentDidAddEntryNotification";
|
|
NSString *const MPDocumentWillDeleteEntryNotification = @"com.hicknhack.macpass.MPDocumentDidDeleteEntryNotification";
|
|
NSString *const MPDocumentDidRevertNotifiation = @"com.hicknhack.macpass.MPDocumentDidRevertNotifiation";
|
|
|
|
NSString *const MPDocumentEntryKey = @"MPDocumentEntryKey";
|
|
NSString *const MPDocumentGroupKey = @"MPDocumentGroupKey";
|
|
|
|
|
|
@interface MPDocument () {
|
|
@private
|
|
BOOL _didLockFile;
|
|
}
|
|
|
|
|
|
@property (strong, nonatomic) KdbTree *tree;
|
|
@property (weak, nonatomic) KdbGroup *root;
|
|
@property (weak, nonatomic, readonly) KdbPassword *passwordHash;
|
|
@property (assign) MPDatabaseVersion version;
|
|
|
|
@property (assign, nonatomic) BOOL secured;
|
|
@property (assign) BOOL decrypted;
|
|
@property (assign) BOOL readOnly;
|
|
|
|
@property (strong) NSURL *lockFileURL;
|
|
|
|
@property (readonly) BOOL useTrash;
|
|
@property (weak, readonly) KdbGroup *trash;
|
|
@property (strong) IBOutlet NSView *warningView;
|
|
@property (weak) IBOutlet NSImageView *warningViewImage;
|
|
|
|
@end
|
|
|
|
|
|
@implementation MPDocument
|
|
|
|
- (id)init
|
|
{
|
|
return [self initWithVersion:MPDatabaseVersion4];
|
|
}
|
|
#pragma mark NSDocument essentials
|
|
- (id)initWithVersion:(MPDatabaseVersion)version {
|
|
self = [super init];
|
|
if(self) {
|
|
_didLockFile = NO;
|
|
_decrypted = YES;
|
|
_secured = NO;
|
|
_locked = NO;
|
|
_readOnly = NO;
|
|
_rootAdapter = [[MPRootAdapter alloc] init];
|
|
_version = version;
|
|
switch(_version) {
|
|
case MPDatabaseVersion3:
|
|
self.tree = [Kdb3Tree templateTree];
|
|
break;
|
|
case MPDatabaseVersion4:
|
|
self.tree = [Kdb4Tree templateTree];
|
|
//self.tree = [Kdb4Tree demoTree];
|
|
break;
|
|
default:
|
|
self = nil;
|
|
return nil;
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self _cleanupLock];
|
|
}
|
|
|
|
- (void)makeWindowControllers {
|
|
MPDocumentWindowController *windowController = [[MPDocumentWindowController alloc] init];
|
|
[self addWindowController:windowController];
|
|
}
|
|
|
|
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
|
|
{
|
|
[super windowControllerDidLoadNib:aController];
|
|
}
|
|
|
|
- (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError {
|
|
NSError *error = nil;
|
|
[KdbWriterFactory persist:self.tree fileURL:url withPassword:self.passwordHash error:&error];
|
|
if(error) {
|
|
NSLog(@"%@", [error localizedDescription]);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError {
|
|
/* FIXME: Logfile handling
|
|
self.lockFileURL = [url URLByAppendingPathExtension:@"lock"];
|
|
if([[NSFileManager defaultManager] fileExistsAtPath:[_lockFileURL path]]) {
|
|
self.readOnly = YES;
|
|
}
|
|
else {
|
|
[[NSFileManager defaultManager] createFileAtPath:[_lockFileURL path] contents:nil attributes:nil];
|
|
_didLockFile = YES;
|
|
self.readOnly = NO;
|
|
}
|
|
*/
|
|
self.decrypted = NO;
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)revertToContentsOfURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError {
|
|
self.tree = nil;
|
|
if([self readFromURL:absoluteURL ofType:typeName error:outError]) {
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPDocumentDidRevertNotifiation object:self];
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)isEntireFileLoaded {
|
|
return _decrypted;
|
|
}
|
|
|
|
- (void)close {
|
|
[self _cleanupLock];
|
|
[super close];
|
|
}
|
|
|
|
#pragma mark Protection
|
|
|
|
- (BOOL)decryptWithPassword:(NSString *)password keyFileURL:(NSURL *)keyFileURL {
|
|
self.key = keyFileURL;
|
|
self.password = [password length] > 0 ? password : nil;
|
|
@try {
|
|
self.tree = [KdbReaderFactory load:[[self fileURL] path] withPassword:self.passwordHash];
|
|
}
|
|
@catch (NSException *exception) {
|
|
return NO;
|
|
}
|
|
|
|
if([self.tree isKindOfClass:[Kdb4Tree class]]) {
|
|
self.version = MPDatabaseVersion4;
|
|
}
|
|
else if( [self.tree isKindOfClass:[Kdb3Tree class]]) {
|
|
self.version = MPDatabaseVersion3;
|
|
}
|
|
self.decrypted = YES;
|
|
return YES;
|
|
}
|
|
|
|
- (void)setPassword:(NSString *)password {
|
|
if(![_password isEqualToString:password]) {
|
|
_password = [password copy];
|
|
[self _updateIsSecured];
|
|
}
|
|
}
|
|
|
|
- (void)setKey:(NSURL *)key {
|
|
if(![[_key absoluteString] isEqualToString:[key absoluteString]]) {
|
|
_key = key;
|
|
[self _updateIsSecured];
|
|
}
|
|
}
|
|
|
|
- (KdbPassword *)passwordHash {
|
|
|
|
return [[KdbPassword alloc] initWithPassword:self.password passwordEncoding:NSUTF8StringEncoding keyFileURL:self.key];
|
|
}
|
|
|
|
+ (BOOL)autosavesInPlace
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)prepareSavePanel:(NSSavePanel *)savePanel {
|
|
if(self.isSecured) {
|
|
[savePanel setAccessoryView:nil];
|
|
return YES;
|
|
}
|
|
if(!self.warningView) {
|
|
[[NSBundle mainBundle] loadNibNamed:@"UnprotectedWarningView" owner:self topLevelObjects:nil];
|
|
[self.warningViewImage setImage:[NSImage imageNamed:NSImageNameCaution]];
|
|
}
|
|
[savePanel setAccessoryView:self.warningView];
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark Data Accesors
|
|
- (void)setTree:(KdbTree *)tree {
|
|
if(_tree != tree) {
|
|
_tree = tree;
|
|
self.rootAdapter.tree = _tree;
|
|
}
|
|
}
|
|
|
|
- (KdbGroup *)root {
|
|
return self.tree.root;
|
|
}
|
|
|
|
- (KdbEntry *)findEntry:(UUID *)uuid {
|
|
return [self.root entryForUUID:uuid];
|
|
}
|
|
|
|
- (KdbGroup *)findGroup:(UUID *)uuid {
|
|
return [self.root groupForUUID:uuid];
|
|
}
|
|
|
|
- (Kdb3Tree *)treeV3 {
|
|
switch (_version) {
|
|
case MPDatabaseVersion3:
|
|
NSAssert([self.tree isKindOfClass:[Kdb3Tree class]], @"Tree has to be Version3");
|
|
return (Kdb3Tree *)self.tree;
|
|
case MPDatabaseVersion4:
|
|
return nil;
|
|
default:
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (Kdb4Tree *)treeV4 {
|
|
switch (_version) {
|
|
case MPDatabaseVersion3:
|
|
return nil;
|
|
case MPDatabaseVersion4:
|
|
NSAssert([self.tree isKindOfClass:[Kdb4Tree class]], @"Tree has to be Version4");
|
|
return (Kdb4Tree *)self.tree;
|
|
default:
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (BOOL)useTrash {
|
|
if(self.treeV4) {
|
|
return self.treeV4.recycleBinEnabled;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (KdbGroup *)trash {
|
|
static KdbGroup *_trash = nil;
|
|
if(self.useTrash) {
|
|
BOOL trashValid = [((Kdb4Group *)_trash).uuid isEqual:self.treeV4.recycleBinUuid];
|
|
if(!trashValid) {
|
|
_trash = [self findGroup:self.treeV4.recycleBinUuid];
|
|
}
|
|
return _trash;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (void)useGroupAsTrash:(KdbGroup *)group {
|
|
if(self.useTrash) {
|
|
Kdb4Group *groupv4 = (Kdb4Group *)group;
|
|
if(![self.treeV4.recycleBinUuid isEqual:groupv4.uuid]) {
|
|
self.treeV4.recycleBinUuid = groupv4.uuid;
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark Data manipulation
|
|
- (KdbEntry *)createEntry:(KdbGroup *)parent {
|
|
if(!parent) {
|
|
return nil; // No parent
|
|
}
|
|
KdbEntry *newEntry = [self.tree createEntry:parent];
|
|
newEntry.title = NSLocalizedString(@"DEFAULT_ENTRY_TITLE", @"Title for a newly created entry");
|
|
if(self.treeV4 && ([self.treeV4.defaultUserName length] > 0)) {
|
|
newEntry.title = self.treeV4.defaultUserName;
|
|
}
|
|
[parent addEntryUndoable:newEntry atIndex:[parent.entries count]];
|
|
NSDictionary *userInfo = @{ MPDocumentEntryKey : newEntry };
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPDocumentDidAddEntryNotification object:self userInfo:userInfo];
|
|
return newEntry;
|
|
}
|
|
|
|
- (KdbGroup *)createGroup:(KdbGroup *)parent {
|
|
if(!parent) {
|
|
return nil; // no parent!
|
|
}
|
|
KdbGroup *newGroup = [self.tree createGroup:parent];
|
|
newGroup.name = NSLocalizedString(@"DEFAULT_GROUP_NAME", @"Title for a newly created group");
|
|
newGroup.image = MPIconFolder;
|
|
[parent addGroupUndoable:newGroup atIndex:[parent.groups count]];
|
|
NSDictionary *userInfo = @{ MPDocumentGroupKey : newGroup };
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPDocumentDidAddGroupNotification object:self userInfo:userInfo];
|
|
return newGroup;
|
|
}
|
|
|
|
- (StringField *)createStringField:(KdbEntry *)entry {
|
|
// TODO: Localize!
|
|
if(![entry isKindOfClass:[Kdb4Entry class]]) {
|
|
return nil;
|
|
}
|
|
Kdb4Entry *entryV4 = (Kdb4Entry *)entry;
|
|
NSString *title = NSLocalizedString(@"DEFAULT_CUSTOM_FIELD_TITLE", @"Default Titel for new Custom-Fields");
|
|
NSString *value = NSLocalizedString(@"DEFAULT_CUSTOM_FIELD_VALUE", @"Default Value for new Custom-Fields");
|
|
StringField *newStringField = [StringField stringFieldWithKey:title andValue:value];
|
|
[self addStringField:newStringField toEntry:entryV4 atIndex:[entryV4.stringFields count]];
|
|
return newStringField;
|
|
}
|
|
|
|
|
|
- (void)moveGroup:(KdbGroup *)group toGroup:(KdbGroup *)target index:(NSInteger)index {
|
|
NSInteger oldIndex = [group.parent.groups indexOfObject:group];
|
|
if(group.parent == target && oldIndex == index) {
|
|
return; // No changes
|
|
}
|
|
[[[self undoManager] prepareWithInvocationTarget:self] moveGroup:group toGroup:group.parent index:oldIndex];
|
|
if(self.trash == target) {
|
|
[[self undoManager] setActionName:@"UNDO_DELETE_GROUP"];
|
|
}
|
|
else {
|
|
[[self undoManager] setActionName:@"MOVE_GROUP"];
|
|
}
|
|
[group.parent removeObjectFromGroupsAtIndex:oldIndex];
|
|
if(index < 0 || index > [target.groups count] ) {
|
|
index = [target.groups count];
|
|
}
|
|
[target insertObject:group inGroupsAtIndex:index];
|
|
}
|
|
|
|
- (BOOL)group:(KdbGroup *)group isMoveableToGroup:(KdbGroup *)target {
|
|
if(target == nil) {
|
|
return NO;
|
|
}
|
|
BOOL isMovable = YES;
|
|
|
|
KdbGroup *ancestor = target.parent;
|
|
while(ancestor.parent) {
|
|
if(ancestor == group) {
|
|
isMovable = NO;
|
|
break;
|
|
}
|
|
ancestor = ancestor.parent;
|
|
}
|
|
return isMovable;
|
|
}
|
|
|
|
- (void)deleteEntry:(KdbEntry *)entry {
|
|
if(self.useTrash) {
|
|
if(!self.trash) {
|
|
[self _createTrashGroup];
|
|
}
|
|
[entry moveToTrashUndoable:self.trash atIndex:[self.trash.entries count]];
|
|
}
|
|
else {
|
|
[entry deleteUndoable];
|
|
}
|
|
}
|
|
|
|
- (void)deleteGroup:(KdbGroup *)group {
|
|
if(self.useTrash) {
|
|
if(!self.trash) {
|
|
[self _createTrashGroup];
|
|
}
|
|
[group moveToTrashUndoable:self.trash atIndex:[self.trash.groups count]];
|
|
}
|
|
else {
|
|
[group deleteUndoable];
|
|
}
|
|
}
|
|
|
|
#pragma mark CustomFields
|
|
|
|
- (void)addStringField:(StringField *)field toEntry:(Kdb4Entry *)entry atIndex:(NSUInteger)index {
|
|
[[[self undoManager] prepareWithInvocationTarget:self] removeStringField:field formEntry:entry];
|
|
[[self undoManager] setActionName:NSLocalizedString(@"UNDO_ADD_STRING_FIELD", @"Add Stringfield Undo")];
|
|
[entry insertObject:field inStringFieldsAtIndex:index];
|
|
}
|
|
|
|
- (void)removeStringField:(StringField *)field formEntry:(Kdb4Entry *)entry {
|
|
NSInteger index = [entry.stringFields indexOfObject:field];
|
|
if(NSNotFound == index) {
|
|
return; // Nothing found to be removed
|
|
}
|
|
[[[self undoManager] prepareWithInvocationTarget:self] addStringField:field toEntry:entry atIndex:index];
|
|
[[self undoManager] setActionName:NSLocalizedString(@"UNDO_DELETE_STRING_FIELD", @"Delte Stringfield undo")];
|
|
[entry removeObjectFromStringFieldsAtIndex:index];
|
|
}
|
|
|
|
#pragma mark Actions
|
|
|
|
- (void)emptyTrash:(id)sender {
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
[alert setAlertStyle:NSWarningAlertStyle];
|
|
[alert setMessageText:NSLocalizedString(@"WARNING_ON_EMPTY_TRASH_TITLE", "")];
|
|
[alert setInformativeText:NSLocalizedString(@"WARNING_ON_EMPTY_TRASH_DESCRIPTION", "Informative Text displayed when clearing the Trash")];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"EMPTY_TRASH", "Empty Trash")];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"CANCEL", "Cancel")];
|
|
|
|
[[alert buttons][1] setKeyEquivalent:[NSString stringWithFormat:@"%c", 0x1b]];
|
|
|
|
NSWindow *window = [[self windowControllers][0] window];
|
|
[alert beginSheetModalForWindow:window modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:NULL];
|
|
}
|
|
|
|
- (void) alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
|
|
if(returnCode == NSAlertFirstButtonReturn) {
|
|
[self _emptyTrash];
|
|
}
|
|
}
|
|
|
|
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
|
|
if([menuItem action] == [MPActionHelper actionOfType:MPActionEmptyTrash]) {
|
|
BOOL hasGroups = [self.trash.groups count] > 0;
|
|
BOOL hasEntries = [self.trash.entries count] > 0;
|
|
return (hasEntries || hasGroups);
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark Private
|
|
- (void)_updateIsSecured {
|
|
BOOL securePassword = ([self.password length] > 0);
|
|
BOOL secureKey = (nil != self.key);
|
|
self.secured = (secureKey || securePassword);
|
|
}
|
|
|
|
- (void)_cleanupLock {
|
|
if(_didLockFile) {
|
|
[[NSFileManager defaultManager] removeItemAtURL:_lockFileURL error:nil];
|
|
_didLockFile = NO;
|
|
}
|
|
}
|
|
|
|
- (KdbGroup *)_createTrashGroup {
|
|
/* Maybe push the stuff to the Tree? */
|
|
if(self.version == MPDatabaseVersion3) {
|
|
return nil;
|
|
}
|
|
else if(self.version == MPDatabaseVersion4) {
|
|
KdbGroup *trash = [self.tree createGroup:self.tree.root];
|
|
trash.name = NSLocalizedString(@"TRASH", @"Name for the trash group");
|
|
trash.image = MPIconTrash;
|
|
[self.tree.root insertObject:trash inGroupsAtIndex:[self.tree.root.groups count]];
|
|
self.treeV4.recycleBinUuid = ((Kdb4Group *)trash).uuid;
|
|
return trash;
|
|
}
|
|
else {
|
|
NSAssert(NO, @"Database with unknown version: %ld", _version);
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (void)_emptyTrash {
|
|
for(KdbEntry *entry in [self.trash childEntries]) {
|
|
[[self undoManager] removeAllActionsWithTarget:entry];
|
|
}
|
|
for(KdbGroup *group in [self.trash childGroups]) {
|
|
[[self undoManager] removeAllActionsWithTarget:group];
|
|
}
|
|
[self.trash clear];
|
|
}
|
|
|
|
@end
|