001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.oauth; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BorderLayout; 007import java.awt.Component; 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.io.IOException; 015import java.net.Authenticator.RequestorType; 016import java.net.PasswordAuthentication; 017 018import javax.swing.AbstractAction; 019import javax.swing.BorderFactory; 020import javax.swing.JLabel; 021import javax.swing.JOptionPane; 022import javax.swing.JPanel; 023import javax.swing.JTabbedPane; 024import javax.swing.event.DocumentEvent; 025import javax.swing.event.DocumentListener; 026import javax.swing.text.JTextComponent; 027import javax.swing.text.html.HTMLEditorKit; 028 029import org.openstreetmap.josm.Main; 030import org.openstreetmap.josm.data.Preferences; 031import org.openstreetmap.josm.data.oauth.OAuthToken; 032import org.openstreetmap.josm.gui.HelpAwareOptionPane; 033import org.openstreetmap.josm.gui.PleaseWaitRunnable; 034import org.openstreetmap.josm.gui.SideButton; 035import org.openstreetmap.josm.gui.help.HelpUtil; 036import org.openstreetmap.josm.gui.util.GuiHelper; 037import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 038import org.openstreetmap.josm.gui.widgets.HtmlPanel; 039import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 040import org.openstreetmap.josm.gui.widgets.JosmPasswordField; 041import org.openstreetmap.josm.gui.widgets.JosmTextField; 042import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator; 043import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel; 044import org.openstreetmap.josm.io.OsmApi; 045import org.openstreetmap.josm.io.OsmTransferException; 046import org.openstreetmap.josm.io.auth.CredentialsAgent; 047import org.openstreetmap.josm.io.auth.CredentialsAgentException; 048import org.openstreetmap.josm.io.auth.CredentialsManager; 049import org.openstreetmap.josm.tools.ImageProvider; 050import 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 */ 058public class FullyAutomaticAuthorizationUI extends AbstractAuthorizationUI { 059 060 private JosmTextField tfUserName; 061 private JosmPasswordField 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 constructed panel for the privileges 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 constructed panel for the creditentials 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 JosmTextField(), 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 JosmPasswordField(), 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 * @param pref Preferences structure 183 */ 184 @Override 185 public void initFromPreferences(Preferences pref) { 186 super.initFromPreferences(pref); 187 CredentialsAgent cm = CredentialsManager.getInstance(); 188 try { 189 PasswordAuthentication pa = cm.lookup(RequestorType.SERVER, OsmApi.getOsmApi().getHost()); 190 if (pa == null) { 191 tfUserName.setText(""); 192 tfPassword.setText(""); 193 } else { 194 tfUserName.setText(pa.getUserName() == null ? "" : pa.getUserName()); 195 tfPassword.setText(pa.getPassword() == null ? "" : String.valueOf(pa.getPassword())); 196 } 197 } catch(CredentialsAgentException e) { 198 e.printStackTrace(); 199 tfUserName.setText(""); 200 tfPassword.setText(""); 201 } 202 } 203 204 /** 205 * Builds the panel with the action button for starting the authorisation 206 * 207 * @return constructed button panel 208 */ 209 protected JPanel buildActionButtonPanel() { 210 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); 211 212 RunAuthorisationAction runAuthorisationAction= new RunAuthorisationAction(); 213 tfPassword.getDocument().addDocumentListener(runAuthorisationAction); 214 tfUserName.getDocument().addDocumentListener(runAuthorisationAction); 215 pnl.add(new SideButton(runAuthorisationAction)); 216 return pnl; 217 } 218 219 /** 220 * Builds the panel which displays the generated Access Token. 221 * 222 * @return constructed panel for the results 223 */ 224 protected JPanel buildResultsPanel() { 225 JPanel pnl = new JPanel(new GridBagLayout()); 226 GridBagConstraints gc = new GridBagConstraints(); 227 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 228 229 // the message panel 230 gc.anchor = GridBagConstraints.NORTHWEST; 231 gc.fill = GridBagConstraints.HORIZONTAL; 232 gc.weightx = 1.0; 233 JMultilineLabel msg = new JMultilineLabel(""); 234 msg.setFont(msg.getFont().deriveFont(Font.PLAIN)); 235 String lbl = tr("Accept Access Token"); 236 msg.setText(tr("<html>" 237 + "You have successfully retrieved an OAuth Access Token from the OSM website. " 238 + "Click on <strong>{0}</strong> to accept the token. JOSM will use it in " 239 + "subsequent requests to gain access to the OSM API." 240 + "</html>",lbl)); 241 pnl.add(msg, gc); 242 243 // infos about the access token 244 gc.gridy = 1; 245 gc.insets = new Insets(5,0,0,0); 246 pnl.add(pnlAccessTokenInfo = new AccessTokenInfoPanel(), gc); 247 248 // the actions 249 JPanel pnl1 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 250 pnl1.add(new SideButton(new BackAction())); 251 pnl1.add(new SideButton(new TestAccessTokenAction())); 252 gc.gridy = 2; 253 pnl.add(pnl1, gc); 254 255 // filler - grab the remaining space 256 gc.gridy = 3; 257 gc.fill = GridBagConstraints.BOTH; 258 gc.weightx = 1.0; 259 gc.weighty = 1.0; 260 pnl.add(new JPanel(), gc); 261 262 return pnl; 263 } 264 265 protected void build() { 266 setLayout(new BorderLayout()); 267 pnlPropertiesPanel = buildPropertiesPanel(); 268 pnlActionButtonsPanel = buildActionButtonPanel(); 269 pnlResult = buildResultsPanel(); 270 271 prepareUIForEnteringRequest(); 272 } 273 274 /** 275 * Prepares the UI for the first step in the automatic process: entering the authentication 276 * and authorisation parameters. 277 * 278 */ 279 protected void prepareUIForEnteringRequest() { 280 removeAll(); 281 add(pnlPropertiesPanel, BorderLayout.CENTER); 282 add(pnlActionButtonsPanel, BorderLayout.SOUTH); 283 pnlPropertiesPanel.revalidate(); 284 pnlActionButtonsPanel.revalidate(); 285 validate(); 286 repaint(); 287 288 setAccessToken(null); 289 } 290 291 /** 292 * Prepares the UI for the second step in the automatic process: displaying the access token 293 * 294 */ 295 protected void prepareUIForResultDisplay() { 296 removeAll(); 297 add(pnlResult, BorderLayout.CENTER); 298 validate(); 299 repaint(); 300 } 301 302 protected String getOsmUserName() { 303 return tfUserName.getText(); 304 } 305 306 protected String getOsmPassword() { 307 return String.valueOf(tfPassword.getPassword()); 308 } 309 310 /** 311 * Constructs a new {@code FullyAutomaticAuthorizationUI} for the given API URL. 312 * @param apiUrl The OSM API URL 313 * @since 5422 314 */ 315 public FullyAutomaticAuthorizationUI(String apiUrl) { 316 super(apiUrl); 317 build(); 318 } 319 320 @Override 321 public boolean isSaveAccessTokenToPreferences() { 322 return pnlAccessTokenInfo.isSaveToPreferences(); 323 } 324 325 @Override 326 protected void setAccessToken(OAuthToken accessToken) { 327 super.setAccessToken(accessToken); 328 pnlAccessTokenInfo.setAccessToken(accessToken); 329 } 330 331 /** 332 * Starts the authorisation process 333 */ 334 class RunAuthorisationAction extends AbstractAction implements DocumentListener{ 335 public RunAuthorisationAction() { 336 putValue(NAME, tr("Authorize now")); 337 putValue(SMALL_ICON, ImageProvider.get("oauth", "oauth")); 338 putValue(SHORT_DESCRIPTION, tr("Click to redirect you to the authorization form on the JOSM web site")); 339 updateEnabledState(); 340 } 341 342 @Override 343 public void actionPerformed(ActionEvent evt) { 344 Main.worker.submit(new FullyAutomaticAuthorisationTask(FullyAutomaticAuthorizationUI.this)); 345 } 346 347 protected void updateEnabledState() { 348 setEnabled(valPassword.isValid() && valUserName.isValid()); 349 } 350 351 @Override 352 public void changedUpdate(DocumentEvent e) { 353 updateEnabledState(); 354 } 355 356 @Override 357 public void insertUpdate(DocumentEvent e) { 358 updateEnabledState(); 359 } 360 361 @Override 362 public void removeUpdate(DocumentEvent e) { 363 updateEnabledState(); 364 } 365 } 366 367 /** 368 * Action to go back to step 1 in the process 369 */ 370 class BackAction extends AbstractAction { 371 public BackAction() { 372 putValue(NAME, tr("Back")); 373 putValue(SHORT_DESCRIPTION, tr("Run the automatic authorization steps again")); 374 putValue(SMALL_ICON, ImageProvider.get("dialogs", "previous")); 375 } 376 377 @Override 378 public void actionPerformed(ActionEvent arg0) { 379 prepareUIForEnteringRequest(); 380 } 381 } 382 383 /** 384 * Action to test an access token. 385 */ 386 class TestAccessTokenAction extends AbstractAction { 387 public TestAccessTokenAction() { 388 putValue(NAME, tr("Test Access Token")); 389 putValue(SMALL_ICON, ImageProvider.get("about")); 390 } 391 392 @Override 393 public void actionPerformed(ActionEvent arg0) { 394 Main.worker.submit(new TestAccessTokenTask( 395 FullyAutomaticAuthorizationUI.this, 396 getApiUrl(), 397 getAdvancedPropertiesPanel().getAdvancedParameters(), 398 getAccessToken() 399 )); 400 } 401 } 402 403 404 static private class UserNameValidator extends AbstractTextComponentValidator { 405 public UserNameValidator(JTextComponent tc) { 406 super(tc); 407 } 408 409 @Override 410 public boolean isValid() { 411 return getComponent().getText().trim().length() > 0; 412 } 413 414 @Override 415 public void validate() { 416 if (isValid()) { 417 feedbackValid(tr("Please enter your OSM user name")); 418 } else { 419 feedbackInvalid(tr("The user name cannot be empty. Please enter your OSM user name")); 420 } 421 } 422 } 423 424 static private class PasswordValidator extends AbstractTextComponentValidator { 425 426 public PasswordValidator(JTextComponent tc) { 427 super(tc); 428 } 429 430 @Override 431 public boolean isValid() { 432 return getComponent().getText().trim().length() > 0; 433 } 434 435 @Override 436 public void validate() { 437 if (isValid()) { 438 feedbackValid(tr("Please enter your OSM password")); 439 } else { 440 feedbackInvalid(tr("The password cannot be empty. Please enter your OSM password")); 441 } 442 } 443 } 444 445 class FullyAutomaticAuthorisationTask extends PleaseWaitRunnable { 446 private boolean canceled; 447 private OsmOAuthAuthorizationClient authClient; 448 449 public FullyAutomaticAuthorisationTask(Component parent) { 450 super(parent, tr("Authorize JOSM to access the OSM API"), false /* don't ignore exceptions */); 451 } 452 453 @Override 454 protected void cancel() { 455 canceled = true; 456 } 457 458 @Override 459 protected void finish() {} 460 461 protected void alertAuthorisationFailed(OsmOAuthAuthorizationException e) { 462 HelpAwareOptionPane.showOptionDialog( 463 FullyAutomaticAuthorizationUI.this, 464 tr("<html>" 465 + "The automatic process for retrieving an OAuth Access Token<br>" 466 + "from the OSM server failed.<br><br>" 467 + "Please try again or choose another kind of authorization process,<br>" 468 + "i.e. semi-automatic or manual authorization." 469 +"</html>"), 470 tr("OAuth authorization failed"), 471 JOptionPane.ERROR_MESSAGE, 472 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#FullyAutomaticProcessFailed") 473 ); 474 } 475 476 protected void alertInvalidLoginUrl() { 477 HelpAwareOptionPane.showOptionDialog( 478 FullyAutomaticAuthorizationUI.this, 479 tr("<html>" 480 + "The automatic process for retrieving an OAuth Access Token<br>" 481 + "from the OSM server failed because JOSM was not able to build<br>" 482 + "a valid login URL from the OAuth Authorize Endpoint URL ''{0}''.<br><br>" 483 + "Please check your advanced setting and try again." 484 + "</html>", 485 getAdvancedPropertiesPanel().getAdvancedParameters().getAuthoriseUrl()), 486 tr("OAuth authorization failed"), 487 JOptionPane.ERROR_MESSAGE, 488 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#FullyAutomaticProcessFailed") 489 ); 490 } 491 492 protected void alertLoginFailed(OsmLoginFailedException e) { 493 String loginUrl = null; 494 try { 495 loginUrl = authClient.buildOsmLoginUrl(); 496 } catch(OsmOAuthAuthorizationException e1) { 497 alertInvalidLoginUrl(); 498 return; 499 } 500 HelpAwareOptionPane.showOptionDialog( 501 FullyAutomaticAuthorizationUI.this, 502 tr("<html>" 503 + "The automatic process for retrieving an OAuth Access Token<br>" 504 + "from the OSM server failed. JOSM failed to log into {0}<br>" 505 + "for user {1}.<br><br>" 506 + "Please check username and password and try again." 507 +"</html>", 508 loginUrl, 509 getOsmUserName()), 510 tr("OAuth authorization failed"), 511 JOptionPane.ERROR_MESSAGE, 512 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#FullyAutomaticProcessFailed") 513 ); 514 } 515 516 protected void handleException(final OsmOAuthAuthorizationException e) { 517 Runnable r = new Runnable() { 518 @Override 519 public void run() { 520 if (e instanceof OsmLoginFailedException) { 521 alertLoginFailed((OsmLoginFailedException)e); 522 } else { 523 alertAuthorisationFailed(e); 524 } 525 } 526 }; 527 e.printStackTrace(); 528 GuiHelper.runInEDT(r); 529 } 530 531 @Override 532 protected void realRun() throws SAXException, IOException, OsmTransferException { 533 try { 534 getProgressMonitor().setTicksCount(3); 535 authClient = new OsmOAuthAuthorizationClient( 536 getAdvancedPropertiesPanel().getAdvancedParameters() 537 ); 538 OAuthToken requestToken = authClient.getRequestToken( 539 getProgressMonitor().createSubTaskMonitor(1, false) 540 ); 541 getProgressMonitor().worked(1); 542 if (canceled)return; 543 authClient.authorise( 544 requestToken, 545 getOsmUserName(), 546 getOsmPassword(), 547 pnlOsmPrivileges.getPrivileges(), 548 getProgressMonitor().createSubTaskMonitor(1, false) 549 ); 550 getProgressMonitor().worked(1); 551 if (canceled)return; 552 final OAuthToken accessToken = authClient.getAccessToken( 553 getProgressMonitor().createSubTaskMonitor(1,false) 554 ); 555 getProgressMonitor().worked(1); 556 if (canceled)return; 557 GuiHelper.runInEDT(new Runnable() { 558 @Override 559 public void run() { 560 prepareUIForResultDisplay(); 561 setAccessToken(accessToken); 562 } 563 }); 564 } catch(final OsmOAuthAuthorizationException e) { 565 handleException(e); 566 } 567 } 568 } 569}