001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BorderLayout; 007import java.awt.Dimension; 008import java.awt.FlowLayout; 009import java.awt.Font; 010import java.awt.GridBagConstraints; 011import java.awt.GridBagLayout; 012import java.awt.Insets; 013import java.awt.event.ActionEvent; 014import java.awt.event.FocusAdapter; 015import java.awt.event.FocusEvent; 016import java.awt.event.KeyEvent; 017import java.awt.event.KeyListener; 018import java.awt.event.WindowAdapter; 019import java.awt.event.WindowEvent; 020import java.util.Objects; 021 022import javax.swing.AbstractAction; 023import javax.swing.BorderFactory; 024import javax.swing.JCheckBox; 025import javax.swing.JComponent; 026import javax.swing.JDialog; 027import javax.swing.JLabel; 028import javax.swing.JPanel; 029import javax.swing.JTextField; 030import javax.swing.KeyStroke; 031 032import org.openstreetmap.josm.Main; 033import org.openstreetmap.josm.gui.SideButton; 034import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 035import org.openstreetmap.josm.gui.help.HelpUtil; 036import org.openstreetmap.josm.gui.preferences.server.ProxyPreferencesPanel; 037import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 038import org.openstreetmap.josm.gui.widgets.JosmPasswordField; 039import org.openstreetmap.josm.gui.widgets.JosmTextField; 040import org.openstreetmap.josm.io.OsmApi; 041import org.openstreetmap.josm.tools.ImageProvider; 042import org.openstreetmap.josm.tools.WindowGeometry; 043 044public class CredentialDialog extends JDialog { 045 046 public static CredentialDialog getOsmApiCredentialDialog(String username, String password, String host, 047 String saveUsernameAndPasswordCheckboxText) { 048 CredentialDialog dialog = new CredentialDialog(saveUsernameAndPasswordCheckboxText); 049 if (Objects.equals(OsmApi.getOsmApi().getHost(), host)) { 050 dialog.prepareForOsmApiCredentials(username, password); 051 } else { 052 dialog.prepareForOtherHostCredentials(username, password, host); 053 } 054 dialog.pack(); 055 return dialog; 056 } 057 058 public static CredentialDialog getHttpProxyCredentialDialog(String username, String password, String host, 059 String saveUsernameAndPasswordCheckboxText) { 060 CredentialDialog dialog = new CredentialDialog(saveUsernameAndPasswordCheckboxText); 061 dialog.prepareForProxyCredentials(username, password); 062 dialog.pack(); 063 return dialog; 064 } 065 066 private boolean canceled; 067 protected CredentialPanel pnlCredentials; 068 private String saveUsernameAndPasswordCheckboxText; 069 070 public boolean isCanceled() { 071 return canceled; 072 } 073 074 protected void setCanceled(boolean canceled) { 075 this.canceled = canceled; 076 } 077 078 @Override 079 public void setVisible(boolean visible) { 080 if (visible) { 081 WindowGeometry.centerInWindow(Main.parent, new Dimension(350, 300)).applySafe(this); 082 } 083 super.setVisible(visible); 084 } 085 086 protected JPanel createButtonPanel() { 087 JPanel pnl = new JPanel(new FlowLayout()); 088 pnl.add(new SideButton(new OKAction())); 089 pnl.add(new SideButton(new CancelAction())); 090 pnl.add(new SideButton(new ContextSensitiveHelpAction(HelpUtil.ht("/Dialog/Password")))); 091 return pnl; 092 } 093 094 protected void build() { 095 getContentPane().setLayout(new BorderLayout()); 096 getContentPane().add(createButtonPanel(), BorderLayout.SOUTH); 097 098 addWindowListener(new WindowEventHander()); 099 getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( 100 KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "escape"); 101 getRootPane().getActionMap().put("escape", new CancelAction()); 102 103 getRootPane().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 104 } 105 106 public CredentialDialog(String saveUsernameAndPasswordCheckboxText) { 107 this.saveUsernameAndPasswordCheckboxText = saveUsernameAndPasswordCheckboxText; 108 setModalityType(ModalityType.DOCUMENT_MODAL); 109 try { 110 setAlwaysOnTop(true); 111 } catch (SecurityException e) { 112 Main.warn(tr("Failed to put Credential Dialog always on top. Caught security exception.")); 113 } 114 build(); 115 } 116 117 public void prepareForOsmApiCredentials(String username, String password) { 118 setTitle(tr("Enter credentials for OSM API")); 119 getContentPane().add(pnlCredentials = new OsmApiCredentialsPanel(this), BorderLayout.CENTER); 120 pnlCredentials.init(username, password); 121 validate(); 122 } 123 124 public void prepareForOtherHostCredentials(String username, String password, String host) { 125 setTitle(tr("Enter credentials for host")); 126 getContentPane().add(pnlCredentials = new OtherHostCredentialsPanel(this, host), BorderLayout.CENTER); 127 pnlCredentials.init(username, password); 128 validate(); 129 } 130 131 public void prepareForProxyCredentials(String username, String password) { 132 setTitle(tr("Enter credentials for HTTP proxy")); 133 getContentPane().add(pnlCredentials = new HttpProxyCredentialsPanel(this), BorderLayout.CENTER); 134 pnlCredentials.init(username, password); 135 validate(); 136 } 137 138 public String getUsername() { 139 if (pnlCredentials == null) return null; 140 return pnlCredentials.getUserName(); 141 } 142 143 public char[] getPassword() { 144 if (pnlCredentials == null) return null; 145 return pnlCredentials.getPassword(); 146 } 147 148 public boolean isSaveCredentials() { 149 if (pnlCredentials == null) return false; 150 return pnlCredentials.isSaveCredentials(); 151 } 152 153 protected static class CredentialPanel extends JPanel { 154 protected JosmTextField tfUserName; 155 protected JosmPasswordField tfPassword; 156 protected JCheckBox cbSaveCredentials; 157 protected JMultilineLabel lblHeading; 158 protected JMultilineLabel lblWarning; 159 protected CredentialDialog owner; // owner Dependency Injection to use Key listeners for username and password text fields 160 161 protected void build() { 162 tfUserName = new JosmTextField(20); 163 tfPassword = new JosmPasswordField(20); 164 tfUserName.addFocusListener(new SelectAllOnFocusHandler()); 165 tfPassword.addFocusListener(new SelectAllOnFocusHandler()); 166 tfUserName.addKeyListener(new TFKeyListener(owner, tfUserName, tfPassword)); 167 tfPassword.addKeyListener(new TFKeyListener(owner, tfPassword, tfUserName)); 168 cbSaveCredentials = new JCheckBox(owner.saveUsernameAndPasswordCheckboxText); 169 170 setLayout(new GridBagLayout()); 171 GridBagConstraints gc = new GridBagConstraints(); 172 gc.gridwidth = 2; 173 gc.gridheight = 1; 174 gc.fill = GridBagConstraints.HORIZONTAL; 175 gc.weightx = 1.0; 176 gc.weighty = 0.0; 177 gc.insets = new Insets(0, 0, 10, 0); 178 add(lblHeading = new JMultilineLabel(""), gc); 179 180 gc.gridx = 0; 181 gc.gridy = 1; 182 gc.gridwidth = 1; 183 gc.gridheight = 1; 184 gc.fill = GridBagConstraints.HORIZONTAL; 185 gc.weightx = 0.0; 186 gc.weighty = 0.0; 187 gc.insets = new Insets(0, 0, 10, 10); 188 add(new JLabel(tr("Username")), gc); 189 gc.gridx = 1; 190 gc.gridy = 1; 191 gc.weightx = 1.0; 192 add(tfUserName, gc); 193 gc.gridx = 0; 194 gc.gridy = 2; 195 gc.weightx = 0.0; 196 add(new JLabel(tr("Password")), gc); 197 198 gc.gridx = 1; 199 gc.gridy = 2; 200 gc.weightx = 0.0; 201 add(tfPassword, gc); 202 203 gc.gridx = 0; 204 gc.gridy = 3; 205 gc.gridwidth = 2; 206 gc.gridheight = 1; 207 gc.fill = GridBagConstraints.BOTH; 208 gc.weightx = 1.0; 209 gc.weighty = 0.0; 210 lblWarning = new JMultilineLabel(""); 211 lblWarning.setFont(lblWarning.getFont().deriveFont(Font.ITALIC)); 212 add(lblWarning, gc); 213 214 gc.gridx = 0; 215 gc.gridy = 4; 216 gc.weighty = 0.0; 217 add(cbSaveCredentials, gc); 218 219 // consume the remaining space 220 gc.gridx = 0; 221 gc.gridy = 5; 222 gc.weighty = 1.0; 223 add(new JPanel(), gc); 224 225 } 226 227 public CredentialPanel(CredentialDialog owner) { 228 this.owner = owner; 229 } 230 231 public void init(String username, String password) { 232 username = username == null ? "" : username; 233 password = password == null ? "" : password; 234 tfUserName.setText(username); 235 tfPassword.setText(password); 236 cbSaveCredentials.setSelected(!username.isEmpty() && !password.isEmpty()); 237 } 238 239 public void startUserInput() { 240 tfUserName.requestFocusInWindow(); 241 } 242 243 public String getUserName() { 244 return tfUserName.getText(); 245 } 246 247 public char[] getPassword() { 248 return tfPassword.getPassword(); 249 } 250 251 public boolean isSaveCredentials() { 252 return cbSaveCredentials.isSelected(); 253 } 254 255 protected final void updateWarningLabel(String url) { 256 boolean https = url != null && url.startsWith("https"); 257 if (https) { 258 lblWarning.setText(null); 259 } else { 260 lblWarning.setText(tr("Warning: The password is transferred unencrypted.")); 261 } 262 lblWarning.setVisible(!https); 263 } 264 } 265 266 private static class OsmApiCredentialsPanel extends CredentialPanel { 267 268 @Override 269 protected void build() { 270 super.build(); 271 tfUserName.setToolTipText(tr("Please enter the user name of your OSM account")); 272 tfPassword.setToolTipText(tr("Please enter the password of your OSM account")); 273 String apiUrl = OsmApi.getOsmApi().getBaseUrl(); 274 lblHeading.setText( 275 "<html>" + tr("Authenticating at the OSM API ''{0}'' failed. Please enter a valid username and a valid password.", 276 apiUrl) + "</html>"); 277 updateWarningLabel(apiUrl); 278 } 279 280 OsmApiCredentialsPanel(CredentialDialog owner) { 281 super(owner); 282 build(); 283 } 284 } 285 286 private static class OtherHostCredentialsPanel extends CredentialPanel { 287 288 private String host; 289 290 @Override 291 protected void build() { 292 super.build(); 293 tfUserName.setToolTipText(tr("Please enter the user name of your account")); 294 tfPassword.setToolTipText(tr("Please enter the password of your account")); 295 lblHeading.setText( 296 "<html>" + tr("Authenticating at the host ''{0}'' failed. Please enter a valid username and a valid password.", 297 host) + "</html>"); 298 updateWarningLabel(host); 299 } 300 301 OtherHostCredentialsPanel(CredentialDialog owner, String host) { 302 super(owner); 303 this.host = host; 304 build(); 305 } 306 } 307 308 private static class HttpProxyCredentialsPanel extends CredentialPanel { 309 @Override 310 protected void build() { 311 super.build(); 312 tfUserName.setToolTipText(tr("Please enter the user name for authenticating at your proxy server")); 313 tfPassword.setToolTipText(tr("Please enter the password for authenticating at your proxy server")); 314 lblHeading.setText( 315 "<html>" + tr("Authenticating at the HTTP proxy ''{0}'' failed. Please enter a valid username and a valid password.", 316 Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_HOST) + ':' + 317 Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_PORT)) + "</html>"); 318 lblWarning.setText("<html>" + 319 tr("Warning: depending on the authentication method the proxy server uses the password may be transferred unencrypted.") 320 + "</html>"); 321 } 322 323 HttpProxyCredentialsPanel(CredentialDialog owner) { 324 super(owner); 325 build(); 326 } 327 } 328 329 private static class SelectAllOnFocusHandler extends FocusAdapter { 330 @Override 331 public void focusGained(FocusEvent e) { 332 if (e.getSource() instanceof JTextField) { 333 JTextField tf = (JTextField) e.getSource(); 334 tf.selectAll(); 335 } 336 } 337 } 338 339 /** 340 * Listener for username and password text fields key events. 341 * When user presses Enter: 342 * If current text field is empty (or just contains a sequence of spaces), nothing happens (or all spaces become selected). 343 * If current text field is not empty, but the next one is (or just contains a sequence of spaces), focuses the next text field. 344 * If both text fields contain characters, submits the form by calling owner's {@link OKAction}. 345 */ 346 private static class TFKeyListener implements KeyListener { 347 protected CredentialDialog owner; // owner Dependency Injection to call OKAction 348 protected JTextField currentTF; 349 protected JTextField nextTF; 350 351 TFKeyListener(CredentialDialog owner, JTextField currentTF, JTextField nextTF) { 352 this.owner = owner; 353 this.currentTF = currentTF; 354 this.nextTF = nextTF; 355 } 356 357 @Override 358 public void keyPressed(KeyEvent e) { 359 if (e.getKeyChar() == KeyEvent.VK_ENTER) { 360 if (currentTF.getText().trim().isEmpty()) { 361 currentTF.selectAll(); 362 return; 363 } else if (nextTF.getText().trim().isEmpty()) { 364 nextTF.requestFocusInWindow(); 365 nextTF.selectAll(); 366 return; 367 } else { 368 OKAction okAction = owner.new OKAction(); 369 okAction.actionPerformed(null); 370 } 371 } 372 } 373 374 @Override 375 public void keyReleased(KeyEvent e) { 376 } 377 378 @Override 379 public void keyTyped(KeyEvent e) { 380 } 381 } 382 383 class OKAction extends AbstractAction { 384 OKAction() { 385 putValue(NAME, tr("Authenticate")); 386 putValue(SHORT_DESCRIPTION, tr("Authenticate with the supplied username and password")); 387 putValue(SMALL_ICON, ImageProvider.get("ok")); 388 } 389 390 @Override 391 public void actionPerformed(ActionEvent arg0) { 392 setCanceled(false); 393 setVisible(false); 394 } 395 } 396 397 class CancelAction extends AbstractAction { 398 CancelAction() { 399 putValue(NAME, tr("Cancel")); 400 putValue(SHORT_DESCRIPTION, tr("Cancel authentication")); 401 putValue(SMALL_ICON, ImageProvider.get("cancel")); 402 } 403 404 public void cancel() { 405 setCanceled(true); 406 setVisible(false); 407 } 408 409 @Override 410 public void actionPerformed(ActionEvent arg0) { 411 cancel(); 412 } 413 } 414 415 class WindowEventHander extends WindowAdapter { 416 417 @Override 418 public void windowActivated(WindowEvent e) { 419 if (pnlCredentials != null) { 420 pnlCredentials.startUserInput(); 421 } 422 } 423 424 @Override 425 public void windowClosing(WindowEvent e) { 426 new CancelAction().cancel(); 427 } 428 } 429}