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