001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Dimension;
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.awt.event.FocusAdapter;
015import java.awt.event.FocusEvent;
016import java.awt.event.KeyAdapter;
017import java.awt.event.KeyEvent;
018import java.awt.event.WindowAdapter;
019import java.awt.event.WindowEvent;
020import java.util.Objects;
021
022import javax.swing.AbstractAction;
023import javax.swing.BorderFactory;
024import javax.swing.JCheckBox;
025import javax.swing.JComponent;
026import javax.swing.JDialog;
027import javax.swing.JLabel;
028import javax.swing.JPanel;
029import javax.swing.JTextField;
030import javax.swing.KeyStroke;
031
032import org.openstreetmap.josm.Main;
033import org.openstreetmap.josm.gui.SideButton;
034import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
035import org.openstreetmap.josm.gui.help.HelpUtil;
036import org.openstreetmap.josm.gui.preferences.server.ProxyPreferencesPanel;
037import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
038import org.openstreetmap.josm.gui.widgets.JosmPasswordField;
039import org.openstreetmap.josm.gui.widgets.JosmTextField;
040import org.openstreetmap.josm.io.OsmApi;
041import org.openstreetmap.josm.tools.ImageProvider;
042import org.openstreetmap.josm.tools.WindowGeometry;
043
044public class CredentialDialog extends JDialog {
045
046    public static CredentialDialog getOsmApiCredentialDialog(String username, String password, String host,
047            String saveUsernameAndPasswordCheckboxText) {
048        CredentialDialog dialog = new CredentialDialog(saveUsernameAndPasswordCheckboxText);
049        if (Objects.equals(OsmApi.getOsmApi().getHost(), host)) {
050            dialog.prepareForOsmApiCredentials(username, password);
051        } else {
052            dialog.prepareForOtherHostCredentials(username, password, host);
053        }
054        dialog.pack();
055        return dialog;
056    }
057
058    public static CredentialDialog getHttpProxyCredentialDialog(String username, String password, String host,
059            String saveUsernameAndPasswordCheckboxText) {
060        CredentialDialog dialog = new CredentialDialog(saveUsernameAndPasswordCheckboxText);
061        dialog.prepareForProxyCredentials(username, password);
062        dialog.pack();
063        return dialog;
064    }
065
066    private boolean canceled;
067    protected CredentialPanel pnlCredentials;
068    private final String saveUsernameAndPasswordCheckboxText;
069
070    public boolean isCanceled() {
071        return canceled;
072    }
073
074    protected void setCanceled(boolean canceled) {
075        this.canceled = canceled;
076    }
077
078    @Override
079    public void setVisible(boolean visible) {
080        if (visible) {
081            WindowGeometry.centerInWindow(Main.parent, new Dimension(350, 300)).applySafe(this);
082        }
083        super.setVisible(visible);
084    }
085
086    protected JPanel createButtonPanel() {
087        JPanel pnl = new JPanel(new FlowLayout());
088        pnl.add(new SideButton(new OKAction()));
089        pnl.add(new SideButton(new CancelAction()));
090        pnl.add(new SideButton(new ContextSensitiveHelpAction(HelpUtil.ht("/Dialog/Password"))));
091        return pnl;
092    }
093
094    protected void build() {
095        getContentPane().setLayout(new BorderLayout());
096        getContentPane().add(createButtonPanel(), BorderLayout.SOUTH);
097
098        addWindowListener(new WindowEventHander());
099        getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
100                KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "escape");
101        getRootPane().getActionMap().put("escape", new CancelAction());
102
103        getRootPane().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
104    }
105
106    public CredentialDialog(String saveUsernameAndPasswordCheckboxText) {
107        this.saveUsernameAndPasswordCheckboxText = saveUsernameAndPasswordCheckboxText;
108        setModalityType(ModalityType.DOCUMENT_MODAL);
109        try {
110            setAlwaysOnTop(true);
111        } catch (SecurityException e) {
112            Main.warn(tr("Failed to put Credential Dialog always on top. Caught security exception."));
113        }
114        build();
115    }
116
117    public void prepareForOsmApiCredentials(String username, String password) {
118        setTitle(tr("Enter credentials for OSM API"));
119        pnlCredentials = new OsmApiCredentialsPanel(this);
120        getContentPane().add(pnlCredentials, BorderLayout.CENTER);
121        pnlCredentials.init(username, password);
122        validate();
123    }
124
125    public void prepareForOtherHostCredentials(String username, String password, String host) {
126        setTitle(tr("Enter credentials for host"));
127        pnlCredentials = new OtherHostCredentialsPanel(this, host);
128        getContentPane().add(pnlCredentials, BorderLayout.CENTER);
129        pnlCredentials.init(username, password);
130        validate();
131    }
132
133    public void prepareForProxyCredentials(String username, String password) {
134        setTitle(tr("Enter credentials for HTTP proxy"));
135        pnlCredentials = new HttpProxyCredentialsPanel(this);
136        getContentPane().add(pnlCredentials, BorderLayout.CENTER);
137        pnlCredentials.init(username, password);
138        validate();
139    }
140
141    public String getUsername() {
142        if (pnlCredentials == null)
143            return null;
144        return pnlCredentials.getUserName();
145    }
146
147    public char[] getPassword() {
148        if (pnlCredentials == null)
149            return null;
150        return pnlCredentials.getPassword();
151    }
152
153    public boolean isSaveCredentials() {
154        if (pnlCredentials == null)
155            return false;
156        return pnlCredentials.isSaveCredentials();
157    }
158
159    protected static class CredentialPanel extends JPanel {
160        protected JosmTextField tfUserName;
161        protected JosmPasswordField tfPassword;
162        protected JCheckBox cbSaveCredentials;
163        protected final JMultilineLabel lblHeading = new JMultilineLabel("");
164        protected final JMultilineLabel lblWarning = new JMultilineLabel("");
165        protected CredentialDialog owner; // owner Dependency Injection to use Key listeners for username and password text fields
166
167        protected void build() {
168            tfUserName = new JosmTextField(20);
169            tfPassword = new JosmPasswordField(20);
170            tfUserName.addFocusListener(new SelectAllOnFocusHandler());
171            tfPassword.addFocusListener(new SelectAllOnFocusHandler());
172            tfUserName.addKeyListener(new TFKeyListener(owner, tfUserName, tfPassword));
173            tfPassword.addKeyListener(new TFKeyListener(owner, tfPassword, tfUserName));
174            cbSaveCredentials = new JCheckBox(owner != null ? owner.saveUsernameAndPasswordCheckboxText : "");
175
176            setLayout(new GridBagLayout());
177            GridBagConstraints gc = new GridBagConstraints();
178            gc.gridwidth = 2;
179            gc.gridheight = 1;
180            gc.fill = GridBagConstraints.HORIZONTAL;
181            gc.weightx = 1.0;
182            gc.weighty = 0.0;
183            gc.insets = new Insets(0, 0, 10, 0);
184            add(lblHeading, gc);
185
186            gc.gridx = 0;
187            gc.gridy = 1;
188            gc.gridwidth = 1;
189            gc.gridheight = 1;
190            gc.fill = GridBagConstraints.HORIZONTAL;
191            gc.weightx = 0.0;
192            gc.weighty = 0.0;
193            gc.insets = new Insets(0, 0, 10, 10);
194            add(new JLabel(tr("Username")), gc);
195            gc.gridx = 1;
196            gc.gridy = 1;
197            gc.weightx = 1.0;
198            add(tfUserName, gc);
199            gc.gridx = 0;
200            gc.gridy = 2;
201            gc.weightx = 0.0;
202            add(new JLabel(tr("Password")), gc);
203
204            gc.gridx = 1;
205            gc.gridy = 2;
206            gc.weightx = 0.0;
207            add(tfPassword, gc);
208
209            gc.gridx = 0;
210            gc.gridy = 3;
211            gc.gridwidth = 2;
212            gc.gridheight = 1;
213            gc.fill = GridBagConstraints.BOTH;
214            gc.weightx = 1.0;
215            gc.weighty = 0.0;
216            lblWarning.setFont(lblWarning.getFont().deriveFont(Font.ITALIC));
217            add(lblWarning, gc);
218
219            gc.gridx = 0;
220            gc.gridy = 4;
221            gc.weighty = 0.0;
222            add(cbSaveCredentials, gc);
223
224            // consume the remaining space
225            gc.gridx = 0;
226            gc.gridy = 5;
227            gc.weighty = 1.0;
228            add(new JPanel(), gc);
229
230        }
231
232        public CredentialPanel(CredentialDialog owner) {
233            this.owner = owner;
234        }
235
236        public void init(String username, String password) {
237            username = username == null ? "" : username;
238            password = password == null ? "" : password;
239            tfUserName.setText(username);
240            tfPassword.setText(password);
241            cbSaveCredentials.setSelected(!username.isEmpty() && !password.isEmpty());
242        }
243
244        public void startUserInput() {
245            tfUserName.requestFocusInWindow();
246        }
247
248        public String getUserName() {
249            return tfUserName.getText();
250        }
251
252        public char[] getPassword() {
253            return tfPassword.getPassword();
254        }
255
256        public boolean isSaveCredentials() {
257            return cbSaveCredentials.isSelected();
258        }
259
260        protected final void updateWarningLabel(String url) {
261            boolean https = url != null && url.startsWith("https");
262            if (https) {
263                lblWarning.setText(null);
264            } else {
265                lblWarning.setText(tr("Warning: The password is transferred unencrypted."));
266            }
267            lblWarning.setVisible(!https);
268        }
269    }
270
271    private static class OsmApiCredentialsPanel extends CredentialPanel {
272
273        @Override
274        protected void build() {
275            super.build();
276            tfUserName.setToolTipText(tr("Please enter the user name of your OSM account"));
277            tfPassword.setToolTipText(tr("Please enter the password of your OSM account"));
278            String apiUrl = OsmApi.getOsmApi().getBaseUrl();
279            lblHeading.setText(
280                    "<html>" + tr("Authenticating at the OSM API ''{0}'' failed. Please enter a valid username and a valid password.",
281                            apiUrl) + "</html>");
282            updateWarningLabel(apiUrl);
283        }
284
285        OsmApiCredentialsPanel(CredentialDialog owner) {
286            super(owner);
287            build();
288        }
289    }
290
291    private static class OtherHostCredentialsPanel extends CredentialPanel {
292
293        private final String host;
294
295        @Override
296        protected void build() {
297            super.build();
298            tfUserName.setToolTipText(tr("Please enter the user name of your account"));
299            tfPassword.setToolTipText(tr("Please enter the password of your account"));
300            lblHeading.setText(
301                    "<html>" + tr("Authenticating at the host ''{0}'' failed. Please enter a valid username and a valid password.",
302                            host) + "</html>");
303            updateWarningLabel(host);
304        }
305
306        OtherHostCredentialsPanel(CredentialDialog owner, String host) {
307            super(owner);
308            this.host = host;
309            build();
310        }
311    }
312
313    private static class HttpProxyCredentialsPanel extends CredentialPanel {
314        @Override
315        protected void build() {
316            super.build();
317            tfUserName.setToolTipText(tr("Please enter the user name for authenticating at your proxy server"));
318            tfPassword.setToolTipText(tr("Please enter the password for authenticating at your proxy server"));
319            lblHeading.setText(
320                    "<html>" + tr("Authenticating at the HTTP proxy ''{0}'' failed. Please enter a valid username and a valid password.",
321                            Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_HOST) + ':' +
322                            Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_PORT)) + "</html>");
323            lblWarning.setText("<html>" +
324                    tr("Warning: depending on the authentication method the proxy server uses the password may be transferred unencrypted.")
325                    + "</html>");
326        }
327
328        HttpProxyCredentialsPanel(CredentialDialog owner) {
329            super(owner);
330            build();
331        }
332    }
333
334    private static class SelectAllOnFocusHandler extends FocusAdapter {
335        @Override
336        public void focusGained(FocusEvent e) {
337            if (e.getSource() instanceof JTextField) {
338                JTextField tf = (JTextField) e.getSource();
339                tf.selectAll();
340            }
341        }
342    }
343
344    /**
345     * Listener for username and password text fields key events.
346     * When user presses Enter:
347     *   If current text field is empty (or just contains a sequence of spaces), nothing happens (or all spaces become selected).
348     *   If current text field is not empty, but the next one is (or just contains a sequence of spaces), focuses the next text field.
349     *   If both text fields contain characters, submits the form by calling owner's {@link OKAction}.
350     */
351    private static class TFKeyListener extends KeyAdapter {
352        protected CredentialDialog owner; // owner Dependency Injection to call OKAction
353        protected JTextField currentTF;
354        protected JTextField nextTF;
355
356        TFKeyListener(CredentialDialog owner, JTextField currentTF, JTextField nextTF) {
357            this.owner = owner;
358            this.currentTF = currentTF;
359            this.nextTF = nextTF;
360        }
361
362        @Override
363        public void keyPressed(KeyEvent e) {
364            if (e.getKeyChar() == KeyEvent.VK_ENTER) {
365                if (currentTF.getText().trim().isEmpty()) {
366                    currentTF.selectAll();
367                    return;
368                } else if (nextTF.getText().trim().isEmpty()) {
369                    nextTF.requestFocusInWindow();
370                    nextTF.selectAll();
371                    return;
372                } else {
373                    OKAction okAction = owner.new OKAction();
374                    okAction.actionPerformed(null);
375                }
376            }
377        }
378    }
379
380    class OKAction extends AbstractAction {
381        OKAction() {
382            putValue(NAME, tr("Authenticate"));
383            putValue(SHORT_DESCRIPTION, tr("Authenticate with the supplied username and password"));
384            putValue(SMALL_ICON, ImageProvider.get("ok"));
385        }
386
387        @Override
388        public void actionPerformed(ActionEvent arg0) {
389            setCanceled(false);
390            setVisible(false);
391        }
392    }
393
394    class CancelAction extends AbstractAction {
395        CancelAction() {
396            putValue(NAME, tr("Cancel"));
397            putValue(SHORT_DESCRIPTION, tr("Cancel authentication"));
398            putValue(SMALL_ICON, ImageProvider.get("cancel"));
399        }
400
401        public void cancel() {
402            setCanceled(true);
403            setVisible(false);
404        }
405
406        @Override
407        public void actionPerformed(ActionEvent arg0) {
408            cancel();
409        }
410    }
411
412    class WindowEventHander extends WindowAdapter {
413
414        @Override
415        public void windowActivated(WindowEvent e) {
416            if (pnlCredentials != null) {
417                pnlCredentials.startUserInput();
418            }
419        }
420
421        @Override
422        public void windowClosing(WindowEvent e) {
423            new CancelAction().cancel();
424        }
425    }
426}