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