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