1 package ch
.cyberduck
.ui
.cocoa
;
4 * Copyright (c) 2005 David Kocher. All rights reserved.
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
;
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();
73 public void invalidate() {
74 hostField
.setDelegate(null);
75 hostField
.setDataSource(null);
76 notificationCenter
.removeObserver(this.id());
81 public boolean isSingleton() {
85 public ConnectionController(final WindowController parent
) {
91 protected String
getBundleName() {
96 public void awakeFromNib() {
97 this.protocolSelectionDidChange(null);
98 this.setState(toggleOptionsButton
, preferences
.getBoolean("connection.toggle.options"));
103 protected void beginSheet(final NSWindow window
) {
104 // Reset password input
105 passField
.setStringValue(StringUtils
.EMPTY
);
106 super.beginSheet(window
);
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
);
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);
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();
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
);
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
,
222 private static class HostFieldModel
extends ProxyController
implements NSComboBox
.DataSource
{
224 public NSInteger
numberOfItemsInComboBox(final NSComboBox sender
) {
225 return new NSInteger(BookmarkCollection
.defaultCollection().size());
229 public NSObject
comboBox_objectValueForItemAtIndex(final NSComboBox sender
, final NSInteger row
) {
230 return NSString
.stringWithString(
231 BookmarkNameProvider
.toString(BookmarkCollection
.defaultCollection().get(row
.intValue()))
237 public void hostPopupSelectionDidChange(final NSControl sender
) {
238 String input
= sender
.stringValue();
239 if(StringUtils
.isBlank(input
)) {
242 input
= input
.trim();
243 // First look for equivalent bookmarks
244 for(Host h
: BookmarkCollection
.defaultCollection()) {
245 if(BookmarkNameProvider
.toString(h
).equals(input
)) {
247 this.updateURLLabel();
248 this.readPasswordFromKeychain();
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();
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;
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
));
295 public void cleanup() {
296 alertIcon
.setEnabled(!reachable
);
297 alertIcon
.setImage(reachable ?
null : IconCacheFactory
.<NSImage
>get().iconNamed("alert.tiff"));
302 alertIcon
.setImage(IconCacheFactory
.<NSImage
>get().iconNamed("alert.tiff"));
303 alertIcon
.setEnabled(false);
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:"));
317 public void launchNetworkAssistant(final NSButton sender
) {
318 ReachabilityFactory
.get().diagnose(HostParser
.parse(urlLabel
.stringValue()));
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
,
332 public void pathInputDidEndEditing(final NSNotification sender
) {
333 this.updateURLLabel();
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
,
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();
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
,
366 notificationCenter
.addObserver(this.id(),
367 Foundation
.selector("usernameFieldTextDidEndEditing:"),
368 NSControl
.NSControlTextDidEndEditingNotification
,
372 public void usernameFieldTextDidChange(final NSNotification sender
) {
373 this.updateURLLabel();
376 public void usernameFieldTextDidEndEditing(final NSNotification sender
) {
377 this.readPasswordFromKeychain();
381 private NSTextField passField
;
383 public void setPassField(NSTextField passField
) {
384 this.passField
= passField
;
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());
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
);
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
);
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();
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
;
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);
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;
492 private NSTextField urlLabel
;
494 public void setUrlLabel(NSTextField urlLabel
) {
495 this.urlLabel
= urlLabel
;
496 this.urlLabel
.setAllowsEditingTextAttributes(true);
497 this.urlLabel
.setSelectable(true);
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
);
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());
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())) {
545 if(StringUtils
.isBlank(portField
.stringValue())) {
548 if(StringUtils
.isBlank(usernameField
.stringValue())) {
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
));
573 urlLabel
.setStringValue(StringUtils
.EMPTY
);
577 public void helpButtonClicked(final ID sender
) {
578 new DefaultProviderHelpService().help(
579 ProtocolFactory
.forName(protocolPopup
.selectedItem().representedObject())
584 protected boolean validateInput() {
585 if(StringUtils
.isBlank(hostField
.stringValue())) {
588 if(StringUtils
.isBlank(usernameField
.stringValue())) {
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(
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);
620 host
.setEncoding(encodingPopup
.titleOfSelectedItem());
622 ((BrowserController
) parent
).mount(host
);
624 preferences
.setProperty("connection.toggle.options", this.toggleOptionsButton
.state());