Merge pull request #64 in ITERATE/cyberduck from feature/windows/9074 to master
[cyberduck.git] / source / ch / cyberduck / ui / cocoa / ConnectionController.java
blobd97aca92737b309c8893756d2818339c0f5c3abb
1 package ch.cyberduck.ui.cocoa;
3 /*
4 * Copyright (c) 2005 David Kocher. All rights reserved.
5 * http://cyberduck.ch/
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * Bug fixes, suggestions and comments should be sent to:
18 * dkocher@cyberduck.ch
21 import ch.cyberduck.binding.ProxyController;
22 import ch.cyberduck.binding.application.NSButton;
23 import ch.cyberduck.binding.application.NSCell;
24 import ch.cyberduck.binding.application.NSColor;
25 import ch.cyberduck.binding.application.NSComboBox;
26 import ch.cyberduck.binding.application.NSControl;
27 import ch.cyberduck.binding.application.NSImage;
28 import ch.cyberduck.binding.application.NSMenuItem;
29 import ch.cyberduck.binding.application.NSOpenPanel;
30 import ch.cyberduck.binding.application.NSPanel;
31 import ch.cyberduck.binding.application.NSPopUpButton;
32 import ch.cyberduck.binding.application.NSTextField;
33 import ch.cyberduck.binding.application.NSWindow;
34 import ch.cyberduck.binding.foundation.NSArray;
35 import ch.cyberduck.binding.foundation.NSAttributedString;
36 import ch.cyberduck.binding.foundation.NSNotification;
37 import ch.cyberduck.binding.foundation.NSNotificationCenter;
38 import ch.cyberduck.binding.foundation.NSObject;
39 import ch.cyberduck.binding.foundation.NSString;
40 import ch.cyberduck.core.*;
41 import ch.cyberduck.core.diagnostics.ReachabilityFactory;
42 import ch.cyberduck.core.exception.BackgroundException;
43 import ch.cyberduck.core.ftp.FTPConnectMode;
44 import ch.cyberduck.core.preferences.Preferences;
45 import ch.cyberduck.core.preferences.PreferencesFactory;
46 import ch.cyberduck.core.resources.IconCacheFactory;
47 import ch.cyberduck.core.threading.AbstractBackgroundAction;
49 import org.apache.commons.lang3.StringUtils;
50 import org.apache.commons.lang3.math.NumberUtils;
51 import org.apache.log4j.Logger;
52 import org.rococoa.Foundation;
53 import org.rococoa.ID;
54 import org.rococoa.cocoa.foundation.NSInteger;
55 import org.rococoa.cocoa.foundation.NSSize;
57 /**
58 * @version $Id$
60 public class ConnectionController extends SheetController {
61 private static Logger log = Logger.getLogger(ConnectionController.class);
63 private final HostPasswordStore keychain
64 = PasswordStoreFactory.get();
66 private final NSNotificationCenter notificationCenter
67 = NSNotificationCenter.defaultCenter();
69 private Preferences preferences
70 = PreferencesFactory.get();
72 @Override
73 public void invalidate() {
74 hostField.setDelegate(null);
75 hostField.setDataSource(null);
76 notificationCenter.removeObserver(this.id());
77 super.invalidate();
80 @Override
81 public boolean isSingleton() {
82 return true;
85 public ConnectionController(final WindowController parent) {
86 super(parent);
87 this.loadBundle();
90 @Override
91 protected String getBundleName() {
92 return "Connection";
95 @Override
96 public void awakeFromNib() {
97 this.protocolSelectionDidChange(null);
98 this.setState(toggleOptionsButton, preferences.getBoolean("connection.toggle.options"));
99 super.awakeFromNib();
102 @Override
103 protected void beginSheet(final NSWindow window) {
104 // Reset password input
105 passField.setStringValue(StringUtils.EMPTY);
106 super.beginSheet(window);
109 @Override
110 public void setWindow(final NSWindow window) {
111 window.setContentMinSize(window.frame().size);
112 window.setContentMaxSize(new NSSize(600, window.frame().size.height.doubleValue()));
113 super.setWindow(window);
116 @Outlet
117 private NSPopUpButton protocolPopup;
119 public void setProtocolPopup(NSPopUpButton protocolPopup) {
120 this.protocolPopup = protocolPopup;
121 this.protocolPopup.setEnabled(true);
122 this.protocolPopup.setTarget(this.id());
123 this.protocolPopup.setAction(Foundation.selector("protocolSelectionDidChange:"));
124 this.protocolPopup.removeAllItems();
125 for(Protocol protocol : ProtocolFactory.getEnabledProtocols()) {
126 final String title = protocol.getDescription();
127 this.protocolPopup.addItemWithTitle(title);
128 final NSMenuItem item = this.protocolPopup.itemWithTitle(title);
129 item.setRepresentedObject(String.valueOf(protocol.hashCode()));
130 item.setImage(IconCacheFactory.<NSImage>get().iconNamed(protocol.icon(), 16));
132 final Protocol defaultProtocol
133 = ProtocolFactory.forName(preferences.getProperty("connection.protocol.default"));
134 this.protocolPopup.selectItemAtIndex(
135 protocolPopup.indexOfItemWithRepresentedObject(String.valueOf(defaultProtocol.hashCode()))
139 public void protocolSelectionDidChange(final NSPopUpButton sender) {
140 log.debug("protocolSelectionDidChange:" + sender);
141 final Protocol protocol = ProtocolFactory.forName(protocolPopup.selectedItem().representedObject());
142 portField.setIntValue(protocol.getDefaultPort());
143 portField.setEnabled(protocol.isPortConfigurable());
144 if(!protocol.isHostnameConfigurable()) {
145 hostField.setStringValue(protocol.getDefaultHostname());
146 hostField.setEnabled(false);
147 pathField.setEnabled(true);
149 else {
150 if(!hostField.isEnabled()) {
151 // Was previously configured with a static configuration
152 hostField.setStringValue(protocol.getDefaultHostname());
154 if(!pathField.isEnabled()) {
155 // Was previously configured with a static configuration
156 pathField.setStringValue(StringUtils.EMPTY);
158 if(StringUtils.isNotBlank(protocol.getDefaultHostname())) {
159 // Prefill with default hostname
160 hostField.setStringValue(protocol.getDefaultHostname());
162 usernameField.setEnabled(true);
163 hostField.setEnabled(true);
164 pathField.setEnabled(true);
165 usernameField.cell().setPlaceholderString(StringUtils.EMPTY);
166 passField.cell().setPlaceholderString(StringUtils.EMPTY);
168 hostField.cell().setPlaceholderString(protocol.getDefaultHostname());
169 usernameField.cell().setPlaceholderString(protocol.getUsernamePlaceholder());
170 passField.cell().setPlaceholderString(protocol.getPasswordPlaceholder());
171 connectmodePopup.setEnabled(protocol.getType() == Protocol.Type.ftp);
172 if(!protocol.isEncodingConfigurable()) {
173 encodingPopup.selectItemWithTitle(DEFAULT);
175 encodingPopup.setEnabled(protocol.isEncodingConfigurable());
176 anonymousCheckbox.setEnabled(protocol.isAnonymousConfigurable());
178 this.updateIdentity();
179 this.updateURLLabel();
180 this.readPasswordFromKeychain();
181 this.reachable();
185 * Update Private Key selection
187 private void updateIdentity() {
188 final Protocol protocol = ProtocolFactory.forName(protocolPopup.selectedItem().representedObject());
189 pkCheckbox.setEnabled(protocol.getType() == Protocol.Type.ssh);
190 if(StringUtils.isNotEmpty(hostField.stringValue())) {
191 final Credentials credentials = CredentialsConfiguratorFactory.get(protocol).configure(new Host(hostField.stringValue()));
192 if(credentials.isPublicKeyAuthentication()) {
193 // No previously manually selected key
194 pkLabel.setStringValue(credentials.getIdentity().getAbbreviatedPath());
195 pkCheckbox.setState(NSCell.NSOnState);
197 else {
198 pkCheckbox.setState(NSCell.NSOffState);
199 pkLabel.setStringValue(LocaleFactory.localizedString("No private key selected"));
201 if(StringUtils.isNotBlank(credentials.getUsername())) {
202 usernameField.setStringValue(credentials.getUsername());
207 private NSComboBox hostField;
208 private ProxyController hostFieldModel = new HostFieldModel();
210 public void setHostPopup(NSComboBox hostPopup) {
211 this.hostField = hostPopup;
212 this.hostField.setTarget(this.id());
213 this.hostField.setAction(Foundation.selector("hostPopupSelectionDidChange:"));
214 this.hostField.setUsesDataSource(true);
215 this.hostField.setDataSource(hostFieldModel.id());
216 notificationCenter.addObserver(this.id(),
217 Foundation.selector("hostFieldTextDidChange:"),
218 NSControl.NSControlTextDidChangeNotification,
219 this.hostField);
222 private static class HostFieldModel extends ProxyController implements NSComboBox.DataSource {
223 @Override
224 public NSInteger numberOfItemsInComboBox(final NSComboBox sender) {
225 return new NSInteger(BookmarkCollection.defaultCollection().size());
228 @Override
229 public NSObject comboBox_objectValueForItemAtIndex(final NSComboBox sender, final NSInteger row) {
230 return NSString.stringWithString(
231 BookmarkNameProvider.toString(BookmarkCollection.defaultCollection().get(row.intValue()))
236 @Action
237 public void hostPopupSelectionDidChange(final NSControl sender) {
238 String input = sender.stringValue();
239 if(StringUtils.isBlank(input)) {
240 return;
242 input = input.trim();
243 // First look for equivalent bookmarks
244 for(Host h : BookmarkCollection.defaultCollection()) {
245 if(BookmarkNameProvider.toString(h).equals(input)) {
246 this.hostChanged(h);
247 this.updateURLLabel();
248 this.readPasswordFromKeychain();
249 this.reachable();
250 break;
255 public void hostFieldTextDidChange(final NSNotification sender) {
256 if(ProtocolFactory.isURL(hostField.stringValue())) {
257 this.hostChanged(HostParser.parse(hostField.stringValue()));
259 this.updateURLLabel();
260 this.readPasswordFromKeychain();
261 this.reachable();
264 private void hostChanged(final Host host) {
265 this.updateField(hostField, host.getHostname());
266 this.protocolPopup.selectItemAtIndex(
267 protocolPopup.indexOfItemWithRepresentedObject(String.valueOf(host.getProtocol().hashCode()))
269 this.updateField(portField, String.valueOf(host.getPort()));
270 this.updateField(usernameField, host.getCredentials().getUsername());
271 this.updateField(pathField, host.getDefaultPath());
272 anonymousCheckbox.setState(host.getCredentials().isAnonymousLogin() ? NSCell.NSOnState : NSCell.NSOffState);
273 this.anonymousCheckboxClicked(anonymousCheckbox);
274 this.updateIdentity();
278 * Run the connection reachability test in the background
280 private void reachable() {
281 final String hostname = hostField.stringValue();
282 if(StringUtils.isNotBlank(hostname)) {
283 this.background(new AbstractBackgroundAction<Boolean>() {
284 boolean reachable = false;
286 @Override
287 public Boolean run() throws BackgroundException {
288 if(!preferences.getBoolean("connection.hostname.check")) {
289 return reachable = true;
291 return reachable = ReachabilityFactory.get().isReachable(new Host(hostname));
294 @Override
295 public void cleanup() {
296 alertIcon.setEnabled(!reachable);
297 alertIcon.setImage(reachable ? null : IconCacheFactory.<NSImage>get().iconNamed("alert.tiff"));
301 else {
302 alertIcon.setImage(IconCacheFactory.<NSImage>get().iconNamed("alert.tiff"));
303 alertIcon.setEnabled(false);
307 @Outlet
308 private NSButton alertIcon;
310 public void setAlertIcon(NSButton alertIcon) {
311 this.alertIcon = alertIcon;
312 this.alertIcon.setTarget(this.id());
313 this.alertIcon.setAction(Foundation.selector("launchNetworkAssistant:"));
316 @Action
317 public void launchNetworkAssistant(final NSButton sender) {
318 ReachabilityFactory.get().diagnose(HostParser.parse(urlLabel.stringValue()));
321 @Outlet
322 private NSTextField pathField;
324 public void setPathField(NSTextField pathField) {
325 this.pathField = pathField;
326 notificationCenter.addObserver(this.id(),
327 Foundation.selector("pathInputDidEndEditing:"),
328 NSControl.NSControlTextDidEndEditingNotification,
329 this.pathField);
332 public void pathInputDidEndEditing(final NSNotification sender) {
333 this.updateURLLabel();
336 @Outlet
337 private NSTextField portField;
339 public void setPortField(NSTextField portField) {
340 this.portField = portField;
341 notificationCenter.addObserver(this.id(),
342 Foundation.selector("portFieldTextDidChange:"),
343 NSControl.NSControlTextDidChangeNotification,
344 this.portField);
347 public void portFieldTextDidChange(final NSNotification sender) {
348 if(StringUtils.isBlank(this.portField.stringValue())) {
349 final Protocol protocol = ProtocolFactory.forName(protocolPopup.selectedItem().representedObject());
350 this.portField.setIntValue(protocol.getDefaultPort());
352 this.updateURLLabel();
353 this.reachable();
356 @Outlet
357 private NSTextField usernameField;
359 public void setUsernameField(NSTextField usernameField) {
360 this.usernameField = usernameField;
361 this.usernameField.setStringValue(preferences.getProperty("connection.login.name"));
362 notificationCenter.addObserver(this.id(),
363 Foundation.selector("usernameFieldTextDidChange:"),
364 NSControl.NSControlTextDidChangeNotification,
365 this.usernameField);
366 notificationCenter.addObserver(this.id(),
367 Foundation.selector("usernameFieldTextDidEndEditing:"),
368 NSControl.NSControlTextDidEndEditingNotification,
369 this.usernameField);
372 public void usernameFieldTextDidChange(final NSNotification sender) {
373 this.updateURLLabel();
376 public void usernameFieldTextDidEndEditing(final NSNotification sender) {
377 this.readPasswordFromKeychain();
380 @Outlet
381 private NSTextField passField;
383 public void setPassField(NSTextField passField) {
384 this.passField = passField;
387 @Outlet
388 private NSTextField pkLabel;
390 public void setPkLabel(NSTextField pkLabel) {
391 this.pkLabel = pkLabel;
392 this.pkLabel.setStringValue(LocaleFactory.localizedString("No private key selected"));
393 this.pkLabel.setTextColor(NSColor.disabledControlTextColor());
396 @Outlet
397 private NSButton keychainCheckbox;
399 public void setKeychainCheckbox(NSButton keychainCheckbox) {
400 this.keychainCheckbox = keychainCheckbox;
401 this.keychainCheckbox.setState(preferences.getBoolean("connection.login.useKeychain")
402 && preferences.getBoolean("connection.login.addKeychain") ? NSCell.NSOnState : NSCell.NSOffState);
403 this.keychainCheckbox.setTarget(this.id());
404 this.keychainCheckbox.setAction(Foundation.selector("keychainCheckboxClicked:"));
407 public void keychainCheckboxClicked(final NSButton sender) {
408 final boolean enabled = sender.state() == NSCell.NSOnState;
409 preferences.setProperty("connection.login.addKeychain", enabled);
412 @Outlet
413 private NSButton anonymousCheckbox;
415 public void setAnonymousCheckbox(NSButton anonymousCheckbox) {
416 this.anonymousCheckbox = anonymousCheckbox;
417 this.anonymousCheckbox.setTarget(this.id());
418 this.anonymousCheckbox.setAction(Foundation.selector("anonymousCheckboxClicked:"));
419 this.anonymousCheckbox.setState(NSCell.NSOffState);
422 @Action
423 public void anonymousCheckboxClicked(final NSButton sender) {
424 if(sender.state() == NSCell.NSOnState) {
425 this.usernameField.setEnabled(false);
426 this.usernameField.setStringValue(preferences.getProperty("connection.login.anon.name"));
427 this.passField.setEnabled(false);
428 this.passField.setStringValue(StringUtils.EMPTY);
430 if(sender.state() == NSCell.NSOffState) {
431 this.usernameField.setEnabled(true);
432 this.usernameField.setStringValue(preferences.getProperty("connection.login.name"));
433 this.passField.setEnabled(true);
435 this.updateURLLabel();
438 @Outlet
439 private NSButton pkCheckbox;
441 public void setPkCheckbox(NSButton pkCheckbox) {
442 this.pkCheckbox = pkCheckbox;
443 this.pkCheckbox.setTarget(this.id());
444 this.pkCheckbox.setAction(Foundation.selector("pkCheckboxSelectionDidChange:"));
445 this.pkCheckbox.setState(NSCell.NSOffState);
448 private NSOpenPanel publicKeyPanel;
450 @Action
451 public void pkCheckboxSelectionDidChange(final NSButton sender) {
452 log.debug("pkCheckboxSelectionDidChange");
453 if(sender.state() == NSCell.NSOnState) {
454 publicKeyPanel = NSOpenPanel.openPanel();
455 publicKeyPanel.setCanChooseDirectories(false);
456 publicKeyPanel.setCanChooseFiles(true);
457 publicKeyPanel.setAllowsMultipleSelection(false);
458 publicKeyPanel.setMessage(LocaleFactory.localizedString("Select the private key in PEM or PuTTY format", "Credentials"));
459 publicKeyPanel.setPrompt(LocaleFactory.localizedString("Choose"));
460 publicKeyPanel.beginSheetForDirectory(LocalFactory.get("~/.ssh").getAbsolute(),
461 null, this.window(), this.id(),
462 Foundation.selector("pkSelectionPanelDidEnd:returnCode:contextInfo:"), null);
464 else {
465 passField.setEnabled(true);
466 pkCheckbox.setState(NSCell.NSOffState);
467 pkLabel.setStringValue(LocaleFactory.localizedString("No private key selected"));
468 pkLabel.setTextColor(NSColor.disabledControlTextColor());
472 public void pkSelectionPanelDidEnd_returnCode_contextInfo(NSOpenPanel window, int returncode, ID contextInfo) {
473 if(NSPanel.NSOKButton == returncode) {
474 final NSObject selected = window.filenames().lastObject();
475 if(selected != null) {
476 pkLabel.setAttributedStringValue(NSAttributedString.attributedStringWithAttributes(
477 LocalFactory.get(selected.toString()).getAbbreviatedPath(), TRUNCATE_MIDDLE_ATTRIBUTES));
478 pkLabel.setTextColor(NSColor.textColor());
480 passField.setEnabled(false);
482 if(NSPanel.NSCancelButton == returncode) {
483 passField.setEnabled(true);
484 pkCheckbox.setState(NSCell.NSOffState);
485 pkLabel.setStringValue(LocaleFactory.localizedString("No private key selected"));
486 pkLabel.setTextColor(NSColor.disabledControlTextColor());
488 publicKeyPanel = null;
491 @Outlet
492 private NSTextField urlLabel;
494 public void setUrlLabel(NSTextField urlLabel) {
495 this.urlLabel = urlLabel;
496 this.urlLabel.setAllowsEditingTextAttributes(true);
497 this.urlLabel.setSelectable(true);
500 @Outlet
501 private NSPopUpButton encodingPopup;
503 public void setEncodingPopup(NSPopUpButton encodingPopup) {
504 this.encodingPopup = encodingPopup;
505 this.encodingPopup.setEnabled(true);
506 this.encodingPopup.removeAllItems();
507 this.encodingPopup.addItemWithTitle(DEFAULT);
508 this.encodingPopup.menu().addItem(NSMenuItem.separatorItem());
509 this.encodingPopup.addItemsWithTitles(NSArray.arrayWithObjects(new DefaultCharsetProvider().availableCharsets()));
510 this.encodingPopup.selectItemWithTitle(DEFAULT);
513 @Outlet
514 private NSPopUpButton connectmodePopup;
516 public void setConnectmodePopup(NSPopUpButton connectmodePopup) {
517 this.connectmodePopup = connectmodePopup;
518 this.connectmodePopup.removeAllItems();
519 for(FTPConnectMode m : FTPConnectMode.values()) {
520 this.connectmodePopup.addItemWithTitle(m.toString());
521 this.connectmodePopup.lastItem().setRepresentedObject(m.name());
522 if(m.equals(FTPConnectMode.unknown)) {
523 this.connectmodePopup.selectItem(this.connectmodePopup.lastItem());
524 this.connectmodePopup.menu().addItem(NSMenuItem.separatorItem());
529 @Outlet
530 private NSButton toggleOptionsButton;
532 public void setToggleOptionsButton(NSButton b) {
533 this.toggleOptionsButton = b;
537 * Updating the password field with the actual password if any
538 * is available for this hostname
540 public void readPasswordFromKeychain() {
541 if(preferences.getBoolean("connection.login.useKeychain")) {
542 if(StringUtils.isBlank(hostField.stringValue())) {
543 return;
545 if(StringUtils.isBlank(portField.stringValue())) {
546 return;
548 if(StringUtils.isBlank(usernameField.stringValue())) {
549 return;
551 final Protocol protocol = ProtocolFactory.forName(protocolPopup.selectedItem().representedObject());
552 final String password = keychain.getPassword(protocol.getScheme(),
553 NumberUtils.toInt(portField.stringValue(), -1),
554 hostField.stringValue(), usernameField.stringValue());
555 if(StringUtils.isNotBlank(password)) {
556 this.updateField(passField, password);
561 private void updateURLLabel() {
562 if(StringUtils.isNotBlank(hostField.stringValue())) {
563 final Protocol protocol = ProtocolFactory.forName(protocolPopup.selectedItem().representedObject());
564 final String url = String.format("%s://%s@%s:%d%s",
565 protocol.getScheme(),
566 usernameField.stringValue(),
567 hostField.stringValue(),
568 NumberUtils.toInt(portField.stringValue(), -1),
569 PathNormalizer.normalize(pathField.stringValue()));
570 urlLabel.setAttributedStringValue(HyperlinkAttributedStringFactory.create(url));
572 else {
573 urlLabel.setStringValue(StringUtils.EMPTY);
577 public void helpButtonClicked(final ID sender) {
578 new DefaultProviderHelpService().help(
579 ProtocolFactory.forName(protocolPopup.selectedItem().representedObject())
583 @Override
584 protected boolean validateInput() {
585 if(StringUtils.isBlank(hostField.stringValue())) {
586 return false;
588 if(StringUtils.isBlank(usernameField.stringValue())) {
589 return false;
591 return true;
594 @Override
595 public void callback(final int returncode) {
596 if(returncode == DEFAULT_OPTION) {
597 this.window().endEditingFor(null);
598 final Protocol protocol = ProtocolFactory.forName(protocolPopup.selectedItem().representedObject());
599 final Host host = new Host(
600 protocol,
601 hostField.stringValue(),
602 NumberUtils.toInt(portField.stringValue(), -1),
603 pathField.stringValue());
604 if(protocol.getType() == Protocol.Type.ftp) {
605 host.setFTPConnectMode(FTPConnectMode.valueOf(connectmodePopup.selectedItem().representedObject()));
607 final Credentials credentials = host.getCredentials();
608 credentials.setUsername(usernameField.stringValue());
609 credentials.setPassword(passField.stringValue());
610 credentials.setSaved(keychainCheckbox.state() == NSCell.NSOnState);
611 if(protocol.getScheme().equals(Scheme.sftp)) {
612 if(pkCheckbox.state() == NSCell.NSOnState) {
613 credentials.setIdentity(LocalFactory.get(pkLabel.stringValue()));
616 if(encodingPopup.titleOfSelectedItem().equals(DEFAULT)) {
617 host.setEncoding(null);
619 else {
620 host.setEncoding(encodingPopup.titleOfSelectedItem());
622 ((BrowserController) parent).mount(host);
624 preferences.setProperty("connection.toggle.options", this.toggleOptionsButton.state());