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    }