diff options
Diffstat (limited to 'Source')
-rw-r--r-- | Source/SPConnectionController.h | 141 | ||||
-rw-r--r-- | Source/SPConnectionController.m | 827 | ||||
-rw-r--r-- | Source/SPPreferenceController.h | 19 | ||||
-rw-r--r-- | Source/SPPreferenceController.m | 166 | ||||
-rw-r--r-- | Source/TableDocument.h | 71 | ||||
-rw-r--r-- | Source/TableDocument.m | 907 |
6 files changed, 1337 insertions, 794 deletions
diff --git a/Source/SPConnectionController.h b/Source/SPConnectionController.h new file mode 100644 index 00000000..ad61cfa8 --- /dev/null +++ b/Source/SPConnectionController.h @@ -0,0 +1,141 @@ +// +// $Id: SPConnectionController.h 802 2009-06-03 20:46:57Z stuart02 $ +// +// SPConnectionController.h +// sequel-pro +// +// Created by Rowan Beentje on 28/06/2009. +// Copyright 2009 Arboreal. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> +#import "TableDocument.h" +#import "KeyChain.h" +#import "SPSSHTunnel.h" +#import "CMMCPConnection.h" + +enum spconnection_types +{ + SP_CONNECTION_TCPIP = 0, + SP_CONNECTION_SOCKET = 1, + SP_CONNECTION_SSHTUNNEL = 2 +}; + +@interface SPConnectionController : NSObject { + TableDocument *tableDocument; + NSWindow *documentWindow; + NSSplitView *contentView; + KeyChain *keychain; + NSUserDefaults *prefs; + NSMutableArray *favorites; + SPSSHTunnel *sshTunnel; + CMMCPConnection *mySQLConnection; + BOOL automaticFavoriteSelection; + + int previousType; + int type; + NSString *name; + NSString *host; + NSString *user; + NSString *password; + NSString *database; + NSString *socket; + NSString *port; + NSString *sshHost; + NSString *sshUser; + NSString *sshPassword; + NSString *sshPort; + + NSString *connectionKeychainItemName; + NSString *connectionKeychainItemAccount; + NSString *connectionSSHKeychainItemName; + NSString *connectionSSHKeychainItemAccount; + + IBOutlet NSView *connectionView; + IBOutlet NSSplitView *connectionSplitView; + IBOutlet NSTableView *favoritesTable; + + IBOutlet NSWindow *errorDetailWindow; + IBOutlet NSTextView *errorDetailText; + + IBOutlet NSView *connectionResizeContainer; + IBOutlet NSView *standardConnectionFormContainer; + IBOutlet NSView *socketConnectionFormContainer; + IBOutlet NSView *sshConnectionFormContainer; + + IBOutlet NSTextField *standardSQLHostField; + IBOutlet NSTextField *sshSQLHostField; + IBOutlet NSSecureTextField *standardPasswordField; + IBOutlet NSSecureTextField *socketPasswordField; + IBOutlet NSSecureTextField *sshPasswordField; + IBOutlet NSSecureTextField *sshSSHPasswordField; + + IBOutlet NSButton *addToFavoritesButton; + IBOutlet NSButton *connectButton; + IBOutlet NSProgressIndicator *progressIndicator; + IBOutlet NSTextField *progressIndicatorText; +} + +@property (readwrite, assign) int type; +@property (readwrite, retain) NSString *name; +@property (readwrite, retain) NSString *host; +@property (readwrite, retain) NSString *user; +@property (readwrite, retain) NSString *password; +@property (readwrite, retain) NSString *database; +@property (readwrite, retain) NSString *socket; +@property (readwrite, retain) NSString *port; +@property (readwrite, retain) NSString *sshHost; +@property (readwrite, retain) NSString *sshUser; +@property (readwrite, retain) NSString *sshPassword; +@property (readwrite, retain) NSString *sshPort; + +- (id) initWithDocument:(TableDocument *)theTableDocument; + +// Connection processes +- (IBAction)initiateConnection:(id)sender; +- (void)initiateSSHTunnelConnection; +- (void)sshTunnelCallback:(SPSSHTunnel *)theTunnel; +- (void)initiateMySQLConnection; +- (void)failConnectionWithErrorMessage:(NSString *)theErrorMessage withDetail:(NSString *)errorDetail; +- (void)errorSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; +- (void) addConnectionToDocument; + +// Interface interaction +- (IBAction) editFavorites:(id)sender; +- (IBAction) showHelp:(id)sender; +- (void) resizeTabViewToConnectionType:(unsigned int)theType animating:(BOOL)animate; + +// Connection details interaction +- (BOOL) checkHost; + +// Favorites interaction +- (void) updateFavorites; +- (void) updateFavoriteSelection:(id)sender; +- (id) selectedFavorite; +- (IBAction) addFavorite:(id)sender; + +- (void) splitViewDidResizeSubviews:(NSNotification *)aNotification; + +@end + + +@interface SPFlippedView: NSView +{ +} +- (BOOL)isFlipped; +@end diff --git a/Source/SPConnectionController.m b/Source/SPConnectionController.m new file mode 100644 index 00000000..97ad5983 --- /dev/null +++ b/Source/SPConnectionController.m @@ -0,0 +1,827 @@ +// +// $Id: SPConnectionController.m 802 2009-06-03 20:46:57Z stuart02 $ +// +// SPConnectionController.m +// sequel-pro +// +// Created by Rowan Beentje on 28/06/2009. +// Copyright 2009 Arboreal. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPConnectionController.h" +#import "MainController.h" +#import "SPPreferenceController.h" +#import "ImageAndTextCell.h" + +@implementation SPConnectionController + +@synthesize type; +@synthesize name; +@synthesize host; +@synthesize user; +@synthesize password; +@synthesize database; +@synthesize socket; +@synthesize port; +@synthesize sshHost; +@synthesize sshUser; +@synthesize sshPassword; +@synthesize sshPort; + +/** + * Initialise the connection controller, linking it to the + * parent document and setting up the parent window. + */ +- (id) initWithDocument:(TableDocument *)theTableDocument +{ + if (self = [super init]) { + tableDocument = theTableDocument; + documentWindow = [tableDocument valueForKey:@"tableWindow"]; + contentView = [tableDocument valueForKey:@"contentViewSplitter"]; + connectionKeychainItemName = nil; + connectionKeychainItemAccount = nil; + connectionSSHKeychainItemName = nil; + connectionSSHKeychainItemAccount = nil; + mySQLConnection = nil; + sshTunnel = nil; + + // Load the connection nib + [NSBundle loadNibNamed:@"ConnectionView" owner:self]; + + // Hide the main view and position and display the connection view + [contentView setHidden:YES]; + [connectionView setFrame:[contentView frame]]; + [[documentWindow contentView] addSubview:connectionView]; + [connectionSplitView setPosition:[[tableDocument valueForKey:@"dbTablesTableView"] frame].size.width ofDividerAtIndex:0]; + + // Disable the toolbar icons + NSArray *toolbarItems = [[documentWindow toolbar] items]; + for (int i = 0; i < [toolbarItems count]; i++) [[toolbarItems objectAtIndex:i] setEnabled:NO]; + + // Set up a keychain instance and preferences reference, and create the initial favorites list + keychain = [[KeyChain alloc] init]; + prefs = [[NSUserDefaults standardUserDefaults] retain]; + favorites = nil; + [self updateFavorites]; + + // Register an observer for changes within the favorites + [prefs addObserver:self forKeyPath:@"favorites" options:NSKeyValueObservingOptionNew context:NULL]; + + // Register double click for the favorites view (double click favorite to connect) + [favoritesTable setTarget:self]; + [favoritesTable setDoubleAction:@selector(initiateConnection:)]; + + // Set the focus to the favorites table and select the appropriate row + [documentWindow setInitialFirstResponder:favoritesTable]; + int tableRow; + if ([prefs boolForKey:@"SelectLastFavoriteUsed"] == YES) { + tableRow = [prefs integerForKey:@"LastFavoriteIndex"] + 1; + } else { + tableRow = [prefs integerForKey:@"DefaultFavorite"] + 1; + } + if (tableRow < [favorites count]) { + previousType = [[[favorites objectAtIndex:tableRow] objectForKey:@"type"] intValue]; + [self resizeTabViewToConnectionType:[[[favorites objectAtIndex:tableRow] objectForKey:@"type"] intValue] animating:NO]; + [favoritesTable selectRowIndexes:[NSIndexSet indexSetWithIndex:tableRow] byExtendingSelection:NO]; + [favoritesTable scrollRowToVisible:[favoritesTable selectedRow]]; + } else { + previousType = SP_CONNECTION_TCPIP; + [self resizeTabViewToConnectionType:SP_CONNECTION_TCPIP animating:NO]; + } + + // If the document is set to automatically connect, do so. + if ([tableDocument shouldAutomaticallyConnect]) { + [self performSelector:@selector(initiateConnection:) withObject:self afterDelay:0.0]; + } + } + + return self; +} + +- (void) dealloc +{ + [keychain release]; + [prefs release]; + [favorites release]; + if (mySQLConnection) [mySQLConnection release]; + if (sshTunnel) [sshTunnel disconnect], [sshTunnel release]; + if (connectionKeychainItemName) [connectionKeychainItemName release]; + if (connectionKeychainItemAccount) [connectionKeychainItemAccount release]; + if (connectionSSHKeychainItemName) [connectionSSHKeychainItemName release]; + if (connectionSSHKeychainItemAccount) [connectionSSHKeychainItemAccount release]; + + [super dealloc]; +} + +#pragma mark - +#pragma mark Connection processes + +/* + * Starts the connection process; invoked when user hits the connect button + * or double-clicks on a favourite. + * Error-checks fields as required, and triggers connection of MySQL or any + * connection proxies in use. + */ +- (IBAction)initiateConnection:(id)sender +{ + + // Ensure that host is not empty if this is a TCP/IP or SSH connection + if (([self type] == SP_CONNECTION_TCPIP || [self type] == SP_CONNECTION_SSHTUNNEL) && ![[self host] length]) { + NSRunAlertPanel(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"Insufficient details provided to establish a connection. Please provide at least a host.", @"insufficient details informative message"), NSLocalizedString(@"OK", @"OK button"), nil, nil); + return; + } + + // If SSH is enabled, ensure that the SSH host is not nil + if ([self type] == SP_CONNECTION_SSHTUNNEL && ![[self sshHost] length]) { + NSRunAlertPanel(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"message of panel when ssh details are incomplete"), NSLocalizedString(@"OK", @"OK button"), nil, nil); + return; + } + + // Ensure that a socket connection is not inadvertently used + if (![self checkHost]) return; + + // Basic details have validated - start the connection process animating + [addToFavoritesButton setHidden:YES]; + [connectButton setEnabled:NO]; + [progressIndicator startAnimation:self]; + [progressIndicatorText setHidden:NO]; + [progressIndicatorText display]; + + // If the password(s) are marked as having been originally sourced from a keychain, check whether they + // have been changed or not; if not, leave the mark in place and remove the password from the field + // for increased security. + if (connectionKeychainItemName) { + if ([[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount] isEqualToString:[self password]]) { + [self setPassword:[[NSString string] stringByPaddingToLength:[[self password] length] withString:@"sp" startingAtIndex:0]]; + [[tableDocument undoManager] removeAllActionsWithTarget:standardPasswordField]; + [[tableDocument undoManager] removeAllActionsWithTarget:socketPasswordField]; + [[tableDocument undoManager] removeAllActionsWithTarget:sshPasswordField]; + } else { + [connectionKeychainItemName release], connectionKeychainItemName = nil; + [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; + } + } + if (connectionSSHKeychainItemName) { + if ([[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount] isEqualToString:[self sshPassword]]) { + [self setSshPassword:[[NSString string] stringByPaddingToLength:[[self sshPassword] length] withString:@"sp" startingAtIndex:0]]; + [[tableDocument undoManager] removeAllActionsWithTarget:sshSSHPasswordField]; + } else { + [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; + [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; + } + } + + // Initiate the SSH connection process for tunnels + if ([self type] == SP_CONNECTION_SSHTUNNEL) { + [self performSelector:@selector(initiateSSHTunnelConnection) withObject:nil afterDelay:0.0]; + return; + } + + // ...or start the MySQL connection process directly + [self performSelector:@selector(initiateMySQLConnection) withObject:nil afterDelay:0.0]; +} + +/* + * Initiate the SSH connection process. + * This should only be called as part of initiateConnection:, and will indirectly + * call initiateMySQLConnection if it's successful. + */ +- (void)initiateSSHTunnelConnection +{ + [progressIndicatorText setStringValue:NSLocalizedString(@"SSH connecting...", @"SSH connecting very short status message")]; + [progressIndicatorText display]; + + // Set up the tunnel details + sshTunnel = [[SPSSHTunnel alloc] initToHost:[self sshHost] port:([[self sshPort] length]?[[self sshPort] intValue]:22) login:[self sshUser] tunnellingToPort:([[self port] length]?[[self port] intValue]:3306) onHost:[self host]]; + [sshTunnel setParentWindow:documentWindow]; + + // Add keychain or plaintext password as appropriate - note the checks in initiateConnection. + if (connectionSSHKeychainItemName) { + [sshTunnel setPasswordKeychainName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; + } else { + [sshTunnel setPassword:[self sshPassword]]; + } + + // Set the callback function on the tunnel + [sshTunnel setConnectionStateChangeSelector:@selector(sshTunnelCallback:) delegate:self]; + + // Ask the tunnel to connect. This will call the callback below on success or failure, passing + // itself as an argument - retain count should be one at this point. + [sshTunnel connect]; +} + +/* + * A callback function for the SSH Tunnel setup process - will be called on a connection + * state change, allowing connection to fail or proceed as appropriate. If successful, + * will call initiateMySQLConnection. + */ +- (void)sshTunnelCallback:(SPSSHTunnel *)theTunnel +{ + int newState = [theTunnel state]; + + if (newState == SPSSH_STATE_IDLE) { + [tableDocument setTitlebarStatus:@"SSH Disconnected"]; + [self failConnectionWithErrorMessage:[theTunnel lastError] withDetail:[sshTunnel debugMessages]]; + } else if (newState == SPSSH_STATE_CONNECTED) { + [tableDocument setTitlebarStatus:@"SSH Connected"]; + [self initiateMySQLConnection]; + } else { + [tableDocument setTitlebarStatus:@"SSH Connecting…"]; + } +} + +/* + * Set up the MySQL connection, either through a successful tunnel or directly. + */ +- (void)initiateMySQLConnection +{ + if (sshTunnel) + [progressIndicatorText setStringValue:NSLocalizedString(@"MySQL connecting...", @"MySQL connecting very short status message")]; + else + [progressIndicatorText setStringValue:NSLocalizedString(@"Connecting...", @"Generic connecting very short status message")]; + [progressIndicatorText display]; + + // Initialise to socket if appropriate. + if ([self type] == SP_CONNECTION_SOCKET) { + mySQLConnection = [[CMMCPConnection alloc] initToSocket:[self socket] withLogin:[self user]]; + + // Otherwise, initialise to host, using tunnel if appropriate + } else { + if ([self type] == SP_CONNECTION_SSHTUNNEL) { + mySQLConnection = [[CMMCPConnection alloc] initToHost:@"127.0.0.1" + withLogin:[self user] + usingPort:[sshTunnel localPort]]; + [mySQLConnection setSSHTunnel:sshTunnel]; + } else { + mySQLConnection = [[CMMCPConnection alloc] initToHost:[self host] + withLogin:[self user] + usingPort:([[self port] length]?[[self port] intValue]:3306)]; + } + } + [mySQLConnection setParentWindow:documentWindow]; + + // Set the password as appropriate + if (connectionKeychainItemName) { + [mySQLConnection setPasswordKeychainName:connectionKeychainItemName account:connectionKeychainItemAccount]; + } else { + [mySQLConnection setPassword:[self password]]; + } + + // Connect + [mySQLConnection connect]; + + if (![mySQLConnection isConnected]) { + if (sshTunnel) { + + // If an SSH tunnel is running, temporarily block to allow the tunnel to register changes in state + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; + + // If the state is connection refused, attempt the MySQL connection again with the host using the hostfield value. + if ([sshTunnel state] == SPSSH_STATE_FORWARDING_FAILED) { + if ([sshTunnel localPortFallback]) { + [mySQLConnection setPort:[sshTunnel localPortFallback]]; + [mySQLConnection connect]; + if (![mySQLConnection isConnected]) { + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; + } + } + } + } + + if (![mySQLConnection isConnected]) { + NSString *errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@, or the request timed out.\n\nBe sure that the address is correct and that you have the necessary privileges, or try increasing the connection timeout (currently %i seconds).\n\nMySQL said: %@", @"message of panel when connection to host failed"), [self host], [[prefs objectForKey:@"ConnectionTimeoutValue"] intValue], [mySQLConnection getLastErrorMessage]]; + if (sshTunnel && [sshTunnel state] == SPSSH_STATE_FORWARDING_FAILED) { + errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@ because the port connection via SSH was refused.\n\nPlease ensure that your MySQL host is set up to allow TCP/IP connections (no --skip-networking) and is configured to allow connections from the host you are tunnelling via.\n\nYou may also want to check the port is correct and that you have the necessary privileges.\n\nChecking the error detail will show the SSH debug log which may provide more details.\n\nMySQL said: %@", @"message of panel when SSH port forwarding failed"), [self host], [mySQLConnection getLastErrorMessage]]; + [self failConnectionWithErrorMessage:errorMessage withDetail:[sshTunnel debugMessages]]; + } else { + [self failConnectionWithErrorMessage:errorMessage withDetail:nil]; + } + + if (sshTunnel) [sshTunnel release], sshTunnel = nil; + [mySQLConnection release], mySQLConnection = nil; + return; + } + } + if (![[self database] isEqualToString:@""]) { + if (![mySQLConnection selectDB:[self database]]) { + [self failConnectionWithErrorMessage:[NSString stringWithFormat:NSLocalizedString(@"Connected to host, but unable to connect to database %@.\n\nBe sure that the database exists and that you have the necessary privileges.\n\nMySQL said: %@", @"message of panel when connection to db failed"), [self database], [mySQLConnection getLastErrorMessage]] withDetail:nil]; + if (sshTunnel) [sshTunnel release], sshTunnel = nil; + [mySQLConnection release], mySQLConnection = nil; + return; + } + } + + // Successful connection! + [progressIndicator stopAnimation:self]; + [progressIndicatorText setHidden:YES]; + [addToFavoritesButton setHidden:NO]; + [connectButton setEnabled:YES]; + + // Release the tunnel if set - will now be retained by the connection + if (sshTunnel) [sshTunnel release], sshTunnel = nil; + + // Pass the connection to the document and clean up the interface + [self addConnectionToDocument]; +} + +/* + * Ends a connection attempt by stopping the connection animation and + * displaying a specified error message. + */ +- (void)failConnectionWithErrorMessage:(NSString *)theErrorMessage withDetail:(NSString *)errorDetail +{ + // Clean up the interface + [progressIndicator stopAnimation:self]; + [progressIndicator display]; + [progressIndicatorText setHidden:YES]; + [progressIndicatorText display]; + [addToFavoritesButton setHidden:NO]; + [connectButton setEnabled:YES]; + [tableDocument clearStatusIcon]; + + // Release as appropriate + if (sshTunnel) [sshTunnel disconnect], [sshTunnel release], sshTunnel = nil; + + if (errorDetail) [errorDetailText setString:errorDetail]; + + // Display the connection error message + NSBeginAlertSheet(NSLocalizedString(@"Connection failed!", @"connection failed title"), NSLocalizedString(@"OK", @"OK button"), errorDetail?NSLocalizedString(@"Show detail", @"Show detail button"):nil, nil, documentWindow, self, nil, @selector(errorSheetDidEnd:returnCode:contextInfo:), @"connect", theErrorMessage); +} + + +/** + * Alert sheet callback method - invoked when an error sheet is closed. + */ +- (void)errorSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +{ + [sheet orderOut:self]; + if (returnCode == NSAlertAlternateReturn) [errorDetailWindow makeKeyAndOrderFront:self]; + + // Restore the passwords from keychain for editing if appropriate + if (connectionKeychainItemName) { + [self setPassword:[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]]; + } + if (connectionSSHKeychainItemName) { + [self setSshPassword:[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]]; + } +} + +/** + * Add the connection to the parent document and restore the + * interface, allowing the application to run as normal. + */ +- (void) addConnectionToDocument +{ + + // Hide the connection view and restore the main view + [connectionView setHidden:YES]; + [contentView setHidden:NO]; + + // Restore the toolbar icons + NSArray *toolbarItems = [[documentWindow toolbar] items]; + for (int i = 0; i < [toolbarItems count]; i++) [[toolbarItems objectAtIndex:i] setEnabled:YES]; + + // Pass the connection to the table document, allowing it to set + // up the other classes and the rest of the interface. + [tableDocument setConnection:mySQLConnection]; +} + +#pragma mark - +#pragma mark Interface interaction + +/** + * Opens the preferences window, or brings it to the front, and switch to the favorites tab. + * If a favorite is selected in the connection sheet, it is also select in the prefs window. + */ +- (IBAction) editFavorites:(id)sender +{ + SPPreferenceController *prefsController = [[NSApp delegate] preferenceController]; + + [prefsController showWindow:self]; + [prefsController displayFavoritePreferences:self]; + [prefsController selectFavoriteAtIndex:([favoritesTable selectedRow] - 1)]; +} + +/** + * Show connection help. + */ +- (IBAction) showHelp:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sequelpro.com/docs/Getting_Connected"]]; +} + +#pragma mark - +#pragma mark Connection details interaction and display + +/** + * Trigger a resize action whenever the tab view changes. The connection + * detail forms are held within container views, which are of a fixed width; + * the tabview and buttons are contained within a resizable view which + * is set to dimensions based on the container views, allowing the view + * to be sized according to the detail type. + */ +- (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem +{ + int selectedTabView = [tabView indexOfTabViewItem:tabViewItem]; + + // Deselect any selected favorite for manual changes + if (!automaticFavoriteSelection) [favoritesTable deselectAll:self]; + automaticFavoriteSelection = NO; + + if (selectedTabView == previousType) return; + + [self resizeTabViewToConnectionType:selectedTabView animating:YES]; + + // Update the host as appropriate + if (selectedTabView == SP_CONNECTION_SOCKET) { + [self setHost:@"localhost"]; + } else if ([[self host] isEqualToString:@"localhost"]) { + [self setHost:@""]; + } + + previousType = selectedTabView; +} + +/** + * When a favorite is selected, and the connection details are edited, deselect the favorite; + * this is clearer and also prevents a failed connection from being repopulated with the + * favorite's details instead of the last used details. + */ +- (void) controlTextDidChange:(NSNotification *)aNotification +{ + [favoritesTable deselectAll:self]; +} + +/** + * When a host field finishes editing, ensure that it hasn't been set to "localhost" + * to ensure that socket connections don't inadvertently occur. + */ +- (void) controlTextDidEndEditing:(NSNotification *)notification +{ + if ([notification object] == standardSQLHostField || [notification object] == sshSQLHostField) { + [self checkHost]; + } +} + +/** + * Control tab view resizing based on the supplied connection type, + * with an option defining whether it should be animated or not. + */ +- (void) resizeTabViewToConnectionType:(unsigned int)theType animating:(BOOL)animate +{ + NSRect frameRect, targetResizeRect; + int additionalFormHeight = 55; + + frameRect = [connectionResizeContainer frame]; + + switch (theType) { + case SP_CONNECTION_TCPIP: + targetResizeRect = [standardConnectionFormContainer frame]; + break; + case SP_CONNECTION_SOCKET: + targetResizeRect = [socketConnectionFormContainer frame]; + break; + case SP_CONNECTION_SSHTUNNEL: + targetResizeRect = [sshConnectionFormContainer frame]; + break; + } + + frameRect.size.height = targetResizeRect.size.height + additionalFormHeight; + + if (animate) { + [[connectionResizeContainer animator] setFrame:frameRect]; + } else { + [connectionResizeContainer setFrame:frameRect]; + } +} + +/** + * Check the host field and ensure it isn't set to "localhost" for + * non-socket connections. + */ +- (BOOL) checkHost +{ + if ([self type] != SP_CONNECTION_SOCKET && [[self host] isEqualToString:@"localhost"]) { + NSBeginAlertSheet(NSLocalizedString(@"You have entered 'localhost' for a non-socket connection", @"title of error when using 'localhost' for a network connection"), + NSLocalizedString(@"Use 127.0.0.1", @"Use 127.0.0.1 button"), // Main button + NSLocalizedString(@"Connect via socket", @"Connect via socket button"), // Alternate button + nil, // Other button + documentWindow, // Window to attach to + self, // Modal delegate + @selector(localhostErrorSheetDidEnd:returnCode:contextInfo:), // Did end selector + nil, // Did dismiss selector + nil, // Contextual info for selectors + NSLocalizedString(@"To MySQL, 'localhost' is a special host and means that a socket connection should be used.\n\nDid you mean to use a socket connection, or to connect to the local machine via a port? If you meant to connect via a port, '127.0.0.1' should be used instead of 'localhost'.", @"message of error when using 'localhost' for a network connection")); + return NO; + } + + return YES; +} + +/** + * Alert sheet callback method - invoked when the error sheet is closed. + */ +- (void)localhostErrorSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +{ + [sheet orderOut:self]; + if (returnCode == NSAlertAlternateReturn) { + [self setType:SP_CONNECTION_SOCKET]; + [self setHost:@""]; + } else { + [self setHost:@"127.0.0.1"]; + } +} + +#pragma mark - +#pragma mark Favorites interaction + +/** + * Updates the local favorites array from the user defaults + */ +- (void) updateFavorites +{ + [favoritesTable deselectAll:self]; + if (favorites) [favorites release]; + if ([prefs objectForKey:@"favorites"]) { + favorites = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"favorites"]]; + } else { + favorites = [[NSMutableArray alloc] init]; + } + [favorites insertObject:[NSDictionary dictionaryWithObject:@"FAVORITES" forKey:@"name"] atIndex:0]; + [favoritesTable reloadData]; +} + +/** + * Sets fields for the chosen favorite. + */ +- (void) updateFavoriteSelection:(id)sender +{ + + // If nothing is selected, return without updating the interface + if (![self selectedFavorite]) return; + + automaticFavoriteSelection = YES; + + // Clear the keychain referral items as appropriate + if (connectionKeychainItemName) [connectionKeychainItemName release], connectionKeychainItemName = nil; + if (connectionKeychainItemAccount) [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; + if (connectionSSHKeychainItemName) [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; + if (connectionSSHKeychainItemAccount) [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; + + // Update key-value properties from the selected favourite, using empty strings where not found + [self setType:([self valueForKeyPath:@"selectedFavorite.type"] ? [[self valueForKeyPath:@"selectedFavorite.type"] intValue] : SP_CONNECTION_TCPIP)]; + [self setName:([self valueForKeyPath:@"selectedFavorite.name"] ? [self valueForKeyPath:@"selectedFavorite.name"] : @"")]; + [self setHost:([self valueForKeyPath:@"selectedFavorite.host"] ? [self valueForKeyPath:@"selectedFavorite.host"] : @"")]; + [self setSocket:([self valueForKeyPath:@"selectedFavorite.socket"] ? [self valueForKeyPath:@"selectedFavorite.socket"] : @"")]; + [self setUser:([self valueForKeyPath:@"selectedFavorite.user"] ? [self valueForKeyPath:@"selectedFavorite.user"] : @"")]; + [self setPort:([self valueForKeyPath:@"selectedFavorite.port"] ? [self valueForKeyPath:@"selectedFavorite.port"] : @"")]; + [self setDatabase:([self valueForKeyPath:@"selectedFavorite.database"] ? [self valueForKeyPath:@"selectedFavorite.database"] : @"")]; + [self setSshHost:([self valueForKeyPath:@"selectedFavorite.sshHost"] ? [self valueForKeyPath:@"selectedFavorite.sshHost"] : @"")]; + [self setSshUser:([self valueForKeyPath:@"selectedFavorite.sshUser"] ? [self valueForKeyPath:@"selectedFavorite.sshUser"] : @"")]; + [self setSshPort:([self valueForKeyPath:@"selectedFavorite.sshPort"] ? [self valueForKeyPath:@"selectedFavorite.sshPort"] : @"")]; + + // Check whether the password exists in the keychain, and if so add it; also record the + // keychain details so we can pass around only those details if the password doesn't change + connectionKeychainItemName = [[keychain nameForFavoriteName:[self valueForKeyPath:@"selectedFavorite.name"] id:[self valueForKeyPath:@"selectedFavorite.id"]] retain]; + connectionKeychainItemAccount = [[keychain accountForUser:[self valueForKeyPath:@"selectedFavorite.user"] host:[self valueForKeyPath:@"selectedFavorite.host"] database:[self valueForKeyPath:@"selectedFavorite.database"]] retain]; + [self setPassword:[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]]; + if (![[self password] length]) { + [connectionKeychainItemName release], connectionKeychainItemName = nil; + [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; + } + + // And the same for the SSH password + connectionSSHKeychainItemName = [[keychain nameForSSHForFavoriteName:[self valueForKeyPath:@"selectedFavorite.name"] id:[self valueForKeyPath:@"selectedFavorite.id"]] retain]; + connectionSSHKeychainItemAccount = [[keychain accountForSSHUser:[self valueForKeyPath:@"selectedFavorite.sshUser"] sshHost:[self valueForKeyPath:@"selectedFavorite.sshHost"]] retain]; + [self setSshPassword:[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]]; + if (![[self sshPassword] length]) { + [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; + [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; + } + + [prefs setInteger:([favoritesTable selectedRow] - 1) forKey:@"LastFavoriteIndex"]; +} + +/** + * Returns a KVC-compliant proxy to the currently selected favorite, or nil if nothing selected. + */ +- (id) selectedFavorite +{ + if ([favoritesTable selectedRow] == -1) + return nil; + + return [favorites objectAtIndex:[favoritesTable selectedRow]]; +} + +/** + * Adds the current details as a new favorite, select it, and scroll the selected + * row to visible. + */ +- (IBAction) addFavorite:(id)sender +{ + NSString *thePassword, *theSSHPassword; + NSNumber *favoriteid = [NSNumber numberWithInt:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]]; + NSString *favoriteName = [[self name] length]?[self name]:[NSString stringWithFormat:@"%@@%@", [self user], [self host]]; + if (![[self name] length] && ![[self database] isEqualToString:@""]) + favoriteName = [NSString stringWithFormat:@"%@ %@", [self database], favoriteName]; + + // Ensure that host is not empty if this is a TCP/IP or SSH connection + if (([self type] == SP_CONNECTION_TCPIP || [self type] == SP_CONNECTION_SSHTUNNEL) && ![[self host] length]) { + NSRunAlertPanel(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"Insufficient details provided to establish a connection. Please provide at least a host.", @"insufficient details informative message"), NSLocalizedString(@"OK", @"OK button"), nil, nil); + return; + } + + // If SSH is enabled, ensure that the SSH host is not nil + if ([self type] == SP_CONNECTION_SSHTUNNEL && ![[self sshHost] length]) { + NSRunAlertPanel(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"message of panel when ssh details are incomplete"), NSLocalizedString(@"OK", @"OK button"), nil, nil); + return; + } + + // Ensure that a socket connection is not inadvertently used + if (![self checkHost]) return; + + // Construct the favorite details + NSDictionary *newFavorite = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:[self type]], @"type", + favoriteName, @"name", + [self host], @"host", + [self socket], @"socket", + [self user], @"user", + [self port], @"port", + [self database], @"database", + [self sshHost], @"sshHost", + [self sshUser], @"sshUser", + [self sshPort], @"sshPort", + favoriteid, @"id", + nil]; + + + // Add the new favorite to the user defaults array + NSMutableArray *currentFavorites; + if ([prefs objectForKey:@"favorites"]) { + currentFavorites = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"favorites"]]; + } else { + currentFavorites = [[NSMutableArray alloc] init]; + } + [currentFavorites addObject:newFavorite]; + [prefs setObject:[NSArray arrayWithArray:currentFavorites] forKey:@"favorites"]; + [currentFavorites release]; + + // Add the password to keychain as appropriate + thePassword = [self password]; + if (mySQLConnection && connectionKeychainItemName) { + thePassword = [keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]; + } + if (![thePassword isEqualToString:@""]) { + [keychain addPassword:thePassword + forName:[keychain nameForFavoriteName:favoriteName id:[NSString stringWithFormat:@"%i", [favoriteid intValue]]] + account:[keychain accountForUser:[self user] host:[self host] database:[self database]]]; + } + + // Add the SSH password to keychain as appropriate + theSSHPassword = [self sshPassword]; + if (mySQLConnection && connectionSSHKeychainItemName) { + theSSHPassword = [keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; + } + if (![theSSHPassword isEqualToString:@""]) { + [keychain addPassword:theSSHPassword + forName:[keychain nameForSSHForFavoriteName:favoriteName id:[NSString stringWithFormat:@"%i", [favoriteid intValue]]] + account:[keychain accountForSSHUser:[self sshUser] sshHost:[self sshHost]]]; + } + + // Update the favorites list and selection + [self updateFavorites]; + [favoritesTable selectRowIndexes:[NSIndexSet indexSetWithIndex:[favorites count]-1] byExtendingSelection:NO]; + [favoritesTable scrollRowToVisible:[favoritesTable selectedRow]]; + + [[[NSApp delegate] preferenceController] updateDefaultFavoritePopup]; +} + +/** + * If the favorites list in the preferences change, trigger a reload of + * the favorites table data. + */ +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@"favorites"]) { + [self updateFavorites]; + } +} + +#pragma mark - +#pragma mark Favorites tableview datasource and delegate methods + +/** + * Returns the number of favorites to display + */ +- (int) numberOfRowsInTableView:(NSTableView *)aTableView +{ + return [favorites count]; +} + +/** + * Returns the favorite names to be displayed in the table + */ +- (id) tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +{ + return [[favorites objectAtIndex:rowIndex] objectForKey:@"name"]; +} + +/** + * Loads a favorite, if any are selected. + */ +- (void)tableViewSelectionDidChange:(NSNotification *)aNotification +{ + if ([favoritesTable numberOfSelectedRows] == 1) { + [self updateFavoriteSelection:self]; + [addToFavoritesButton setEnabled:NO]; + } else { + [addToFavoritesButton setEnabled:YES]; + } +} + +/** + * Display the title row + */ +- (BOOL)tableView:(NSTableView *)aTableView isGroupRow:(int)rowIndex +{ + return (rowIndex == 0); +} + +/** + * Don't allow the title row to be selected + */ +- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(int)rowIndex +{ + return (rowIndex != 0); +} + +/** + * Set the title row to display with extra height + */ +- (float)tableView:(NSTableView *)tableView heightOfRow:(int)row +{ + return (row == 0) ? 25 : 17; +} + +/** + * Control the display of rows within the favorites table + */ +- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +{ + [(ImageAndTextCell*)aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + if (rowIndex == 0) { + [(ImageAndTextCell *)aCell setIndentationLevel:0]; + } else { + [(ImageAndTextCell *)aCell setIndentationLevel:1]; + } +} + + +#pragma mark - +#pragma mark NSSplitView delegate methods + +/** + * When the split view is resized, trigger a resize in the hidden table + * width as well, to keep the connection view and connected view in synch. + * TODO: this isn't functional until a BWToolkit bug is fixed - see + * http://bitbucket.org/bwalkin/bwtoolkit/issue/50/bwsplitview-and-splitviewdidresizesubviews + */ +- (void) splitViewDidResizeSubviews:(NSNotification *)aNotification +{ + [contentView setPosition:[favoritesTable frame].size.width ofDividerAtIndex:0]; +} + + +@end + + +#pragma mark - +#pragma mark NSView subclass - flipped view for simpler drawing + +/** + * Add an implementation of a flipped view to simplify drawing. + */ +@implementation SPFlippedView: NSView + +- (BOOL)isFlipped +{ + return YES; +} + +@end diff --git a/Source/SPPreferenceController.h b/Source/SPPreferenceController.h index bc0ff1b2..221508a2 100644 --- a/Source/SPPreferenceController.h +++ b/Source/SPPreferenceController.h @@ -44,15 +44,22 @@ IBOutlet NSTableView *favoritesTableView; IBOutlet NSArrayController *favoritesController; + IBOutlet NSTabView *favoritesTabView; IBOutlet NSTextField *nameField; - IBOutlet NSTextField *hostField; - IBOutlet NSTextField *userField; - IBOutlet NSTextField *databaseField; - IBOutlet NSSecureTextField *passwordField; + IBOutlet NSTextField *standardSQLHostField; + IBOutlet NSTextField *standardUserField; + IBOutlet NSSecureTextField *standardPasswordField; + IBOutlet NSTextField *standardDatabaseField; + IBOutlet NSTextField *socketUserField; + IBOutlet NSSecureTextField *socketPasswordField; + IBOutlet NSTextField *socketDatabaseField; + IBOutlet NSTextField *sshSQLHostField; + IBOutlet NSTextField *sshSQLUserField; + IBOutlet NSSecureTextField *sshSQLPasswordField; + IBOutlet NSTextField *sshDatabaseField; IBOutlet NSTextField *sshHostField; IBOutlet NSTextField *sshUserField; IBOutlet NSSecureTextField *sshPasswordField; - IBOutlet NSTextField *sshPortField; KeyChain *keychain; IBOutlet NSTextField *editorFontName; @@ -94,6 +101,8 @@ // Other - (void)updateDefaultFavoritePopup; - (void)selectFavorites:(NSArray *)favorite; +- (void)selectFavoriteAtIndex:(unsigned int)theIndex; - (void)changeFont:(id)sender; +- (IBAction)favoriteTypeDidChange:(id)sender; @end diff --git a/Source/SPPreferenceController.m b/Source/SPPreferenceController.m index a8430659..9550e897 100644 --- a/Source/SPPreferenceController.m +++ b/Source/SPPreferenceController.m @@ -87,6 +87,9 @@ [favoritesTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; [favoritesTableView reloadData]; + // Hide the tabs on the favorites tab view - left visible in IB for easy use + [favoritesTabView setTabViewType:NSNoTabsNoBorder]; + [self updateDefaultFavoritePopup]; [prefs synchronize]; @@ -221,6 +224,40 @@ password = nil; } + // For versions prior to r981 (~0.9.6), upgrade the favourites to include a connection type for each + if (recordedVersionNumber < 981 && [prefs objectForKey:@"favorites"]) { + int i; + NSMutableArray *favoritesArray = [NSMutableArray arrayWithArray:[prefs objectForKey:@"favorites"]]; + NSMutableDictionary *favorite; + + // Cycle through the favorites + for (i = 0; i < [favoritesArray count]; i++) { + favorite = [NSMutableDictionary dictionaryWithDictionary:[favoritesArray objectAtIndex:i]]; + if ([favorite objectForKey:@"type"]) continue; + + // If the favorite has a socket, or has the host set to "localhost", set to socket-type connection + if ([[favorite objectForKey:@"host"] isEqualToString:@"localhost"] + || ([favorite objectForKey:@"socket"] && [[favorite objectForKey:@"socket"] length])) + { + [favorite setObject:[NSNumber numberWithInt:1] forKey:@"type"]; + + // If SSH details are set, set to tunnel connection + } else if ([favorite objectForKey:@"useSSH"] && [[favorite objectForKey:@"useSSH"] intValue]) { + [favorite setObject:[NSNumber numberWithInt:2] forKey:@"type"]; + + // Default to TCP/IP + } else { + [favorite setObject:[NSNumber numberWithInt:0] forKey:@"type"]; + } + + // Remove SSH tunnel flag - no longer required + [favorite removeObjectForKey:@"useSSH"]; + + [favoritesArray replaceObjectAtIndex:i withObject:[NSDictionary dictionaryWithDictionary:favorite]]; + } + [prefs setObject:[NSArray arrayWithArray:favoritesArray] forKey:@"favorites"]; + } + // Update the prefs revision [prefs setObject:[NSNumber numberWithInt:currentVersionNumber] forKey:@"LastUsedVersion"]; } @@ -236,8 +273,8 @@ NSNumber *favoriteid = [NSNumber numberWithInt:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]]; // Create default favorite - NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"New Favorite", @"", @"", @"", @"", @"", favoriteid, nil] - forKeys:[NSArray arrayWithObjects:@"name", @"host", @"socket", @"user", @"port", @"database", @"id", nil]]; + NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"New Favorite", [NSNumber numberWithInt:0], @"", @"", @"", @"", @"", @"", @"", @"", favoriteid, nil] + forKeys:[NSArray arrayWithObjects:@"name", @"type", @"host", @"socket", @"user", @"port", @"database", @"sshHost", @"sshUser", @"sshPort", @"id", nil]]; [favoritesController addObject:favorite]; [favoritesController setSelectionIndex:[[favoritesController arrangedObjects] count]-1]; @@ -562,7 +599,9 @@ // If no selection is present, blank the field. if ([[favoritesTableView selectedRowIndexes] count] == 0) { - [passwordField setStringValue:@""]; + [standardPasswordField setStringValue:@""]; + [socketPasswordField setStringValue:@""]; + [sshSQLPasswordField setStringValue:@""]; [sshPasswordField setStringValue:@""]; return; } @@ -570,13 +609,14 @@ // Otherwise retrieve and set the password. NSString *keychainName = [keychain nameForFavoriteName:[favoritesController valueForKeyPath:@"selection.name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; NSString *keychainAccount = [keychain accountForUser:[favoritesController valueForKeyPath:@"selection.user"] host:[favoritesController valueForKeyPath:@"selection.host"] database:[favoritesController valueForKeyPath:@"selection.database"]]; - - [passwordField setStringValue:[keychain getPasswordForName:keychainName account:keychainAccount]]; + NSString *passwordValue = [keychain getPasswordForName:keychainName account:keychainAccount]; + [standardPasswordField setStringValue:passwordValue]; + [socketPasswordField setStringValue:passwordValue]; + [sshSQLPasswordField setStringValue:passwordValue]; // Retrieve the SSH keychain password if appropriate. NSString *keychainSSHName = [keychain nameForSSHForFavoriteName:[favoritesController valueForKeyPath:@"selection.name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; NSString *keychainSSHAccount = [keychain accountForSSHUser:[favoritesController valueForKeyPath:@"selection.sshUser"] sshHost:[favoritesController valueForKeyPath:@"selection.sshHost"]]; - [sshPasswordField setStringValue:[keychain getPasswordForName:keychainSSHName account:keychainSSHAccount]]; } @@ -660,7 +700,7 @@ } #pragma mark - -#pragma mark TextField delegate methods +#pragma mark TextField delegate methods and type change action // ------------------------------------------------------------------------------- // control:textShouldEndEditing: @@ -669,42 +709,77 @@ // ------------------------------------------------------------------------------- - (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor { + NSString *nameValue, *hostValue, *userValue, *databaseValue, *passwordValue; NSString *oldKeychainName, *newKeychainName; NSString *oldKeychainAccount, *newKeychainAccount; - // Only proceed for name, host, user or database changes, for both standard and SSH - if (control != nameField && control != hostField && control != userField && control != passwordField && control != databaseField - && control != sshHostField && control != sshUserField && control != sshPasswordField) + // Only proceed for name, host, user or database changes, for standard, socket or SSH + if (control != nameField && control != standardSQLHostField && control != standardUserField + && control != standardPasswordField && control != standardDatabaseField + && control != socketUserField && control != socketPasswordField + && control != socketDatabaseField && control != sshSQLHostField + && control != sshSQLUserField && control != sshSQLPasswordField + && control != sshDatabaseField && control != sshHostField + && control != sshUserField && control != sshPasswordField) return YES; + // Determine the appropriate name, host, user and database details + nameValue = [nameField stringValue]; + switch ([[favoritesController valueForKeyPath:@"selection.type"] intValue]) { + case 0: // Standard + hostValue = [standardSQLHostField stringValue]; + userValue = [standardUserField stringValue]; + databaseValue = [standardDatabaseField stringValue]; + passwordValue = [standardPasswordField stringValue]; + break; + + case 1: // Socket + hostValue = @"localhost"; + userValue = [socketUserField stringValue]; + databaseValue = [socketDatabaseField stringValue]; + passwordValue = [socketPasswordField stringValue]; + break; + + case 2: // SSH + hostValue = [sshSQLHostField stringValue]; + userValue = [sshSQLUserField stringValue]; + databaseValue = [sshDatabaseField stringValue]; + passwordValue = [sshSQLPasswordField stringValue]; + break; + } + // If account/password details have changed, update the keychain to match - if ([nameField stringValue] != [favoritesController valueForKeyPath:@"selection.name"] - || [hostField stringValue] != [favoritesController valueForKeyPath:@"selection.host"] - || [userField stringValue] != [favoritesController valueForKeyPath:@"selection.user"] - || [databaseField stringValue] != [favoritesController valueForKeyPath:@"selection.database"] - || control == passwordField) { + if (![nameValue isEqualToString:[favoritesController valueForKeyPath:@"selection.name"]] + || ![hostValue isEqualToString:[favoritesController valueForKeyPath:@"selection.host"]] + || ![userValue isEqualToString:[favoritesController valueForKeyPath:@"selection.user"]] + || ![databaseValue isEqualToString:[favoritesController valueForKeyPath:@"selection.database"]] + || control == standardPasswordField || control == socketPasswordField || control == sshSQLPasswordField) { // Get the current keychain name and account strings oldKeychainName = [keychain nameForFavoriteName:[favoritesController valueForKeyPath:@"selection.name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; oldKeychainAccount = [keychain accountForUser:[favoritesController valueForKeyPath:@"selection.user"] host:[favoritesController valueForKeyPath:@"selection.host"] database:[favoritesController valueForKeyPath:@"selection.database"]]; // Set up the new keychain name and account strings - newKeychainName = [keychain nameForFavoriteName:[nameField stringValue] id:[favoritesController valueForKeyPath:@"selection.id"]]; - newKeychainAccount = [keychain accountForUser:[userField stringValue] host:[hostField stringValue] database:[databaseField stringValue]]; + newKeychainName = [keychain nameForFavoriteName:nameValue id:[favoritesController valueForKeyPath:@"selection.id"]]; + newKeychainAccount = [keychain accountForUser:userValue host:hostValue database:databaseValue]; // Delete the old keychain item [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount]; // Add the new keychain item if the password field has a value - if ([[passwordField stringValue] length]) - [keychain addPassword:[passwordField stringValue] forName:newKeychainName account:newKeychainAccount]; + if ([passwordValue length]) + [keychain addPassword:passwordValue forName:newKeychainName account:newKeychainAccount]; } - + + // Synch password changes + [standardPasswordField setStringValue:passwordValue]; + [socketPasswordField setStringValue:passwordValue]; + [sshSQLPasswordField setStringValue:passwordValue]; // If SSH account/password details have changed, update the keychain to match - if ([nameField stringValue] != [favoritesController valueForKeyPath:@"selection.name"] - || [sshHostField stringValue] != [favoritesController valueForKeyPath:@"selection.sshHost"] - || [sshUserField stringValue] != [favoritesController valueForKeyPath:@"selection.sshUser"] + if (![[nameField stringValue] isEqualToString:[favoritesController valueForKeyPath:@"selection.name"]] + || ![[sshHostField stringValue] isEqualToString:[favoritesController valueForKeyPath:@"selection.sshHost"]] + || ![[sshUserField stringValue] isEqualToString:[favoritesController valueForKeyPath:@"selection.sshUser"]] || control == sshPasswordField) { // Get the current keychain name and account strings @@ -712,7 +787,7 @@ oldKeychainAccount = [keychain accountForSSHUser:[favoritesController valueForKeyPath:@"selection.sshUser"] sshHost:[favoritesController valueForKeyPath:@"selection.sshHost"]]; // Set up the new keychain name and account strings - newKeychainName = [keychain nameForSSHForFavoriteName:[nameField stringValue] id:[favoritesController valueForKeyPath:@"selection.id"]]; + newKeychainName = [keychain nameForSSHForFavoriteName:nameValue id:[favoritesController valueForKeyPath:@"selection.id"]]; newKeychainAccount = [keychain accountForSSHUser:[sshUserField stringValue] sshHost:[sshHostField stringValue]]; // Delete the old keychain item @@ -727,6 +802,39 @@ return YES; } +// ------------------------------------------------------------------------------- +// favoriteTypeDidChange: +// Update the favorite host when the type changes. +// ------------------------------------------------------------------------------- +- (IBAction)favoriteTypeDidChange:(id)sender +{ + NSString *oldKeychainName, *newKeychainName; + NSString *oldKeychainAccount, *newKeychainAccount; + + // Get the current keychain name and account strings + oldKeychainName = [keychain nameForFavoriteName:[favoritesController valueForKeyPath:@"selection.name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; + oldKeychainAccount = [keychain accountForUser:[favoritesController valueForKeyPath:@"selection.user"] host:[favoritesController valueForKeyPath:@"selection.host"] database:[favoritesController valueForKeyPath:@"selection.database"]]; + + // Delete the old keychain item + [keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount]; + + if ([sender indexOfSelectedItem] == 1) { // Socket + [favoritesController setValue:@"localhost" forKeyPath:@"selection.host"]; + [self control:socketPasswordField textShouldEndEditing:nil]; + } else if ([[favoritesController valueForKeyPath:@"selection.host"] isEqualToString:@"localhost"]) { + [favoritesController setValue:@"" forKeyPath:@"selection.host"]; + [self control:standardPasswordField textShouldEndEditing:nil]; + } + + // Set up the new keychain name and account strings + newKeychainName = [keychain nameForFavoriteName:[favoritesController valueForKeyPath:@"selection.name"] id:[favoritesController valueForKeyPath:@"selection.id"]]; + newKeychainAccount = [keychain accountForUser:[favoritesController valueForKeyPath:@"selection.user"] host:[favoritesController valueForKeyPath:@"selection.host"] database:[favoritesController valueForKeyPath:@"selection.database"]]; + + // Add the new keychain item if the password field has a value + if ([[standardPasswordField stringValue] length]) + [keychain addPassword:[standardPasswordField stringValue] forName:newKeychainName account:newKeychainAccount]; +} + #pragma mark - #pragma mark Window delegate methods @@ -808,6 +916,16 @@ } // ------------------------------------------------------------------------------- +// selectFavoriteAtIndex: +// +// Selects the favorite at the specified index in the favorites list +// ------------------------------------------------------------------------------- +- (void)selectFavoriteAtIndex:(unsigned int)theIndex +{ + [favoritesController setSelectionIndex:theIndex]; +} + +// ------------------------------------------------------------------------------- // query editor font selection // // ------------------------------------------------------------------------------- diff --git a/Source/TableDocument.h b/Source/TableDocument.h index e8702648..4816b4a3 100644 --- a/Source/TableDocument.h +++ b/Source/TableDocument.h @@ -28,9 +28,8 @@ #import <Cocoa/Cocoa.h> #import <MCPKit_bundled/MCPKit_bundled.h> #import <WebKit/WebKit.h> -#import "SPSSHTunnel.h" -@class CMMCPConnection, CMMCPResult; +@class CMMCPConnection, CMMCPResult, SPConnectionController; /** * The TableDocument class controls the primary database view window. @@ -38,7 +37,6 @@ @interface TableDocument : NSDocument { // IBOutlets - IBOutlet id keyChainInstance; IBOutlet id tablesListInstance; IBOutlet id tableSourceInstance; IBOutlet id tableContentInstance; @@ -56,31 +54,12 @@ IBOutlet id titleImageView; IBOutlet id titleStringView; - IBOutlet id connectSheet; IBOutlet id databaseSheet; IBOutlet id variablesSheet; IBOutlet id queryProgressBar; IBOutlet id favoritesButton; - IBOutlet NSTableView *connectFavoritesTableView; - IBOutlet NSArrayController *favoritesController; - IBOutlet id nameField; - IBOutlet id hostField; - IBOutlet id socketField; - IBOutlet id userField; - IBOutlet id passwordField; - IBOutlet id portField; - IBOutlet id databaseField; - IBOutlet id sshCheckbox; - IBOutlet id sshHostField; - IBOutlet id sshUserField; - IBOutlet id sshPasswordField; - IBOutlet id sshPortField; - IBOutlet NSWindow *errorDetailWindow; - IBOutlet NSTextView *errorDetailText; - - IBOutlet NSProgressIndicator *connectProgressBar; - IBOutlet NSTextField *connectProgressStatusText; + IBOutlet id databaseNameField; IBOutlet id databaseEncodingButton; IBOutlet id addDatabaseButton; @@ -101,24 +80,21 @@ IBOutlet id syntaxViewContent; IBOutlet NSWindow *createTableSyntaxWindow; + SPConnectionController *connectionController; + CMMCPConnection *mySQLConnection; - SPSSHTunnel *sshTunnel; NSArray *variables; NSString *selectedDatabase; NSString *mySQLVersion; NSUserDefaults *prefs; - NSString *connectionKeychainItemName; - NSString *connectionKeychainItemAccount; - NSString *connectionSSHKeychainItemName; - NSString *connectionSSHKeychainItemAccount; - NSMenu *selectEncodingMenu; BOOL _supportsEncoding; NSString *_encoding; BOOL _encodingViaLatin1; BOOL _shouldOpenConnectionAutomatically; + BOOL _isConnected; NSToolbar *mainToolbar; NSToolbarItem *chooseDatabaseToolbarItem; @@ -126,35 +102,12 @@ WebView *printWebView; } -//start sheet -- (void)setShouldAutomaticallyConnect:(BOOL)shouldAutomaticallyConnect; -- (IBAction)connectToDB:(id)sender; -- (IBAction)initiateConnection:(id)sender; -- (void)initiateSSHTunnelConnection; -- (void)sshTunnelCallback:(SPSSHTunnel *)theTunnel; -- (void)initiateMySQLConnection; -- (void)failConnectionWithErrorMessage:(NSString *)theErrorMessage withDetail:(NSString *)errorDetail; -- (IBAction)cancelConnectSheet:(id)sender; -- (IBAction)closeSheet:(id)sender; -- (IBAction)chooseFavorite:(id)sender; -- (IBAction)toggleUseSSH:(id)sender; -- (IBAction)editFavorites:(id)sender; -- (id)selectedFavorite; -- (void)connectSheetAddToFavorites:(id)sender; -- (void)addToFavoritesName:(NSString *)name host:(NSString *)host socket:(NSString *)socket - user:(NSString *)user password:(NSString *)password - port:(NSString *)port database:(NSString *)database - useSSH:(BOOL)useSSH // no-longer in use - sshHost:(NSString *)sshHost // no-longer in use - sshUser:(NSString *)sshUser // no-longer in use - sshPassword:(NSString *)sshPassword // no-longer in use - sshPort:(NSString *)sshPort; // no-longer in use -- (IBAction)connectSheetShowHelp:(id)sender; - - (NSString *)getHTMLforPrint; -//connection getter -- (CMMCPConnection *)sharedConnection; +// Connection callback and methods +- (void) setConnection:(CMMCPConnection *)theConnection; +- (void)setShouldAutomaticallyConnect:(BOOL)shouldAutomaticallyConnect; +- (BOOL)shouldAutomaticallyConnect; //database methods - (IBAction)setDatabases:(id)sender; @@ -207,7 +160,6 @@ - (void)willPerformQuery:(NSNotification *)notification; - (void)hasPerformedQuery:(NSNotification *)notification; - (void)applicationWillTerminate:(NSNotification *)notification; -- (void)tunnelStatusChanged:(NSNotification *)notification; //menu methods - (BOOL)validateMenuItem:(NSMenuItem *)anItem; @@ -239,7 +191,6 @@ - (void)willQueryString:(NSString *)query; - (void)queryGaveError:(NSString *)error; -@end +- (IBAction)closeSheet:(id)sender; -extern NSString *TableDocumentFavoritesControllerSelectionIndexDidChange; -extern NSString *TableDocumentFavoritesControllerFavoritesDidChange; +@end diff --git a/Source/TableDocument.m b/Source/TableDocument.m index 9f1b0fa9..292aa884 100644 --- a/Source/TableDocument.m +++ b/Source/TableDocument.m @@ -26,7 +26,6 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "TableDocument.h" -#import "KeyChain.h" #import "TablesList.h" #import "TableSource.h" #import "TableContent.h" @@ -41,11 +40,11 @@ #import "SPDatabaseData.h" #import "SPStringAdditions.h" #import "SPArrayAdditions.h" -#import "SPQueryConsole.h" #import "CMMCPConnection.h" #import "CMMCPResult.h" #import "MainController.h" #import "SPExtendedTableInfo.h" +#import "SPConnectionController.h" #import "SPPreferenceController.h" #import "SPPrintAccessory.h" #import "QLPreviewPanel.h" @@ -54,16 +53,6 @@ #import "MGTemplateEngine.h" #import "ICUTemplateMatcher.h" -NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocumentFavoritesControllerSelectionIndexDidChange"; - -@interface TableDocument (PrivateAPI) - -- (BOOL)_favoriteAlreadyExists:(NSString *)database host:(NSString *)host user:(NSString *)user; - -@end - -#pragma mark - - @implementation TableDocument - (id)init @@ -71,14 +60,11 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum if ((self = [super init])) { _encoding = [@"utf8" retain]; + _isConnected = NO; chooseDatabaseButton = nil; chooseDatabaseToolbarItem = nil; - connectionKeychainItemName = nil; - connectionKeychainItemAccount = nil; - connectionSSHKeychainItemName = nil; - connectionSSHKeychainItemAccount = nil; + connectionController = nil; selectedDatabase = nil; - sshTunnel = nil; printWebView = [[WebView alloc] init]; [printWebView setFrameLoadDelegate:self]; @@ -91,21 +77,21 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum - (void)awakeFromNib { - // Register selection did change handler for favorites controller (used in connect sheet) - [favoritesController addObserver:self forKeyPath:@"selectionIndex" options:NSKeyValueChangeInsertion context:TableDocumentFavoritesControllerSelectionIndexDidChange]; - + + // Set up the toolbar + [self setupToolbar]; + + // Set up the connection controller + connectionController = [[SPConnectionController alloc] initWithDocument:self]; + // Register observers for when the DisplayTableViewVerticalGridlines preference changes [prefs addObserver:tableSourceInstance forKeyPath:@"DisplayTableViewVerticalGridlines" options:NSKeyValueObservingOptionNew context:NULL]; [prefs addObserver:tableContentInstance forKeyPath:@"DisplayTableViewVerticalGridlines" options:NSKeyValueObservingOptionNew context:NULL]; [prefs addObserver:customQueryInstance forKeyPath:@"DisplayTableViewVerticalGridlines" options:NSKeyValueObservingOptionNew context:NULL]; - - // Register observers for when the preference changes + + // Register observers for when the logging preference changes [prefs addObserver:[SPQueryConsole sharedQueryConsole] forKeyPath:@"ConsoleEnableLogging" options:NSKeyValueObservingOptionNew context:NULL]; - - // Register double click for the favorites view (double click favorite to connect) - [connectFavoritesTableView setTarget:self]; - [connectFavoritesTableView setDoubleAction:@selector(initiateConnection:)]; - + // Find the Database -> Database Encoding menu (it's not in our nib, so we can't use interface builder) selectEncodingMenu = [[[[[NSApp mainMenu] itemWithTag:1] submenu] itemWithTag:1] submenu]; @@ -123,18 +109,98 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum av.size.height); [titleAccessoryView setFrame:initialAccessoryViewFrame]; [windowFrame addSubview:titleAccessoryView]; + + // Pull the new window to the front of the app + [tableWindow makeKeyAndOrderFront:self]; } -- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +#pragma mark - +#pragma mark Connection callback and methods + +- (void) setConnection:(CMMCPConnection *)theConnection { - if (context == TableDocumentFavoritesControllerSelectionIndexDidChange) { - [self chooseFavorite:self]; - return; + CMMCPResult *theResult; + id version; + + _isConnected = YES; + mySQLConnection = [theConnection retain]; + + // Register as the connection delegate + [mySQLConnection setDelegate:self]; + + // Set the connection encoding + NSString *encodingName = [prefs objectForKey:@"DefaultEncoding"]; + if ( [encodingName isEqualToString:@"Autodetect"] ) { + [self setConnectionEncoding:[self databaseEncoding] reloadingViews:NO]; + } else { + [self setConnectionEncoding:[self mysqlEncodingFromDisplayEncoding:encodingName] reloadingViews:NO]; } - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + // Get the mysql version + theResult = [mySQLConnection queryString:@"SHOW VARIABLES LIKE 'version'"]; + version = [[theResult fetchRowAsArray] objectAtIndex:1]; + if ( [version isKindOfClass:[NSData class]] ) { + // starting with MySQL 4.1.14 the mysql variables are returned as nsdata + mySQLVersion = [[NSString alloc] initWithData:version encoding:[mySQLConnection encoding]]; + } else { + mySQLVersion = [[NSString stringWithString:version] retain]; + } + + // Update the selected database if appropriate + if (![[connectionController database] isEqualToString:@""]) { + if (selectedDatabase) [selectedDatabase release], selectedDatabase = nil; + selectedDatabase = [[connectionController database] retain]; + } + + // Update the database list + [self setDatabases:self]; + + // For each of the main controllers, assign the current connection + [tablesListInstance setConnection:mySQLConnection]; + [tableSourceInstance setConnection:mySQLConnection]; + [tableContentInstance setConnection:mySQLConnection]; + [tableRelationsInstance setConnection:mySQLConnection]; + [customQueryInstance setConnection:mySQLConnection]; + [tableDumpInstance setConnection:mySQLConnection]; + [spExportControllerInstance setConnection:mySQLConnection]; + [tableDataInstance setConnection:mySQLConnection]; + [extendedTableInfoInstance setConnection:mySQLConnection]; + [databaseDataInstance setConnection:mySQLConnection]; + + // Set the cutom query editor's MySQL version + [customQueryInstance setMySQLversion:mySQLVersion]; + + [self setFileName:[NSString stringWithFormat:@"(MySQL %@) %@@%@ %@", mySQLVersion, [self user], [self host], [self database]]]; + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@", mySQLVersion, [self name], ([self database]?[self database]:@"")]]; + + // Connected Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Connected" + description:[NSString stringWithFormat:NSLocalizedString(@"Connected to %@",@"description for connected growl notification"), [tableWindow title]] + notificationName:@"Connected"]; + } +/** + * Set whether the connection controller should automatically start + * connecting; called by maincontroller, but only for first window. + */ +- (void)setShouldAutomaticallyConnect:(BOOL)shouldAutomaticallyConnect +{ + _shouldOpenConnectionAutomatically = shouldAutomaticallyConnect; +} + +/** + * Allow the connection controller to determine whether connection should + * be automatically triggered. + */ +- (BOOL)shouldAutomaticallyConnect +{ + return _shouldOpenConnectionAutomatically; +} + +#pragma mark - +#pragma mark Printing + - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame { //because I need the webFrame loaded (for preview), I've moved the actuall printing here. @@ -203,8 +269,8 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum if([[self user] length]) [connection setValue:[self user] forKey:@"username"]; [connection setValue:[self host] forKey:@"hostname"]; - if([[portField stringValue] length]) - [connection setValue:[portField stringValue] forKey:@"port"]; + if([[connectionController port] length]) + [connection setValue:[connectionController port] forKey:@"port"]; [connection setValue:selectedDatabase forKey:@"database"]; [connection setValue:versionForPrint forKey:@"version"]; @@ -257,569 +323,6 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum return result; } -- (CMMCPConnection *)sharedConnection -{ - return mySQLConnection; -} - -//start sheet - -/** - * Set whether the connection sheet should automatically start connecting - */ -- (void)setShouldAutomaticallyConnect:(BOOL)shouldAutomaticallyConnect -{ - _shouldOpenConnectionAutomatically = shouldAutomaticallyConnect; -} - -/** - * tries to connect to a database server, shows connect sheet prompting user to - * enter details/select favorite and shoows alert sheets on failure. - */ -- (IBAction)connectToDB:(id)sender -{ - [self clearStatusIcon]; - - // load the details of the currently selected favorite into the text boxes in connect sheet - [self chooseFavorite:self]; - - // run the connect sheet (modal) - [NSApp beginSheet:connectSheet - modalForWindow:tableWindow - modalDelegate:self - didEndSelector:nil - contextInfo:nil]; - - // Connect automatically to the last used or default favourite - // connectSheet must open first. - if (_shouldOpenConnectionAutomatically) { - _shouldOpenConnectionAutomatically = false; - [self initiateConnection:self]; - } -} - - - -/* - * Starts the connection process; invoked when user hits the connect button - * of the connection sheet or double-clicks on a favourite of the connectSheet. - * Error-checks fields as required, and triggers connection of MySQL or any - * proxies in use. - */ -- (IBAction)initiateConnection:(id)sender -{ - - // Error-check required fields before starting a connection - if (![[hostField stringValue] length] && ![[socketField stringValue] length]) { - [self failConnectionWithErrorMessage:NSLocalizedString(@"Insufficient details provided to establish a connection. Please provide at least a host or socket.", @"insufficient details informative message") withDetail:nil]; - return; - } - if ([sshCheckbox state] == NSOnState && ![[sshHostField stringValue] length]) { - [self failConnectionWithErrorMessage:NSLocalizedString(@"Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"message of panel when ssh details are incomplete") withDetail:nil]; - return; - } - - // Basic details have validated - start the connection process animating - [connectProgressBar startAnimation:self]; - [connectProgressStatusText setHidden:NO]; - [connectProgressStatusText display]; - - // If the password(s) are marked as having been originally sourced from a keychain, check whether they - // have been changed or not; if not, leave the mark in place and remove the password from the field - // for increased security. - if (connectionKeychainItemName) { - if ([[keyChainInstance getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount] isEqualToString:[passwordField stringValue]]) { - [passwordField setStringValue:[[NSString string] stringByPaddingToLength:[[passwordField stringValue] length] withString:@"sp" startingAtIndex:0]]; - [[self undoManager] removeAllActionsWithTarget:passwordField]; - } else { - [connectionKeychainItemName release], connectionKeychainItemName = nil; - [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; - } - } - if (connectionSSHKeychainItemName) { - if ([[keyChainInstance getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount] isEqualToString:[sshPasswordField stringValue]]) { - [sshPasswordField setStringValue:[[NSString string] stringByPaddingToLength:[[sshPasswordField stringValue] length] withString:@"sp" startingAtIndex:0]]; - [[self undoManager] removeAllActionsWithTarget:sshPasswordField]; - } else { - [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; - [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; - } - } - - // Initiate the SSH connection process if one has been set - if ([sshCheckbox state] == NSOnState) { - [self initiateSSHTunnelConnection]; - return; - } - - // ...or start the MySQL connection process directly - [self initiateMySQLConnection]; -} - -/* - * Initiate the SSH connection process, while the connection sheet is still open. - * This should only be called as part of initiateConnection:, and will indirectly - * call initiateMySQLConnection if it's successful. - */ -- (void)initiateSSHTunnelConnection -{ - [connectProgressStatusText setStringValue:NSLocalizedString(@"SSH connecting...", @"SSH connecting very short status message")]; - [connectProgressStatusText display]; - - // Set up the tunnel details - sshTunnel = [[SPSSHTunnel alloc] initToHost:[sshHostField stringValue] port:([[sshPortField stringValue] length]?[sshPortField intValue]:22) login:[sshUserField stringValue] tunnellingToPort:([[portField stringValue] length]?[portField intValue]:3306) onHost:[hostField stringValue]]; - [sshTunnel setParentWindow:tableWindow]; - - // Add keychain or plaintext password as appropriate - note the checks in initiateConnection. - if (connectionSSHKeychainItemName) { - [sshTunnel setPasswordKeychainName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; - } else { - [sshTunnel setPassword:[sshPasswordField stringValue]]; - } - - // Set the callback function on the tunnel - [sshTunnel setConnectionStateChangeSelector:@selector(sshTunnelCallback:) delegate:self]; - - // Ask the tunnel to connect. This will call the callback below on success or failure, passing - // itself as an argument - retain count should be one at this point. - [sshTunnel connect]; -} - -/* - * A callback function for the SSH Tunnel setup process - will be called on a connection - * state change, allowing connection to fail or proceed as appropriate. If successful, - * will call initiateMySQLConnection. - */ -- (void)sshTunnelCallback:(SPSSHTunnel *)theTunnel -{ - int newState = [theTunnel state]; - - if (newState == SPSSH_STATE_IDLE) { - [self setTitlebarStatus:@"SSH Disconnected"]; - //[self setStatusIconToImageWithName:@"ssh-disconnected"]; - [self failConnectionWithErrorMessage:[theTunnel lastError] withDetail:[sshTunnel debugMessages]]; - } else if (newState == SPSSH_STATE_CONNECTED) { - [self setTitlebarStatus:@"SSH Connected"]; - //[self setStatusIconToImageWithName:@"ssh-connected"]; - [self initiateMySQLConnection]; - } else { - [self setTitlebarStatus:@"SSH Connecting…"]; - //[self setStatusIconToImageWithName:@"ssh-connecting"]; - } -} - -/* - * Set up the MySQL connection, either through a successful tunnel or directly. - */ -- (void)initiateMySQLConnection -{ - CMMCPResult *theResult; - id version; - - if (sshTunnel) - [connectProgressStatusText setStringValue:NSLocalizedString(@"MySQL connecting...", @"MySQL connecting very short status message")]; - else - [connectProgressStatusText setStringValue:NSLocalizedString(@"Connecting...", @"Generic connecting very short status message")]; - [connectProgressStatusText display]; - - // Initialise to socket if appropriate. - // Note it is currently possible to connect to a socket with a useless SSH tunnel set - // up; this will be improved upon in future UI/code work. - if (![[socketField stringValue] isEqualToString:@""]) { - mySQLConnection = [[CMMCPConnection alloc] initToSocket:[socketField stringValue] - withLogin:[userField stringValue]]; - [hostField setStringValue:@"localhost"]; - - // Otherwise, initialise to host, using tunnel if appropriate - } else { - if (sshTunnel) { - mySQLConnection = [[CMMCPConnection alloc] initToHost:@"127.0.0.1" - withLogin:[userField stringValue] - usingPort:[sshTunnel localPort]]; - [mySQLConnection setSSHTunnel:sshTunnel]; - } else { - mySQLConnection = [[CMMCPConnection alloc] initToHost:[hostField stringValue] - withLogin:[userField stringValue] - usingPort:[portField intValue]]; - } - } - [mySQLConnection setParentWindow:tableWindow]; - - // Set the password as appropriate - if (connectionKeychainItemName) { - [mySQLConnection setPasswordKeychainName:connectionKeychainItemName account:connectionKeychainItemAccount]; - } else { - [mySQLConnection setPassword:[passwordField stringValue]]; - } - - // Connect - [mySQLConnection connect]; - - if (![mySQLConnection isConnected]) { - if (sshTunnel) { - - // If an SSH tunnel is running, temporarily block to allow the tunnel to register changes in state - [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; - - // If the state is connection refused, attempt the MySQL connection again with the host using the hostfield value. - if ([sshTunnel state] == SPSSH_STATE_FORWARDING_FAILED) { - if ([sshTunnel localPortFallback]) { - [mySQLConnection setPort:[sshTunnel localPortFallback]]; - [mySQLConnection connect]; - if (![mySQLConnection isConnected]) { - [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; - } - } - } - } - - if (![mySQLConnection isConnected]) { - NSString *errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@, or the request timed out.\n\nBe sure that the address is correct and that you have the necessary privileges, or try increasing the connection timeout (currently %i seconds).\n\nMySQL said: %@", @"message of panel when connection to host failed"), [hostField stringValue], [[prefs objectForKey:@"ConnectionTimeoutValue"] intValue], [mySQLConnection getLastErrorMessage]]; - if (sshTunnel && [sshTunnel state] == SPSSH_STATE_FORWARDING_FAILED) { - errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@ because the port connection via SSH was refused.\n\nPlease ensure that your MySQL host is set up to allow TCP/IP connections (no --skip-networking) and is configured to allow connections from the host you are tunnelling via.\n\nYou may also want to check the port is correct and that you have the necessary privileges.\n\nChecking the error detail will show the SSH debug log which may provide more details.\n\nMySQL said: %@", @"message of panel when SSH port forwarding failed"), [hostField stringValue], [mySQLConnection getLastErrorMessage]]; - [self failConnectionWithErrorMessage:errorMessage withDetail:[sshTunnel debugMessages]]; - } else { - [self failConnectionWithErrorMessage:errorMessage withDetail:nil]; - } - - if (sshTunnel) [sshTunnel release], sshTunnel = nil; - [mySQLConnection release], mySQLConnection = nil; - return; - } - } - if (![[databaseField stringValue] isEqualToString:@""]) { - if ([mySQLConnection selectDB:[databaseField stringValue]]) { - if (selectedDatabase) [selectedDatabase release], selectedDatabase = nil; - selectedDatabase = [[databaseField stringValue] retain]; - } else { - [self failConnectionWithErrorMessage:[NSString stringWithFormat:NSLocalizedString(@"Connected to host, but unable to connect to database %@.\n\nBe sure that the database exists and that you have the necessary privileges.\n\nMySQL said: %@", @"message of panel when connection to db failed"), [databaseField stringValue], [mySQLConnection getLastErrorMessage]] withDetail:nil]; - if (sshTunnel) [sshTunnel release], sshTunnel = nil; - [mySQLConnection release], mySQLConnection = nil; - return; - } - } - - // Successful connection! Close the connection sheet - [connectSheet orderOut:nil]; - [NSApp endSheet:connectSheet]; - [connectProgressBar stopAnimation:self]; - [connectProgressStatusText setHidden:YES]; - - // Set up the connection. - // Register as a delegate - [mySQLConnection setDelegate:self]; - - // Release the tunnel if set - will now be retained by the connection - if (sshTunnel) [sshTunnel release], sshTunnel = nil; - - // Set encoding - NSString *encodingName = [prefs objectForKey:@"DefaultEncoding"]; - if ( [encodingName isEqualToString:@"Autodetect"] ) { - [self setConnectionEncoding:[self databaseEncoding] reloadingViews:NO]; - } else { - [self setConnectionEncoding:[self mysqlEncodingFromDisplayEncoding:encodingName] reloadingViews:NO]; - } - - // Get the mysql version - theResult = [mySQLConnection queryString:@"SHOW VARIABLES LIKE 'version'"]; - version = [[theResult fetchRowAsArray] objectAtIndex:1]; - if ( [version isKindOfClass:[NSData class]] ) { - // starting with MySQL 4.1.14 the mysql variables are returned as nsdata - mySQLVersion = [[NSString alloc] initWithData:version encoding:[mySQLConnection encoding]]; - } else { - mySQLVersion = [[NSString stringWithString:version] retain]; - } - - [self setDatabases:self]; - - // For each of the main controllers assign the current connection - [tablesListInstance setConnection:mySQLConnection]; - [tableSourceInstance setConnection:mySQLConnection]; - [tableContentInstance setConnection:mySQLConnection]; - [tableRelationsInstance setConnection:mySQLConnection]; - [customQueryInstance setConnection:mySQLConnection]; - [tableDumpInstance setConnection:mySQLConnection]; - [spExportControllerInstance setConnection:mySQLConnection]; - [tableDataInstance setConnection:mySQLConnection]; - [extendedTableInfoInstance setConnection:mySQLConnection]; - [databaseDataInstance setConnection:mySQLConnection]; - - // Set the cutom query editor's MySQL version - [customQueryInstance setMySQLversion:mySQLVersion]; - - [self setFileName:[NSString stringWithFormat:@"(MySQL %@) %@@%@ %@", mySQLVersion, [userField stringValue], - [hostField stringValue], [databaseField stringValue]]]; - [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/%@", mySQLVersion, [self name], [databaseField stringValue]]]; - - // Connected Growl notification - [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Connected" - description:[NSString stringWithFormat:NSLocalizedString(@"Connected to %@",@"description for connected growl notification"), [tableWindow title]] - notificationName:@"Connected"]; -} - -/* - * Ends a connection attempt by stopping the connect sheet animation, - * stopping the document-modal sheet, and displaying a specified error - * message. The button on the error message will open the connection - * sheet again with the failed details. - */ -- (void)failConnectionWithErrorMessage:(NSString *)theErrorMessage withDetail:(NSString *)errorDetail -{ - // Clean up the interface - [connectProgressBar stopAnimation:self]; - [connectProgressBar display]; - [connectProgressStatusText setHidden:YES]; - [connectProgressStatusText display]; - - // Stop the modal sheet - [connectSheet orderOut:nil]; - [NSApp endSheet:connectSheet]; - - // Release as appropriate - if (sshTunnel) [sshTunnel disconnect], [sshTunnel release], sshTunnel = nil; - - if (errorDetail) [errorDetailText setString:errorDetail]; - - // Display the connection error message - NSBeginAlertSheet(NSLocalizedString(@"Connection failed!", @"connection failed title"), NSLocalizedString(@"OK", @"OK button"), errorDetail?NSLocalizedString(@"Show detail", @"Show detail button"):nil, nil, tableWindow, self, nil, @selector(sheetDidEnd:returnCode:contextInfo:), @"connect", theErrorMessage); -} - -- (IBAction)cancelConnectSheet:(id)sender -{ - [NSApp endSheet:connectSheet]; - [tableWindow close]; -} - -/** - * Invoked when user hits the cancel button of the connectSheet - * stops modal session with code 0 - * reused when user hits the close button of the variablseSheet or of the createTableSyntaxSheet - */ -- (IBAction)closeSheet:(id)sender -{ - [NSApp stopModalWithCode:0]; -} - -/** - * sets fields for the chosen favorite. - */ -- (IBAction)chooseFavorite:(id)sender -{ - if (![self selectedFavorite]) - return; - - if (connectionKeychainItemName) [connectionKeychainItemName release], connectionKeychainItemName = nil; - if (connectionKeychainItemAccount) [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; - if (connectionSSHKeychainItemName) [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; - if (connectionSSHKeychainItemAccount) [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; - - [nameField setStringValue:([self valueForKeyPath:@"selectedFavorite.name"] ? [self valueForKeyPath:@"selectedFavorite.name"] : @"")]; - [hostField setStringValue:([self valueForKeyPath:@"selectedFavorite.host"] ? [self valueForKeyPath:@"selectedFavorite.host"] : @"")]; - [socketField setStringValue:([self valueForKeyPath:@"selectedFavorite.socket"] ? [self valueForKeyPath:@"selectedFavorite.socket"] : @"")]; - [userField setStringValue:([self valueForKeyPath:@"selectedFavorite.user"] ? [self valueForKeyPath:@"selectedFavorite.user"] : @"")]; - [portField setStringValue:([self valueForKeyPath:@"selectedFavorite.port"] ? [self valueForKeyPath:@"selectedFavorite.port"] : @"")]; - [databaseField setStringValue:([self valueForKeyPath:@"selectedFavorite.database"] ? [self valueForKeyPath:@"selectedFavorite.database"] : @"")]; - [sshCheckbox setState:([self valueForKeyPath:@"selectedFavorite.useSSH"] ? ([[self valueForKeyPath:@"selectedFavorite.useSSH"] boolValue]?NSOnState:NSOffState) : NSOffState)]; - [self toggleUseSSH:self]; - [sshHostField setStringValue:([self valueForKeyPath:@"selectedFavorite.sshHost"] ? [self valueForKeyPath:@"selectedFavorite.sshHost"] : @"")]; - [sshUserField setStringValue:([self valueForKeyPath:@"selectedFavorite.sshUser"] ? [self valueForKeyPath:@"selectedFavorite.sshUser"] : @"")]; - [sshPortField setStringValue:([self valueForKeyPath:@"selectedFavorite.sshPort"] ? [self valueForKeyPath:@"selectedFavorite.sshPort"] : @"")]; - - // Check whether the password exists in the keychain, and if so add it; also record the - // keychain details so we can pass around only those details if the password doesn't change - connectionKeychainItemName = [[keyChainInstance nameForFavoriteName:[self valueForKeyPath:@"selectedFavorite.name"] id:[self valueForKeyPath:@"selectedFavorite.id"]] retain]; - connectionKeychainItemAccount = [[keyChainInstance accountForUser:[self valueForKeyPath:@"selectedFavorite.user"] host:[self valueForKeyPath:@"selectedFavorite.host"] database:[self valueForKeyPath:@"selectedFavorite.database"]] retain]; - if ([keyChainInstance passwordExistsForName:connectionKeychainItemName account:connectionKeychainItemAccount]) { - [passwordField setStringValue:[keyChainInstance getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]]; - } else { - [connectionKeychainItemName release], connectionKeychainItemName = nil; - [connectionKeychainItemAccount release], connectionKeychainItemAccount = nil; - [passwordField setStringValue:@""]; - } - - // And the same for the SSH password - connectionSSHKeychainItemName = [[NSString alloc] initWithString:[keyChainInstance nameForSSHForFavoriteName:[self valueForKeyPath:@"selectedFavorite.name"] id:[self valueForKeyPath:@"selectedFavorite.id"]]]; - connectionSSHKeychainItemAccount = [[NSString alloc] initWithString:[keyChainInstance accountForSSHUser:[self valueForKeyPath:@"selectedFavorite.sshUser"] sshHost:[self valueForKeyPath:@"selectedFavorite.sshHost"]]]; - if ([keyChainInstance passwordExistsForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]) { - [sshPasswordField setStringValue:[keyChainInstance getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]]; - } else { - [connectionSSHKeychainItemName release], connectionSSHKeychainItemName = nil; - [connectionSSHKeychainItemAccount release], connectionSSHKeychainItemAccount = nil; - [sshPasswordField setStringValue:@""]; - } - - [prefs setInteger:[favoritesController selectionIndex] forKey:@"LastFavoriteIndex"]; -} - -/** - * Updates the interface when the "Use SSH Tunnel" checkbox is ticked/unticked - */ -- (IBAction)toggleUseSSH:(id)sender -{ - BOOL sshIsEnabledValue = ([sshCheckbox state] == NSOnState); - [sshHostField setEnabled:sshIsEnabledValue]; - [sshUserField setEnabled:sshIsEnabledValue]; - [sshPasswordField setEnabled:sshIsEnabledValue]; - [sshPortField setEnabled:sshIsEnabledValue]; - - if (sender == sshCheckbox) [favoritesController setSelectionIndexes:[NSIndexSet indexSet]]; -} - -/** - * Opens the preferences window, or brings it to the front, and switch to the favorites tab. - * If a favorite is selected in the connection sheet, it is also select in the prefs window. - */ -- (IBAction)editFavorites:(id)sender -{ - SPPreferenceController *prefsController = [[NSApp delegate] preferenceController]; - - [prefsController showWindow:self]; - [prefsController displayFavoritePreferences:self]; - [prefsController selectFavorites:[favoritesController selectedObjects]]; -} - -/** - * returns a KVC-compliant proxy to the currently selected favorite, or nil if nothing selected. - * - * see [NSObjectController selection] - */ -- (id)selectedFavorite -{ - if ([favoritesController selectionIndex] == NSNotFound) - return nil; - - return [favoritesController selection]; -} - -- (void)connectSheetAddToFavorites:(id)sender -{ - [self addToFavoritesName:[nameField stringValue] host:[hostField stringValue] socket:[socketField stringValue] user:[userField stringValue] password:[passwordField stringValue] port:[portField stringValue] database:[databaseField stringValue] useSSH:([sshCheckbox state] == NSOnState) sshHost:[sshHostField stringValue] sshUser:[sshUserField stringValue] sshPassword:[sshPasswordField stringValue] sshPort:[sshPortField stringValue]]; - [connectFavoritesTableView scrollRowToVisible:[connectFavoritesTableView selectedRow]]; -} - -/** - * add actual connection to favorites - */ -- (void)addToFavoritesName:(NSString *)name host:(NSString *)host socket:(NSString *)socket - user:(NSString *)user password:(NSString *)password - port:(NSString *)port database:(NSString *)database - useSSH:(BOOL)useSSH - sshHost:(NSString *)sshHost sshUser:(NSString *)sshUser - sshPassword:(NSString *)sshPassword sshPort:(NSString *)sshPort -{ - NSString *favoriteName = [name length]?name:[NSString stringWithFormat:@"%@@%@", user, host]; - NSNumber *favoriteid = [NSNumber numberWithInt:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]]; - if (![name length] && ![database isEqualToString:@""]) - favoriteName = [NSString stringWithFormat:@"%@ %@", database, favoriteName]; - - // Ensure that host and socket are not nil - if ([host isEqualToString:@""] && [socket isEqualToString:@""]) { - NSRunAlertPanel(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"Insufficient details provided to establish a connection. Please provide at least a host or socket.", @"insufficient details informative message"), NSLocalizedString(@"OK", @"OK button"), nil, nil); - return; - } - - // If SSH is enabled, ensure that the SSH host is not nil - if ([sshCheckbox state] == NSOnState && ![[sshHostField stringValue] length]) { - NSRunAlertPanel(NSLocalizedString(@"Insufficient connection details", @"insufficient details message"), NSLocalizedString(@"Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"message of panel when ssh details are incomplete"), NSLocalizedString(@"OK", @"OK button"), nil, nil); - return; - } - - // Write favorites and password(s) - NSDictionary *newFavorite = [NSDictionary dictionaryWithObjectsAndKeys: - favoriteName, @"name", - host, @"host", - socket, @"socket", - user, @"user", - port, @"port", - database, @"database", - [NSNumber numberWithBool:useSSH], @"useSSH", - sshHost, @"sshHost", - sshUser, @"sshUser", - sshPort, @"sshPort", - favoriteid, @"id", - nil]; - if (![password isEqualToString:@""]) { - [keyChainInstance addPassword:password - forName:[keyChainInstance nameForFavoriteName:favoriteName id:[NSString stringWithFormat:@"%i", [favoriteid intValue]]] - account:[keyChainInstance accountForUser:user host:host database:database]]; - } - if (![sshPassword isEqualToString:@""]) { - [keyChainInstance addPassword:password - forName:[keyChainInstance nameForSSHForFavoriteName:favoriteName id:[NSString stringWithFormat:@"%i", [favoriteid intValue]]] - account:[keyChainInstance accountForSSHUser:sshUser sshHost:sshHost]]; - } - - [favoritesController addObject:newFavorite]; - [favoritesController setSelectedObjects:[NSArray arrayWithObject:newFavorite]]; - [[[NSApp delegate] preferenceController] updateDefaultFavoritePopup]; -} - -/** - * alert sheets method - * invoked when alertSheet get closed - * if contextInfo == connect -> reopens the connectSheet - * if contextInfo == removedatabase -> tries to remove the selected database - */ -- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo -{ - if ([contextInfo isEqualToString:@"connect"]) { - [sheet orderOut:self]; - if (returnCode == NSAlertAlternateReturn) [errorDetailWindow makeKeyAndOrderFront:self]; - - // Restore the passwords from keychain for editing if appropriate - if (connectionKeychainItemName) { - [passwordField setStringValue:[keyChainInstance getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]]; - } - if (connectionSSHKeychainItemName) { - [sshPasswordField setStringValue:[keyChainInstance getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]]; - } - - [self connectToDB:nil]; - return; - } - - if ([contextInfo isEqualToString:@"removedatabase"]) { - if (returnCode != NSAlertDefaultReturn) - return; - - [mySQLConnection queryString:[NSString stringWithFormat:@"DROP DATABASE %@", [[self database] backtickQuotedString]]]; - if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { - // error while deleting db - [self performSelector:@selector(showErrorSheetWith:) - withObject:[NSArray arrayWithObjects:NSLocalizedString(@"Error", @"error"), - [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove database.\nMySQL said: %@", @"message of panel when removing db failed"), - [mySQLConnection getLastErrorMessage]], - nil] - afterDelay:0.3]; - return; - } - - // db deleted with success - selectedDatabase = nil; - [self setDatabases:self]; - [tablesListInstance setConnection:mySQLConnection]; - [tableDumpInstance setConnection:mySQLConnection]; - [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/", mySQLVersion, [self name]]]; - } -} - -/* - * Show Error sheet (can be called from inside of a endSheet selector) - * via [self performSelector:@selector(showErrorSheetWithTitle:) withObject: afterDelay:] - */ --(void)showErrorSheetWith:(id)error -{ - // error := first object is the title , second the message, only one button OK - NSBeginAlertSheet([error objectAtIndex:0], NSLocalizedString(@"OK", @"OK button"), - nil, nil, tableWindow, self, nil, nil, nil, - [error objectAtIndex:1]); -} - -- (IBAction)connectSheetShowHelp:(id)sender -{ - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sequelpro.com/docs/Getting_Connected"]]; -} - #pragma mark - #pragma mark Database methods @@ -990,6 +493,51 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum [alert beginSheetModalForWindow:tableWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:@"removedatabase"]; } +/** + * alert sheets method + * invoked when alertSheet get closed + * if contextInfo == removedatabase -> tries to remove the selected database + */ +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo +{ + if ([contextInfo isEqualToString:@"removedatabase"]) { + if (returnCode != NSAlertDefaultReturn) + return; + + [mySQLConnection queryString:[NSString stringWithFormat:@"DROP DATABASE %@", [[self database] backtickQuotedString]]]; + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + // error while deleting db + [self performSelector:@selector(showErrorSheetWith:) + withObject:[NSArray arrayWithObjects:NSLocalizedString(@"Error", @"error"), + [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove database.\nMySQL said: %@", @"message of panel when removing db failed"), + [mySQLConnection getLastErrorMessage]], + nil] + afterDelay:0.3]; + return; + } + + // db deleted with success + selectedDatabase = nil; + [self setDatabases:self]; + [tablesListInstance setConnection:mySQLConnection]; + [tableDumpInstance setConnection:mySQLConnection]; + [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@/", mySQLVersion, [self name]]]; + } +} + +/* + * Show Error sheet (can be called from inside of a endSheet selector) + * via [self performSelector:@selector(showErrorSheetWithTitle:) withObject: afterDelay:] + */ +-(void)showErrorSheetWith:(id)error +{ + // error := first object is the title , second the message, only one button OK + NSBeginAlertSheet([error objectAtIndex:0], NSLocalizedString(@"OK", @"OK button"), + nil, nil, tableWindow, self, nil, nil, nil, + [error objectAtIndex:1]); +} + + /* * Reset the current selected database name */ @@ -1657,22 +1205,12 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum #pragma mark Other Methods /** - * Returns the host + * Invoked when user hits the cancel button or close button in + * dialogs such as the variableSheet or the createTableSyntaxSheet */ -- (NSString *)host -{ - return [hostField stringValue]; -} - -/** - * Returns the name - */ -- (NSString *)name +- (IBAction)closeSheet:(id)sender { - if ([[nameField stringValue] length]) { - return [nameField stringValue]; - } - return [NSString stringWithFormat:@"%@@%@", [userField stringValue], [hostField stringValue]]; + [NSApp stopModalWithCode:0]; } /** @@ -1742,7 +1280,31 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum notificationName:@"Disconnected"]; } -// Getter methods +#pragma mark - +#pragma mark Getter methods + +/** + * Returns the host + */ +- (NSString *)host +{ + if ([connectionController type] == SP_CONNECTION_SOCKET) return @"localhost"; + return [connectionController host]; +} + +/** + * Returns the name + */ +- (NSString *)name +{ + if ([[connectionController name] length]) { + return [connectionController name]; + } + if ([connectionController type] == SP_CONNECTION_SOCKET) { + return [NSString stringWithFormat:@"%@@%@", [connectionController user], [connectionController host]]; + } + return [NSString stringWithFormat:@"%@@%@", [connectionController user], [connectionController host]]; +} /** * Returns the currently selected database @@ -1773,10 +1335,11 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum */ - (NSString *)user { - return [userField stringValue]; + return [connectionController user]; } -// Notification center methods +#pragma mark - +#pragma mark Notification center methods /** * Invoked before a query is performed @@ -1814,14 +1377,8 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum [tablesListInstance selectionShouldChangeInTableView:nil]; } -/** - * The status of the tunnel has changed - */ -- (void)tunnelStatusChanged:(NSNotification *)notification -{ -} - -// Menu methods +#pragma mark - +#pragma mark Menu methods /** * Passes the request to the tableDump object @@ -1886,6 +1443,15 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum */ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + if (!_isConnected) { + if ([menuItem action] == @selector(newDocument:) || + [menuItem action] == @selector(terminate:)) + { + return YES; + } else { + return NO; + } + } if ([menuItem action] == @selector(import:) || [menuItem action] == @selector(export:) || [menuItem action] == @selector(exportMultipleTables:) || @@ -1917,7 +1483,7 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum } if ([menuItem action] == @selector(addConnectionToFavorites:)) { - return (![self _favoriteAlreadyExists:[self database] host:[self host] user:[self user]]); + return ([connectionController selectedFavorite]?NO:YES); } return [super validateMenuItem:menuItem]; @@ -2021,23 +1587,12 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum // Obviously don't add if it already exists. We shouldn't really need this as the menu item validation // enables or disables the menu item based on the same method. Although to be safe do the check anyway // as we don't know what's calling this method. - if ([self _favoriteAlreadyExists:[self database] host:[self host] user:[self user]]) { + if ([connectionController selectedFavorite]) { return; } - // Add current connection to favorites - NSString *password, *sshPassword; - if (connectionKeychainItemName) { - password = [keyChainInstance getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]; - } else { - password = [passwordField stringValue]; - } - if (connectionSSHKeychainItemName) { - sshPassword = [keyChainInstance getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]; - } else { - sshPassword = [sshPasswordField stringValue]; - } - [self addToFavoritesName:[nameField stringValue] host:[hostField stringValue] socket:[socketField stringValue] user:[userField stringValue] password:password port:[portField stringValue] database:[databaseField stringValue] useSSH:([sshCheckbox state] == NSOnState) sshHost:[sshHostField stringValue] sshUser:[sshUserField stringValue] sshPassword:sshPassword sshPort:[sshPortField stringValue]]; + // Request the connection controller to add its details to favorites + [connectionController addFavorite:self]; } /** @@ -2121,10 +1676,7 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum // attach the toolbar to the document window [tableWindow setToolbar:mainToolbar]; - - // select the structure toolbar item - [self viewStructure:self]; - + // update the toolbar item size [self updateChooseDatabaseToolbarItemWidth]; } @@ -2299,6 +1851,8 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum */ - (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem; { + if (!_isConnected) return NO; + NSString *identifier = [toolbarItem itemIdentifier]; // Toggle console item @@ -2321,7 +1875,8 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum return YES; } -// NSDocument methods +#pragma mark - +#pragma mark NSDocument methods /** * Returns the name of the nib file @@ -2366,17 +1921,6 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum [[theCol dataCell] setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; } } - - //set up toolbar - [self setupToolbar]; - // [self connectToDB:nil]; - [self performSelector:@selector(connectToDB:) withObject:tableWindow afterDelay:0.0f]; - - if([prefs boolForKey:@"SelectLastFavoriteUsed"] == YES){ - [favoritesController setSelectionIndex:[prefs integerForKey:@"LastFavoriteIndex"]]; - } else { - [favoritesController setSelectionIndex:[prefs integerForKey:@"DefaultFavorite"]]; - } } // NSWindow delegate methods @@ -2387,7 +1931,6 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum - (void)windowWillClose:(NSNotification *)aNotification { if ([mySQLConnection isConnected]) [self closeConnection]; - if (sshTunnel) [sshTunnel disconnect], [sshTunnel release], sshTunnel = nil; if ([[[SPQueryConsole sharedQueryConsole] window] isVisible]) [self toggleConsole:self]; [[customQueryInstance helpWebViewWindow] release]; [createTableSyntaxWindow orderOut:nil]; @@ -2428,24 +1971,14 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum } #pragma mark - -#pragma mark Connection sheet delegate methods +#pragma mark Database name field delegate methods /** - * When a favorite is selected, and the connection details are edited, deselect the favorite; - * this is clearer and also prevents a failed connection from being repopulated with the - * favorite's details instead of the last used details. + * When adding a database, enable the button only if the new name has a length. */ - (void) controlTextDidChange:(NSNotification *)aNotification { - if ([aNotification object] == nameField || [aNotification object] == hostField - || [aNotification object] == userField || [aNotification object] == passwordField - || [aNotification object] == databaseField || [aNotification object] == socketField - || [aNotification object] == portField || [aNotification object] == sshHostField - || [aNotification object] == sshUserField || [aNotification object] == sshPasswordField - || [aNotification object] == sshPortField) { - [favoritesController setSelectionIndexes:[NSIndexSet indexSet]]; - } - else if ([aNotification object] == databaseNameField) { + if ([aNotification object] == databaseNameField) { [addDatabaseButton setEnabled:([[databaseNameField stringValue] length] > 0)]; } } @@ -2581,12 +2114,6 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum return theValue; } -- (IBAction)terminate:(id)sender -{ - [[NSApp orderedDocuments] makeObjectsPerformSelector:@selector(cancelConnectSheet:) withObject:nil]; - [NSApp terminate:sender]; -} - - (void)dealloc { [chooseDatabaseButton release]; @@ -2594,39 +2121,9 @@ NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocum [variables release]; [selectedDatabase release]; [mySQLVersion release]; + [connectionController release]; [super dealloc]; } -@end - -#pragma mark - - -@implementation TableDocument (PrivateAPI) - -/** - * Checks to see if a favorite with the supplied details already exists. - */ -- (BOOL)_favoriteAlreadyExists:(NSString *)database host:(NSString *)host user:(NSString *)user -{ - NSArray *favorites = [favoritesController arrangedObjects]; - - // Ensure database, host, and user match prefs format - if (!database) database = @""; - if (!host) host = @""; - if (!user) user = @""; - - // Loop the favorites and check their details - for (NSDictionary *favorite in favorites) - { - if ([[favorite objectForKey:@"database"] isEqualToString:database] && - [[favorite objectForKey:@"host"] isEqualToString:host] && - [[favorite objectForKey:@"user"] isEqualToString:user]) { - return YES; - } - } - - return NO; -} - -@end +@end
\ No newline at end of file |