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.KeyEvent;
017import java.awt.event.KeyListener;
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 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        getContentPane().add(pnlCredentials = new OsmApiCredentialsPanel(this), BorderLayout.CENTER);
120        pnlCredentials.init(username, password);
121        validate();
122    }
123
124    public void prepareForOtherHostCredentials(String username, String password, String host) {
125        setTitle(tr("Enter credentials for host"));
126        getContentPane().add(pnlCredentials = new OtherHostCredentialsPanel(this, host), BorderLayout.CENTER);
127        pnlCredentials.init(username, password);
128        validate();
129    }
130
131    public void prepareForProxyCredentials(String username, String password) {
132        setTitle(tr("Enter credentials for HTTP proxy"));
133        getContentPane().add(pnlCredentials = new HttpProxyCredentialsPanel(this), BorderLayout.CENTER);
134        pnlCredentials.init(username, password);
135        validate();
136    }
137
138    public String getUsername() {
139        if (pnlCredentials == null) return null;
140        return pnlCredentials.getUserName();
141    }
142
143    public char[] getPassword() {
144        if (pnlCredentials == null) return null;
145        return pnlCredentials.getPassword();
146    }
147
148    public boolean isSaveCredentials() {
149        if (pnlCredentials == null) return false;
150        return pnlCredentials.isSaveCredentials();
151    }
152
153    protected static class CredentialPanel extends JPanel {
154        protected JosmTextField tfUserName;
155        protected JosmPasswordField tfPassword;
156        protected JCheckBox cbSaveCredentials;
157        protected JMultilineLabel lblHeading;
158        protected JMultilineLabel lblWarning;
159        protected CredentialDialog owner; // owner Dependency Injection to use Key listeners for username and password text fields
160
161        protected void build() {
162            tfUserName = new JosmTextField(20);
163            tfPassword = new JosmPasswordField(20);
164            tfUserName.addFocusListener(new SelectAllOnFocusHandler());
165            tfPassword.addFocusListener(new SelectAllOnFocusHandler());
166            tfUserName.addKeyListener(new TFKeyListener(owner, tfUserName, tfPassword));
167            tfPassword.addKeyListener(new TFKeyListener(owner, tfPassword, tfUserName));
168            cbSaveCredentials =  new JCheckBox(owner.saveUsernameAndPasswordCheckboxText);
169
170            setLayout(new GridBagLayout());
171            GridBagConstraints gc = new GridBagConstraints();
172            gc.gridwidth = 2;
173            gc.gridheight = 1;
174            gc.fill = GridBagConstraints.HORIZONTAL;
175            gc.weightx = 1.0;
176            gc.weighty = 0.0;
177            gc.insets = new Insets(0, 0, 10, 0);
178            add(lblHeading = new JMultilineLabel(""), gc);
179
180            gc.gridx = 0;
181            gc.gridy = 1;
182            gc.gridwidth = 1;
183            gc.gridheight = 1;
184            gc.fill = GridBagConstraints.HORIZONTAL;
185            gc.weightx = 0.0;
186            gc.weighty = 0.0;
187            gc.insets = new Insets(0, 0, 10, 10);
188            add(new JLabel(tr("Username")), gc);
189            gc.gridx = 1;
190            gc.gridy = 1;
191            gc.weightx = 1.0;
192            add(tfUserName, gc);
193            gc.gridx = 0;
194            gc.gridy = 2;
195            gc.weightx = 0.0;
196            add(new JLabel(tr("Password")), gc);
197
198            gc.gridx = 1;
199            gc.gridy = 2;
200            gc.weightx = 0.0;
201            add(tfPassword, gc);
202
203            gc.gridx = 0;
204            gc.gridy = 3;
205            gc.gridwidth = 2;
206            gc.gridheight = 1;
207            gc.fill = GridBagConstraints.BOTH;
208            gc.weightx = 1.0;
209            gc.weighty = 0.0;
210            lblWarning = new JMultilineLabel("");
211            lblWarning.setFont(lblWarning.getFont().deriveFont(Font.ITALIC));
212            add(lblWarning, gc);
213
214            gc.gridx = 0;
215            gc.gridy = 4;
216            gc.weighty = 0.0;
217            add(cbSaveCredentials, gc);
218
219            // consume the remaining space
220            gc.gridx = 0;
221            gc.gridy = 5;
222            gc.weighty = 1.0;
223            add(new JPanel(), gc);
224
225        }
226
227        public CredentialPanel(CredentialDialog owner) {
228            this.owner = owner;
229        }
230
231        public void init(String username, String password) {
232            username = username == null ? "" : username;
233            password = password == null ? "" : password;
234            tfUserName.setText(username);
235            tfPassword.setText(password);
236            cbSaveCredentials.setSelected(!username.isEmpty() && !password.isEmpty());
237        }
238
239        public void startUserInput() {
240            tfUserName.requestFocusInWindow();
241        }
242
243        public String getUserName() {
244            return tfUserName.getText();
245        }
246
247        public char[] getPassword() {
248            return tfPassword.getPassword();
249        }
250
251        public boolean isSaveCredentials() {
252            return cbSaveCredentials.isSelected();
253        }
254
255        protected final void updateWarningLabel(String url) {
256            boolean https = url != null && url.startsWith("https");
257            if (https) {
258                lblWarning.setText(null);
259            } else {
260                lblWarning.setText(tr("Warning: The password is transferred unencrypted."));
261            }
262            lblWarning.setVisible(!https);
263        }
264    }
265
266    private static class OsmApiCredentialsPanel extends CredentialPanel {
267
268        @Override
269        protected void build() {
270            super.build();
271            tfUserName.setToolTipText(tr("Please enter the user name of your OSM account"));
272            tfPassword.setToolTipText(tr("Please enter the password of your OSM account"));
273            String apiUrl = OsmApi.getOsmApi().getBaseUrl();
274            lblHeading.setText(
275                    "<html>" + tr("Authenticating at the OSM API ''{0}'' failed. Please enter a valid username and a valid password.",
276                            apiUrl) + "</html>");
277            updateWarningLabel(apiUrl);
278        }
279
280        OsmApiCredentialsPanel(CredentialDialog owner) {
281            super(owner);
282            build();
283        }
284    }
285
286    private static class OtherHostCredentialsPanel extends CredentialPanel {
287
288        private String host;
289
290        @Override
291        protected void build() {
292            super.build();
293            tfUserName.setToolTipText(tr("Please enter the user name of your account"));
294            tfPassword.setToolTipText(tr("Please enter the password of your account"));
295            lblHeading.setText(
296                    "<html>" + tr("Authenticating at the host ''{0}'' failed. Please enter a valid username and a valid password.",
297                            host) + "</html>");
298            updateWarningLabel(host);
299        }
300
301        OtherHostCredentialsPanel(CredentialDialog owner, String host) {
302            super(owner);
303            this.host = host;
304            build();
305        }
306    }
307
308    private static class HttpProxyCredentialsPanel extends CredentialPanel {
309        @Override
310        protected void build() {
311            super.build();
312            tfUserName.setToolTipText(tr("Please enter the user name for authenticating at your proxy server"));
313            tfPassword.setToolTipText(tr("Please enter the password for authenticating at your proxy server"));
314            lblHeading.setText(
315                    "<html>" + tr("Authenticating at the HTTP proxy ''{0}'' failed. Please enter a valid username and a valid password.",
316                            Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_HOST) + ':' +
317                            Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_PORT)) + "</html>");
318            lblWarning.setText("<html>" +
319                    tr("Warning: depending on the authentication method the proxy server uses the password may be transferred unencrypted.")
320                    + "</html>");
321        }
322
323        HttpProxyCredentialsPanel(CredentialDialog owner) {
324            super(owner);
325            build();
326        }
327    }
328
329    private static class SelectAllOnFocusHandler extends FocusAdapter {
330        @Override
331        public void focusGained(FocusEvent e) {
332            if (e.getSource() instanceof JTextField) {
333                JTextField tf = (JTextField) e.getSource();
334                tf.selectAll();
335            }
336        }
337    }
338
339    /**
340     * Listener for username and password text fields key events.
341     * When user presses Enter:
342     *   If current text field is empty (or just contains a sequence of spaces), nothing happens (or all spaces become selected).
343     *   If current text field is not empty, but the next one is (or just contains a sequence of spaces), focuses the next text field.
344     *   If both text fields contain characters, submits the form by calling owner's {@link OKAction}.
345     */
346    private static class TFKeyListener implements KeyListener {
347        protected CredentialDialog owner; // owner Dependency Injection to call OKAction
348        protected JTextField currentTF;
349        protected JTextField nextTF;
350
351        TFKeyListener(CredentialDialog owner, JTextField currentTF, JTextField nextTF) {
352            this.owner = owner;
353            this.currentTF = currentTF;
354            this.nextTF = nextTF;
355        }
356
357        @Override
358        public void keyPressed(KeyEvent e) {
359            if (e.getKeyChar() == KeyEvent.VK_ENTER) {
360                if (currentTF.getText().trim().isEmpty()) {
361                    currentTF.selectAll();
362                    return;
363                } else if (nextTF.getText().trim().isEmpty()) {
364                    nextTF.requestFocusInWindow();
365                    nextTF.selectAll();
366                    return;
367                } else {
368                    OKAction okAction = owner.new OKAction();
369                    okAction.actionPerformed(null);
370                }
371            }
372        }
373
374        @Override
375        public void keyReleased(KeyEvent e) {
376        }
377
378        @Override
379        public void keyTyped(KeyEvent e) {
380        }
381    }
382
383    class OKAction extends AbstractAction {
384        OKAction() {
385            putValue(NAME, tr("Authenticate"));
386            putValue(SHORT_DESCRIPTION, tr("Authenticate with the supplied username and password"));
387            putValue(SMALL_ICON, ImageProvider.get("ok"));
388        }
389
390        @Override
391        public void actionPerformed(ActionEvent arg0) {
392            setCanceled(false);
393            setVisible(false);
394        }
395    }
396
397    class CancelAction extends AbstractAction {
398        CancelAction() {
399            putValue(NAME, tr("Cancel"));
400            putValue(SHORT_DESCRIPTION, tr("Cancel authentication"));
401            putValue(SMALL_ICON, ImageProvider.get("cancel"));
402        }
403
404        public void cancel() {
405            setCanceled(true);
406            setVisible(false);
407        }
408
409        @Override
410        public void actionPerformed(ActionEvent arg0) {
411            cancel();
412        }
413    }
414
415    class WindowEventHander extends WindowAdapter {
416
417        @Override
418        public void windowActivated(WindowEvent e) {
419            if (pnlCredentials != null) {
420                pnlCredentials.startUserInput();
421            }
422        }
423
424        @Override
425        public void windowClosing(WindowEvent e) {
426            new CancelAction().cancel();
427        }
428    }
429}