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}