source: josm/trunk/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java

Last change on this file was 19008, checked in by taylor.smock, 6 weeks ago

Fix an issue with custom OAuth2 parameters where the custom parameters would be replaced by default parameters

  • Property svn:eol-style set to native
File size: 18.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.oauth;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.FlowLayout;
10import java.awt.Font;
11import java.awt.GridBagLayout;
12import java.awt.event.ActionEvent;
13import java.awt.event.ComponentAdapter;
14import java.awt.event.ComponentEvent;
15import java.awt.event.WindowAdapter;
16import java.awt.event.WindowEvent;
17import java.beans.PropertyChangeEvent;
18import java.beans.PropertyChangeListener;
19import java.lang.reflect.InvocationTargetException;
20import java.net.URL;
21import java.util.Objects;
22import java.util.Optional;
23import java.util.concurrent.Executor;
24import java.util.concurrent.FutureTask;
25import java.util.function.Consumer;
26
27import javax.swing.AbstractAction;
28import javax.swing.BorderFactory;
29import javax.swing.JButton;
30import javax.swing.JDialog;
31import javax.swing.JOptionPane;
32import javax.swing.JPanel;
33import javax.swing.JScrollPane;
34import javax.swing.SwingUtilities;
35import javax.swing.UIManager;
36import javax.swing.text.html.HTMLEditorKit;
37
38import org.openstreetmap.josm.data.oauth.IOAuthParameters;
39import org.openstreetmap.josm.data.oauth.IOAuthToken;
40import org.openstreetmap.josm.data.oauth.OAuth20Authorization;
41import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
42import org.openstreetmap.josm.data.oauth.OAuthParameters;
43import org.openstreetmap.josm.data.oauth.OAuthVersion;
44import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
45import org.openstreetmap.josm.gui.MainApplication;
46import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
47import org.openstreetmap.josm.gui.help.HelpUtil;
48import org.openstreetmap.josm.gui.util.GuiHelper;
49import org.openstreetmap.josm.gui.util.WindowGeometry;
50import org.openstreetmap.josm.gui.widgets.HtmlPanel;
51import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
52import org.openstreetmap.josm.io.auth.CredentialsManager;
53import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
54import org.openstreetmap.josm.spi.preferences.Config;
55import org.openstreetmap.josm.tools.GBC;
56import org.openstreetmap.josm.tools.ImageProvider;
57import org.openstreetmap.josm.tools.InputMapUtils;
58import org.openstreetmap.josm.tools.UserCancelException;
59import org.openstreetmap.josm.tools.Utils;
60
61/**
62 * This wizard walks the user to the necessary steps to retrieve an OAuth Access Token which
63 * allows JOSM to access the OSM API on the users behalf.
64 * @since 2746
65 */
66public class OAuthAuthorizationWizard extends JDialog {
67 private boolean canceled;
68 private final AuthorizationProcedure procedure;
69 private final String apiUrl;
70 private final OAuthVersion oAuthVersion;
71
72 private FullyAutomaticAuthorizationUI pnlFullyAutomaticAuthorisationUI;
73 private ManualAuthorizationUI pnlManualAuthorisationUI;
74 private JScrollPane spAuthorisationProcedureUI;
75 private final transient Executor executor;
76
77 /**
78 * Launches the wizard, {@link OAuthAccessTokenHolder#setAccessToken(String, IOAuthToken)} sets the token
79 * and {@link OAuthAccessTokenHolder#setSaveToPreferences(boolean) saves to preferences}.
80 * @param callback Callback to run when authorization is finished
81 * @throws UserCancelException if user cancels the operation
82 */
83 public void showDialog(Consumer<Optional<IOAuthToken>> callback) throws UserCancelException {
84 if ((this.oAuthVersion == OAuthVersion.OAuth20 || this.oAuthVersion == OAuthVersion.OAuth21)
85 && this.procedure == AuthorizationProcedure.FULLY_AUTOMATIC) {
86 authorize(true, callback, this.apiUrl, this.oAuthVersion, getOAuthParameters());
87 } else {
88 setVisible(true);
89 if (isCanceled()) {
90 throw new UserCancelException();
91 }
92 OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
93 holder.setAccessToken(apiUrl, getAccessToken());
94 holder.setSaveToPreferences(isSaveAccessTokenToPreferences());
95 }
96 }
97
98 /**
99 * Perform the oauth dance
100 *
101 * @param startRemoteControl {@code true} to start remote control if it is not already running
102 * @param callback The callback to use to notify that the OAuth dance succeeded
103 * @param apiUrl The API URL to get the token for
104 * @param oAuthVersion The OAuth version that the authorization dance is force
105 * @param oAuthParameters The OAuth parameters to use
106 */
107 static void authorize(boolean startRemoteControl, Consumer<Optional<IOAuthToken>> callback, String apiUrl,
108 OAuthVersion oAuthVersion, IOAuthParameters oAuthParameters) {
109 final boolean remoteControlIsRunning = Boolean.TRUE.equals(RemoteControl.PROP_REMOTECONTROL_ENABLED.get());
110 // TODO: Ask user if they want to start remote control?
111 if (!remoteControlIsRunning && startRemoteControl) {
112 RemoteControl.start();
113 }
114 new OAuth20Authorization().authorize(
115 Optional.ofNullable(oAuthParameters).orElseGet(() -> OAuthParameters.createDefault(apiUrl, oAuthVersion)),
116 token -> {
117 if (!remoteControlIsRunning) {
118 RemoteControl.stop();
119 }
120 OAuthAccessTokenHolder.getInstance().setAccessToken(apiUrl, token.orElse(null));
121 OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
122 if (!token.isPresent()) {
123 GuiHelper.runInEDT(() -> JOptionPane.showMessageDialog(MainApplication.getMainPanel(),
124 tr("Authentication failed, please check browser for details."),
125 tr("OAuth Authentication Failed"),
126 JOptionPane.ERROR_MESSAGE));
127 }
128 if (callback != null) {
129 callback.accept(token);
130 }
131 }, OsmScopes.read_gpx, OsmScopes.write_gpx,
132 OsmScopes.read_prefs, OsmScopes.write_prefs,
133 OsmScopes.write_api, OsmScopes.write_notes);
134 }
135
136 /**
137 * Builds the row with the action buttons
138 *
139 * @return panel with buttons
140 */
141 protected JPanel buildButtonRow() {
142 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
143
144 AcceptAccessTokenAction actAcceptAccessToken = new AcceptAccessTokenAction();
145 pnlFullyAutomaticAuthorisationUI.addPropertyChangeListener(actAcceptAccessToken);
146 pnlManualAuthorisationUI.addPropertyChangeListener(actAcceptAccessToken);
147
148 pnl.add(new JButton(actAcceptAccessToken));
149 pnl.add(new JButton(new CancelAction()));
150 pnl.add(new JButton(new ContextSensitiveHelpAction(HelpUtil.ht("/Dialog/OAuthAuthorisationWizard"))));
151
152 return pnl;
153 }
154
155 /**
156 * Builds the panel with general information in the header
157 *
158 * @return panel with information display
159 */
160 protected JPanel buildHeaderInfoPanel() {
161 JPanel pnl = new JPanel(new GridBagLayout());
162 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
163
164 // OAuth in a nutshell ...
165 HtmlPanel pnlMessage = new HtmlPanel();
166 pnlMessage.setText("<html><body>"
167 + tr("With OAuth you grant JOSM the right to upload map data and GPS tracks "
168 + "on your behalf (<a href=\"{0}\">more info...</a>).", "https://wiki.openstreetmap.org/wiki/OAuth")
169 + "</body></html>"
170 );
171 pnlMessage.enableClickableHyperlinks();
172 pnl.add(pnlMessage, GBC.eol().fill(GBC.HORIZONTAL));
173
174 // the authorisation procedure
175 JMultilineLabel lbl = new JMultilineLabel(AuthorizationProcedure.FULLY_AUTOMATIC.getDescription());
176 lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
177 pnl.add(lbl, GBC.std());
178
179 if (!Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
180 final HtmlPanel pnlWarning = new HtmlPanel();
181 final HTMLEditorKit kit = (HTMLEditorKit) pnlWarning.getEditorPane().getEditorKit();
182 kit.getStyleSheet().addRule(".warning-body {"
183 + "background-color:rgb(253,255,221);padding: 10pt; "
184 + "border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}");
185 kit.getStyleSheet().addRule("ol {margin-left: 1cm}");
186 pnlWarning.setText("<html><body>"
187 + "<p class=\"warning-body\">"
188 + tr("<strong>Warning:</strong> Since you are using not the default OSM API, " +
189 "make sure to set an OAuth consumer key and secret in the <i>Advanced OAuth parameters</i>.")
190 + "</p>"
191 + "</body></html>");
192 pnl.add(pnlWarning, GBC.eop().fill());
193 }
194
195 return pnl;
196 }
197
198 /**
199 * Refreshes the view of the authorisation panel, depending on the authorisation procedure
200 * currently selected
201 */
202 protected void refreshAuthorisationProcedurePanel() {
203 switch(procedure) {
204 case FULLY_AUTOMATIC:
205 spAuthorisationProcedureUI.getViewport().setView(pnlFullyAutomaticAuthorisationUI);
206 pnlFullyAutomaticAuthorisationUI.revalidate();
207 break;
208 case MANUALLY:
209 spAuthorisationProcedureUI.getViewport().setView(pnlManualAuthorisationUI);
210 pnlManualAuthorisationUI.revalidate();
211 break;
212 default:
213 throw new UnsupportedOperationException("Unsupported auth type: " + procedure);
214 }
215 validate();
216 repaint();
217 }
218
219 /**
220 * builds the UI
221 */
222 protected final void build() {
223 getContentPane().setLayout(new BorderLayout());
224 getContentPane().add(buildHeaderInfoPanel(), BorderLayout.NORTH);
225
226 setTitle(tr("Get an Access Token for ''{0}''", apiUrl));
227 this.setMinimumSize(new Dimension(500, 300));
228
229 pnlFullyAutomaticAuthorisationUI = new FullyAutomaticAuthorizationUI(apiUrl, executor, oAuthVersion);
230 pnlManualAuthorisationUI = new ManualAuthorizationUI(apiUrl, executor, oAuthVersion);
231
232 spAuthorisationProcedureUI = GuiHelper.embedInVerticalScrollPane(new JPanel());
233 spAuthorisationProcedureUI.getVerticalScrollBar().addComponentListener(
234 new ComponentAdapter() {
235 @Override
236 public void componentShown(ComponentEvent e) {
237 spAuthorisationProcedureUI.setBorder(UIManager.getBorder("ScrollPane.border"));
238 }
239
240 @Override
241 public void componentHidden(ComponentEvent e) {
242 spAuthorisationProcedureUI.setBorder(null);
243 }
244 }
245 );
246 getContentPane().add(spAuthorisationProcedureUI, BorderLayout.CENTER);
247 getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
248
249 addWindowListener(new WindowEventHandler());
250 InputMapUtils.addEscapeAction(getRootPane(), new CancelAction());
251
252 refreshAuthorisationProcedurePanel();
253
254 HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/OAuthAuthorisationWizard"));
255 }
256
257 /**
258 * Creates the wizard.
259 *
260 * @param parent the component relative to which the dialog is displayed
261 * @param procedure the authorization procedure to use
262 * @param apiUrl the API URL. Must not be null.
263 * @param executor the executor used for running the HTTP requests for the authorization
264 * @param oAuthVersion The OAuth version this wizard is for
265 * @param advancedParameters The OAuth parameters to initialize the wizard with
266 * @throws IllegalArgumentException if apiUrl is null
267 */
268 public OAuthAuthorizationWizard(Component parent, AuthorizationProcedure procedure, String apiUrl,
269 Executor executor, OAuthVersion oAuthVersion, IOAuthParameters advancedParameters) {
270 super(GuiHelper.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
271 this.procedure = Objects.requireNonNull(procedure, "procedure");
272 this.apiUrl = Objects.requireNonNull(apiUrl, "apiUrl");
273 this.executor = executor;
274 this.oAuthVersion = oAuthVersion;
275 build();
276 if (advancedParameters != null) {
277 pnlFullyAutomaticAuthorisationUI.getAdvancedPropertiesPanel().setAdvancedParameters(advancedParameters);
278 pnlManualAuthorisationUI.getAdvancedPropertiesPanel().setAdvancedParameters(advancedParameters);
279 }
280 }
281
282 /**
283 * Replies true if the dialog was canceled
284 *
285 * @return true if the dialog was canceled
286 */
287 public boolean isCanceled() {
288 return canceled;
289 }
290
291 protected AbstractAuthorizationUI getCurrentAuthorisationUI() {
292 switch(procedure) {
293 case FULLY_AUTOMATIC: return pnlFullyAutomaticAuthorisationUI;
294 case MANUALLY: return pnlManualAuthorisationUI;
295 default: return null;
296 }
297 }
298
299 /**
300 * Replies the Access Token entered using the wizard
301 *
302 * @return the access token. May be null if the wizard was canceled.
303 */
304 public IOAuthToken getAccessToken() {
305 return getCurrentAuthorisationUI().getAccessToken();
306 }
307
308 /**
309 * Replies the current OAuth parameters.
310 *
311 * @return the current OAuth parameters.
312 */
313 public IOAuthParameters getOAuthParameters() {
314 return getCurrentAuthorisationUI().getOAuthParameters();
315 }
316
317 /**
318 * Replies true if the currently selected Access Token shall be saved to
319 * the preferences.
320 *
321 * @return true if the currently selected Access Token shall be saved to
322 * the preferences
323 */
324 public boolean isSaveAccessTokenToPreferences() {
325 return getCurrentAuthorisationUI().isSaveAccessTokenToPreferences();
326 }
327
328 /**
329 * Initializes the dialog with values from the preferences
330 *
331 */
332 public void initFromPreferences() {
333 pnlFullyAutomaticAuthorisationUI.initialize(apiUrl);
334 pnlManualAuthorisationUI.initialize(apiUrl);
335 }
336
337 @Override
338 public void setVisible(boolean visible) {
339 if (visible) {
340 pack();
341 new WindowGeometry(
342 getClass().getName() + ".geometry",
343 WindowGeometry.centerInWindow(
344 MainApplication.getMainFrame(),
345 getPreferredSize()
346 )
347 ).applySafe(this);
348 initFromPreferences();
349 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
350 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
351 }
352 super.setVisible(visible);
353 }
354
355 protected void setCanceled(boolean canceled) {
356 this.canceled = canceled;
357 }
358
359 /**
360 * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}.
361 * @param serverUrl the URL to OSM server
362 * @throws InterruptedException if we're interrupted while waiting for the event dispatching thread to finish OAuth authorization task
363 * @throws InvocationTargetException if an exception is thrown while running OAuth authorization task
364 * @since 12803
365 */
366 public static void obtainAccessToken(final URL serverUrl) throws InvocationTargetException, InterruptedException {
367 final Runnable authTask = new FutureTask<>(() -> {
368 // Concerning Utils.newDirectExecutor: Main worker cannot be used since this connection is already
369 // executed via main worker. The OAuth connections would block otherwise.
370 final OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
371 MainApplication.getMainFrame(),
372 AuthorizationProcedure.FULLY_AUTOMATIC,
373 serverUrl.toString(), Utils.newDirectExecutor(),
374 OAuthVersion.OAuth20, null);
375 wizard.showDialog(null);
376 return wizard;
377 });
378 // exception handling differs from implementation at GuiHelper.runInEDTAndWait()
379 if (SwingUtilities.isEventDispatchThread()) {
380 authTask.run();
381 } else {
382 SwingUtilities.invokeAndWait(authTask);
383 }
384 }
385
386 class CancelAction extends AbstractAction {
387
388 /**
389 * Constructs a new {@code CancelAction}.
390 */
391 CancelAction() {
392 putValue(NAME, tr("Cancel"));
393 new ImageProvider("cancel").getResource().attachImageIcon(this);
394 putValue(SHORT_DESCRIPTION, tr("Close the dialog and cancel authorization"));
395 }
396
397 public void cancel() {
398 setCanceled(true);
399 setVisible(false);
400 }
401
402 @Override
403 public void actionPerformed(ActionEvent evt) {
404 cancel();
405 }
406 }
407
408 class AcceptAccessTokenAction extends AbstractAction implements PropertyChangeListener {
409
410 /**
411 * Constructs a new {@code AcceptAccessTokenAction}.
412 */
413 AcceptAccessTokenAction() {
414 putValue(NAME, tr("Accept Access Token"));
415 new ImageProvider("ok").getResource().attachImageIcon(this);
416 putValue(SHORT_DESCRIPTION, tr("Close the dialog and accept the Access Token"));
417 updateEnabledState(null);
418 }
419
420 @Override
421 public void actionPerformed(ActionEvent evt) {
422 setCanceled(false);
423 setVisible(false);
424 }
425
426 /**
427 * Update the enabled state
428 * @param token The token to use
429 * @since 18991
430 */
431 public final void updateEnabledState(IOAuthToken token) {
432 setEnabled(token != null);
433 }
434
435 @Override
436 public void propertyChange(PropertyChangeEvent evt) {
437 if (!evt.getPropertyName().equals(AbstractAuthorizationUI.ACCESS_TOKEN_PROP))
438 return;
439 updateEnabledState((IOAuthToken) evt.getNewValue());
440 }
441 }
442
443 class WindowEventHandler extends WindowAdapter {
444 @Override
445 public void windowClosing(WindowEvent e) {
446 new CancelAction().cancel();
447 }
448 }
449}
Note: See TracBrowser for help on using the repository browser.