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 }