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