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 Preferences preferences
67 = PreferencesFactory
.get();
70 public void invalidate() {
71 hostField
.setDelegate(null);
72 hostField
.setDataSource(null);
77 public boolean isSingleton() {
81 public ConnectionController(final WindowController parent
) {
87 protected String
getBundleName() {
92 public void awakeFromNib() {
93 this.protocolSelectionDidChange(null);
94 this.setState(toggleOptionsButton
, preferences
.getBoolean("connection.toggle.options"));
99 public void beginSheet() {
100 // Reset password input
101 passField
.setStringValue(StringUtils
.EMPTY
);
106 public void setWindow(final NSWindow window
) {
107 window
.setContentMinSize(window
.frame().size
);
108 window
.setContentMaxSize(new NSSize(600, window
.frame().size
.height
.doubleValue()));
109 super.setWindow(window
);
113 private NSPopUpButton protocolPopup
;
115 public void setProtocolPopup(NSPopUpButton protocolPopup
) {
116 this.protocolPopup
= protocolPopup
;
117 this.protocolPopup
.setEnabled(true);
118 this.protocolPopup
.setTarget(this.id());
119 this.protocolPopup
.setAction(Foundation
.selector("protocolSelectionDidChange:"));
120 this.protocolPopup
.removeAllItems();
121 for(Protocol protocol
: ProtocolFactory
.getEnabledProtocols()) {
122 final String title
= protocol
.getDescription();
123 this.protocolPopup
.addItemWithTitle(title
);
124 final NSMenuItem item
= this.protocolPopup
.itemWithTitle(title
);
125 item
.setRepresentedObject(String
.valueOf(protocol
.hashCode()));
126 item
.setImage(IconCacheFactory
.<NSImage
>get().iconNamed(protocol
.icon(), 16));
128 final Protocol defaultProtocol
129 = ProtocolFactory
.forName(preferences
.getProperty("connection.protocol.default"));
130 this.protocolPopup
.selectItemAtIndex(
131 protocolPopup
.indexOfItemWithRepresentedObject(String
.valueOf(defaultProtocol
.hashCode()))
135 public void protocolSelectionDidChange(final NSPopUpButton sender
) {
136 log
.debug("protocolSelectionDidChange:" + sender
);
137 final Protocol protocol
= ProtocolFactory
.forName(protocolPopup
.selectedItem().representedObject());
138 portField
.setIntValue(protocol
.getDefaultPort());
139 portField
.setEnabled(protocol
.isPortConfigurable());
140 if(!protocol
.isHostnameConfigurable()) {
141 hostField
.setStringValue(protocol
.getDefaultHostname());
142 hostField
.setEnabled(false);
143 pathField
.setEnabled(true);
146 if(!hostField
.isEnabled()) {
147 // Was previously configured with a static configuration
148 hostField
.setStringValue(protocol
.getDefaultHostname());
150 if(!pathField
.isEnabled()) {
151 // Was previously configured with a static configuration
152 pathField
.setStringValue(StringUtils
.EMPTY
);
154 if(StringUtils
.isNotBlank(protocol
.getDefaultHostname())) {
155 // Prefill with default hostname
156 hostField
.setStringValue(protocol
.getDefaultHostname());
158 usernameField
.setEnabled(true);
159 hostField
.setEnabled(true);
160 pathField
.setEnabled(true);
161 usernameField
.cell().setPlaceholderString(StringUtils
.EMPTY
);
162 passField
.cell().setPlaceholderString(StringUtils
.EMPTY
);
164 hostField
.cell().setPlaceholderString(protocol
.getDefaultHostname());
165 usernameField
.cell().setPlaceholderString(protocol
.getUsernamePlaceholder());
166 passField
.cell().setPlaceholderString(protocol
.getPasswordPlaceholder());
167 connectmodePopup
.setEnabled(protocol
.getType() == Protocol
.Type
.ftp
);
168 if(!protocol
.isEncodingConfigurable()) {
169 encodingPopup
.selectItemWithTitle(DEFAULT
);
171 encodingPopup
.setEnabled(protocol
.isEncodingConfigurable());
172 anonymousCheckbox
.setEnabled(protocol
.isAnonymousConfigurable());
174 this.updateIdentity();
175 this.updateURLLabel();
176 this.readPasswordFromKeychain();
181 * Update Private Key selection
183 private void updateIdentity() {
184 final Protocol protocol
= ProtocolFactory
.forName(protocolPopup
.selectedItem().representedObject());
185 pkCheckbox
.setEnabled(protocol
.getType() == Protocol
.Type
.ssh
);
186 if(StringUtils
.isNotEmpty(hostField
.stringValue())) {
187 final Credentials credentials
= CredentialsConfiguratorFactory
.get(protocol
).configure(new Host(hostField
.stringValue()));
188 if(credentials
.isPublicKeyAuthentication()) {
189 // No previously manually selected key
190 pkLabel
.setStringValue(credentials
.getIdentity().getAbbreviatedPath());
191 pkCheckbox
.setState(NSCell
.NSOnState
);
194 pkCheckbox
.setState(NSCell
.NSOffState
);
195 pkLabel
.setStringValue(LocaleFactory
.localizedString("No private key selected"));
197 if(StringUtils
.isNotBlank(credentials
.getUsername())) {
198 usernameField
.setStringValue(credentials
.getUsername());
203 private NSComboBox hostField
;
204 private ProxyController hostFieldModel
= new HostFieldModel();
206 public void setHostPopup(NSComboBox hostPopup
) {
207 this.hostField
= hostPopup
;
208 this.hostField
.setTarget(this.id());
209 this.hostField
.setAction(Foundation
.selector("hostPopupSelectionDidChange:"));
210 this.hostField
.setUsesDataSource(true);
211 this.hostField
.setDataSource(hostFieldModel
.id());
212 NSNotificationCenter
.defaultCenter().addObserver(this.id(),
213 Foundation
.selector("hostFieldTextDidChange:"),
214 NSControl
.NSControlTextDidChangeNotification
,
218 private static class HostFieldModel
extends ProxyController
implements NSComboBox
.DataSource
{
220 public NSInteger
numberOfItemsInComboBox(final NSComboBox sender
) {
221 return new NSInteger(BookmarkCollection
.defaultCollection().size());
225 public NSObject
comboBox_objectValueForItemAtIndex(final NSComboBox sender
, final NSInteger row
) {
226 return NSString
.stringWithString(
227 BookmarkNameProvider
.toString(BookmarkCollection
.defaultCollection().get(row
.intValue()))
233 public void hostPopupSelectionDidChange(final NSControl sender
) {
234 String input
= sender
.stringValue();
235 if(StringUtils
.isBlank(input
)) {
238 input
= input
.trim();
239 // First look for equivalent bookmarks
240 for(Host h
: BookmarkCollection
.defaultCollection()) {
241 if(BookmarkNameProvider
.toString(h
).equals(input
)) {
243 this.updateURLLabel();
244 this.readPasswordFromKeychain();
251 public void hostFieldTextDidChange(final NSNotification sender
) {
252 final Host parsed
= HostParser
.parse(hostField
.stringValue());
253 if(ProtocolFactory
.isURL(hostField
.stringValue())) {
254 this.hostChanged(parsed
);
257 this.updateField(hostField
, parsed
.getHostname());
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 return reachable
= ReachabilityFactory
.get().isReachable(new Host(hostname
));
292 public void cleanup() {
293 alertIcon
.setEnabled(!reachable
);
294 alertIcon
.setImage(reachable ?
null : IconCacheFactory
.<NSImage
>get().iconNamed("alert.tiff"));
299 alertIcon
.setImage(IconCacheFactory
.<NSImage
>get().iconNamed("alert.tiff"));
300 alertIcon
.setEnabled(false);
305 private NSButton alertIcon
;
307 public void setAlertIcon(NSButton alertIcon
) {
308 this.alertIcon
= alertIcon
;
309 this.alertIcon
.setTarget(this.id());
310 this.alertIcon
.setAction(Foundation
.selector("launchNetworkAssistant:"));
314 public void launchNetworkAssistant(final NSButton sender
) {
315 ReachabilityFactory
.get().diagnose(HostParser
.parse(urlLabel
.stringValue()));
319 private NSTextField pathField
;
321 public void setPathField(NSTextField pathField
) {
322 this.pathField
= pathField
;
323 NSNotificationCenter
.defaultCenter().addObserver(this.id(),
324 Foundation
.selector("pathInputDidEndEditing:"),
325 NSControl
.NSControlTextDidEndEditingNotification
,
329 public void pathInputDidEndEditing(final NSNotification sender
) {
330 this.updateURLLabel();
334 private NSTextField portField
;
336 public void setPortField(NSTextField portField
) {
337 this.portField
= portField
;
338 NSNotificationCenter
.defaultCenter().addObserver(this.id(),
339 Foundation
.selector("portFieldTextDidChange:"),
340 NSControl
.NSControlTextDidChangeNotification
,
344 public void portFieldTextDidChange(final NSNotification sender
) {
345 if(StringUtils
.isBlank(this.portField
.stringValue())) {
346 final Protocol protocol
= ProtocolFactory
.forName(protocolPopup
.selectedItem().representedObject());
347 this.portField
.setIntValue(protocol
.getDefaultPort());
349 this.updateURLLabel();
354 private NSTextField usernameField
;
356 public void setUsernameField(NSTextField usernameField
) {
357 this.usernameField
= usernameField
;
358 this.usernameField
.setStringValue(preferences
.getProperty("connection.login.name"));
359 NSNotificationCenter
.defaultCenter().addObserver(this.id(),
360 Foundation
.selector("usernameFieldTextDidChange:"),
361 NSControl
.NSControlTextDidChangeNotification
,
363 NSNotificationCenter
.defaultCenter().addObserver(this.id(),
364 Foundation
.selector("usernameFieldTextDidEndEditing:"),
365 NSControl
.NSControlTextDidEndEditingNotification
,
369 public void usernameFieldTextDidChange(final NSNotification sender
) {
370 this.updateURLLabel();
373 public void usernameFieldTextDidEndEditing(final NSNotification sender
) {
374 this.readPasswordFromKeychain();
378 private NSTextField passField
;
380 public void setPassField(NSTextField passField
) {
381 this.passField
= passField
;
385 private NSTextField pkLabel
;
387 public void setPkLabel(NSTextField pkLabel
) {
388 this.pkLabel
= pkLabel
;
389 this.pkLabel
.setStringValue(LocaleFactory
.localizedString("No private key selected"));
390 this.pkLabel
.setTextColor(NSColor
.disabledControlTextColor());
394 private NSButton keychainCheckbox
;
396 public void setKeychainCheckbox(NSButton keychainCheckbox
) {
397 this.keychainCheckbox
= keychainCheckbox
;
398 this.keychainCheckbox
.setState(preferences
.getBoolean("connection.login.useKeychain")
399 && preferences
.getBoolean("connection.login.addKeychain") ? NSCell
.NSOnState
: NSCell
.NSOffState
);
400 this.keychainCheckbox
.setTarget(this.id());
401 this.keychainCheckbox
.setAction(Foundation
.selector("keychainCheckboxClicked:"));
404 public void keychainCheckboxClicked(final NSButton sender
) {
405 final boolean enabled
= sender
.state() == NSCell
.NSOnState
;
406 preferences
.setProperty("connection.login.addKeychain", enabled
);
410 private NSButton anonymousCheckbox
;
412 public void setAnonymousCheckbox(NSButton anonymousCheckbox
) {
413 this.anonymousCheckbox
= anonymousCheckbox
;
414 this.anonymousCheckbox
.setTarget(this.id());
415 this.anonymousCheckbox
.setAction(Foundation
.selector("anonymousCheckboxClicked:"));
416 this.anonymousCheckbox
.setState(NSCell
.NSOffState
);
420 public void anonymousCheckboxClicked(final NSButton sender
) {
421 if(sender
.state() == NSCell
.NSOnState
) {
422 this.usernameField
.setEnabled(false);
423 this.usernameField
.setStringValue(preferences
.getProperty("connection.login.anon.name"));
424 this.passField
.setEnabled(false);
425 this.passField
.setStringValue(StringUtils
.EMPTY
);
427 if(sender
.state() == NSCell
.NSOffState
) {
428 this.usernameField
.setEnabled(true);
429 this.usernameField
.setStringValue(preferences
.getProperty("connection.login.name"));
430 this.passField
.setEnabled(true);
432 this.updateURLLabel();
436 private NSButton pkCheckbox
;
438 public void setPkCheckbox(NSButton pkCheckbox
) {
439 this.pkCheckbox
= pkCheckbox
;
440 this.pkCheckbox
.setTarget(this.id());
441 this.pkCheckbox
.setAction(Foundation
.selector("pkCheckboxSelectionDidChange:"));
442 this.pkCheckbox
.setState(NSCell
.NSOffState
);
445 private NSOpenPanel publicKeyPanel
;
448 public void pkCheckboxSelectionDidChange(final NSButton sender
) {
449 log
.debug("pkCheckboxSelectionDidChange");
450 if(sender
.state() == NSCell
.NSOnState
) {
451 publicKeyPanel
= NSOpenPanel
.openPanel();
452 publicKeyPanel
.setCanChooseDirectories(false);
453 publicKeyPanel
.setCanChooseFiles(true);
454 publicKeyPanel
.setAllowsMultipleSelection(false);
455 publicKeyPanel
.setMessage(LocaleFactory
.localizedString("Select the private key in PEM or PuTTY format", "Credentials"));
456 publicKeyPanel
.setPrompt(LocaleFactory
.localizedString("Choose"));
457 publicKeyPanel
.beginSheetForDirectory(LocalFactory
.get("~/.ssh").getAbsolute(),
458 null, this.window(), this.id(),
459 Foundation
.selector("pkSelectionPanelDidEnd:returnCode:contextInfo:"), null);
462 passField
.setEnabled(true);
463 pkCheckbox
.setState(NSCell
.NSOffState
);
464 pkLabel
.setStringValue(LocaleFactory
.localizedString("No private key selected"));
465 pkLabel
.setTextColor(NSColor
.disabledControlTextColor());
469 public void pkSelectionPanelDidEnd_returnCode_contextInfo(NSOpenPanel window
, int returncode
, ID contextInfo
) {
470 if(NSPanel
.NSOKButton
== returncode
) {
471 final NSObject selected
= window
.filenames().lastObject();
472 if(selected
!= null) {
473 pkLabel
.setAttributedStringValue(NSAttributedString
.attributedStringWithAttributes(
474 LocalFactory
.get(selected
.toString()).getAbbreviatedPath(), TRUNCATE_MIDDLE_ATTRIBUTES
));
475 pkLabel
.setTextColor(NSColor
.textColor());
477 passField
.setEnabled(false);
479 if(NSPanel
.NSCancelButton
== returncode
) {
480 passField
.setEnabled(true);
481 pkCheckbox
.setState(NSCell
.NSOffState
);
482 pkLabel
.setStringValue(LocaleFactory
.localizedString("No private key selected"));
483 pkLabel
.setTextColor(NSColor
.disabledControlTextColor());
485 publicKeyPanel
= null;
489 private NSTextField urlLabel
;
491 public void setUrlLabel(NSTextField urlLabel
) {
492 this.urlLabel
= urlLabel
;
493 this.urlLabel
.setAllowsEditingTextAttributes(true);
494 this.urlLabel
.setSelectable(true);
498 private NSPopUpButton encodingPopup
;
500 public void setEncodingPopup(NSPopUpButton encodingPopup
) {
501 this.encodingPopup
= encodingPopup
;
502 this.encodingPopup
.setEnabled(true);
503 this.encodingPopup
.removeAllItems();
504 this.encodingPopup
.addItemWithTitle(DEFAULT
);
505 this.encodingPopup
.menu().addItem(NSMenuItem
.separatorItem());
506 this.encodingPopup
.addItemsWithTitles(NSArray
.arrayWithObjects(new DefaultCharsetProvider().availableCharsets()));
507 this.encodingPopup
.selectItemWithTitle(DEFAULT
);
511 private NSPopUpButton connectmodePopup
;
513 public void setConnectmodePopup(NSPopUpButton connectmodePopup
) {
514 this.connectmodePopup
= connectmodePopup
;
515 this.connectmodePopup
.removeAllItems();
516 for(FTPConnectMode m
: FTPConnectMode
.values()) {
517 this.connectmodePopup
.addItemWithTitle(m
.toString());
518 this.connectmodePopup
.lastItem().setRepresentedObject(m
.name());
519 if(m
.equals(FTPConnectMode
.unknown
)) {
520 this.connectmodePopup
.selectItem(this.connectmodePopup
.lastItem());
521 this.connectmodePopup
.menu().addItem(NSMenuItem
.separatorItem());
527 private NSButton toggleOptionsButton
;
529 public void setToggleOptionsButton(NSButton b
) {
530 this.toggleOptionsButton
= b
;
534 * Updating the password field with the actual password if any
535 * is available for this hostname
537 public void readPasswordFromKeychain() {
538 if(preferences
.getBoolean("connection.login.useKeychain")) {
539 if(StringUtils
.isBlank(hostField
.stringValue())) {
542 if(StringUtils
.isBlank(portField
.stringValue())) {
545 if(StringUtils
.isBlank(usernameField
.stringValue())) {
548 final Protocol protocol
= ProtocolFactory
.forName(protocolPopup
.selectedItem().representedObject());
549 final String password
= keychain
.getPassword(protocol
.getScheme(),
550 NumberUtils
.toInt(portField
.stringValue(), -1),
551 hostField
.stringValue(), usernameField
.stringValue());
552 if(StringUtils
.isNotBlank(password
)) {
553 this.updateField(passField
, password
);
558 private void updateURLLabel() {
559 if(StringUtils
.isNotBlank(hostField
.stringValue())) {
560 final Protocol protocol
= ProtocolFactory
.forName(protocolPopup
.selectedItem().representedObject());
561 final String url
= String
.format("%s://%s@%s:%d%s",
562 protocol
.getScheme(),
563 usernameField
.stringValue(),
564 hostField
.stringValue(),
565 NumberUtils
.toInt(portField
.stringValue(), -1),
566 PathNormalizer
.normalize(pathField
.stringValue()));
567 urlLabel
.setAttributedStringValue(HyperlinkAttributedStringFactory
.create(url
));
570 urlLabel
.setStringValue(StringUtils
.EMPTY
);
574 public void helpButtonClicked(final ID sender
) {
575 new DefaultProviderHelpService().help(
576 ProtocolFactory
.forName(protocolPopup
.selectedItem().representedObject())
581 protected boolean validateInput() {
582 if(StringUtils
.isBlank(hostField
.stringValue())) {
585 if(StringUtils
.isBlank(usernameField
.stringValue())) {
592 public void callback(final int returncode
) {
593 if(returncode
== DEFAULT_OPTION
) {
594 this.window().endEditingFor(null);
595 final Protocol protocol
= ProtocolFactory
.forName(protocolPopup
.selectedItem().representedObject());
596 final Host host
= new Host(
598 hostField
.stringValue(),
599 NumberUtils
.toInt(portField
.stringValue(), -1),
600 pathField
.stringValue());
601 if(protocol
.getType() == Protocol
.Type
.ftp
) {
602 host
.setFTPConnectMode(FTPConnectMode
.valueOf(connectmodePopup
.selectedItem().representedObject()));
604 final Credentials credentials
= host
.getCredentials();
605 credentials
.setUsername(usernameField
.stringValue());
606 credentials
.setPassword(passField
.stringValue());
607 credentials
.setSaved(keychainCheckbox
.state() == NSCell
.NSOnState
);
608 if(protocol
.getScheme().equals(Scheme
.sftp
)) {
609 if(pkCheckbox
.state() == NSCell
.NSOnState
) {
610 credentials
.setIdentity(LocalFactory
.get(pkLabel
.stringValue()));
613 if(encodingPopup
.titleOfSelectedItem().equals(DEFAULT
)) {
614 host
.setEncoding(null);
617 host
.setEncoding(encodingPopup
.titleOfSelectedItem());
619 ((BrowserController
) parent
).mount(host
);
621 preferences
.setProperty("connection.toggle.options", this.toggleOptionsButton
.state());