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