//
//  SPConnectionController.m
//  sequel-pro
//
//  Created by Stuart Connolly (stuconnolly.com) on November 15, 2010.
//  Copyright (c) 2010 Stuart Connolly. All rights reserved.
//
//  Permission is hereby granted, free of charge, to any person
//  obtaining a copy of this software and associated documentation
//  files (the "Software"), to deal in the Software without
//  restriction, including without limitation the rights to use,
//  copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the
//  Software is furnished to do so, subject to the following
//  conditions:
//
//  The above copyright notice and this permission notice shall be
//  included in all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//  OTHER DEALINGS IN THE SOFTWARE.
//
//  More info at <https://github.com/sequelpro/sequelpro>

#import "SPConnectionController.h"
#import "SPDatabaseDocument.h"
#import "SPAppController.h"
#import "SPPreferenceController.h"
#import "ImageAndTextCell.h"
#import "RegexKitLite.h"
#import "SPAlertSheets.h"
#import "SPKeychain.h"
#import "SPSSHTunnel.h"
#import "SPTableTextFieldCell.h"
#import "SPFavoritesController.h"
#import "SPFavoriteNode.h"
#import "SPGeneralPreferencePane.h"
#import "SPTreeNode.h"
#import "SPFavoritesExporter.h"
#import "SPFavoritesImporter.h"
#import "SPThreadAdditions.h"
#import "SPFavoriteColorSupport.h"
#import "SPNamedNode.h"
#import "SPWindowController.h"
#import "SPFavoritesOutlineView.h"
#import "SPCategoryAdditions.h"
#import "SPFavoriteTextFieldCell.h"
#import "SPGroupNode.h"
#import "SPSplitView.h"
#import "SPColorSelectorView.h"
#import "SPFunctions.h"
#import "SPBundleHTMLOutputController.h"

#import <SPMySQL/SPMySQL.h>

#import "sequel-ace-Swift.h"

// Constants
static NSString *SPRemoveNode              = @"RemoveNode";
static NSString *SPExportFavoritesFilename = @"SequelProFavorites.plist";
static NSString *SPLocalhostAddress        = @"127.0.0.1";

static NSString *SPDatabaseImage           = @"database-small";
static NSString *SPQuickConnectImage       = @"quick-connect-icon.pdf";
static NSString *SPQuickConnectImageWhite  = @"quick-connect-icon-white.pdf";

static NSString *SPConnectionViewNibName   = @"ConnectionView";

const static NSInteger SPUseServerTimeZoneTag = -1;
const static NSInteger SPUseSystemTimeZoneTag = -2;


@interface SPConnectionController ()

// Privately redeclare as read/write to get the synthesized setter
@property (readwrite, assign) BOOL isEditingConnection;
@property (readwrite, assign) BOOL allowSplitViewResizing;

- (void)_saveCurrentDetailsCreatingNewFavorite:(BOOL)createNewFavorite validateDetails:(BOOL)validateDetails;
- (BOOL)_checkHost;
- (void)_sortFavorites;
- (void)_sortTreeNode:(SPTreeNode *)node usingKey:(NSString *)key;
- (void)_favoriteTypeDidChange;
- (void)_reloadFavoritesViewData;
- (void)_updateFavoriteFirstResponder;
- (void)_restoreConnectionInterface;
- (void)_selectNode:(SPTreeNode *)node;
- (void)_scrollToSelectedNode;
- (void)_removeNode:(SPTreeNode *)node;
- (void)_removeAllPasswordsForNode:(SPTreeNode *)node;

- (NSNumber *)_createNewFavoriteID;
- (SPTreeNode *)_favoriteNodeForFavoriteID:(NSInteger)favoriteID;
- (NSString *)_stripInvalidCharactersFromString:(NSString *)subject;

- (NSString *)_generateNameForConnection;

- (void)_startEditingConnection;

- (void)_documentWillClose:(NSNotification *)notification;

static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, void *key);

#pragma mark - SPConnectionControllerDelegate

- (void)_stopEditingConnection;

#pragma mark - SPConnectionHandlerPrivateAPI

- (void)_showConnectionTestResult:(NSString *)resultString;

#pragma mark - SPConnectionControllerDelegate_Private_API

- (void)_setNodeIsExpanded:(BOOL)expanded fromNotification:(NSNotification *)notification;

#pragma mark - SPConnectionControllerInitializer_Private_API

- (void)_restoreOutlineViewStateNode:(SPTreeNode *)node;
- (void)_processFavoritesDataChange:(NSNotification *)aNotification;
- (void)scrollViewFrameChanged:(NSNotification *)aNotification;

@end

@implementation SPConnectionController

@synthesize delegate;
@synthesize type;
@synthesize name;
@synthesize host;
@synthesize user;
@synthesize password;
@synthesize database;
@synthesize socket;
@synthesize port;
@synthesize colorIndex;
@synthesize timeZoneMode;
@synthesize timeZoneIdentifier;
@synthesize allowDataLocalInfile;
@synthesize enableClearTextPlugin;
@synthesize useSSL;
@synthesize sslKeyFileLocationEnabled;
@synthesize sslKeyFileLocation;
@synthesize sslCertificateFileLocationEnabled;
@synthesize sslCertificateFileLocation;
@synthesize sslCACertFileLocationEnabled;
@synthesize sslCACertFileLocation;
@synthesize sshHost;
@synthesize sshUser;
@synthesize sshPassword;
@synthesize sshKeyLocationEnabled;
@synthesize sshKeyLocation;
@synthesize sshPort;
@synthesize useCompression;
@synthesize bookmarks;
@synthesize resolvedBookmarks;
@synthesize allowSplitViewResizing;

@synthesize connectionKeychainID = connectionKeychainID;
@synthesize connectionKeychainItemName;
@synthesize connectionKeychainItemAccount;
@synthesize connectionSSHKeychainItemName;
@synthesize connectionSSHKeychainItemAccount;
@synthesize socketHelpWindowUUID;
@synthesize isConnecting;
@synthesize isEditingConnection;

+ (void)initialize {
}

- (NSString *)keychainPassword
{
	NSString *kcItemName = [self connectionKeychainItemName];
	// If no keychain item is available, return an empty password
	if (!kcItemName) return nil;

	// Otherwise, pull the password from the keychain using the details from this connection
	NSString *kcPassword = [keychain getPasswordForName:kcItemName account:[self connectionKeychainItemAccount]];

	return kcPassword;
}

- (NSString *)keychainPasswordForSSH
{
	if (![self connectionKeychainItemName]) return nil;

	// Otherwise, pull the password from the keychain using the details from this connection
	NSString *kcSSHPassword = [keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount];

	return kcSSHPassword;
}

#pragma mark -
#pragma mark Connection processes

-(BOOL)connected{
	
	SPReachability *reachability = [SPReachability reachabilityForInternetConnection];
	NetworkStatus networkStatus = [reachability currentReachabilityStatus];
	return networkStatus != NotReachable;

}


/**
 * 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
{
	// If this action was triggered via a double-click on the favorites outline view,
	// ensure that one of the connections was double-clicked, not the area above or below
	if (sender == favoritesOutlineView && [favoritesOutlineView clickedRow] <= 0) return;

	// If triggered via the "Test Connection" button, set the state - otherwise clear it
	isTestingConnection = (sender == testConnectButton);
	
	NSFileManager *fileManager = [NSFileManager defaultManager];
	
	// Ensure that host is not empty if this is a TCP/IP or SSH connection
	if (([self type] == SPTCPIPConnection || [self type] == SPSSHTunnelConnection) && ![[self host] length]) {
		SPOnewayAlertSheet(
			NSLocalizedString(@"Insufficient connection details", @"insufficient details message"),
			[dbDocument parentWindow],
			NSLocalizedString(@"Insufficient details provided to establish a connection. Please enter at least the hostname.", @"insufficient details informative message")
		);
		return;
	}

	// If SSH is enabled, ensure that the SSH host is not nil
	if ([self type] == SPSSHTunnelConnection && ![[self sshHost] length]) {
		SPOnewayAlertSheet(
			NSLocalizedString(@"Insufficient connection details", @"insufficient details message"),
			[dbDocument parentWindow],
			NSLocalizedString(@"Insufficient details provided to establish a connection. Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"insufficient SSH tunnel details informative message")
		);
		return;
	}

	// If an SSH key has been provided, verify it exists
	if ([self type] == SPSSHTunnelConnection && sshKeyLocationEnabled && sshKeyLocation) {
		if (![fileManager fileExistsAtPath:[sshKeyLocation stringByExpandingTildeInPath]]) {
			[self setSshKeyLocationEnabled:NSOffState];
			SPOnewayAlertSheet(
				NSLocalizedString(@"SSH Key not found", @"SSH key check error"),
				[dbDocument parentWindow],
				NSLocalizedString(@"A SSH key location was specified, but no file was found in the specified location.  Please re-select the key and try again.", @"SSH key not found message")
			);
			return;
		}
	}

	// Ensure that a socket connection is not inadvertently used
	if (![self _checkHost]) return;

	// If SSL keys have been supplied, verify they exist
	if (([self type] == SPTCPIPConnection || [self type] == SPSocketConnection) && [self useSSL]) {
		
		if (sslKeyFileLocationEnabled && sslKeyFileLocation && 
			![fileManager fileExistsAtPath:[sslKeyFileLocation stringByExpandingTildeInPath]])
		{
			[self setSslKeyFileLocationEnabled:NSOffState];
			[self setSslKeyFileLocation:nil];
			
			SPOnewayAlertSheet(
				NSLocalizedString(@"SSL Key File not found", @"SSL key file check error"),
				[dbDocument parentWindow],
				NSLocalizedString(@"A SSL key file location was specified, but no file was found in the specified location.  Please re-select the key file and try again.", @"SSL key file not found message")
			);
			
			return;
		}
		
		if (sslCertificateFileLocationEnabled && sslCertificateFileLocation && 
			![fileManager fileExistsAtPath:[sslCertificateFileLocation stringByExpandingTildeInPath]])
		{
			[self setSslCertificateFileLocationEnabled:NSOffState];
			[self setSslCertificateFileLocation:nil];
			
			SPOnewayAlertSheet(
				NSLocalizedString(@"SSL Certificate File not found", @"SSL certificate file check error"),
				[dbDocument parentWindow],
				NSLocalizedString(@"A SSL certificate location was specified, but no file was found in the specified location.  Please re-select the certificate and try again.", @"SSL certificate file not found message")
			);
			
			return;
		}
		
		if (sslCACertFileLocationEnabled && sslCACertFileLocation && 
			![fileManager fileExistsAtPath:[sslCACertFileLocation stringByExpandingTildeInPath]])
		{
			[self setSslCACertFileLocationEnabled:NSOffState];
			[self setSslCACertFileLocation:nil];
			
			SPOnewayAlertSheet(
				NSLocalizedString(@"SSL Certificate Authority File not found", @"SSL certificate authority file check error"),
				[dbDocument parentWindow],
				NSLocalizedString(@"A SSL Certificate Authority certificate location was specified, but no file was found in the specified location.  Please re-select the Certificate Authority certificate and try again.", @"SSL CA certificate file not found message")
			);
			
			return;
		}
	}

	// Basic details have validated - start the connection process animating
	isConnecting = YES;
	cancellingConnection = NO;

	// Disable the favorites outline view to prevent further connections attempts
	[favoritesOutlineView setEnabled:NO];

	[helpButton setHidden:YES];
	[connectButton setEnabled:NO];
	[testConnectButton setEnabled:NO];
	[progressIndicator startAnimation:self];
	[progressIndicatorText setHidden:NO];

	// Start the current tab's progress indicator
	[dbDocument setIsProcessing:YES];

	// 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 && !isTestingConnection) {
		if ([[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount] isEqualToString:[self password]]) {
			[self setPassword:[[NSString string] stringByPaddingToLength:[[self password] length] withString:@"sp" startingAtIndex:0]];
			
			[[standardPasswordField undoManager] removeAllActionsWithTarget:standardPasswordField];
			[[socketPasswordField undoManager] removeAllActionsWithTarget:socketPasswordField];
			[[sshPasswordField undoManager] removeAllActionsWithTarget:sshPasswordField];
		}
		else {
			SPClear(connectionKeychainItemName);
			SPClear(connectionKeychainItemAccount);
		}
	}
	
	if (connectionSSHKeychainItemName && !isTestingConnection) {
		if ([[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount] isEqualToString:[self sshPassword]]) {
			[self setSshPassword:[[NSString string] stringByPaddingToLength:[[self sshPassword] length] withString:@"sp" startingAtIndex:0]];
			[[sshSSHPasswordField undoManager] removeAllActionsWithTarget:sshSSHPasswordField];
		} 
		else {
			SPClear(connectionSSHKeychainItemName);
			SPClear(connectionSSHKeychainItemAccount);
		}
	}

	// Inform the delegate that we are starting the connection process
	if (delegate && [delegate respondsToSelector:@selector(connectionControllerInitiatingConnection:)]) {
		[delegate connectionControllerInitiatingConnection:self];
	}

	// Trim whitespace and newlines from the host field before attempting to connect
	[self setHost:[[self host] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];

	// Initiate the SSH connection process for tunnels
	if ([self type] == SPSSHTunnelConnection) {
		[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];
}

/**
 * Cancels the current connection - both SSH and MySQL.
 */
- (IBAction)cancelConnection:(id)sender
{
	[connectButton setEnabled:NO];

	[progressIndicatorText setStringValue:NSLocalizedString(@"Cancelling...", @"cancelling task status message")];
	[progressIndicatorText display];

	cancellingConnection = YES;

	// Cancel the MySQL connection - handing it off to a background thread - if one is present
	if (mySQLConnection) {
		[mySQLConnection setDelegate:nil];
		[NSThread detachNewThreadWithName:SPCtxt(@"SPConnectionController cancellation background disconnect",dbDocument) target:mySQLConnection selector:@selector(disconnect) object:nil];
		[mySQLConnection autorelease];
		mySQLConnection = nil;
	}

	// Cancel the SSH tunnel if present
	if (sshTunnel) {
		[sshTunnel disconnect];
		SPClear(sshTunnel);
	}

	// Restore the connection interface
	[self _restoreConnectionInterface];
}

#pragma mark -
#pragma mark Interface interaction

/**
 * Registered to be the double click action of the favorites outline view.
 */
- (void)nodeDoubleClicked:(id)sender
{
	SPTreeNode *node = [favoritesOutlineView itemForDoubleAction];
		
	if (node) {
		if (node == quickConnectItem) {
			return;
		}

		// Only proceed to initiate a connection if a leaf node (i.e. a favorite and not a group) was double clicked.
		if (![node isGroup]) {
			[self initiateConnection:self];
		} 

		// Otherwise start editing the group node's name
		else {
			[favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES];
		}
	}
}

/**
 * Opens the SSH/SSL key selection window, ready to select a key file.
 */
- (IBAction)chooseKeyLocation:(NSButton *)sender
{
	NSString *directoryPath = nil;
	NSString *filePath = nil;
	NSView *accessoryView = nil;

	// If the button was toggled off, ensure editing is ended
	if ([sender state] == NSOffState) {
		[self _startEditingConnection];
	}

	// Switch details by sender.
	// First, SSH keys:
	if (sender == sshSSHKeyButton) {

		// If the custom key location is currently disabled - after the button
		// action - leave it disabled and return without showing the sheet.
		if (!sshKeyLocationEnabled) {
			[self setSshKeyLocation:nil];
			return;
		}

		filePath = nil;
		directoryPath = @"~/.ssh";
		accessoryView = sshKeyLocationHelp;
	}
	// SSL key file location:
	else if (sender == standardSSLKeyFileButton || sender == socketSSLKeyFileButton || sender == sslOverSSHKeyFileButton) {
		if ([sender state] == NSOffState) {
			[self setSslKeyFileLocation:nil];
			return;
		}
		
		accessoryView = sslKeyFileLocationHelp;
	}
	// SSL certificate file location:
	else if (sender == standardSSLCertificateButton || sender == socketSSLCertificateButton || sender == sslOverSSHCertificateButton) {
		if ([sender state] == NSOffState) {
			[self setSslCertificateFileLocation:nil];
			return;
		}
		
		accessoryView = sslCertificateLocationHelp;
	}
	// SSL CA certificate file location:
	else if (sender == standardSSLCACertButton || sender == socketSSLCACertButton || sender == sslOverSSHCACertButton) {
		if ([sender state] == NSOffState) {
			[self setSslCACertFileLocation:nil];
			return;
		}
		
		accessoryView = sslCACertLocationHelp;
	}
	
	keySelectionPanel = [[NSOpenPanel openPanel] retain]; // retain/release needed on OS X ≤ 10.6 according to Apple doc
	
	[keySelectionPanel setCanChooseFiles:YES];
	[keySelectionPanel setCanChooseDirectories:YES];
	[keySelectionPanel setCanCreateDirectories:YES];
	
	[keySelectionPanel setShowsHiddenFiles:[prefs boolForKey:SPHiddenKeyFileVisibilityKey]];
	[keySelectionPanel setAccessoryView:accessoryView];
	//on os x 10.11+ the accessory view will be hidden by default and has to be made visible
	if(accessoryView && [keySelectionPanel respondsToSelector:@selector(setAccessoryViewDisclosed:)]) {
		[keySelectionPanel setAccessoryViewDisclosed:YES];
	}
	[keySelectionPanel setDelegate:self];
	[keySelectionPanel beginSheetModalForWindow:[dbDocument parentWindow] completionHandler:^(NSInteger returnCode)
	{
		NSString *selectedFilePath=[[keySelectionPanel URL] path];
																		   
		//delay the release so it won't happen while this block is still executing.
		// jamesstout notes
		// replacing dispatch_get_current_queue with:
		// currentQueue = The operation queue that started the operation
		// underlyingQueue = The dispatch queue used to execute operations
		// just so happens that in this case it's the main queue anyway
		dispatch_async(NSOperationQueue.currentQueue.underlyingQueue, ^{
			SPClear(keySelectionPanel);
		});
		
		NSError *err=nil;
		
		if([keySelectionPanel.URL startAccessingSecurityScopedResource] == YES){
		
			NSLog(@"got access to: %@", keySelectionPanel.URL.absoluteString);
			
			// a bit of duplicated code here,
			// same code is in the export controler
			//TODO: put this in a utility/helper class
			BOOL __block beenHereBefore = NO;
			
			// have we been here before?
			[self.bookmarks enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
				
				if(dict[keySelectionPanel.URL.absoluteString] != nil){
					NSLog(@"beenHereBefore: %@", dict[keySelectionPanel.URL.absoluteString]);
					beenHereBefore = YES;
					*stop = YES;
				}
			}];
			
			if(beenHereBefore == NO){
				// create a bookmark
				NSError *error = nil;
				// this needs to be read-only to handle keys with 400 perms so we add the bitwise OR NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess
				NSData *tmpAppScopedBookmark = [keySelectionPanel.URL bookmarkDataWithOptions:(NSURLBookmarkCreationWithSecurityScope
																							   | NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess)
															   includingResourceValuesForKeys:nil
																				relativeToURL:nil
																						error:&error];
				// save to prefs
				if(tmpAppScopedBookmark && !error) {
					[bookmarks addObject:@{keySelectionPanel.URL.absoluteString : tmpAppScopedBookmark}];
					[prefs setObject:bookmarks forKey:SPSecureBookmarks];
				}
			}
			
		}
		// SSH key file selection
		if (sender == sshSSHKeyButton) {
			if (returnCode == NSModalResponseCancel) {
				[self setSshKeyLocationEnabled:NSOffState];
				[self setSshKeyLocation:nil];
				return;
			}
			
			[self setSshKeyLocation:selectedFilePath];
		}
		// SSL key file selection
		else if (sender == standardSSLKeyFileButton || sender == socketSSLKeyFileButton || sender == sslOverSSHKeyFileButton) {
			if (returnCode == NSModalResponseCancel) {
				[self setSslKeyFileLocationEnabled:NSOffState];
				[self setSslKeyFileLocation:nil];
				return;
			}
			
			if( [self validateKeyFile:keySelectionPanel.URL error:&err] == NO ){
				NSLog(@"Problem with key file - %@ : %@",[err localizedDescription], [err localizedRecoverySuggestion]);
				[self showValidationAlertForError:err];
				return; // don't copy the bad key
			}

			[self setSslKeyFileLocation:selectedFilePath];
		}
		// SSL certificate file selection
		else if (sender == standardSSLCertificateButton || sender == socketSSLCertificateButton || sender == sslOverSSHCertificateButton) {
			if (returnCode == NSModalResponseCancel) {
				[self setSslCertificateFileLocationEnabled:NSOffState];
				[self setSslCertificateFileLocation:nil];
				return;
			}
			
			if( [self validateCertFile:keySelectionPanel.URL error:&err] == NO ){
				NSLog(@"Problem with cert file - %@ : %@",[err localizedDescription], [err localizedRecoverySuggestion]);
				[self showValidationAlertForError:err];
				return; // don't copy the bad cert
			}
			
			[self setSslCertificateFileLocation:selectedFilePath];
		}
		// SSL CA certificate file selection
		else if (sender == standardSSLCACertButton || sender == socketSSLCACertButton || sender == sslOverSSHCACertButton) {
			if (returnCode == NSModalResponseCancel) {
				[self setSslCACertFileLocationEnabled:NSOffState];
				[self setSslCACertFileLocation:nil];
				return;
			}
			
			[self setSslCACertFileLocation:selectedFilePath];
		}
		
		[self _startEditingConnection];
	}];
}

-(void)showValidationAlertForError:(NSError*)err{
	
	NSAlert *alert = [[NSAlert alloc] init];
	alert.messageText = [err localizedDescription];
	alert.informativeText = [err localizedRecoverySuggestion];
	[alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")];
	[alert beginSheetModalForWindow:[dbDocument parentWindow] completionHandler:nil];
}

-(BOOL)validateKeyFile:(NSURL *)url error:(NSError **)outError{
	
	NSError *err = nil;
	NSData *file = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&err];
	if(err) {
		*outError = err;
		return NO;
	}
	
	NSString *stringFromData = [[NSString alloc] initWithData:file encoding:NSASCIIStringEncoding];

	// SEE: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Strings/Articles/stringsParagraphBreaks.html#//apple_ref/doc/uid/TP40005016-SW3
	// need to handle \n, \r, \r\n,
	
	BOOL __block foundValidFirstLine = NO;
	BOOL __block foundValidLastLine = NO;
	
	if(stringFromData){
		NSRange range = NSMakeRange(0, stringFromData.length);
		
		[stringFromData enumerateSubstringsInRange:range
										   options:NSStringEnumerationByParagraphs
										usingBlock:^(NSString * _Nullable paragraph, NSRange paragraphRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
			
			if ([paragraph containsString:@"PRIVATE KEY-----"] && [paragraph containsString:@"-----BEGIN"]) {
				foundValidFirstLine = YES;
			}
			if ([paragraph containsString:@"PRIVATE KEY-----"] && [paragraph containsString:@"-----END"]) {
				foundValidLastLine = YES;
			}
		}];
	}
	
	if(foundValidFirstLine == YES && foundValidLastLine == YES){
		return YES;
	}
	else{
		*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{
			NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"“%@” is not a valid private key file.", @"connection view : ssl : key file picker : wrong format error title"),[url lastPathComponent]],
			NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Make sure the file contains a RSA private key and is using PEM encoding.", @"connection view : ssl : key file picker : wrong format error description"),
			NSURLErrorKey: url
		}];
		
		return NO;
	}
}

-(BOOL)validateCertFile:(NSURL *)url error:(NSError **)outError{
	
	NSError *err = nil;
	NSData *file = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&err];
	if(err) {
		*outError = err;
		return NO;
	}
	
	NSString *stringFromData = [[NSString alloc] initWithData:file encoding:NSASCIIStringEncoding];

	// SEE: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Strings/Articles/stringsParagraphBreaks.html#//apple_ref/doc/uid/TP40005016-SW3
	// need to handle \n, \r, \r\n,
	
	BOOL __block foundValidFirstLine = NO;
	BOOL __block foundValidLastLine = NO;
	
	if(stringFromData){
		NSRange range = NSMakeRange(0, stringFromData.length);
		
		[stringFromData enumerateSubstringsInRange:range
										   options:NSStringEnumerationByParagraphs
										usingBlock:^(NSString * _Nullable paragraph, NSRange paragraphRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
			
			if ([paragraph containsString:@"CERTIFICATE-----"] && [paragraph containsString:@"-----BEGIN"]) {
				foundValidFirstLine = YES;
			}
			if ([paragraph containsString:@"CERTIFICATE-----"] && [paragraph containsString:@"-----END"]) {
				foundValidLastLine = YES;
			}
		}];
	}
	
	if(foundValidFirstLine == YES && foundValidLastLine == YES){
		return YES;
	}
	else{
		
		*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{
			NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"“%@” is not a valid client certificate file.", @"connection view : ssl : client cert file picker : wrong format error title"),[url lastPathComponent]],
			NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Make sure the file contains a X.509 client certificate and is using PEM encoding.", @"connection view : ssl : client cert picker : wrong format error description"),
			NSURLErrorKey: url
		}];
		
		return NO;
	}
}

// quick check to stop users selecting .pub files
- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url{
	if([url.pathExtension isEqualToString:@"pub"]){
		return NO;
	}
	return YES;
}

- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError **)outError
{
	
	// see https://developer.apple.com/documentation/foundation/nsurl/1410597-checkresourceisreachableandretur?language=objc
	// If your app must perform operations on the file, such as opening it or copying resource properties,
	//it is more efficient to attempt the operation and handle any failure that may occur.
	
	//unknown, accept by default
	return YES;
	
	/* And now, an intermission from the mysql source code:
	 
  if (!cert_file &&  key_file)
	 cert_file= key_file;
  
  if (!key_file &&  cert_file)
	 key_file= cert_file;

	 */
}

/**
 * Show connection help webpage.
 */
- (IBAction)showHelp:(id)sender
{
	[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SPLOCALIZEDURL_CONNECTIONHELP]];
}

/**
 * Resize parts of the interface to reflect SSL status.
 */
- (IBAction)updateSSLInterface:(id)sender
{
	[self _startEditingConnection];
	[self resizeTabViewToConnectionType:[self type] animating:YES];
}

/**
 * Toggle hidden file visiblity in response to accessory view changes
 */
- (IBAction)updateKeyLocationFileVisibility:(id)sender
{
	[keySelectionPanel setShowsHiddenFiles:[prefs boolForKey:SPHiddenKeyFileVisibilityKey]];
}

/**
 * Update the interface in response to external split view size changes.
 */
- (void)updateSplitViewSize
{
	if ([dbDocument getConnection]) {
		return;
	}

	[connectionSplitView setDelegate:nil];
	[connectionSplitView setPosition:[[[databaseConnectionView subviews] firstObject] frame].size.width ofDividerAtIndex:0];
	[connectionSplitView setDelegate:self];
}

- (IBAction)updateClearTextPlugin:(id)sender
{
	[self _startEditingConnection];
}

#pragma mark -
#pragma mark Connection details interaction and display

/**
 * Control tab view resizing based on the supplied connection type,
 * with an option defining whether it should be animated or not.
 */
- (void)resizeTabViewToConnectionType:(NSUInteger)theType animating:(BOOL)animate
{
	NSRect frameRect, targetResizeRect;

	// Use a magic number which needs to be added to the form when calculating resizes -
	// including the height of the button areas below.
	NSInteger additionalFormHeight = 92;

	frameRect = [connectionResizeContainer frame];

	switch (theType) {
		case SPTCPIPConnection:
			targetResizeRect = [standardConnectionFormContainer frame];
			if ([self useSSL]) additionalFormHeight += [standardConnectionSSLDetailsContainer frame].size.height;
			break;
		case SPSocketConnection:
			targetResizeRect = [socketConnectionFormContainer frame];
			if ([self useSSL]) additionalFormHeight += [socketConnectionSSLDetailsContainer frame].size.height;
			break;
		case SPSSHTunnelConnection:
			targetResizeRect = [sshConnectionFormContainer frame];
			if ([self useSSL]) additionalFormHeight += [sshConnectionSSLDetailsContainer frame].size.height;
			break;
	}

	frameRect.size.height = targetResizeRect.size.height + additionalFormHeight;

	if (animate && initComplete) {
		[[connectionResizeContainer animator] setFrame:frameRect];
	} 
	else {
		[connectionResizeContainer setFrame:frameRect];
	}
}

#pragma mark -
#pragma mark Favorites interaction

/**
 * Sorts the favorites table view based on the selected sort by item.
 */
- (void)sortFavorites:(id)sender
{
    SPFavoritesSortItem previousSortItem = currentSortItem;
	currentSortItem  = (SPFavoritesSortItem)[[sender menu] indexOfItem:sender];

	[prefs setInteger:currentSortItem forKey:SPFavoritesSortedBy];

	// Perform sorting
	[self _sortFavorites];

	if (previousSortItem > SPFavoritesSortUnsorted) [[[sender menu] itemAtIndex:previousSortItem] setState:NSOffState];

	[[[sender menu] itemAtIndex:currentSortItem] setState:NSOnState];
}

/**
 * Reverses the favorites table view sorting based on the selected criteria.
 */
- (void)reverseSortFavorites:(NSMenuItem *)sender
{
	reverseFavoritesSort = (![sender state]);

	[prefs setBool:reverseFavoritesSort forKey:SPFavoritesSortedInReverse];

	// Perform re-sorting
	[self _sortFavorites];

	[sender setState:reverseFavoritesSort];
}

/**
 * Sets fields for the chosen favorite.
 */
- (void)updateFavoriteSelection:(id)sender
{

	// Clear the keychain referral items as appropriate
	[self setConnectionKeychainID:nil];
	if (connectionKeychainItemName) SPClear(connectionKeychainItemName);
	if (connectionKeychainItemAccount) SPClear(connectionKeychainItemAccount);
	if (connectionSSHKeychainItemName) SPClear(connectionSSHKeychainItemName);
	if (connectionSSHKeychainItemAccount) SPClear(connectionSSHKeychainItemAccount);

	SPTreeNode *node = [self selectedFavoriteNode];
	if ([node isGroup]) node = nil;
	
	// Update key-value properties from the selected favourite, using empty strings where not found
	NSDictionary *fav = [[node representedObject] nodeFavorite];
	
	// Keep a copy of the favorite as it currently stands
	if (currentFavorite) SPClear(currentFavorite);
	currentFavorite = [fav copy];
	
	[connectionResizeContainer setHidden:NO];
	[self _stopEditingConnection];

	// Set up the type, also storing it in the previous type store to prevent type "changes" triggering actions
	NSUInteger connectionType = ([fav objectForKey:SPFavoriteTypeKey] ? [[fav objectForKey:SPFavoriteTypeKey] integerValue] : SPTCPIPConnection);
	previousType = connectionType;
	[self setType:connectionType];

	// Standard details
	[self setName:([fav objectForKey:SPFavoriteNameKey] ? [fav objectForKey:SPFavoriteNameKey] : @"")];
	[self setHost:([fav objectForKey:SPFavoriteHostKey] ? [fav objectForKey:SPFavoriteHostKey] : @"")];
	[self setSocket:([fav objectForKey:SPFavoriteSocketKey] ? [fav objectForKey:SPFavoriteSocketKey] : @"")];
	[self setUser:([fav objectForKey:SPFavoriteUserKey] ? [fav objectForKey:SPFavoriteUserKey] : @"")];
	[self setColorIndex:([fav objectForKey:SPFavoriteColorIndexKey]? [[fav objectForKey:SPFavoriteColorIndexKey] integerValue] : -1)];
	[self setPort:([fav objectForKey:SPFavoritePortKey] ? [fav objectForKey:SPFavoritePortKey] : @"")];
	[self setDatabase:([fav objectForKey:SPFavoriteDatabaseKey] ? [fav objectForKey:SPFavoriteDatabaseKey] : @"")];
	[self setUseCompression:([fav objectForKey:SPFavoriteUseCompressionKey] ? [[fav objectForKey:SPFavoriteUseCompressionKey] boolValue] : YES)];

	// Time Zone details
	switch ([fav objectForKey:SPFavoriteTimeZoneModeKey] ? [[fav objectForKey:SPFavoriteTimeZoneModeKey] intValue] : SPConnectionTimeZoneModeUseServerTZ) {
		case SPConnectionTimeZoneModeUseSystemTZ: {
			[standardTimeZoneField selectItemWithTag:SPUseSystemTimeZoneTag];
			[socketTimeZoneField selectItemWithTag:SPUseSystemTimeZoneTag];
			[sshTimeZoneField selectItemWithTag:SPUseSystemTimeZoneTag];
			[self setTimeZoneMode:SPConnectionTimeZoneModeUseSystemTZ];
			[self setTimeZoneIdentifier:@""];
			break;
		}
		case SPConnectionTimeZoneModeUseFixedTZ: {
			NSString *tzIdentifier = [fav objectForKey:SPFavoriteTimeZoneIdentifierKey];
			[standardTimeZoneField selectItemWithTitle:tzIdentifier];
			[socketTimeZoneField selectItemWithTitle:tzIdentifier];
			[sshTimeZoneField selectItemWithTitle:tzIdentifier];
			[self setTimeZoneMode:SPConnectionTimeZoneModeUseFixedTZ];
			[self setTimeZoneIdentifier:tzIdentifier];
			break;
		}
		default: {
			[standardTimeZoneField selectItemWithTag:SPUseServerTimeZoneTag];
			[socketTimeZoneField selectItemWithTag:SPUseServerTimeZoneTag];
			[sshTimeZoneField selectItemWithTag:SPUseServerTimeZoneTag];
			[self setTimeZoneMode:SPConnectionTimeZoneModeUseServerTZ];
			[self setTimeZoneIdentifier:@""];
			break;
		}
	}
	
	//Special prefs
	[self setAllowDataLocalInfile:([fav objectForKey:SPFavoriteAllowDataLocalInfileKey] ? [[fav objectForKey:SPFavoriteAllowDataLocalInfileKey] intValue] : NSOffState)];
	
	// Clear text plugin
	[self setEnableClearTextPlugin:([fav objectForKey:SPFavoriteEnableClearTextPluginKey] ? [[fav objectForKey:SPFavoriteEnableClearTextPluginKey] intValue] : NSOffState)];
	
	// SSL details
	[self setUseSSL:([fav objectForKey:SPFavoriteUseSSLKey] ? [[fav objectForKey:SPFavoriteUseSSLKey] intValue] : NSOffState)];
	[self setSslKeyFileLocationEnabled:([fav objectForKey:SPFavoriteSSLKeyFileLocationEnabledKey] ? [[fav objectForKey:SPFavoriteSSLKeyFileLocationEnabledKey] intValue] : NSOffState)];
	[self setSslKeyFileLocation:([fav objectForKey:SPFavoriteSSLKeyFileLocationKey] ? [fav objectForKey:SPFavoriteSSLKeyFileLocationKey] : @"")];
	[self setSslCertificateFileLocationEnabled:([fav objectForKey:SPFavoriteSSLCertificateFileLocationEnabledKey] ? [[fav objectForKey:SPFavoriteSSLCertificateFileLocationEnabledKey] intValue] : NSOffState)];
	[self setSslCertificateFileLocation:([fav objectForKey:SPFavoriteSSLCertificateFileLocationKey] ? [fav objectForKey:SPFavoriteSSLCertificateFileLocationKey] : @"")];
	[self setSslCACertFileLocationEnabled:([fav objectForKey:SPFavoriteSSLCACertFileLocationEnabledKey] ? [[fav objectForKey:SPFavoriteSSLCACertFileLocationEnabledKey] intValue] : NSOffState)];
	[self setSslCACertFileLocation:([fav objectForKey:SPFavoriteSSLCACertFileLocationKey] ? [fav objectForKey:SPFavoriteSSLCACertFileLocationKey] : @"")];

	// SSH details
	[self setSshHost:([fav objectForKey:SPFavoriteSSHHostKey] ? [fav objectForKey:SPFavoriteSSHHostKey] : @"")];
	[self setSshUser:([fav objectForKey:SPFavoriteSSHUserKey] ? [fav objectForKey:SPFavoriteSSHUserKey] : @"")];
	[self setSshKeyLocationEnabled:([fav objectForKey:SPFavoriteSSHKeyLocationEnabledKey] ? [[fav objectForKey:SPFavoriteSSHKeyLocationEnabledKey] intValue] : NSOffState)];
	[self setSshKeyLocation:([fav objectForKey:SPFavoriteSSHKeyLocationKey] ? [fav objectForKey:SPFavoriteSSHKeyLocationKey] : @"")];
	[self setSshPort:([fav objectForKey:SPFavoriteSSHPortKey] ? [fav objectForKey:SPFavoriteSSHPortKey] : @"")];

	// Trigger an interface update
	[self resizeTabViewToConnectionType:[self type] animating:(sender == self)];

	// 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:[fav objectForKey:SPFavoriteNameKey] id:[fav objectForKey:SPFavoriteIDKey]] retain];
	connectionKeychainItemAccount = [[keychain accountForUser:[fav objectForKey:SPFavoriteUserKey] host:(([self type] == SPSocketConnection) ? @"localhost" : [fav objectForKey:SPFavoriteHostKey]) database:[fav objectForKey:SPFavoriteDatabaseKey]] retain];

	[self setPassword:[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]];

	if (![[self password] length]) {
		[self setPassword:nil];
		SPClear(connectionKeychainItemName);
		SPClear(connectionKeychainItemAccount);
	}

	// Store the selected favorite ID for use with the document on connection
	if ([fav objectForKey:SPFavoriteIDKey]) [self setConnectionKeychainID:[[fav objectForKey:SPFavoriteIDKey] stringValue]];

	// And the same for the SSH password
	connectionSSHKeychainItemName = [[keychain nameForSSHForFavoriteName:[fav objectForKey:SPFavoriteNameKey] id:[fav objectForKey:SPFavoriteIDKey]] retain];
	connectionSSHKeychainItemAccount = [[keychain accountForSSHUser:[fav objectForKey:SPFavoriteSSHUserKey] sshHost:[fav objectForKey:SPFavoriteSSHHostKey]] retain];

	[self setSshPassword:[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]];

	if (![[self sshPassword] length]) {
		[self setSshPassword:nil];
		SPClear(connectionSSHKeychainItemName);
		SPClear(connectionSSHKeychainItemAccount);
	}

	[prefs setInteger:[[fav objectForKey:SPFavoriteIDKey] integerValue] forKey:SPLastFavoriteID];

	[self updateFavoriteNextKeyView];
}
	
/**
 * Set the next KeyView to password field if the password is empty
 */
- (void)updateFavoriteNextKeyView
{
	switch ([self type])
	{
		case SPTCPIPConnection:
			[favoritesOutlineView setNextKeyView:(![[standardPasswordField stringValue] length]) ? standardPasswordField : standardNameField];
			break;
		case SPSocketConnection:
			[favoritesOutlineView setNextKeyView:(![[socketPasswordField stringValue] length]) ? socketPasswordField : socketNameField];
			break;
		case SPSSHTunnelConnection:
			if (![[sshPasswordField stringValue] length]) {
				[favoritesOutlineView setNextKeyView:sshPasswordField];
			}
			else if (![[sshSSHPasswordField stringValue] length]) {
				[favoritesOutlineView setNextKeyView:sshSSHPasswordField];
			}
			else {
				[favoritesOutlineView setNextKeyView:sshNameField];
			}
			break;
	}
}

/**
 * Returns the selected favorite data dictionary or nil if nothing is selected.
 */
- (NSMutableDictionary *)selectedFavorite
{
	SPTreeNode *node = [self selectedFavoriteNode];
	
	return (![node isGroup]) ? [(SPFavoriteNode *)[node representedObject] nodeFavorite] : nil;
}

/**
 * Returns the selected favorite node or nil if nothing is selected.
 */
- (SPTreeNode *)selectedFavoriteNode
{
	NSArray *nodes = [self selectedFavoriteNodes];
	
	return (SPTreeNode *)[nodes objectOrNilAtIndex:0];
}

/**
 * Returns an array of selected favorite nodes.
 */
- (NSArray *)selectedFavoriteNodes
{
	NSMutableArray *nodes = [NSMutableArray array];
	NSIndexSet *indexes = [favoritesOutlineView selectedRowIndexes];

	[indexes enumerateIndexesUsingBlock:^(NSUInteger currentIndex, BOOL * _Nonnull stop) {
		[nodes addObject:[favoritesOutlineView itemAtRow:currentIndex]];
	}];

	return nodes;
}

/**
 * Saves the current connection favorite.
 */
- (IBAction)saveFavorite:(id)sender
{
	[self _saveCurrentDetailsCreatingNewFavorite:NO validateDetails:YES];
}

/**
 * Adds a new connection favorite.
 */
- (IBAction)addFavorite:(id)sender
{
	NSNumber *favoriteID = [self _createNewFavoriteID];
	
	NSArray *objects = @[
		NSLocalizedString(@"New Favorite", @"new favorite name"),
		@0,
		@"",
		@"",
		@"",
		@(-1),
		@"",
		@0,
		@"",
		@(NSOffState),
		@(NSOffState),
		@(NSOffState),
		@(NSOffState),
		@(NSOffState),
		@"",
		@"",
		@"",
		@(NSOffState),
		@"",
		@"",
		favoriteID
	];
	
	NSArray *keys = @[
		SPFavoriteNameKey,
		SPFavoriteTypeKey,
		SPFavoriteHostKey,
		SPFavoriteSocketKey,
		SPFavoriteUserKey,
		SPFavoriteColorIndexKey,
		SPFavoritePortKey,
		SPFavoriteTimeZoneModeKey,
		SPFavoriteTimeZoneIdentifierKey,
		SPFavoriteAllowDataLocalInfileKey,
		SPFavoriteEnableClearTextPluginKey,
		SPFavoriteUseSSLKey,
		SPFavoriteSSLKeyFileLocationEnabledKey,
		SPFavoriteSSLCertificateFileLocationEnabledKey,
		SPFavoriteSSLCACertFileLocationEnabledKey,
		SPFavoriteDatabaseKey,
		SPFavoriteSSHHostKey,
		SPFavoriteSSHUserKey,
		SPFavoriteSSHKeyLocationEnabledKey,
		SPFavoriteSSHKeyLocationKey,
		SPFavoriteSSHPortKey,
		SPFavoriteIDKey
	];

	// Create default favorite
	NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithObjects:objects forKeys:keys];

	SPTreeNode *selectedNode = [self selectedFavoriteNode];
	
	SPTreeNode *parent = ([selectedNode isGroup] && selectedNode != quickConnectItem) ? selectedNode : (SPTreeNode *)[selectedNode parentNode];
	
	SPTreeNode *node = [favoritesController addFavoriteNodeWithData:favorite asChildOfNode:parent];
	
	// Ensure the parent is expanded
	[favoritesOutlineView expandItem:parent];
	
	[self _sortFavorites];
	[self _selectNode:node];

	[[[SPAppDelegate preferenceController] generalPreferencePane] updateDefaultFavoritePopup];

	favoriteNameFieldWasAutogenerated = YES;

	[favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES];
}

/**
 * Adds the current details as a new connection favorite, selects it, and scrolls the selected
 * row to be visible.
 */
- (IBAction)addFavoriteUsingCurrentDetails:(id)sender
{
	[self _saveCurrentDetailsCreatingNewFavorite:YES validateDetails:YES];
}

/**
 * Adds a new group node to the favorites tree with a default name. Once added it is selected for editing.
 */
- (IBAction)addGroup:(id)sender
{
	SPTreeNode *selectedNode = [self selectedFavoriteNode];
	
	SPTreeNode *parent = ([selectedNode isGroup] && selectedNode != quickConnectItem) ? selectedNode : (SPTreeNode *)[selectedNode parentNode];

	// Ensure the parent is expanded
	[favoritesOutlineView expandItem:parent];
	
	SPTreeNode *node = [favoritesController addGroupNodeWithName:NSLocalizedString(@"New Folder", @"new folder placeholder name") asChildOfNode:parent];
	
	[self _reloadFavoritesViewData];
	[self _selectNode:node];
	
	[favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES];
}

/**
 * Removes the selected node.
 */
- (IBAction)removeNode:(id)sender
{
	if ([favoritesOutlineView numberOfSelectedRows] == 1) {
		
		BOOL suppressWarning = NO;
		SPTreeNode *node = [self selectedFavoriteNode];

		NSString *message = @"";
		NSString *informativeMessage = @"";
		
		if (![node isGroup]) {
			message            = [NSString stringWithFormat:NSLocalizedString(@"Delete favorite '%@'?", @"delete database message"), [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteNameKey]];
			informativeMessage = [NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the favorite '%@'? This operation cannot be undone.", @"delete database informative message"), [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteNameKey]];
		}
		else if ([[node childNodes] count] > 0) {
			message            = [NSString stringWithFormat:NSLocalizedString(@"Delete group '%@'?", @"delete database message"), [[node representedObject] nodeName]];
			informativeMessage = [NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the group '%@'? All groups and favorites within this group will also be deleted. This operation cannot be undone.", @"delete database informative message"), [[node representedObject] nodeName]];
		}
		else {
			suppressWarning = YES;
		}
		
		if (!suppressWarning) {
			
			NSAlert *alert = [[NSAlert alloc] init];
			
			// jamesstout notes
			// Alerts should be created with the -init method and setting properties. - NSAlert.h L132
			alert.messageText = message;
			alert.informativeText = [NSString stringWithFormat:@"%@", informativeMessage];
			[alert addButtonWithTitle:NSLocalizedString(@"Delete", @"delete button")]; // first button is delete
			[alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"cancel button")]; // second is cancel
			
			NSArray *buttons = [alert buttons];
			
			// Change the alert's cancel button to have the key equivalent of return
			[[buttons objectAtIndex:0] setKeyEquivalent:@"d"];
			[[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
			[[buttons objectAtIndex:1] setKeyEquivalent:@"\r"];
			
			[alert setAlertStyle:NSCriticalAlertStyle];
			
			[alert beginSheetModalForWindow:[dbDocument parentWindow]
			                  modalDelegate:self
			                 didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
			                    contextInfo:SPRemoveNode];
		}
		else {
			[self _removeNode:node];
		}
	}
}

/**
 * Duplicates the selected connection favorite.
 */
- (IBAction)duplicateFavorite:(id)sender
{
	if ([favoritesOutlineView numberOfSelectedRows] == 1) {
		
		NSMutableDictionary *favorite = [NSMutableDictionary dictionaryWithDictionary:[self selectedFavorite]];
		
		NSNumber *favoriteID = [self _createNewFavoriteID];
				
		// Update the unique ID
		[favorite setObject:favoriteID forKey:SPFavoriteIDKey];
		
		// Alter the name for clarity
		[favorite setObject:[NSString stringWithFormat:NSLocalizedString(@"%@ Copy", @"Initial favourite name after duplicating a previous favourite"), [favorite objectForKey:SPFavoriteNameKey]] forKey:SPFavoriteNameKey];
		
		SPTreeNode *selectedNode = [self selectedFavoriteNode];
		
		SPTreeNode *parent = ([selectedNode isGroup]) ? selectedNode : (SPTreeNode *)[selectedNode parentNode];
		
		SPTreeNode *node = [favoritesController addFavoriteNodeWithData:favorite asChildOfNode:parent];
		
		[self _reloadFavoritesViewData];
		[self _selectNode:node];
		
		[[[SPAppDelegate preferenceController] generalPreferencePane] updateDefaultFavoritePopup];
	}
}

/**
 * Switches the selected favorite/group to editing mode so it can be renamed.
 */
- (IBAction)renameNode:(id)sender
{
	if ([favoritesOutlineView numberOfSelectedRows] == 1) {
		[favoritesOutlineView editColumn:0 row:[favoritesOutlineView selectedRow] withEvent:nil select:YES];
	}
}

/**
 * Marks the selected favorite as the default.
 */
- (IBAction)makeSelectedFavoriteDefault:(id)sender
{
	NSInteger favoriteID = [[[self selectedFavorite] objectForKey:SPFavoriteIDKey] integerValue];
	
	[prefs setInteger:favoriteID forKey:SPDefaultFavorite];
}

- (void)selectQuickConnectItem
{
	return [self _selectNode:quickConnectItem];
}

#pragma mark -
#pragma mark Import/export favorites

/**
 * Displays an open panel, allowing the user to import their favorites.
 */
- (IBAction)importFavorites:(id)sender
{
	NSOpenPanel *openPanel = [NSOpenPanel openPanel];

	[openPanel setAllowedFileTypes:@[@"plist"]];

	[openPanel beginSheetModalForWindow:[dbDocument parentWindow] completionHandler:^(NSInteger returnCode)
	{
		if (returnCode == NSModalResponseOK) {
			SPFavoritesImporter *importer = [[SPFavoritesImporter alloc] init];

			[importer setDelegate:(NSObject<SPFavoritesImportProtocol> *)self];

			[importer importFavoritesFromFileAtPath:[[openPanel URL] path]];
		}
	}];
}

/**
 * Displays a save panel, allowing the user to export their favorites.
 */
- (IBAction)exportFavorites:(id)sender
{
	// additional empty selection check
	if(![[self selectedFavoriteNodes] count]) return;
	
	NSSavePanel *savePanel = [NSSavePanel savePanel];
	
	// suggest the name of the favorite or a default name for multiple selection
	NSString *fileName = ([[self selectedFavoriteNodes] count] == 1)? [[(id<SPNamedNode>)[[self selectedFavoriteNode] representedObject] nodeName] stringByAppendingPathExtension:@"plist"] : nil;
	// This if() is so we can also catch nil due to favorite corruption (NSSavePanel will @throw if nil is passed in)
	if(!fileName) fileName = SPExportFavoritesFilename;

	[savePanel setAccessoryView:exportPanelAccessoryView];
	[savePanel setNameFieldStringValue:fileName];

	[savePanel beginSheetModalForWindow:[dbDocument parentWindow] completionHandler:^(NSInteger returnCode)
	{
		if (returnCode == NSModalResponseOK) {
			SPFavoritesExporter *exporter = [[[SPFavoritesExporter alloc] init] autorelease];

			[exporter setDelegate:self];

			[exporter writeFavorites:[self selectedFavoriteNodes] toFile:[[savePanel URL] path]];
		 }
	 }];
}



- (IBAction)allowLocalDataInfileChanged:(id)sender {
    [self _startEditingConnection];
}

#pragma mark -
#pragma mark Time Zone changes

- (IBAction)didChangeSelectedTimeZone:(NSPopUpButton *)sender
{
	NSMenuItem *selectedItem = [sender selectedItem];
	switch (selectedItem.tag) {
		case SPUseServerTimeZoneTag:
			[self setTimeZoneMode:SPConnectionTimeZoneModeUseServerTZ];
			[self setTimeZoneIdentifier:@""];
			break;
		case SPUseSystemTimeZoneTag:
			[self setTimeZoneMode:SPConnectionTimeZoneModeUseSystemTZ];
			[self setTimeZoneIdentifier:@""];
			break;
		default:
			[self setTimeZoneMode:SPConnectionTimeZoneModeUseFixedTZ];
			[self setTimeZoneIdentifier:selectedItem.title];
			break;
	}


	[standardTimeZoneField selectItemAtIndex:sender.indexOfSelectedItem];
	[sshTimeZoneField selectItemAtIndex:sender.indexOfSelectedItem];
	[socketTimeZoneField selectItemAtIndex:sender.indexOfSelectedItem];

	[self _startEditingConnection];
}

#pragma mark -
#pragma mark Accessors

/**
 * Returns the main outline view instance.
 */
- (SPFavoritesOutlineView *)favoritesOutlineView
{
	return favoritesOutlineView;
}

#pragma mark -
#pragma mark Key Value Observing

/**
 * This method is called as part of Key Value Observing.
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
	// reload the bookmarks, when the observer detected a change in them
	//
	// thanks a lot to @jamesstout for pointing this out!
	if ([keyPath isEqualToString:SPSecureBookmarks]) {
		id o;

		if((o = [prefs objectForKey:SPSecureBookmarks])){
			[bookmarks setArray:o];
		}

		[self reRequestSecureAccess];
	}
}

#pragma mark -
#pragma mark Sheet methods

/**
 * Called when the user dismisses the remove node sheet.
 */
- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
{
	// Remove the current favorite/group node
	if ([contextInfo isEqualToString:SPRemoveNode]) {
		if (returnCode == NSAlertFirstButtonReturn || returnCode == NSAlertAlternateReturn) {
			[self _removeNode:[self selectedFavoriteNode]];
		}
	}
}


/**
 * Alert sheet callback method - invoked when the error sheet is closed.
 */
- (void)localhostErrorSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
	if (returnCode == NSAlertAlternateReturn) {
		[self setType:SPSocketConnection];
		[self setHost:@""];
	} 
	else {
		[self setHost:@"127.0.0.1"];
	}
}

#pragma mark -
#pragma mark Private API

/**
 * Take the current details and either save them to the currently selected
 * favourite, or create a new connection favourite using them.
 * If creating a new favourite, also select it and ensure the selected
 * favourite is visible.
 */
- (void)_saveCurrentDetailsCreatingNewFavorite:(BOOL)createNewFavorite validateDetails:(BOOL)validateDetails
{
	// Complete any active editing
	if ([[connectionView window] firstResponder]) {
		[[connectionView window] endEditingFor:[[connectionView window] firstResponder]];
	}

	// Ensure that host is not empty if this is a TCP/IP or SSH connection
	if (validateDetails && ([self type] == SPTCPIPConnection || [self type] == SPSSHTunnelConnection) && ![[self host] length]) {
		SPOnewayAlertSheet(
			NSLocalizedString(@"Insufficient connection details", @"insufficient details message"),
			[dbDocument parentWindow],
			NSLocalizedString(@"Insufficient details provided to establish a connection. Please provide at least a host.", @"insufficient details informative message")
		);
		return;
	}
	
	// If SSH is enabled, ensure that the SSH host is not nil
	if (validateDetails && [self type] == SPSSHTunnelConnection && ![[self sshHost] length]) {
		SPOnewayAlertSheet(
			NSLocalizedString(@"Insufficient connection details", @"insufficient details message"),
			[dbDocument parentWindow],
			NSLocalizedString(@"Please enter the hostname for the SSH Tunnel, or disable the SSH Tunnel.", @"message of panel when ssh details are incomplete")
		);
		return;
	}

	// Ensure that a socket connection is not inadvertently used
	if (![self _checkHost]) return;


	// Set up the favourite, or get the mutable dictionary for the current favourite.
	NSMutableDictionary *theFavorite;
	if (createNewFavorite) {
		theFavorite = [NSMutableDictionary dictionary];
		[theFavorite setObject:[self _createNewFavoriteID] forKey:SPFavoriteIDKey];
	} else {
		if (!currentFavorite) {
			[NSException raise:NSInternalInconsistencyException format:@"Tried to save a current favourite with no currentFavorite"];
		}
		theFavorite = [self selectedFavorite];
	}
	
	void (^_setOrRemoveKey)(NSString *, id) = ^(NSString *key, id value) {
		if (value) {
			[theFavorite setObject:value forKey:key];
		} else {
			[theFavorite removeObjectForKey:key];
		}
	};

	// Set the name - either taking the provided name, or generating one.
	if ([[self name] length]) {
		[theFavorite setObject:[self name] forKey:SPFavoriteNameKey];
	} else {
		NSString *favoriteName = [self _generateNameForConnection];
		if (!favoriteName) {
			favoriteName = NSLocalizedString(@"Untitled", @"Name for an untitled connection");
		}
		[theFavorite setObject:favoriteName forKey:SPFavoriteNameKey];
	}

	// Set standard details for the connection
	[theFavorite setObject:[NSNumber numberWithInteger:[self type]] forKey:SPFavoriteTypeKey];
	_setOrRemoveKey(SPFavoriteHostKey, [self host]);
	_setOrRemoveKey(SPFavoriteSocketKey, [self socket]);
	_setOrRemoveKey(SPFavoriteUserKey, [self user]);
	_setOrRemoveKey(SPFavoritePortKey, [self port]);
	_setOrRemoveKey(SPFavoriteDatabaseKey, [self database]);
	[theFavorite setObject:[NSNumber numberWithInteger:[self colorIndex]] forKey:SPFavoriteColorIndexKey];
	[theFavorite setObject:[NSNumber numberWithInteger:[self timeZoneMode]] forKey:SPFavoriteTimeZoneModeKey];
	_setOrRemoveKey(SPFavoriteTimeZoneIdentifierKey, [self timeZoneIdentifier]);
	//Special prefs
	[theFavorite setObject:[NSNumber numberWithInteger:[self allowDataLocalInfile]] forKey:SPFavoriteAllowDataLocalInfileKey];
	// Clear text plugin
	[theFavorite setObject:[NSNumber numberWithInteger:[self enableClearTextPlugin]] forKey:SPFavoriteEnableClearTextPluginKey];
	// SSL details
	[theFavorite setObject:[NSNumber numberWithInteger:[self useSSL]] forKey:SPFavoriteUseSSLKey];
	[theFavorite setObject:[NSNumber numberWithInteger:[self sslKeyFileLocationEnabled]] forKey:SPFavoriteSSLKeyFileLocationEnabledKey];
	_setOrRemoveKey(SPFavoriteSSLKeyFileLocationKey, [self sslKeyFileLocation]);
	[theFavorite setObject:[NSNumber numberWithInteger:[self sslCertificateFileLocationEnabled]] forKey:SPFavoriteSSLCertificateFileLocationEnabledKey];
	_setOrRemoveKey(SPFavoriteSSLCertificateFileLocationKey, [self sslCertificateFileLocation]);
	[theFavorite setObject:[NSNumber numberWithInteger:[self sslCACertFileLocationEnabled]] forKey:SPFavoriteSSLCACertFileLocationEnabledKey];
	_setOrRemoveKey(SPFavoriteSSLCACertFileLocationKey, [self sslCACertFileLocation]);
	
	// SSH details
	_setOrRemoveKey(SPFavoriteSSHHostKey, [self sshHost]);
	_setOrRemoveKey(SPFavoriteSSHUserKey, [self sshUser]);
	_setOrRemoveKey(SPFavoriteSSHPortKey, [self sshPort]);
	[theFavorite setObject:[NSNumber numberWithInteger:[self sshKeyLocationEnabled]] forKey:SPFavoriteSSHKeyLocationEnabledKey];
	_setOrRemoveKey(SPFavoriteSSHKeyLocationKey, [self sshKeyLocation]);
	

	/*
	 * Password handling for the SQL connection
	 */
	NSString *oldKeychainName, *oldKeychainAccount, *newKeychainName, *newKeychainAccount;;
	NSString *oldHostnameForPassword = ([[currentFavorite objectForKey:SPFavoriteTypeKey] integerValue] == SPSocketConnection) ? @"localhost" : [currentFavorite objectForKey:SPFavoriteHostKey];
	NSString *newHostnameForPassword = ([self type] == SPSocketConnection) ? @"localhost" : [self host];

	// Grab the password for this connection
	// Add the password to keychain as appropriate
	NSString *sqlPassword = [self password];
	if (mySQLConnection && connectionKeychainItemName) {
		sqlPassword = [keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount];
	}

	// If creating a new favourite, always add the password to the keychain if it's set
	if (createNewFavorite && [sqlPassword length]) {
		[keychain addPassword:sqlPassword
		              forName:[keychain nameForFavoriteName:[theFavorite objectForKey:SPFavoriteNameKey] id:[theFavorite objectForKey:SPFavoriteIDKey]]
		              account:[keychain accountForUser:[self user] host:newHostnameForPassword database:[self database]]];
	}

	// If not creating a new favourite...
	if (!createNewFavorite) {

		// Get the old keychain name and account strings
		oldKeychainName = [keychain nameForFavoriteName:[currentFavorite objectForKey:SPFavoriteNameKey] id:[currentFavorite objectForKey:SPFavoriteIDKey]];
		oldKeychainAccount = [keychain accountForUser:[currentFavorite objectForKey:SPFavoriteUserKey] host:oldHostnameForPassword database:[currentFavorite objectForKey:SPFavoriteDatabaseKey]];
		
		// If there's no new password, remove the old item from the keychain
		if (![sqlPassword length]) {
			[keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount];

		// Otherwise, set up the new keychain name and account strings and create or edit the item
		} else {
			newKeychainName = [keychain nameForFavoriteName:[theFavorite objectForKey:SPFavoriteNameKey] id:[theFavorite objectForKey:SPFavoriteIDKey]];
			newKeychainAccount = [keychain accountForUser:[self user] host:newHostnameForPassword database:[self database]];
			if ([keychain passwordExistsForName:oldKeychainName account:oldKeychainAccount]) {
				[keychain updateItemWithName:oldKeychainName account:oldKeychainAccount toName:newKeychainName account:newKeychainAccount password:sqlPassword];
			} else {
				[keychain addPassword:sqlPassword forName:newKeychainName account:newKeychainAccount];
			}
		}
	}
	sqlPassword = nil;

	/*
	 * Password handling for the SSH connection
	 */
	NSString *theSSHPassword = [self sshPassword];
	if (mySQLConnection && connectionSSHKeychainItemName) {
		theSSHPassword = [keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount];
	}

	// If creating a new favourite, always add the password if it's set
	if (createNewFavorite && [theSSHPassword length]) {
		[keychain addPassword:theSSHPassword
		              forName:[keychain nameForSSHForFavoriteName:[theFavorite objectForKey:SPFavoriteNameKey] id:[theFavorite objectForKey:SPFavoriteIDKey]]
		              account:[keychain accountForSSHUser:[self sshUser] sshHost:[self sshHost]]];
	}

	// If not creating a new favourite...
	if (!createNewFavorite) {

		// Get the old keychain name and account strings
		oldKeychainName = [keychain nameForSSHForFavoriteName:[currentFavorite objectForKey:SPFavoriteNameKey] id:[currentFavorite objectForKey:SPFavoriteIDKey]];
		oldKeychainAccount = [keychain accountForSSHUser:[currentFavorite objectForKey:SPFavoriteSSHUserKey] sshHost:[currentFavorite objectForKey:SPFavoriteSSHHostKey]];
		
		// If there's no new password, remove the old item from the keychain
		if (![theSSHPassword length]) {
			[keychain deletePasswordForName:oldKeychainName account:oldKeychainAccount];

		// Otherwise, set up the new keychain name and account strings and create or edit the item
		} else {
			newKeychainName = [keychain nameForSSHForFavoriteName:[theFavorite objectForKey:SPFavoriteNameKey] id:[theFavorite objectForKey:SPFavoriteIDKey]];
			newKeychainAccount = [keychain accountForSSHUser:[self sshUser] sshHost:[self sshHost]];
			if ([keychain passwordExistsForName:oldKeychainName account:oldKeychainAccount]) {
				[keychain updateItemWithName:oldKeychainName account:oldKeychainAccount toName:newKeychainName account:newKeychainAccount password:theSSHPassword];
			} else {
				[keychain addPassword:theSSHPassword forName:newKeychainName account:newKeychainAccount];
			}
		}
	}
	theSSHPassword = nil;

	/*
	 * Saving the connection
	 */

	// If creating the connection, add to the favourites tree.
	if (createNewFavorite) {
		SPTreeNode *selectedNode = [self selectedFavoriteNode];
		SPTreeNode *parentNode = nil;

		// If the current node is a group node, create the favorite as a child of it
		if ([selectedNode isGroup] && selectedNode != quickConnectItem) {
			parentNode = selectedNode;

		// Otherwise, create the new node as a sibling of the selected node if possible
		} else if ([selectedNode parentNode] && [selectedNode parentNode] != favoritesRoot) {
			parentNode = (SPTreeNode *)[selectedNode parentNode];
		}

		// Ensure the parent is expanded
		[favoritesOutlineView expandItem:parentNode];

		// Add the new node and select it
		SPTreeNode *newNode = [favoritesController addFavoriteNodeWithData:theFavorite asChildOfNode:parentNode];

		[self _sortFavorites];

		[self _selectNode:newNode];

		// Update the favorites popup button in the preferences
		[[[SPAppDelegate preferenceController] generalPreferencePane] updateDefaultFavoritePopup];

	// Otherwise, if editing the favourite, update it
	} else {
		[[[self selectedFavoriteNode] representedObject] setNodeFavorite:theFavorite];

		// Save the new data to disk
		[favoritesController saveFavorites];

		[self _stopEditingConnection];

		if (currentFavorite) SPClear(currentFavorite);
		currentFavorite = [theFavorite copy];

		[self _sortFavorites];
		[self _scrollToSelectedNode];
	}
	
	// after saving the favorite, the name is never autogenerated (ie. overridable), regardless of the value (#3015)
	favoriteNameFieldWasAutogenerated = NO;

	[[NSNotificationCenter defaultCenter] postNotificationName:SPConnectionFavoritesChangedNotification object:self];
}

/**
 * Check the host field and ensure it isn't set to 'localhost' for non-socket connections.
 */
- (BOOL)_checkHost
{
	if ([self type] != SPSSHTunnelConnection && [self type] != SPSocketConnection && [[self host] isEqualToString:@"localhost"]) {
		SPBeginAlertSheet(
			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
			[dbDocument parentWindow], // Window to attach to
			self, // Modal delegate
			@selector(localhostErrorSheetDidEnd:returnCode:contextInfo:), // Did end selector
			NULL, // 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;
}

/**
 * Sorts the connection favorites based on the selected criteria.
 */

- (void)_sortFavorites
{
	NSString *sortKey = SPFavoriteNameKey;

	switch (currentSortItem)
	{
		case SPFavoritesSortNameItem:
			sortKey = SPFavoriteNameKey;
			break;
		case SPFavoritesSortHostItem:
			sortKey = SPFavoriteHostKey;
			break;
		case SPFavoritesSortTypeItem:
			sortKey = SPFavoriteTypeKey;
			break;
		case SPFavoritesSortColorItem:
			sortKey = SPFavoriteColorIndexKey;
			break;
		case SPFavoritesSortUnsorted:
			return;
	}
	
	// Store a copy of the selected nodes for re-selection
	NSArray *preSortSelection = [self selectedFavoriteNodes];

	[self _sortTreeNode:[[favoritesRoot childNodes] objectAtIndex:0] usingKey:sortKey];

	[favoritesController saveFavorites];
	 
	[self _reloadFavoritesViewData];

	// Update the selection to account for sorted favourites
	NSMutableIndexSet *restoredSelection = [NSMutableIndexSet indexSet];
	for (SPTreeNode *eachNode in preSortSelection) {
		[restoredSelection addIndex:[favoritesOutlineView rowForItem:eachNode]];
	}
	[favoritesOutlineView selectRowIndexes:restoredSelection byExtendingSelection:NO];

	[[NSNotificationCenter defaultCenter] postNotificationName:SPConnectionFavoritesChangedNotification object:self];
}

/**
 * Sorts the supplied tree node using the supplied sort key.
 *
 * @param node The tree node to sort
 * @param key  The sort key to sort by
 */
- (void)_sortTreeNode:(SPTreeNode *)node usingKey:(NSString *)key
{	
	NSMutableArray *nodes = [[node mutableChildNodes] mutableCopy];
	
	// If this node only has one child and it's not another group node, don't bother proceeding
	if (([nodes count] == 1) && (![[nodes objectAtIndex:0] isGroup])) {
		[nodes release];
		return;
	}

	for (SPTreeNode *treeNode in nodes)
	{
		if ([treeNode isGroup]) {
			[self _sortTreeNode:treeNode usingKey:key];
		}
	}
	
	[nodes sortUsingFunction:_compareFavoritesUsingKey context:key];

	if (reverseFavoritesSort) [nodes reverse];

	[[node mutableChildNodes] setArray:nodes];
	
	[nodes release];
}

/**
 * Sort function used by NSMutableArray's sortUsingFunction:
 *
 * @param favorite1 The first of the favorites to compare (and determine sort order)
 * @param favorite2 The second of the favorites to compare
 * @param key       The sort key to perform the comparison by
 * 
 * @return An integer (NSComparisonResult) indicating the order of the comparison
 */
static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, void *key)
{
	NSString *dictKey = (NSString *)key;
	id value1, value2;
	
	BOOL isNamedComparison = [dictKey isEqualToString:SPFavoriteNameKey];
	// Group nodes can only be compared using their names.
	// If this is a named comparison or both nodes are group nodes use their
	// names. Otherwise let the group nodes win (ie. they will be placed at the
	// top ordered alphabetically for all other comparison keys)

	if ([favorite1 isGroup]) {
		if (isNamedComparison || [favorite2 isGroup]) {
			value1 = [[favorite1 representedObject] nodeName];
		} else {
			return NSOrderedAscending; // the left object is a group, the right is not -> left wins
		}
	} else {
		value1 = [[(SPFavoriteNode *)[(SPTreeNode *)favorite1 representedObject] nodeFavorite] objectForKey:dictKey];
	}

	if ([favorite2 isGroup]) {
		if (isNamedComparison || [favorite1 isGroup]) {
			value2 = [[favorite2 representedObject] nodeName];
		} else {
			return NSOrderedDescending; // the left object is not a group, the right is -> left loses
		}
	} else {
		value2 = [[(SPFavoriteNode *)[(SPTreeNode *)favorite2 representedObject] nodeFavorite] objectForKey:dictKey];
	}
	
	//if a value is undefined count it as "loser"
	if(!value1 && value2) return NSOrderedDescending;
	if(value1 && !value2) return NSOrderedAscending;
	if(!value1 && !value2) return NSOrderedSame;

	if ([value1 isKindOfClass:[NSString class]]) {
		return [value1 caseInsensitiveCompare:value2];
	}
	return [value1 compare:value2];
}

/**
 * Updates the favorite's host when the type changes.
 */

- (void)_favoriteTypeDidChange
{
	NSDictionary *favorite = [self selectedFavorite];

	// If either socket or host is localhost, clear.
	if ((previousType != SPSocketConnection) && [[favorite objectForKey:SPFavoriteHostKey] isEqualToString:@"localhost"]) {
		[self setHost:@""];
	}

	// Update the name for newly added favorites if not already touched by the user, by triggering a KVO update
	if (![[self name] length] || favoriteNameFieldWasAutogenerated) {
		NSString *favoriteName = [self _generateNameForConnection];
		if (favoriteName) {
			[self setName:favoriteName];
		}
	}
}

/**
 * Convenience method for reloading the outline view, expanding the root item and scrolling to the selected item.
 */
- (void)_reloadFavoritesViewData
{	
	[favoritesOutlineView reloadData];
	[favoritesOutlineView expandItem:[[favoritesRoot childNodes] objectAtIndex:0] expandChildren:NO];
	
	[self _scrollToSelectedNode];
}

/**
 * Update the first responder status on password fields if they are empty and
 * some host details are set, usually as a response to favourite selection changes.
 */
- (void)_updateFavoriteFirstResponder
{
	// Skip auto-selection changes if there is no user set
	if (![[self user] length]) return;

	switch ([self type]) 
	{
		case SPTCPIPConnection:
			if (![[standardPasswordField stringValue] length]) {
				[[dbDocument parentWindow] makeFirstResponder:standardPasswordField];
			}
			break;
		case SPSocketConnection:
			if (![[socketPasswordField stringValue] length]) {
				[[dbDocument parentWindow] makeFirstResponder:socketPasswordField];
			}
			break;
		case SPSSHTunnelConnection:
			if (![[sshPasswordField stringValue] length]) {
				[[dbDocument parentWindow] makeFirstResponder:sshPasswordField];
			}
			break;
	}
}

/**
 * Restores the connection interface to its original state.
 */
- (void)_restoreConnectionInterface
{
	// Must be performed on the main thread
	if (![NSThread isMainThread]) return [[self onMainThread] _restoreConnectionInterface];

	// Reset the window title
	[dbDocument updateWindowTitle:self];
	[[dbDocument parentTabViewItem] setLabel:[dbDocument displayName]];
	
	// Stop the current tab's progress indicator
	[dbDocument setIsProcessing:NO];

	// Reset the UI
	[helpButton setHidden:NO];
	[helpButton display];
	[connectButton setTitle:NSLocalizedString(@"Connect", @"connect button")];
	[connectButton setEnabled:YES];
	[connectButton display];
	[testConnectButton setEnabled:YES];
	[progressIndicator stopAnimation:self];
	[progressIndicator display];
	[progressIndicatorText setHidden:YES];
	[progressIndicatorText display];
	[dbDocument setTitlebarStatus:@""];

	// If not testing a connection, Update the password fields, restoring passwords that may have
	// been bulleted out during connection
	if (!isTestingConnection) {
		if (connectionKeychainItemName) {
			[self setPassword:[keychain getPasswordForName:connectionKeychainItemName account:connectionKeychainItemAccount]];
		}
		if (connectionSSHKeychainItemName) {
			[self setSshPassword:[keychain getPasswordForName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount]];
		}
	}

	// Re-enable favorites table view
	[favoritesOutlineView setEnabled:YES];
	[favoritesOutlineView display];

	// Revert the connect button back to its original selector
	[connectButton setAction:@selector(initiateConnection:)];
}

/**
 * Selected the supplied node in the favorites outline view.
 *
 * @param node The node to select
 */
- (void)_selectNode:(SPTreeNode *)node
{
	[favoritesOutlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[favoritesOutlineView rowForItem:node]] byExtendingSelection:NO];
	[self _scrollToSelectedNode];
}

/**
 * Scroll to the currently selected node.
 */
- (void)_scrollToSelectedNode
{
	// Don't scroll if no node is currently selected
	if ([favoritesOutlineView selectedRow] == -1) return;

	[favoritesOutlineView scrollRowToVisible:[favoritesOutlineView selectedRow]];
}

/**
 * Removes the supplied tree node.
 *
 * @param node The node to remove
 */
- (void)_removeNode:(SPTreeNode *)node
{
	[self _removeAllPasswordsForNode:node];

	[favoritesController removeFavoriteNode:node];
	
	[self _reloadFavoritesViewData];

	// Select Quick Connect item to prevent empty selection
	[self selectQuickConnectItem];
	
	[connectionResizeContainer setHidden:NO];
	[connectionInstructionsTextField setStringValue:NSLocalizedString(@"Enter connection details below, or choose a favorite", @"enter connection details label")];
	
	[[[SPAppDelegate preferenceController] generalPreferencePane] updateDefaultFavoritePopup];
}

/**
 * Removes all passwords for the supplied tree node and any child nodes.
 *
 * @param node The node to remove all passwords within and for.
 */
- (void)_removeAllPasswordsForNode:(SPTreeNode *)node
{

	// If the supplied node is a group node, remove all passwords for any children
	if ([node isGroup]) {
		for (SPTreeNode *childNode in [node childNodes]) {
			[self _removeAllPasswordsForNode:childNode];
		}
		return;
	}

	// Otherwise, remove details for the supplied node.

		NSDictionary *favorite = [[node representedObject] nodeFavorite];
		
		// Get selected favorite's details
		NSString *favoriteName     = [favorite objectForKey:SPFavoriteNameKey];
		NSString *favoriteUser     = [favorite objectForKey:SPFavoriteUserKey];
		NSString *favoriteHost     = [favorite objectForKey:SPFavoriteHostKey];
		NSString *favoriteDatabase = [favorite objectForKey:SPFavoriteDatabaseKey];
		NSString *favoriteSSHUser  = [favorite objectForKey:SPFavoriteSSHUserKey];
		NSString *favoriteSSHHost  = [favorite objectForKey:SPFavoriteSSHHostKey];
		NSString *favoriteID       = [favorite objectForKey:SPFavoriteIDKey];

		// Remove passwords from the Keychain
		[keychain deletePasswordForName:[keychain nameForFavoriteName:favoriteName id:favoriteID]
		                        account:[keychain accountForUser:favoriteUser host:((type == SPSocketConnection) ? @"localhost" : favoriteHost) database:favoriteDatabase]];
		[keychain deletePasswordForName:[keychain nameForSSHForFavoriteName:favoriteName id:favoriteID]
		                        account:[keychain accountForSSHUser:favoriteSSHUser sshHost:favoriteSSHHost]];

		// Reset last used favorite
		if ([[favorite objectForKey:SPFavoriteIDKey] integerValue] == [prefs integerForKey:SPLastFavoriteID]) {
			[prefs setInteger:0 forKey:SPLastFavoriteID];
		}

		// If required, reset the default favorite
		if ([[favorite objectForKey:SPFavoriteIDKey] integerValue] == [prefs integerForKey:SPDefaultFavorite]) {
			[prefs setInteger:[prefs integerForKey:SPLastFavoriteID] forKey:SPDefaultFavorite];
		}
	}

/**
 * Creates a new favorite ID based on the UNIX epoch time.
 */
- (NSNumber *)_createNewFavoriteID
{
	return [NSNumber numberWithInteger:[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]];
}

/**
 * Returns the favorite node for the conection favorite with the supplied ID.
 */
- (SPTreeNode *)_favoriteNodeForFavoriteID:(NSInteger)favoriteID
{	
	SPTreeNode *favoriteNode = nil;

	if (!favoritesRoot) return favoriteNode;

	if (!favoriteID) return quickConnectItem;

	for (SPTreeNode *node in [favoritesRoot allChildLeafs]) 
	{
		if ([[[[node representedObject] nodeFavorite] objectForKey:SPFavoriteIDKey] integerValue] == favoriteID) {
			favoriteNode = node;
		}
	}

	return favoriteNode;
}

/**
 * Strips any invalid characters form the supplied string. Invalid is defined as any characters that should
 * not be allowed to be enetered on the connection screen.
 */
- (NSString *)_stripInvalidCharactersFromString:(NSString *)subject
{
	NSString *result = [subject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
	
	return [result stringByReplacingOccurrencesOfString:@"\n" withString:@""];
}

/**
 * Generate a name for the current connection based on any other populated details.
 * Currently uses the host and database fields.
 * If a name cannot be generated because there are insufficient other details, returns nil.
 */
- (NSString *)_generateNameForConnection
{
	NSString *aName;
	
	if ([self type] != SPSocketConnection && ![[self host] length]) {
		return nil;
	}
		
	aName = ([self type] == SPSocketConnection) ? @"localhost" : [self host];
	
	if ([[self database] length]) {
		aName = [NSString stringWithFormat:@"%@/%@", aName, [self database]];
	}

	return aName;
}

		
/**
 * If editing is not already active, mark editing as starting, triggering UI updates
 * to match.
 */
- (void)_startEditingConnection
{
	// If not connecting, hide the connection status text to reflect changes
	if (!isConnecting) {
		[progressIndicatorText setHidden:YES];
	}

	if (isEditingConnection) return;

	// Fade and move the edit button area in
	[editButtonsView setAlphaValue:0.0f];
	[editButtonsView setHidden:NO];
	[editButtonsView setFrameOrigin:NSMakePoint([editButtonsView frame].origin.x, [editButtonsView frame].origin.y - 30)];
	// The animation is started async because there is a bug/oddity with layer-backed views and animating frameOrigin (at least in 10.13):
	// If both calls to -setFrameOrigin: are in the same method, CA would only animate the difference between those calls (which is 0 here).
	// This works fine when not using layers, but then there is another issue with the progress indicator (#2903)
	SPMainLoopAsync(^{
		[NSAnimationContext beginGrouping];
		[[editButtonsView animator] setFrameOrigin:NSMakePoint([editButtonsView frame].origin.x, [editButtonsView frame].origin.y + 30)];
		[[editButtonsView animator] setAlphaValue:1.0f];
		[NSAnimationContext endGrouping];
	});

	// Update the "Save" button state as appropriate
	[saveFavoriteButton setEnabled:([self selectedFavorite] != nil)];

	// Show the area to allow saving the changes
	[self setIsEditingConnection:YES];
	[favoritesOutlineView setNeedsDisplayInRect:[favoritesOutlineView rectOfRow:[favoritesOutlineView selectedRow]]];
}

/**
 * If editing is active, mark editing as complete, triggering UI updates to match.
 */
- (void)_stopEditingConnection
{
	if (!isEditingConnection) return;

	[self setIsEditingConnection:NO];

	[editButtonsView setHidden:YES];
	[progressIndicatorText setHidden:YES];
	[favoritesOutlineView display];
}

- (void)_documentWillClose:(NSNotification *)notification
{
	cancellingConnection = YES;
	dbDocument = nil;

	if (mySQLConnection) {
		[mySQLConnection setDelegate:nil];
		[NSThread detachNewThreadWithName:SPCtxt(@"SPConnectionController close background disconnect", dbDocument) target:mySQLConnection selector:@selector(disconnect) object:nil];
		[mySQLConnection autorelease];
		mySQLConnection = nil;
	}
	
	if (sshTunnel) (void)([sshTunnel setConnectionStateChangeSelector:nil delegate:nil]), SPClear(sshTunnel);
	
	
	for(NSURL *url in resolvedBookmarks){
		[url stopAccessingSecurityScopedResource];
	}
}

#pragma mark - SPConnectionHandler

/**
 * Set up the MySQL connection, either through a successful tunnel or directly in the background.
 */
- (void)initiateMySQLConnection
{
	if (isTestingConnection) {
		if (sshTunnel) {
			[progressIndicatorText setStringValue:NSLocalizedString(@"Testing MySQL...", @"MySQL connection test very short status message")];
		}
		else {
			[progressIndicatorText setStringValue:NSLocalizedString(@"Testing connection...", @"Connection test very short status message")];
		}
	}
	else 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];

	[connectButton setTitle:NSLocalizedString(@"Cancel", @"cancel button")];
	[connectButton setAction:@selector(cancelConnection:)];
	[connectButton setEnabled:YES];
	[connectButton display];

	[NSThread detachNewThreadWithName:SPCtxt(@"SPConnectionController MySQL connection task", dbDocument)
	                           target:self
	                         selector:@selector(initiateMySQLConnectionInBackground)
	                           object:nil];
}

/**
 * Initiates the core of the MySQL connection process on a background thread.
 */
- (void)initiateMySQLConnectionInBackground
{
	@autoreleasepool {
		mySQLConnection = [[SPMySQLConnection alloc] init];

		// Set up shared details
		[mySQLConnection setUsername:[self user]];

		// Initialise to socket if appropriate.
		if ([self type] == SPSocketConnection) {
			[mySQLConnection setUseSocket:YES];
			[mySQLConnection setSocketPath:[self socket]];

			// Otherwise, initialise to host, using tunnel if appropriate
		}
		else {
			[mySQLConnection setUseSocket:NO];

			if ([self type] == SPSSHTunnelConnection) {
				[mySQLConnection setHost:@"127.0.0.1"];

				[mySQLConnection setPort:[sshTunnel localPort]];
				[mySQLConnection setProxy:sshTunnel];
			}
			else {
				[mySQLConnection setHost:[self host]];

				if ([[self port] length]) [mySQLConnection setPort:[[self port] integerValue]];
			}
		}

		// Only set the password if there is no Keychain item set and the connection is not being tested.
		// The connection will otherwise ask the delegate for passwords in the Keychain.
		if ((!connectionKeychainItemName || isTestingConnection) && [self password]) {
			[mySQLConnection setPassword:[self password]];
		}
		
		if([self allowDataLocalInfile]) {
			[mySQLConnection setAllowDataLocalInfile:YES];
		}
		
		// Enable Clear Text plugin when enabled
		if ([self enableClearTextPlugin]) {
			[mySQLConnection setEnableClearTextPlugin:YES];
		}

		// Enable SSL if set
		if ([self useSSL]) {
			[mySQLConnection setUseSSL:YES];

			if ([self sslKeyFileLocationEnabled]) {
				[mySQLConnection setSslKeyFilePath:[self sslKeyFileLocation]];
			}

			if ([self sslCertificateFileLocationEnabled]) {
				[mySQLConnection setSslCertificatePath:[self sslCertificateFileLocation]];
			}

			if ([self sslCACertFileLocationEnabled]) {
				[mySQLConnection setSslCACertificatePath:[self sslCACertFileLocation]];
			}

			NSString *userSSLCipherList = [prefs stringForKey:SPSSLCipherListKey];
			if(userSSLCipherList) {
				//strip out disabled ciphers (e.g. in "foo:bar:--:baz")
				NSRange markerPos = [userSSLCipherList rangeOfRegex:@":?--"];
				if(markerPos.location != NSNotFound) {
					userSSLCipherList = [userSSLCipherList substringToIndex:markerPos.location];
				}
				[mySQLConnection setSslCipherList:userSSLCipherList];
			}
		}

		if(![self useCompression]) [mySQLConnection removeClientFlags:SPMySQLClientFlagCompression];

		// Connection delegate must be set before actual connection attempt is made
		[mySQLConnection setDelegate:dbDocument];

		// Set whether or not we should enable delegate logging according to the prefs
		[mySQLConnection setDelegateQueryLogging:[prefs boolForKey:SPConsoleEnableLogging]];

		// Set options from preferences
		[mySQLConnection setTimeout:[[prefs objectForKey:SPConnectionTimeoutValue] integerValue]];
		[mySQLConnection setUseKeepAlive:[[prefs objectForKey:SPUseKeepAlive] boolValue]];
		[mySQLConnection setKeepAliveInterval:[[prefs objectForKey:SPKeepAliveInterval] floatValue]];

		// Connect
		[mySQLConnection connect];

		if (![mySQLConnection isConnected]) {
			if (sshTunnel && !cancellingConnection) {

				// This is a race condition we cannot fix "properly":
				// For meaningful error handling we need to also consider the debug output from the SSH connection.
				// The SSH debug output might be sligthly delayed though (flush, delegates, ...) or
				// there might not even by any output at all (when it is purely a libmysql issue).
				// TL;DR: No guaranteed events we could wait for, just trying our luck.
				[NSThread sleepForTimeInterval:0.1]; // 100ms

				// If the state is connection refused, attempt the MySQL connection again with the host using the hostfield value.
				if ([sshTunnel state] == SPMySQLProxyForwardingFailed) {
					if ([sshTunnel localPortFallback]) {
						[mySQLConnection setPort:[sshTunnel localPortFallback]];
						[mySQLConnection connect];

						if (![mySQLConnection isConnected]) {
							[NSThread sleepForTimeInterval:0.1]; //100ms
						}
					}
				}
			}

			if (![mySQLConnection isConnected]) {
				if (!cancellingConnection) {
					NSString *errorMessage;
					if (sshTunnel && [sshTunnel state] == SPMySQLProxyForwardingFailed) {
						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 lastErrorMessage]];
						[[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"SSH port forwarding failed", @"title when ssh tunnel port forwarding failed") errorMessage:errorMessage detail:[sshTunnel debugMessages] rawErrorText:[mySQLConnection lastErrorMessage]];
					}
					else if ([mySQLConnection lastErrorID] == 1045) { // "Access denied" error
						errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect to host %@ because access was denied.\n\nDouble-check your username and password and ensure that access from your current location is permitted.\n\nMySQL said: %@", @"message of panel when connection to host failed due to access denied error"), [self host], [mySQLConnection lastErrorMessage]];
						[[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Access denied!", @"connection failed due to access denied title") errorMessage:errorMessage detail:nil rawErrorText:[mySQLConnection lastErrorMessage]];
					}
					else if ([self type] == SPSocketConnection && (![self socket] || ![[self socket] length]) && ![mySQLConnection socketPath]) {
						errorMessage = [NSString stringWithFormat:NSLocalizedString(@"The socket file could not be found in any common location. Please supply the correct socket location.\n\nMySQL said: %@", @"message of panel when connection to socket failed because optional socket could not be found"), [mySQLConnection lastErrorMessage]];
						[[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Socket not found!", @"socket not found title") errorMessage:errorMessage detail:nil rawErrorText:[mySQLConnection lastErrorMessage]];
					}
					else if ([self type] == SPSocketConnection) {
						errorMessage = [NSString stringWithFormat:NSLocalizedString(@"Unable to connect via the socket, or the request timed out.\n\nDouble-check that the socket path is correct and that you have the necessary privileges, and that the server is running.\n\nMySQL said: %@", @"message of panel when connection to host failed"), [mySQLConnection lastErrorMessage]];
						[[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Socket connection failed!", @"socket connection failed title") errorMessage:errorMessage detail:nil rawErrorText:[mySQLConnection lastErrorMessage]];
					}
					else {
						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 %ld seconds).\n\nMySQL said: %@", @"message of panel when connection to host failed"), [self host], (long)[[prefs objectForKey:SPConnectionTimeoutValue] integerValue], [mySQLConnection lastErrorMessage]];
						[[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Connection failed!", @"connection failed title") errorMessage:errorMessage detail:nil rawErrorText:[mySQLConnection lastErrorMessage]];
					}
				}

				// Tidy up
				isConnecting = NO;

				if (sshTunnel) (void)([sshTunnel disconnect]), SPClear(sshTunnel);

				SPClear(mySQLConnection);
				if (!cancellingConnection) [self _restoreConnectionInterface];

				return;
			}
		}

		if ([self database] && ![[self database] isEqualToString:@""]) {
			if (![mySQLConnection selectDatabase:[self database]]) {
				if (!isTestingConnection) {
					[[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"Could not select database", @"message when database selection failed") errorMessage:[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 lastErrorMessage]] detail:nil rawErrorText:[mySQLConnection lastErrorMessage]];
				}

				// Tidy up
				isConnecting = NO;

				if (sshTunnel) SPClear(sshTunnel);

				SPClear(mySQLConnection);
				[self _restoreConnectionInterface];
				if (isTestingConnection) {
					[self _showConnectionTestResult:NSLocalizedString(@"Invalid database", @"Invalid database very short status message")];
				}

				return;
			}
		}

		switch (timeZoneMode) {
			case SPConnectionTimeZoneModeUseSystemTZ:
				[mySQLConnection updateTimeZoneIdentifier:NSTimeZone.systemTimeZone.name];
				break;
			case SPConnectionTimeZoneModeUseFixedTZ:
				[mySQLConnection updateTimeZoneIdentifier:timeZoneIdentifier];
				break;
			default:
				break;
		}

		// Connection established
		[self performSelectorOnMainThread:@selector(mySQLConnectionEstablished) withObject:nil waitUntilDone:NO];
	}
}

/**
 * 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
{
	if (isTestingConnection) {
		[progressIndicatorText setStringValue:NSLocalizedString(@"Testing SSH...", @"SSH testing very short status message")];
	} else {
		[progressIndicatorText setStringValue:NSLocalizedString(@"SSH connecting...", @"SSH connecting very short status message")];
	}
	[progressIndicatorText display];

	[connectButton setTitle:NSLocalizedString(@"Cancel", @"cancel button")];
	[connectButton setAction:@selector(cancelConnection:)];
	[connectButton setEnabled:YES];
	[connectButton display];

	// Trim whitespace and newlines from the SSH host field before attempting to connect
	[self setSshHost:[[self sshHost] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];

	// Set up the tunnel details
	sshTunnel = [[SPSSHTunnel alloc] initToHost:[self sshHost] port:[[self sshPort] integerValue] login:[self sshUser] tunnellingToPort:([[self port] length]?[[self port] integerValue]:3306) onHost:[self host]];
	
	if(sshTunnel == nil) {
						[dbDocument setTitlebarStatus:NSLocalizedString(@"SSH Disconnected", @"SSH disconnected titlebar marker")];

				[[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"SSH connection failed!", @"SSH connection failed title")
												errorMessage:@"Failed to Initialize SSH Handle"
													  detail:@"Could not initiate ssh connection worker."
												rawErrorText:@"Could not initiate ssh connection worker."];
		return;
	}
	
	[sshTunnel setParentWindow:[dbDocument parentWindow]];

	// Add keychain or plaintext password as appropriate - note the checks in initiateConnection.
	if (connectionSSHKeychainItemName && !isTestingConnection) {
		[sshTunnel setPasswordKeychainName:connectionSSHKeychainItemName account:connectionSSHKeychainItemAccount];
	} else if (sshPassword) {
		[sshTunnel setPassword:[self sshPassword]];
	}

	// Set the public key path if appropriate
	if (sshKeyLocationEnabled && sshKeyLocation) {
		[sshTunnel setKeyFilePath:sshKeyLocation];
	}

	// 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];
}

/**
 * Called on the main thread once the MySQL connection is established on the background thread. Either the
 * connection was cancelled or it was successful.
 */
- (void)mySQLConnectionEstablished
{
	isConnecting = NO;

	// If the user is only testing the connection, kill the connection
	// once established and reset the UI.  Also catch connection cancels.
	if (isTestingConnection || cancellingConnection) {

		// Clean up any connections remaining, and reset the UI
		[self cancelConnection:self];

		if (isTestingConnection) {
			[self _showConnectionTestResult:NSLocalizedString(@"Connection succeeded", @"Connection success very short status message")];
		}

		return;
	}

	[progressIndicatorText setStringValue:NSLocalizedString(@"Connected", @"connection established message")];
	[progressIndicatorText display];

	// Stop the current tab's progress indicator
	[dbDocument setIsProcessing:NO];

	// Successful connection!
	[connectButton setEnabled:NO];
	[connectButton display];
	[progressIndicator stopAnimation:self];
	[progressIndicatorText setHidden:YES];

	// If SSL was enabled, check it was established correctly
	if (useSSL && ([self type] == SPTCPIPConnection || [self type] == SPSocketConnection)) {
		if (![mySQLConnection isConnectedViaSSL]) {
			SPOnewayAlertSheet(
				NSLocalizedString(@"SSL connection not established", @"SSL requested but not used title"),
				[dbDocument parentWindow],
				NSLocalizedString(@"You requested that the connection should be established using SSL, but MySQL made the connection without SSL.\n\nThis may be because the server does not support SSL connections, or has SSL disabled; or insufficient details were supplied to establish an SSL connection.\n\nThis connection is not encrypted.", @"SSL connection requested but not established error detail")
			);
		}
		else {
			[dbDocument setStatusIconToImageWithName:@"titlebarlock"];
		}
	}

	// Re-enable favorites table view
	[favoritesOutlineView setEnabled:YES];
	[favoritesOutlineView display];

	// Release the tunnel if set - will now be retained by the connection
	if (sshTunnel) SPClear(sshTunnel);

	// Pass the connection to the document and clean up the interface
	[self addConnectionToDocument];
}

/**
 * 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
{
	if (cancellingConnection) return;

	NSInteger newState = [theTunnel state];

	// If the user cancelled the password prompt dialog, continue with no further action.
	if ([theTunnel passwordPromptCancelled]) {
		[self _restoreConnectionInterface];

		return;
	}

	if (newState == SPMySQLProxyIdle) {

		[dbDocument setTitlebarStatus:NSLocalizedString(@"SSH Disconnected", @"SSH disconnected titlebar marker")];

		[[self onMainThread] failConnectionWithTitle:NSLocalizedString(@"SSH connection failed!", @"SSH connection failed title")
		                                errorMessage:[theTunnel lastError]
		                                      detail:[sshTunnel debugMessages]
		                                rawErrorText:[theTunnel lastError]];
	}
	else if (newState == SPMySQLProxyConnected) {
		[dbDocument setTitlebarStatus:NSLocalizedString(@"SSH Connected", @"SSH connected titlebar marker")];

		[self initiateMySQLConnection];
	}
	else {
		[dbDocument setTitlebarStatus:NSLocalizedString(@"SSH Connecting…", @"SSH connecting titlebar marker")];
	}
}

/**
 * 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 removeFromSuperviewWithoutNeedingDisplay];
	[databaseConnectionView setHidden:NO];

	// Restore the toolbar icons
	NSArray *toolbarItems = [[[dbDocument parentWindow] toolbar] items];

	for (NSUInteger 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.
	[dbDocument setConnection:mySQLConnection];
}

/**
 * Ends a connection attempt by stopping the connection animation and
 * displaying a specified error message.
 */
- (void)failConnectionWithTitle:(NSString *)theTitle errorMessage:(NSString *)theErrorMessage detail:(NSString *)errorDetail rawErrorText:(NSString *)rawErrorText
{
	BOOL isSSHTunnelBindError = NO;

	[self _restoreConnectionInterface];

	// Release as appropriate
	if (sshTunnel) {
		(void)([sshTunnel disconnect]), SPClear(sshTunnel);

		// If the SSH tunnel connection failed because the port it was trying to bind to was already in use take note
		// of it so we can give the user the option of connecting via standard connection and use the existing tunnel.
		if ([rawErrorText rangeOfString:@"bind"].location != NSNotFound) {
			isSSHTunnelBindError = YES;
		}
	}

	if (errorDetail) [errorDetailText setString:errorDetail];

	// Inform the delegate that the connection attempt failed
	if (delegate && [delegate respondsToSelector:@selector(connectionControllerConnectAttemptFailed:)]) {
		[[(id)delegate onMainThread] connectionControllerConnectAttemptFailed:self];
	}

	// Only display the connection error message if there is a window visible
	if ([[dbDocument parentWindow] isVisible]) {
		SPBeginAlertSheet(theTitle, NSLocalizedString(@"OK", @"OK button"), (errorDetail) ? NSLocalizedString(@"Show Detail", @"Show detail button") : nil, (isSSHTunnelBindError) ? NSLocalizedString(@"Use Standard Connection", @"use standard connection button") : nil, [dbDocument parentWindow], self, @selector(connectionFailureSheetDidEnd:returnCode:contextInfo:), @"connect", theErrorMessage);
	}
}

/**
 * Alert sheet callback method - invoked when an error sheet is closed.
 */
- (void)connectionFailureSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
	if (returnCode == NSAlertFirstButtonReturn || returnCode == NSAlertAlternateReturn) {
		[errorDetailText setFont:[NSFont userFontOfSize:12]];
		[errorDetailText setAlignment:NSLeftTextAlignment];
		[errorDetailWindow makeKeyAndOrderFront:self];
	}

	// Currently only SSH port bind errors offer a 3rd option in the error dialog, but if this ever changes
	// this will definitely need to be updated.
	else if (returnCode == NSAlertSecondButtonReturn || returnCode == NSAlertOtherReturn) {

		// Extract the local port number that SSH attempted to bind to from the debug output
		NSString *tunnelPort = [[[errorDetailText string] componentsMatchedByRegex:@"LOCALHOST:([0-9]+)" capture:1L] lastObject];

		// Change the connection type to standard TCP/IP
		[self setType:SPTCPIPConnection];

		// Change connection details
		[self setPort:tunnelPort];
		[self setHost:SPLocalhostAddress];

		// Change to standard TCP/IP connection view
		[self resizeTabViewToConnectionType:SPTCPIPConnection animating:YES];

		// Initiate the connection after a half second delay to give the connection view a chance to resize
		[self performSelector:@selector(initiateConnection:) withObject:self afterDelay:0.5];
	}
	
	// we're not connecting anymore, it failed.
	isConnecting = NO;
	// update tab and window title
	[dbDocument updateWindowTitle:self];
}

#pragma mark - SPConnectionHandlerPrivateAPI

/**
 * Display a connection test error or success message
 */
- (void)_showConnectionTestResult:(NSString *)resultString
{
	if (![NSThread isMainThread]) {
		[[self onMainThread] _showConnectionTestResult:resultString];
	}

	[helpButton setHidden:NO];
	[progressIndicator stopAnimation:self];
	[progressIndicatorText setStringValue:resultString];
	[progressIndicatorText setHidden:NO];
}

#pragma mark - SPConnectionControllerDelegate

#pragma mark SplitView 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 sync.
 */
- (void)splitViewDidResizeSubviews:(NSNotification *)notification
{
	if (initComplete) {
		allowSplitViewResizing = YES;
		[databaseConnectionView setPosition:[[[connectionSplitView subviews] objectAtIndex:0] frame].size.width ofDividerAtIndex:0];
	}
}

- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)dividerIndex
{
	return 145.f;
}


#pragma mark -
#pragma mark Outline view delegate methods


- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item
{
	return ([[(SPTreeNode *)item parentNode] parentNode] == nil);
}

- (void)outlineViewSelectionIsChanging:(NSNotification *)notification
{
	if (isEditingConnection) {
		[self _stopEditingConnection];

		[[notification object] setNeedsDisplay:YES];
	}
}

- (void)outlineViewSelectionDidChange:(NSNotification *)notification
{
	NSInteger selected = [favoritesOutlineView numberOfSelectedRows];

	if (isEditingConnection) {
		[self _stopEditingConnection];
		[[notification object] setNeedsDisplay:YES];
	}

	if (selected == 1) {
		[self updateFavoriteSelection:self];

		favoriteNameFieldWasAutogenerated = NO;
		[connectionResizeContainer setHidden:NO];
		[connectionInstructionsTextField setStringValue:NSLocalizedString(@"Enter connection details below, or choose a favorite", @"enter connection details label")];
	}
	else if (selected > 1) {
		[connectionResizeContainer setHidden:YES];
		[connectionInstructionsTextField setStringValue:NSLocalizedString(@"Please choose a favorite", @"please choose a favorite connection view label")];
	}
}

- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
	if (item == quickConnectItem) {
		return (NSCell *)quickConnectCell;
	}

	return [tableColumn dataCellForRow:[outlineView rowForItem:item]];
}

- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
	SPTreeNode *node = (SPTreeNode *)item;
	SPFavoriteTextFieldCell *favoriteCell = (SPFavoriteTextFieldCell *)cell;

	// Set an image as appropriate; the quick connect image for that entry, no image for other
	// top-level items, the folder image for group nodes, or the database image for other nodes.
	if (![[node parentNode] parentNode]) {
		if (node == quickConnectItem) {
			if ([outlineView rowForItem:item] == [outlineView selectedRow]) {
				[favoriteCell setImage:[NSImage imageNamed:SPQuickConnectImageWhite]];
			}
			else {
				[favoriteCell setImage:[NSImage imageNamed:SPQuickConnectImage]];
			}
		}
		else {
			[favoriteCell setImage:nil];
		}
		[favoriteCell setLabelColor:nil];
	}
	else {
		if ([node isGroup]) {
			[favoriteCell setImage:folderImage];
			[favoriteCell setLabelColor:nil];
		}
		else {
			[favoriteCell setImage:[NSImage imageNamed:SPDatabaseImage]];
			NSColor *bgColor = nil;
			NSNumber *colorIndexObj = [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteColorIndexKey];
			if(colorIndexObj != nil) {
				bgColor = [[SPFavoriteColorSupport sharedInstance] colorForIndex:[colorIndexObj integerValue]];
			}
			[favoriteCell setLabelColor:bgColor];
		}
	}

	// If a favourite item is being edited, draw the text in bold to show state
	if (isEditingConnection && ![node isGroup] && [outlineView rowForItem:item] == [outlineView selectedRow]) {
		NSMutableAttributedString *editedCellString = [[cell attributedStringValue] mutableCopy];
		[editedCellString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithDeviceWhite:0.25f alpha:1.f] range:NSMakeRange(0, [editedCellString length])];
		[cell setAttributedStringValue:editedCellString];
		[editedCellString release];
	}
}

- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
{
	return 24.f;
}

- (NSString *)outlineView:(NSOutlineView *)outlineView toolTipForCell:(NSCell *)cell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)tableColumn item:(id)item mouseLocation:(NSPoint)mouseLocation
{
	NSString *toolTip = nil;

	SPTreeNode *node = (SPTreeNode *)item;

	if (![node isGroup]) {

		NSString *favoriteName = [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteNameKey];
		NSString *favoriteHostname = [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteHostKey];

		toolTip = ([favoriteHostname length]) ? [NSString stringWithFormat:@"%@ (%@)", favoriteName, favoriteHostname] : favoriteName;
	}

	// Only display a tooltip for group nodes that are a descendant of the root node
	else if ([[node parentNode] parentNode]) {

		NSUInteger favCount = 0;
		NSUInteger groupCount = 0;

		for (SPTreeNode *eachNode in [node childNodes])
		{
			if ([eachNode isGroup]) {
				groupCount++;
			}
			else {
				favCount++;
			}
		}

		NSMutableArray *tooltipParts = [NSMutableArray arrayWithCapacity:2];

		if (favCount || !groupCount) {
			[tooltipParts addObject:[NSString stringWithFormat:((favCount == 1) ? NSLocalizedString(@"%lu favorite", @"favorite singular label (%d == 1)") : NSLocalizedString(@"%lu favorites", @"favorites plural label (%d != 1)")), (unsigned long)favCount]];
		}

		if (groupCount) {
			[tooltipParts addObject:[NSString stringWithFormat:((groupCount == 1) ? NSLocalizedString(@"%lu group", @"favorite group singular label (%d == 1)") : NSLocalizedString(@"%lu groups", @"favorite groups plural label (%d != 1)")), (unsigned long)groupCount]];
		}

		toolTip = [NSString stringWithFormat:@"%@ - %@", [[node representedObject] nodeName], [tooltipParts componentsJoinedByString:@", "]];
	}

	return toolTip;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item
{
	// If this is a top level item, only allow the "Quick Connect" item to be selectable
	if (![[item parentNode] parentNode]) {
		return item == quickConnectItem;
	}

	// Otherwise allow all items to be selectable
	return YES;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
	return (item != quickConnectItem && ![item isLeaf]);
}

- (BOOL)outlineView:(NSOutlineView *)outlineView shouldShowOutlineCellForItem:(id)item
{
	return ([[item parentNode] parentNode] != nil);
}

- (BOOL)outlineView:(NSOutlineView *)outlineView shouldCollapseItem:(id)item
{
	return ([[item parentNode] parentNode] != nil);
}

- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
	NSEvent *event = [NSApp currentEvent];
	BOOL shiftTabbedIn = ([event type] == NSKeyDown && [[event characters] length] && [[event characters] characterAtIndex:0] == NSBackTabCharacter);

	if (shiftTabbedIn && [(SPFavoritesOutlineView *)outlineView justGainedFocus]) {
		return NO;
	}

	return item != quickConnectItem;
}

- (void)outlineViewItemDidCollapse:(NSNotification *)notification
{
	[self _setNodeIsExpanded:NO fromNotification:notification];
}

- (void)outlineViewItemDidExpand:(NSNotification *)notification
{
	[self _setNodeIsExpanded:YES fromNotification:notification];
}


#pragma mark -
#pragma mark Outline view drag & drop


- (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
{
	// Prevent a drag which includes the outline title group from taking place
	for (id item in items)
	{
		if (![[item parentNode] parentNode]) return NO;
	}

	// If the user is in the process of changing a node's name, trigger a save and prevent dragging.
	if (isEditingItemName) {
		[favoritesController saveFavorites];

		[self _reloadFavoritesViewData];

		isEditingItemName = NO;

		return NO;
	}

	[pboard declareTypes:@[SPFavoritesPasteboardDragType] owner:self];

	BOOL result = [pboard setData:[NSData data] forType:SPFavoritesPasteboardDragType];

	draggedNodes = items;

	return result;
}

- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)childIndex
{
	NSDragOperation result = NSDragOperationNone;

	// Prevent the top level or the quick connect item from being a target
	if (!item || item == quickConnectItem) return result;

	// Prevent dropping favorites on other favorites (non-groups)
	if ((childIndex == NSOutlineViewDropOnItemIndex) && (![item isGroup])) return result;

	// Ensure that none of the dragged nodes are being dragged into children of themselves; if they are,
	// prevent the drag.
	id itemToCheck = item;

	do {
		if ([draggedNodes containsObject:itemToCheck]) {
			return result;
		}
	}
	while ((itemToCheck = [itemToCheck parentNode]));

	if ([info draggingSource] == outlineView) {
		[outlineView setDropItem:item dropChildIndex:childIndex];

		result = NSDragOperationMove;
	}

	return result;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)childIndex
{
	BOOL acceptedDrop = NO;

	if ((!item) || ([info draggingSource] != outlineView)) return acceptedDrop;

	SPTreeNode *node = item ? item : [[[[favoritesRoot childNodes] objectAtIndex:0] childNodes] objectAtIndex:0];

	// Cache the selected nodes for selection restoration afterwards
	NSArray *preDragSelection = [self selectedFavoriteNodes];

	// Disable all automatic sorting
	currentSortItem = -1;
	reverseFavoritesSort = NO;

	[prefs setInteger:currentSortItem forKey:SPFavoritesSortedBy];
	[prefs setBool:NO forKey:SPFavoritesSortedInReverse];

	// Uncheck sort by menu items
	for (NSMenuItem *menuItem in [[favoritesSortByMenuItem submenu] itemArray])
	{
		[menuItem setState:NSOffState];
	}

	if (![draggedNodes count]) return acceptedDrop;

	if ([node isGroup]) {
		if (childIndex == NSOutlineViewDropOnItemIndex) {
			childIndex = 0;
		}
		[outlineView expandItem:node];
	}
	else {
		if (childIndex == NSOutlineViewDropOnItemIndex) {
			childIndex = 0;
		}
	}

	if (![[node representedObject] nodeName]) {
		node = [[favoritesRoot childNodes] objectAtIndex:0];
	}

	NSMutableArray *childNodeArray = [node mutableChildNodes];

	for (SPTreeNode *treeNode in draggedNodes)
	{
		// Remove the node from its old location
		NSInteger oldIndex = [childNodeArray indexOfObject:treeNode];
		NSInteger newIndex = childIndex;

		if (oldIndex != NSNotFound) {

			[childNodeArray removeObjectAtIndex:oldIndex];

			if (childIndex > oldIndex) {
				newIndex--;
			}
		}
		else {
			[[[treeNode parentNode] mutableChildNodes] removeObject:treeNode];
		}

		[childNodeArray insertObject:treeNode atIndex:newIndex];

		newIndex++;
	}

	[favoritesController saveFavorites];

	[self _reloadFavoritesViewData];

	[[NSNotificationCenter defaultCenter] postNotificationName:SPConnectionFavoritesChangedNotification object:self];

	[[[SPAppDelegate preferenceController] generalPreferencePane] updateDefaultFavoritePopup];

	// Update the selection to account for rearranged faourites
	NSMutableIndexSet *restoredSelection = [NSMutableIndexSet indexSet];

	for (SPTreeNode *eachNode in preDragSelection)
	{
		[restoredSelection addIndex:[favoritesOutlineView rowForItem:eachNode]];
	}

	[favoritesOutlineView selectRowIndexes:restoredSelection byExtendingSelection:NO];

	acceptedDrop = YES;

	return acceptedDrop;
}


#pragma mark -
#pragma mark Textfield delegate methods


/**
 * React to control text changes in the connection interface
 */
- (void)controlTextDidChange:(NSNotification *)notification
{
	id field = [notification object];

	// Ignore changes in the outline view edit fields
	if ([field isKindOfClass:[NSOutlineView class]]) {
		return;
	}

	// If a 'name' field was edited, and is now of zero length, trigger a replacement
	// with a standard suggestion
	if (((field == standardNameField) || (field == socketNameField) || (field == sshNameField)) && [self selectedFavoriteNode]) {
		if (![[self _stripInvalidCharactersFromString:[field stringValue]] length]) {
			[self controlTextDidEndEditing:notification];
		}
	}

	[self _startEditingConnection];

	if (favoriteNameFieldWasAutogenerated && (field != standardNameField && field != socketNameField && field != sshNameField)) {
		[self setName:[self _generateNameForConnection]];
	}

	if (field == standardSQLHostField || field == standardUserField || field == sshSQLHostField || field == sshUserField) {
		standardPasswordField.stringValue = @"";
		sshPasswordField.stringValue = @"";
	}
}

/**
 * React to the end of control text changes in the connection interface.
 */
- (void)controlTextDidEndEditing:(NSNotification *)notification
{
	id field = [notification object];

	// Handle updates to the 'name' field of the selected favourite.  The favourite name should
	// have leading or trailing spaces removed at the end of editing, and if it's left empty,
	// should have a default name set.
	if (((field == standardNameField) || (field == socketNameField) || (field == sshNameField)) && [self selectedFavoriteNode]) {

		NSString *favoriteName = [self _stripInvalidCharactersFromString:[field stringValue]];

		if (![favoriteName length]) {
			favoriteName = [self _generateNameForConnection];

			if (favoriteName) {
				[self setName:favoriteName];
			}

			// Enable user@host update in reaction to other UI changes
			favoriteNameFieldWasAutogenerated = YES;
		}
		else if (![[field stringValue] isEqualToString:[self _generateNameForConnection]]) {
			favoriteNameFieldWasAutogenerated = NO;
			[self setName:favoriteName];
		}
	}

	// When a host field finishes editing, ensure that it hasn't been set to "localhost" to
	// ensure that socket connections don't inadvertently occur.
	if (field == standardSQLHostField || field == sshSQLHostField) {
		[self _checkHost];
	}
}


#pragma mark -
#pragma mark Tab bar delegate methods


/**
 * 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
{
	NSInteger selectedTabView = [tabView indexOfTabViewItem:tabViewItem];
	
	
	if (selectedTabView == previousType) return;
	
	[self _startEditingConnection];
	
	[self resizeTabViewToConnectionType:selectedTabView animating:YES];
	
	// Update the host as appropriate
	if ((selectedTabView != SPSocketConnection) && [[self host] isEqualToString:@"localhost"]) {
		[self setHost:@""];
	}
	
	if (selectedTabView == SPSocketConnection) {
		
		// check we don't already have this window open
		BOOL correspondingWindowFound = NO;
		for(id win in [NSApp windows]) {
			if([[win delegate] isKindOfClass:[SPBundleHTMLOutputController class]]) {
				if([[[win delegate] windowUUID] isEqualToString:socketHelpWindowUUID]) {
					correspondingWindowFound = YES;
					SPLog(@"correspondingWindowFound: %hhd", correspondingWindowFound);
					break;
				}
			}
		}
		
		if(correspondingWindowFound == NO){
			if([prefs boolForKey:SPConnectionShownSocketHelp] == NO){
				SPLog(@"SPSocketConnection chosen, no current window open, and not show before");

				NSError *error = nil;
				
				// show socket help
				self.socketHelpWindowUUID = [NSString stringWithNewUUID];
				SPBundleHTMLOutputController *bundleController = [[SPBundleHTMLOutputController alloc] init];
				[bundleController setWindowUUID:socketHelpWindowUUID];
				
				NSDictionary *tmpDic2 = @{@"x" : @225, @"y" : @536, @"w" : @768, @"h" : @425};
				NSDictionary *tmpDict = @{SPConnectionShownSocketHelp : @YES, @"frame" : tmpDic2};
				
				if ([self connected]) {
					SPLog(@"Connected, loading remote URL");
					[bundleController displayURLString:SPDocsSocketConnection withOptions:tmpDict];
				}
				else{
					SPLog(@"Not connected, loading local file");
					NSString *path = [[NSBundle mainBundle] pathForResource:@"local-connection" ofType:@"html"];
					SPLog(@"path: %@", path);
					
					NSString *html = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
					
					if(error == nil){
						// slightly larger
						NSMutableDictionary *mutDict = [tmpDict mutableCopy];
						mutDict[@"frame"] = @{@"x" : @225, @"y" : @536, @"w" : @768, @"h" : @600};
						
						[bundleController displayHTMLContent:html withOptions:mutDict];
					}
				}
				if(error == nil){
					[SPAppDelegate addHTMLOutputController:bundleController];
				}
				// set straight away, or wait for them to close the window?
				//[prefs setBool:YES forKey:SPConnectionShownSocketHelp];
			}
		}
	}
	
	
	previousType = selectedTabView;
	
	[self _favoriteTypeDidChange];
}


#pragma mark -
#pragma mark Color Selector delegate

- (void)colorSelectorDidChange:(SPColorSelectorView *)sel
{
	[self _startEditingConnection];
}

#pragma mark -
#pragma mark Scroll view notifications


/**
 * As the scrollview resizes, keep the details centered within it if
 * the detail frame is larger than the scrollview size; otherwise, pin
 * the detail frame to the top of the scrollview.
 */
- (void)scrollViewFrameChanged:(NSNotification *)aNotification
{
	NSRect scrollViewFrame = [connectionDetailsScrollView frame];
	NSRect scrollDocumentFrame = [[connectionDetailsScrollView documentView] frame];
	NSRect connectionDetailsFrame = [connectionResizeContainer frame];

	// Scroll view is smaller than contents - keep positioned at top.
	if (scrollViewFrame.size.height < connectionDetailsFrame.size.height + 10) {
		if (connectionDetailsFrame.origin.y != 0) {
			connectionDetailsFrame.origin.y = 0;
			[connectionResizeContainer setFrame:connectionDetailsFrame];
			scrollDocumentFrame.size.height = connectionDetailsFrame.size.height + 10;
			[[connectionDetailsScrollView documentView] setFrame:scrollDocumentFrame];
		}
	}
	// Otherwise, center
	else {
		connectionDetailsFrame.origin.y = (scrollViewFrame.size.height - connectionDetailsFrame.size.height)/3;
		// the division may lead to values that are not valid for the current screen size (e.g. non-integer values on a
		// @1x non-retina screen). The OS works something out when not using layer-backed views, but in the latter
		// case the result will look like garbage if we don't fix this.
		connectionDetailsFrame = [connectionDetailsScrollView backingAlignedRect:connectionDetailsFrame options:NSAlignAllEdgesNearest];
		[connectionResizeContainer setFrame:connectionDetailsFrame];
		scrollDocumentFrame.size.height = scrollViewFrame.size.height;
		[[connectionDetailsScrollView documentView] setFrame:scrollDocumentFrame];
	}
}


#pragma mark -
#pragma mark Menu Validation


/**
 * Menu item validation.
 */
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
	SEL action = [menuItem action];

	SPTreeNode *node = [self selectedFavoriteNode];
	NSInteger selectedRows = [favoritesOutlineView numberOfSelectedRows];

	if ((action == @selector(sortFavorites:)) || (action == @selector(reverseSortFavorites:))) {

		if ([[favoritesRoot allChildLeafs] count] < 2) return NO;

		// Loop all the items in the sort by menu only checking the currently selected one
		for (NSMenuItem *item in [[menuItem menu] itemArray])
		{
			[item setState:([[menuItem menu] indexOfItem:item] == currentSortItem)];
		}

		// Check or uncheck the reverse sort item
		if (action == @selector(reverseSortFavorites:)) {
			[menuItem setState:reverseFavoritesSort];
		}

		return YES;
	}

	// import does not depend on a selection
	if(action == @selector(importFavorites:)) return YES;

	if (node == quickConnectItem) return NO;

	// Remove/rename the selected node
	if (action == @selector(removeNode:) || action == @selector(renameNode:)) {
		return selectedRows == 1;
	}

	// Duplicate and make the selected favorite the default
	if (action == @selector(duplicateFavorite:)) {
		return ((selectedRows == 1) && (![node isGroup]));
	}

	// Make selected favorite the default
	if (action == @selector(makeSelectedFavoriteDefault:)) {
		NSInteger favoriteID = [[[self selectedFavorite] objectForKey:SPFavoriteIDKey] integerValue];

		return ((selectedRows == 1) && (![node isGroup]) && (favoriteID != [prefs integerForKey:SPDefaultFavorite]));
	}

	// Favorites export
	if (action == @selector(exportFavorites:)) {

		if ([[favoritesRoot allChildLeafs] count] == 0 || selectedRows == 0) {
			return NO;
		}
		else if (selectedRows > 1) {
			[menuItem setTitle:NSLocalizedString(@"Export Selected...", @"export selected favorites menu item")];
		}
	}

	return YES;
}


#pragma mark -
#pragma mark Favorites import/export delegate methods


/**
 * Called by the favorites importer when the imported data is available.
 */
- (void)favoritesImportData:(NSArray *)data
{
	SPTreeNode *newNode;
	NSMutableArray *importedNodes = [NSMutableArray array];
	NSMutableIndexSet *importedIndexSet = [NSMutableIndexSet indexSet];

	// Add each of the imported favorites to the root node
	for (NSMutableDictionary *favorite in data)
	{
		newNode = [favoritesController addFavoriteNodeWithData:favorite asChildOfNode:nil];
		[importedNodes addObject:newNode];
	}

	if (currentSortItem > SPFavoritesSortUnsorted) {
		[self _sortFavorites];
	}

	[self _reloadFavoritesViewData];

	// Select the new nodes and scroll into view
	for (SPTreeNode *eachNode in importedNodes)
	{
		[importedIndexSet addIndex:[favoritesOutlineView rowForItem:eachNode]];
	}

	[favoritesOutlineView selectRowIndexes:importedIndexSet byExtendingSelection:NO];

	[self _scrollToSelectedNode];
}

/**
 * Called by the favorites importer when the import completes.
 */
- (void)favoritesImportCompletedWithError:(NSError *)error
{
	if (error) {
		
		NSAlert *alert = [[NSAlert alloc] init];
		
		// jamesstout notes
		// Alerts should be created with the -init method and setting properties. - NSAlert.h L132
		alert.messageText = NSLocalizedString(@"Favorites import error", @"favorites import error message");
		alert.informativeText = [NSString stringWithFormat:NSLocalizedString(@"The following error occurred during the import process:\n\n%@", @"favorites import error informative message"), [error localizedDescription]];
		[alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")];

		// jamesstout notes
		// API_DEPRECATED("Use -beginSheetModalForWindow:completionHandler: instead" - - NSAlert.h L136
		[alert beginSheetModalForWindow:[dbDocument parentWindow] completionHandler:nil];
	}
}


#pragma mark -
#pragma mark Private API


/**
 * Sets the expanded state of the node from the supplied outline view notification.
 *
 * @param expanded     The state of the node
 * @param notification The notification genrated from the state change
 */
- (void)_setNodeIsExpanded:(BOOL)expanded fromNotification:(NSNotification *)notification
{
	SPGroupNode *node = [[[notification userInfo] valueForKey:@"NSObject"] representedObject];

	[node setNodeIsExpanded:expanded];
}


#pragma mark - SPConnectionControllerInitializer

/**
 * Initialise the connection controller, linking it to the parent document and setting up the parent window.
 */
- (id)initWithDocument:(SPDatabaseDocument *)document
{
	if ((self = [super init])) {

		// Weak reference
		dbDocument = document;

		databaseConnectionSuperview = [dbDocument databaseView];
		databaseConnectionView = dbDocument->contentViewSplitter;

		// Keychain references
		connectionKeychainItemName = nil;
		connectionKeychainItemAccount = nil;
		connectionSSHKeychainItemName = nil;
		connectionSSHKeychainItemAccount = nil;

		initComplete = NO;
		isEditingItemName = NO;
		isConnecting = NO;
		isTestingConnection = NO;
		sshTunnel = nil;
		mySQLConnection = nil;
		cancellingConnection = NO;
		favoriteNameFieldWasAutogenerated = NO;
		allowSplitViewResizing = NO;

		[self loadNib];

		NSArray *colorList = [[SPFavoriteColorSupport sharedInstance] userColorList];
		[sshColorField setColorList:colorList];
		[sshColorField      bind:@"selectedTag" toObject:self withKeyPath:@"colorIndex" options:nil];
		[standardColorField setColorList:colorList];
		[standardColorField bind:@"selectedTag" toObject:self withKeyPath:@"colorIndex" options:nil];
		[socketColorField setColorList:colorList];
		[socketColorField   bind:@"selectedTag" toObject:self withKeyPath:@"colorIndex" options:nil];

		// An instance of NSMenuItem can not be assigned to more than one menu so we have to clone items.
		// Cannot bulk set items on macOS < 10.14, must removeAllItems and addItem https://github.com/Sequel-Ace/Sequel-Ace/issues/403
		[standardTimeZoneField.menu removeAllItems];
		[sshTimeZoneField.menu removeAllItems];
		[socketTimeZoneField.menu removeAllItems];
		for (NSMenuItem *menuItem in [self generateTimeZoneMenuItems]) {
			[standardTimeZoneField.menu addItem:[menuItem copy]];
			[sshTimeZoneField.menu addItem:[menuItem copy]];
			[socketTimeZoneField.menu addItem:[menuItem copy]];
		}

		[self registerForNotifications];

		// Hide the main view and position and display the connection view
		[databaseConnectionView setHidden:YES];
		[connectionView setFrame:[databaseConnectionView frame]];
		[databaseConnectionSuperview addSubview:connectionView];

		// Set up the splitview
		[connectionSplitView setMinSize:150.f ofSubviewAtIndex:0];
		[connectionSplitView setMinSize:445.f ofSubviewAtIndex:1];

		// Generic folder image for use in the outline view's groups
		folderImage = [[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGenericFolderIcon)] retain];
		[folderImage setSize:NSMakeSize(16, 16)];

		// Set up a keychain instance and preferences reference, and create the initial favorites list
		keychain = [[SPKeychain alloc] init];
		prefs = [[NSUserDefaults standardUserDefaults] retain];
		
		bookmarks = [[NSMutableArray alloc] init];
		resolvedBookmarks = [[NSMutableArray alloc] init];

		id o;
		if((o = [prefs objectForKey:SPSecureBookmarks])){
			[bookmarks setArray:o];
		}
		
		// we need to re-request access to places we've been before..
		[self reRequestSecureAccess];
		
		// add an observer to get re-read the bookmarks when they change
		[prefs addObserver:self
				forKeyPath:SPSecureBookmarks
				options:NSKeyValueObservingOptionNew
				   context:NULL];

		// Create a reference to the favorites controller, forcing the data to be loaded from disk
		// and the tree to be constructed.
		favoritesController = [SPFavoritesController sharedFavoritesController];

		// Tree references
		favoritesRoot = [favoritesController favoritesTree];
		currentFavorite = nil;

		// Create the "Quick Connect" placeholder group
		quickConnectItem = [[SPTreeNode treeNodeWithRepresentedObject:[SPGroupNode groupNodeWithName:[NSLocalizedString(@"Quick Connect", @"Quick connect item label") uppercaseString]]] retain];
		[quickConnectItem setIsGroup:YES];

		// Create a NSOutlineView cell for the Quick Connect group
		quickConnectCell = [[SPFavoriteTextFieldCell alloc] init];
		[quickConnectCell setDrawsDividerUnderCell:YES];
		[quickConnectCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];

		// Update the UI
		[self _reloadFavoritesViewData];
		[self setUpFavoritesOutlineView];
		[self _restoreOutlineViewStateNode:favoritesRoot];

		// Set up the selected favourite, and scroll after a small delay to fix animation delay on Lion
		[self setUpSelectedConnectionFavorite];
		if ([favoritesOutlineView selectedRow] != -1) {
			[self performSelector:@selector(_scrollToSelectedNode) withObject:nil afterDelay:0.0];
		}

		// Set sort items
		currentSortItem = (SPFavoritesSortItem)[prefs integerForKey:SPFavoritesSortedBy];
		reverseFavoritesSort = [prefs boolForKey:SPFavoritesSortedInReverse];

		initComplete = YES;
	}

	return self;
}

// TODO: this is called once per connection screen - but the timezones don't change right? Should be static/class method?
- (NSArray<NSMenuItem *> *)generateTimeZoneMenuItems
{
	NSArray<NSString *> *timeZoneIdentifiers = [NSTimeZone.knownTimeZoneNames sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
	// timeZoneIdentifiers.count + (fixed entries and separators) + (separators for time zone prefixes)
	NSMutableArray<NSMenuItem *> *timeZoneMenuItems = [NSMutableArray arrayWithCapacity:timeZoneIdentifiers.count + 4 + 11];

	// Use Server Time Zone
	NSMenuItem *useServerTimeZoneMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Use Server Time Zone", @"Leave the server time zone in place when connecting") action:nil keyEquivalent:@""];
	useServerTimeZoneMenuItem.tag = SPUseServerTimeZoneTag;
	[timeZoneMenuItems addObject:useServerTimeZoneMenuItem];

	[timeZoneMenuItems addObject:NSMenuItem.separatorItem];

	// Use System Time Zone
	NSMenuItem *useSystemTimeZoneMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Use System Time Zone", @"Set the time zone currently used by the user when connecting") action:nil keyEquivalent:@""];
	useSystemTimeZoneMenuItem.tag = SPUseSystemTimeZoneTag;
	[timeZoneMenuItems addObject:useSystemTimeZoneMenuItem];

	// Add all identifier entries and insert a separator every time the prefix changes
	NSString *previousPrefix = @"";
	for (NSString *tzIdentifier in timeZoneIdentifiers) {
		NSString *currentPrefix = [tzIdentifier componentsSeparatedByString:@"/"].firstObject;
		if (![currentPrefix isEqualToString:previousPrefix]) {
			previousPrefix = currentPrefix;
			[timeZoneMenuItems addObject:NSMenuItem.separatorItem];
		}
		NSMenuItem *entry = [[NSMenuItem alloc] initWithTitle:tzIdentifier action:nil keyEquivalent:@""];
		[timeZoneMenuItems addObject:entry]; // adding to an array retains the object
		[entry release]; // so we can release here. otherwise we leak.
	}

	return timeZoneMenuItems;
}

-(void)reRequestSecureAccess{
	
	SPLog(@"reRequestSecureAccess to saved bookmarks");

	[self.bookmarks enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
		
		[dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSData *obj, BOOL *stop2) {
			
			NSError *error = nil;
			
			NSURL *tmpURL = [NSURL URLByResolvingBookmarkData:obj
													  options:NSURLBookmarkResolutionWithSecurityScope
												relativeToURL:nil
										  bookmarkDataIsStale:nil
														error:&error];
			
			if(!error){
				[tmpURL startAccessingSecurityScopedResource];
				[resolvedBookmarks addObject:tmpURL];
			}
			else{
				SPLog(@"Problem resolving bookmark - %@ : %@",key, [error localizedDescription]);
			}
		}];
	}];
}

/**
 * Loads the connection controllers UI nib.
 */
- (void)loadNib
{

	// Load the connection nib, keeping references to the top-level objects for later release
	nibObjectsToRelease = [[NSMutableArray alloc] init];

	NSArray *connectionViewTopLevelObjects = nil;
	NSNib *nibLoader = [[NSNib alloc] initWithNibNamed:SPConnectionViewNibName bundle:[NSBundle mainBundle]];

	[nibLoader instantiateWithOwner:self topLevelObjects:&connectionViewTopLevelObjects];
	[nibObjectsToRelease addObjectsFromArray:connectionViewTopLevelObjects];
	[nibLoader release];

}

/**
 * Registers for various notifications.
 */
- (void)registerForNotifications
{
	[[NSNotificationCenter defaultCenter] addObserver:self
	                                         selector:@selector(_documentWillClose:)
	                                             name:SPDocumentWillCloseNotification
	                                           object:dbDocument];

	[[NSNotificationCenter defaultCenter] addObserver:self
	                                         selector:@selector(scrollViewFrameChanged:)
	                                             name:NSViewFrameDidChangeNotification
	                                           object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self
	                                         selector:@selector(_processFavoritesDataChange:)
	                                             name:SPConnectionFavoritesChangedNotification
	                                           object:nil];

	// Registered to be notified of changes to connection information
	[self addObserver:self
	       forKeyPath:SPFavoriteTypeKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteNameKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteHostKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteUserKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteColorIndexKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteDatabaseKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSocketKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoritePortKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];
	
	[self addObserver:self
		   forKeyPath:SPFavoriteAllowDataLocalInfileKey
			  options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
			  context:NULL];
	
	[self addObserver:self
		   forKeyPath:SPFavoriteEnableClearTextPluginKey
			  options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
			  context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteUseSSLKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSHHostKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSHUserKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSHPortKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSHKeyLocationEnabledKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSHKeyLocationKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSLKeyFileLocationEnabledKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSLKeyFileLocationKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSLCertificateFileLocationEnabledKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSLCertificateFileLocationKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSLCACertFileLocationEnabledKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];

	[self addObserver:self
	       forKeyPath:SPFavoriteSSLCACertFileLocationKey
	          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
	          context:NULL];
}

/**
 * Performs any set up necessary for the favorities outline view.
 */
- (void)setUpFavoritesOutlineView
{
	// Register double click action for the favorites outline view (double click favorite to connect)
	[favoritesOutlineView setTarget:self];
	[favoritesOutlineView setDoubleAction:@selector(nodeDoubleClicked:)];

	// Register drag types for the favorites outline view
	[favoritesOutlineView registerForDraggedTypes:@[SPFavoritesPasteboardDragType]];
	[favoritesOutlineView setDraggingSourceOperationMask:NSDragOperationMove forLocal:YES];

	NSFont *tableFont = [NSUserDefaults getFont];
	[favoritesOutlineView setRowHeight:2.0f+NSSizeToCGSize([@"{ǞṶḹÜ∑zgyf" sizeWithAttributes:@{NSFontAttributeName : tableFont}]).height];

	[favoritesOutlineView setFont:tableFont];
	for (NSTableColumn *col in [favoritesOutlineView tableColumns]) {
		[[col dataCell] setFont:tableFont];
	}
}

/**
 * Sets up the selected connection favorite according to the user's preferences.
 */
- (void)setUpSelectedConnectionFavorite
{
	SPTreeNode *favorite = [self _favoriteNodeForFavoriteID:[prefs integerForKey:[prefs boolForKey:SPSelectLastFavoriteUsed] ? SPLastFavoriteID : SPDefaultFavorite]];

	if (favorite) {

		if (favorite == quickConnectItem) {
			[self _selectNode:favorite];
		}
		else {
			NSNumber *typeNumber = [[[favorite representedObject] nodeFavorite] objectForKey:SPFavoriteTypeKey];
			previousType = typeNumber ? [typeNumber integerValue] : SPTCPIPConnection;

			[self _selectNode:favorite];
			[self resizeTabViewToConnectionType:[[[[favorite representedObject] nodeFavorite] objectForKey:SPFavoriteTypeKey] integerValue] animating:NO];
		}

		[self _scrollToSelectedNode];
	}
	else {
		previousType = SPTCPIPConnection;

		[self resizeTabViewToConnectionType:SPTCPIPConnection animating:NO];
	}
}

#pragma mark -
#pragma mark Private API

/**
 * Responds to notifications that the favorites root has changed,
 * and updates the interface to match.
 */
- (void)_processFavoritesDataChange:(NSNotification *)aNotification
{
	// Check the supplied notification for the sender; if the sender
	// was this object, ignore it
	if ([aNotification object] == self) return;

	NSArray *selectedFavoriteNodes = [self selectedFavoriteNodes];

	[self _reloadFavoritesViewData];

	NSMutableIndexSet *selectionIndexes = [NSMutableIndexSet indexSet];

	for (SPTreeNode *eachNode in selectedFavoriteNodes)
	{
		NSInteger anIndex = [favoritesOutlineView rowForItem:eachNode];

		if (anIndex == -1) continue;

		[selectionIndexes addIndex:anIndex];
	}

	[favoritesOutlineView selectRowIndexes:selectionIndexes byExtendingSelection:NO];
}

/**
 * Restores the outline views group nodes expansion state.
 *
 * @param node The node to traverse
 */
- (void)_restoreOutlineViewStateNode:(SPTreeNode *)node
{
	if ([node isGroup]) {
		if ([[node representedObject] nodeIsExpanded]) {
			[favoritesOutlineView expandItem:node];
		}
		else {
			[favoritesOutlineView collapseItem:node];
		}

		for (SPTreeNode *childNode in [node childNodes])
		{
			if ([childNode isGroup]) {
				[self _restoreOutlineViewStateNode:childNode];
			}
		}
	}
}

#pragma mark - SPConnectionControllerDataSource


/**
 * Return the number of children for the specified item in the favourites tree.
 * Note that to support the "Quick Connect" entry, the returned count is amended
 * for the top level.
 */
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
	SPTreeNode *node = (item == nil ? favoritesRoot : (SPTreeNode *)item);

	// If at the root, return the count plus one for the "Quick Connect" entry
	if (!item) {
		return [[node childNodes] count] + 1;
	}

	return [[node childNodes] count];
}

/**
 * Return the branch at the specified index of a supplied tree level.
 * Note that to support the "Quick Connect" entry, children of the top level
 * have their offsets amended.
 */
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)childIndex ofItem:(id)item
{
	// For the top level of the tree, return the "Quick Connect" child for position zero;
	// amend all other positions to compensate for the faked position.
	if (!item) {
		if (childIndex == 0) {
			return quickConnectItem;
		}

		childIndex--;
	}

	SPTreeNode *node = (item == nil ? favoritesRoot : (SPTreeNode *)item);

	return NSArrayObjectAtIndex([node childNodes], childIndex);
}

- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
	SPTreeNode *node = (SPTreeNode *)item;

	return (![node isGroup]) ? [[[node representedObject] nodeFavorite] objectForKey:SPFavoriteNameKey] : [[node representedObject] nodeName];
}

- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
	NSString *newName = [object stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

	if ([newName length]) {

		// Get the node that was renamed
		SPTreeNode *node = [self selectedFavoriteNode];

		if (![node isGroup]) {

			// Updating the name triggers a KVO update
			[self setName:newName];
			[self _saveCurrentDetailsCreatingNewFavorite:NO validateDetails:NO];
		}
		else {
			[[node representedObject] setNodeName:newName];

			[favoritesController saveFavorites];

			[self _reloadFavoritesViewData];
		}
	}
}


#pragma mark -

- (void)dealloc
{
	[[NSNotificationCenter defaultCenter] removeObserver:self];
	[NSObject cancelPreviousPerformRequestsWithTarget:self];

	// Unregister observers
	[self removeObserver:self forKeyPath:SPFavoriteTypeKey];
	[self removeObserver:self forKeyPath:SPFavoriteNameKey];
	[self removeObserver:self forKeyPath:SPFavoriteHostKey];
	[self removeObserver:self forKeyPath:SPFavoriteUserKey];
	[self removeObserver:self forKeyPath:SPFavoriteColorIndexKey];
	[self removeObserver:self forKeyPath:SPFavoriteDatabaseKey];
	[self removeObserver:self forKeyPath:SPFavoriteSocketKey];
	[self removeObserver:self forKeyPath:SPFavoritePortKey];
	[self removeObserver:self forKeyPath:SPFavoriteAllowDataLocalInfileKey];
	[self removeObserver:self forKeyPath:SPFavoriteEnableClearTextPluginKey];
	[self removeObserver:self forKeyPath:SPFavoriteUseSSLKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSHHostKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSHUserKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSHPortKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSHKeyLocationEnabledKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSHKeyLocationKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSLKeyFileLocationEnabledKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSLKeyFileLocationKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSLCertificateFileLocationEnabledKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSLCertificateFileLocationKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSLCACertFileLocationEnabledKey];
	[self removeObserver:self forKeyPath:SPFavoriteSSLCACertFileLocationKey];

	SPClear(keychain);
	SPClear(prefs);

	SPClear(folderImage);
	SPClear(quickConnectItem);
	SPClear(quickConnectCell);

	
	for(NSURL *url in resolvedBookmarks){
		[url stopAccessingSecurityScopedResource];
	}

	SPClear(nibObjectsToRelease);
	SPClear(bookmarks);
	SPClear(resolvedBookmarks);

	[self setConnectionKeychainID:nil];
	if (connectionKeychainItemName)       SPClear(connectionKeychainItemName);
	if (connectionKeychainItemAccount)    SPClear(connectionKeychainItemAccount);
	if (connectionSSHKeychainItemName)    SPClear(connectionSSHKeychainItemName);
	if (connectionSSHKeychainItemAccount) SPClear(connectionSSHKeychainItemAccount);

	if (currentFavorite) SPClear(currentFavorite);

	[super dealloc];
}

/**
 * Called by the favorites exporter when the export completes.
 */
- (void)favoritesExportCompletedWithError:(NSError *)error
{
	if (error) {
		NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Favorites export error", @"favorites export error message")
		                                 defaultButton:NSLocalizedString(@"OK", @"OK")
		                               alternateButton:nil
		                                   otherButton:nil
		                     informativeTextWithFormat:NSLocalizedString(@"The following error occurred during the export process:\n\n%@", @"favorites export error informative message"), [error localizedDescription]];

		[alert beginSheetModalForWindow:[dbDocument parentWindow]
		                  modalDelegate:self
		                 didEndSelector:NULL
		                    contextInfo:NULL];
	}
}

@end

