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