001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.gui.oauth; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.awt.BorderLayout; 007 import java.awt.Component; 008 import java.awt.FlowLayout; 009 import java.awt.Font; 010 import java.awt.GridBagConstraints; 011 import java.awt.GridBagLayout; 012 import java.awt.Insets; 013 import java.awt.event.ActionEvent; 014 import java.io.IOException; 015 import java.net.PasswordAuthentication; 016 import java.net.Authenticator.RequestorType; 017 018 import javax.swing.AbstractAction; 019 import javax.swing.BorderFactory; 020 import javax.swing.JLabel; 021 import javax.swing.JOptionPane; 022 import javax.swing.JPanel; 023 import javax.swing.JPasswordField; 024 import javax.swing.JTabbedPane; 025 import javax.swing.JTextField; 026 import javax.swing.event.DocumentEvent; 027 import javax.swing.event.DocumentListener; 028 import javax.swing.text.JTextComponent; 029 import javax.swing.text.html.HTMLEditorKit; 030 031 import org.openstreetmap.josm.Main; 032 import org.openstreetmap.josm.data.Preferences; 033 import org.openstreetmap.josm.data.oauth.OAuthToken; 034 import org.openstreetmap.josm.gui.HelpAwareOptionPane; 035 import org.openstreetmap.josm.gui.JMultilineLabel; 036 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 037 import org.openstreetmap.josm.gui.SideButton; 038 import org.openstreetmap.josm.gui.help.HelpUtil; 039 import org.openstreetmap.josm.gui.util.GuiHelper; 040 import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 041 import org.openstreetmap.josm.gui.widgets.HtmlPanel; 042 import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator; 043 import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel; 044 import org.openstreetmap.josm.io.OsmApi; 045 import org.openstreetmap.josm.io.OsmTransferException; 046 import org.openstreetmap.josm.io.auth.CredentialsAgent; 047 import org.openstreetmap.josm.io.auth.CredentialsAgentException; 048 import org.openstreetmap.josm.io.auth.CredentialsManager; 049 import org.openstreetmap.josm.tools.ImageProvider; 050 import org.xml.sax.SAXException; 051 052 /** 053 * This is an UI which supports a JOSM user to get an OAuth Access Token in a fully 054 * automatic process. 055 * 056 * @since 2746 057 */ 058 public class FullyAutomaticAuthorizationUI extends AbstractAuthorizationUI { 059 060 private JTextField tfUserName; 061 private JPasswordField tfPassword; 062 private UserNameValidator valUserName; 063 private PasswordValidator valPassword; 064 private AccessTokenInfoPanel pnlAccessTokenInfo; 065 private OsmPrivilegesPanel pnlOsmPrivileges; 066 private JPanel pnlPropertiesPanel; 067 private JPanel pnlActionButtonsPanel; 068 private JPanel pnlResult; 069 070 /** 071 * Builds the panel with the three privileges the user can grant JOSM 072 * 073 * @return 074 */ 075 protected VerticallyScrollablePanel buildGrantsPanel() { 076 pnlOsmPrivileges = new OsmPrivilegesPanel(); 077 return pnlOsmPrivileges; 078 } 079 080 /** 081 * Builds the panel for entering the username and password 082 * 083 * @return 084 */ 085 protected VerticallyScrollablePanel buildUserNamePasswordPanel() { 086 VerticallyScrollablePanel pnl = new VerticallyScrollablePanel(new GridBagLayout()); 087 GridBagConstraints gc = new GridBagConstraints(); 088 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 089 090 gc.anchor = GridBagConstraints.NORTHWEST; 091 gc.fill = GridBagConstraints.HORIZONTAL; 092 gc.weightx = 1.0; 093 gc.gridwidth = 2; 094 HtmlPanel pnlMessage = new HtmlPanel(); 095 HTMLEditorKit kit = (HTMLEditorKit)pnlMessage.getEditorPane().getEditorKit(); 096 kit.getStyleSheet().addRule(".warning-body {background-color:rgb(253,255,221);padding: 10pt; border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}"); 097 kit.getStyleSheet().addRule("ol {margin-left: 1cm}"); 098 pnlMessage.setText("<html><body><p class=\"warning-body\">" 099 + tr("Please enter your OSM user name and password. The password will <strong>not</strong> be saved " 100 + "in clear text in the JOSM preferences and it will be submitted to the OSM server <strong>only once</strong>. " 101 + "Subsequent data upload requests don''t use your password any more.") 102 + "</p>" 103 + "</body></html>"); 104 pnl.add(pnlMessage, gc); 105 106 // the user name input field 107 gc.gridy = 1; 108 gc.gridwidth = 1; 109 gc.anchor = GridBagConstraints.NORTHWEST; 110 gc.fill = GridBagConstraints.HORIZONTAL; 111 gc.weightx = 0.0; 112 gc.insets = new Insets(0,0,3,3); 113 pnl.add(new JLabel(tr("Username: ")), gc); 114 115 gc.gridx = 1; 116 gc.weightx = 1.0; 117 pnl.add(tfUserName = new JTextField(), gc); 118 SelectAllOnFocusGainedDecorator.decorate(tfUserName); 119 valUserName = new UserNameValidator(tfUserName); 120 valUserName.validate(); 121 122 // the password input field 123 gc.anchor = GridBagConstraints.NORTHWEST; 124 gc.fill = GridBagConstraints.HORIZONTAL; 125 gc.gridy = 2; 126 gc.gridx = 0; 127 gc.weightx = 0.0; 128 pnl.add(new JLabel(tr("Password: ")), gc); 129 130 gc.gridx = 1; 131 gc.weightx = 1.0; 132 pnl.add(tfPassword = new JPasswordField(), gc); 133 SelectAllOnFocusGainedDecorator.decorate(tfPassword); 134 valPassword = new PasswordValidator(tfPassword); 135 valPassword.validate(); 136 137 gc.gridy = 3; 138 gc.gridx = 0; 139 gc.anchor = GridBagConstraints.NORTHWEST; 140 gc.fill = GridBagConstraints.HORIZONTAL; 141 gc.weightx = 1.0; 142 gc.gridwidth = 2; 143 pnlMessage = new HtmlPanel(); 144 kit = (HTMLEditorKit)pnlMessage.getEditorPane().getEditorKit(); 145 kit.getStyleSheet().addRule(".warning-body {background-color:rgb(253,255,221);padding: 10pt; border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}"); 146 kit.getStyleSheet().addRule("ol {margin-left: 1cm}"); 147 pnlMessage.setText("<html><body>" 148 + "<p class=\"warning-body\">" 149 + tr("<strong>Warning:</strong> JOSM does login <strong>once</strong> using a secure connection.") 150 + "</p>" 151 + "</body></html>"); 152 pnl.add(pnlMessage, gc); 153 154 // filler - grab remaining space 155 gc.gridy = 4; 156 gc.gridwidth = 2; 157 gc.fill = GridBagConstraints.BOTH; 158 gc.weightx = 1.0; 159 gc.weighty = 1.0; 160 pnl.add(new JPanel(), gc); 161 162 return pnl; 163 } 164 165 protected JPanel buildPropertiesPanel() { 166 JPanel pnl = new JPanel(new BorderLayout()); 167 168 JTabbedPane tpProperties = new JTabbedPane(); 169 tpProperties.add(VerticallyScrollablePanel.embed(buildUserNamePasswordPanel())); 170 tpProperties.add(VerticallyScrollablePanel.embed(buildGrantsPanel())); 171 tpProperties.add(VerticallyScrollablePanel.embed(getAdvancedPropertiesPanel())); 172 tpProperties.setTitleAt(0, tr("Basic")); 173 tpProperties.setTitleAt(1, tr("Granted rights")); 174 tpProperties.setTitleAt(2, tr("Advanced OAuth properties")); 175 176 pnl.add(tpProperties, BorderLayout.CENTER); 177 return pnl; 178 } 179 180 /** 181 * Initializes the panel with values from the preferences 182 */ 183 @Override 184 public void initFromPreferences(Preferences pref) { 185 super.initFromPreferences(pref); 186 CredentialsAgent cm = CredentialsManager.getInstance(); 187 try { 188 PasswordAuthentication pa = cm.lookup(RequestorType.SERVER, OsmApi.getOsmApi().getHost()); 189 if (pa == null) { 190 tfUserName.setText(""); 191 tfPassword.setText(""); 192 } else { 193 tfUserName.setText(pa.getUserName() == null ? "" : pa.getUserName()); 194 tfPassword.setText(pa.getPassword() == null ? "" : String.valueOf(pa.getPassword())); 195 } 196 } catch(CredentialsAgentException e) { 197 e.printStackTrace(); 198 tfUserName.setText(""); 199 tfPassword.setText(""); 200 } 201 } 202 203 /** 204 * Builds the panel with the action button for starting the authorisation 205 * 206 * @return 207 */ 208 protected JPanel buildActionButtonPanel() { 209 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); 210 211 RunAuthorisationAction runAuthorisationAction= new RunAuthorisationAction(); 212 tfPassword.getDocument().addDocumentListener(runAuthorisationAction); 213 tfUserName.getDocument().addDocumentListener(runAuthorisationAction); 214 pnl.add(new SideButton(runAuthorisationAction)); 215 return pnl; 216 } 217 218 /** 219 * Builds the panel which displays the generated Access Token. 220 * 221 * @return 222 */ 223 protected JPanel buildResultsPanel() { 224 JPanel pnl = new JPanel(new GridBagLayout()); 225 GridBagConstraints gc = new GridBagConstraints(); 226 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 227 228 // the message panel 229 gc.anchor = GridBagConstraints.NORTHWEST; 230 gc.fill = GridBagConstraints.HORIZONTAL; 231 gc.weightx = 1.0; 232 JMultilineLabel msg = new JMultilineLabel(""); 233 msg.setFont(msg.getFont().deriveFont(Font.PLAIN)); 234 String lbl = tr("Accept Access Token"); 235 msg.setText(tr("<html>" 236 + "You have successfully retrieved an OAuth Access Token from the OSM website. " 237 + "Click on <strong>{0}</strong> to accept the token. JOSM will use it in " 238 + "subsequent requests to gain access to the OSM API." 239 + "</html>",lbl)); 240 pnl.add(msg, gc); 241 242 // infos about the access token 243 gc.gridy = 1; 244 gc.insets = new Insets(5,0,0,0); 245 pnl.add(pnlAccessTokenInfo = new AccessTokenInfoPanel(), gc); 246 247 // the actions 248 JPanel pnl1 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 249 pnl1.add(new SideButton(new BackAction())); 250 pnl1.add(new SideButton(new TestAccessTokenAction())); 251 gc.gridy = 2; 252 pnl.add(pnl1, gc); 253 254 // filler - grab the remaining space 255 gc.gridy = 3; 256 gc.fill = GridBagConstraints.BOTH; 257 gc.weightx = 1.0; 258 gc.weighty = 1.0; 259 pnl.add(new JPanel(), gc); 260 261 return pnl; 262 } 263 264 protected void build() { 265 setLayout(new BorderLayout()); 266 pnlPropertiesPanel = buildPropertiesPanel(); 267 pnlActionButtonsPanel = buildActionButtonPanel(); 268 pnlResult = buildResultsPanel(); 269 270 prepareUIForEnteringRequest(); 271 } 272 273 /** 274 * Prepares the UI for the first step in the automatic process: entering the authentication 275 * and authorisation parameters. 276 * 277 */ 278 protected void prepareUIForEnteringRequest() { 279 removeAll(); 280 add(pnlPropertiesPanel, BorderLayout.CENTER); 281 add(pnlActionButtonsPanel, BorderLayout.SOUTH); 282 pnlPropertiesPanel.revalidate(); 283 pnlActionButtonsPanel.revalidate(); 284 validate(); 285 repaint(); 286 287 setAccessToken(null); 288 } 289 290 /** 291 * Prepares the UI for the second step in the automatic process: displaying the access token 292 * 293 */ 294 protected void prepareUIForResultDisplay() { 295 removeAll(); 296 add(pnlResult, BorderLayout.CENTER); 297 validate(); 298 repaint(); 299 } 300 301 protected String getOsmUserName() { 302 return tfUserName.getText(); 303 } 304 305 protected String getOsmPassword() { 306 return String.valueOf(tfPassword.getPassword()); 307 } 308 309 /** 310 * Constructs a new {@code FullyAutomaticAuthorizationUI} for the given API URL. 311 * @param apiUrl The OSM API URL 312 * @since 5422 313 */ 314 public FullyAutomaticAuthorizationUI(String apiUrl) { 315 super(apiUrl); 316 build(); 317 } 318 319 @Override 320 public boolean isSaveAccessTokenToPreferences() { 321 return pnlAccessTokenInfo.isSaveToPreferences(); 322 } 323 324 @Override 325 protected void setAccessToken(OAuthToken accessToken) { 326 super.setAccessToken(accessToken); 327 pnlAccessTokenInfo.setAccessToken(accessToken); 328 } 329 330 /** 331 * Starts the authorisation process 332 */ 333 class RunAuthorisationAction extends AbstractAction implements DocumentListener{ 334 public RunAuthorisationAction() { 335 putValue(NAME, tr("Authorize now")); 336 putValue(SMALL_ICON, ImageProvider.get("oauth", "oauth")); 337 putValue(SHORT_DESCRIPTION, tr("Click to redirect you to the authorization form on the JOSM web site")); 338 updateEnabledState(); 339 } 340 341 public void actionPerformed(ActionEvent evt) { 342 Main.worker.submit(new FullyAutomaticAuthorisationTask(FullyAutomaticAuthorizationUI.this)); 343 } 344 345 protected void updateEnabledState() { 346 setEnabled(valPassword.isValid() && valUserName.isValid()); 347 } 348 349 public void changedUpdate(DocumentEvent e) { 350 updateEnabledState(); 351 } 352 353 public void insertUpdate(DocumentEvent e) { 354 updateEnabledState(); 355 } 356 357 public void removeUpdate(DocumentEvent e) { 358 updateEnabledState(); 359 } 360 } 361 362 /** 363 * Action to go back to step 1 in the process 364 */ 365 class BackAction extends AbstractAction { 366 public BackAction() { 367 putValue(NAME, tr("Back")); 368 putValue(SHORT_DESCRIPTION, tr("Run the automatic authorization steps again")); 369 putValue(SMALL_ICON, ImageProvider.get("dialogs", "previous")); 370 } 371 372 public void actionPerformed(ActionEvent arg0) { 373 prepareUIForEnteringRequest(); 374 } 375 } 376 377 /** 378 * Action to test an access token. 379 */ 380 class TestAccessTokenAction extends AbstractAction { 381 public TestAccessTokenAction() { 382 putValue(NAME, tr("Test Access Token")); 383 /* putValue(SHORT_DESCRIPTION, ""); */ 384 putValue(SMALL_ICON, ImageProvider.get("about")); 385 } 386 387 public void actionPerformed(ActionEvent arg0) { 388 Main.worker.submit(new TestAccessTokenTask( 389 FullyAutomaticAuthorizationUI.this, 390 getApiUrl(), 391 getAdvancedPropertiesPanel().getAdvancedParameters(), 392 getAccessToken() 393 )); 394 } 395 } 396 397 398 static private class UserNameValidator extends AbstractTextComponentValidator { 399 public UserNameValidator(JTextComponent tc) { 400 super(tc); 401 } 402 403 @Override 404 public boolean isValid() { 405 return getComponent().getText().trim().length() > 0; 406 } 407 408 @Override 409 public void validate() { 410 if (isValid()) { 411 feedbackValid(tr("Please enter your OSM user name")); 412 } else { 413 feedbackInvalid(tr("The user name cannot be empty. Please enter your OSM user name")); 414 } 415 } 416 } 417 418 static private class PasswordValidator extends AbstractTextComponentValidator { 419 420 public PasswordValidator(JTextComponent tc) { 421 super(tc); 422 } 423 424 @Override 425 public boolean isValid() { 426 return getComponent().getText().trim().length() > 0; 427 } 428 429 @Override 430 public void validate() { 431 if (isValid()) { 432 feedbackValid(tr("Please enter your OSM password")); 433 } else { 434 feedbackInvalid(tr("The password cannot be empty. Please enter your OSM password")); 435 } 436 } 437 } 438 439 class FullyAutomaticAuthorisationTask extends PleaseWaitRunnable { 440 private boolean canceled; 441 private OsmOAuthAuthorizationClient authClient; 442 443 public FullyAutomaticAuthorisationTask(Component parent) { 444 super(parent, tr("Authorize JOSM to access the OSM API"), false /* don't ignore exceptions */); 445 } 446 447 @Override 448 protected void cancel() { 449 canceled = true; 450 } 451 452 @Override 453 protected void finish() {} 454 455 protected void alertAuthorisationFailed(OsmOAuthAuthorizationException e) { 456 HelpAwareOptionPane.showOptionDialog( 457 FullyAutomaticAuthorizationUI.this, 458 tr("<html>" 459 + "The automatic process for retrieving an OAuth Access Token<br>" 460 + "from the OSM server failed.<br><br>" 461 + "Please try again or choose another kind of authorization process,<br>" 462 + "i.e. semi-automatic or manual authorization." 463 +"</html>"), 464 tr("OAuth authorization failed"), 465 JOptionPane.ERROR_MESSAGE, 466 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#FullyAutomaticProcessFailed") 467 ); 468 } 469 470 protected void alertInvalidLoginUrl() { 471 HelpAwareOptionPane.showOptionDialog( 472 FullyAutomaticAuthorizationUI.this, 473 tr("<html>" 474 + "The automatic process for retrieving an OAuth Access Token<br>" 475 + "from the OSM server failed because JOSM was not able to build<br>" 476 + "a valid login URL from the OAuth Authorize Endpoint URL ''{0}''.<br><br>" 477 + "Please check your advanced setting and try again." 478 + "</html>", 479 getAdvancedPropertiesPanel().getAdvancedParameters().getAuthoriseUrl()), 480 tr("OAuth authorization failed"), 481 JOptionPane.ERROR_MESSAGE, 482 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#FullyAutomaticProcessFailed") 483 ); 484 } 485 486 protected void alertLoginFailed(OsmLoginFailedException e) { 487 String loginUrl = null; 488 try { 489 loginUrl = authClient.buildOsmLoginUrl(); 490 } catch(OsmOAuthAuthorizationException e1) { 491 alertInvalidLoginUrl(); 492 return; 493 } 494 HelpAwareOptionPane.showOptionDialog( 495 FullyAutomaticAuthorizationUI.this, 496 tr("<html>" 497 + "The automatic process for retrieving an OAuth Access Token<br>" 498 + "from the OSM server failed. JOSM failed to log into {0}<br>" 499 + "for user {1}.<br><br>" 500 + "Please check username and password and try again." 501 +"</html>", 502 loginUrl, 503 getOsmUserName()), 504 tr("OAuth authorization failed"), 505 JOptionPane.ERROR_MESSAGE, 506 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#FullyAutomaticProcessFailed") 507 ); 508 } 509 510 protected void handleException(final OsmOAuthAuthorizationException e) { 511 Runnable r = new Runnable() { 512 public void run() { 513 if (e instanceof OsmLoginFailedException) { 514 alertLoginFailed((OsmLoginFailedException)e); 515 } else { 516 alertAuthorisationFailed(e); 517 } 518 } 519 }; 520 e.printStackTrace(); 521 GuiHelper.runInEDT(r); 522 } 523 524 @Override 525 protected void realRun() throws SAXException, IOException, OsmTransferException { 526 try { 527 getProgressMonitor().setTicksCount(3); 528 authClient = new OsmOAuthAuthorizationClient( 529 getAdvancedPropertiesPanel().getAdvancedParameters() 530 ); 531 OAuthToken requestToken = authClient.getRequestToken( 532 getProgressMonitor().createSubTaskMonitor(1, false) 533 ); 534 getProgressMonitor().worked(1); 535 if (canceled)return; 536 authClient.authorise( 537 requestToken, 538 getOsmUserName(), 539 getOsmPassword(), 540 pnlOsmPrivileges.getPrivileges(), 541 getProgressMonitor().createSubTaskMonitor(1, false) 542 ); 543 getProgressMonitor().worked(1); 544 if (canceled)return; 545 final OAuthToken accessToken = authClient.getAccessToken( 546 getProgressMonitor().createSubTaskMonitor(1,false) 547 ); 548 getProgressMonitor().worked(1); 549 if (canceled)return; 550 GuiHelper.runInEDT(new Runnable() { 551 public void run() { 552 prepareUIForResultDisplay(); 553 setAccessToken(accessToken); 554 } 555 }); 556 } catch(final OsmOAuthAuthorizationException e) { 557 handleException(e); 558 } 559 } 560 } 561 }