// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.npm; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.net.Authenticator.RequestorType; import java.net.PasswordAuthentication; import java.util.ArrayList; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.GroupLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JSeparator; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.border.EmptyBorder; import javax.swing.border.EtchedBorder; import org.netbeans.spi.keyring.KeyringProvider; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.oauth.OAuthToken; import org.openstreetmap.josm.gui.preferences.server.ProxyPreferencesPanel; import org.openstreetmap.josm.gui.widgets.HtmlPanel; import org.openstreetmap.josm.io.auth.CredentialsAgentException; import org.openstreetmap.josm.io.auth.CredentialsManager; import org.openstreetmap.josm.io.OsmApi; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.PlatformHookOsx; import org.openstreetmap.josm.tools.PlatformHookUnixoid; import org.openstreetmap.josm.tools.PlatformHookWindows; import org.openstreetmap.josm.tools.WindowGeometry; public class InitializationWizard extends JDialog { protected boolean canceled = false; protected JButton btnCancel, btnBack, btnNext; protected Action nextAction, finishAction; protected JPanel cardPanel; List<WizardPanel> panels = new ArrayList<>(); int panelIndex; private CardLayout cardLayout; public InitializationWizard() { super(JOptionPane.getFrameForComponent(Main.parent), tr("Native password manager plugin"), ModalityType.DOCUMENT_MODAL); build(); NPMType npm = detectNativePasswordManager(); WizardPanel firstPanel; if (npm == null) { firstPanel = new NothingFoundPanel(); } else { firstPanel = new SelectionPanel(npm, this); } panelIndex = 0; panels.add(firstPanel); cardPanel.add(firstPanel.getPanel(), firstPanel.getId()); updateButtons(); } private void build() { Container c = getContentPane(); c.setLayout(new BorderLayout()); addWindowListener(new WindowEventHandler()); getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel"); getRootPane().getActionMap().put("cancel", new CancelAction()); cardLayout = new CardLayout(); cardPanel = new JPanel(cardLayout); cardPanel.setBorder(new EmptyBorder(new Insets(5, 10, 5, 10))); c.add(cardPanel, BorderLayout.CENTER); nextAction = new NextAction(); finishAction = new FinishAction(); btnCancel = new JButton(new CancelAction()); btnBack = new JButton(new BackAction()); btnNext = new JButton(nextAction); Box buttonsBox = new Box(BoxLayout.X_AXIS); buttonsBox.setBorder(new EmptyBorder(new Insets(5, 10, 5, 10))); buttonsBox.add(btnCancel); buttonsBox.add(Box.createHorizontalStrut(30)); buttonsBox.add(btnBack); buttonsBox.add(Box.createHorizontalStrut(10)); buttonsBox.add(btnNext); JPanel buttonsPanel = new JPanel(new BorderLayout()); buttonsPanel.add(new JSeparator(), BorderLayout.NORTH); buttonsPanel.add(buttonsBox, BorderLayout.EAST); c.add(buttonsPanel, BorderLayout.SOUTH); } private void updateButtons() { btnBack.setEnabled(panelIndex > 0); if (panels.get(panelIndex).isLast()) { btnNext.setAction(finishAction); } else { btnNext.setAction(nextAction); } } /** * A WizardPanel represents one page in the wizard dialog. * The user usually proceeds from one panel to the next, but can go back as well. */ public interface WizardPanel { /* unique id */ String getId(); /* get the Panel Compoment */ JPanel getPanel(); /* return true if this page is the last and the 'next' button should change into 'finish' */ boolean isLast(); /* Provide the next WizardPanel. * Not called when isLast() returns true. */ WizardPanel provideNext(); /* The action to execute, when the user finall hits 'OK' and not 'Cancel' */ void onOkAction(); } abstract private static class AbstractWizardPanel implements WizardPanel { /** * Put a logo to the left, as is customary for wizard dialogs */ @Override public JPanel getPanel() { JPanel p = new JPanel(new BorderLayout()); JLabel image = new JLabel(ImageProvider.get("lock-large")); image.setBorder( BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(EtchedBorder.RAISED), BorderFactory.createEmptyBorder(10, 10, 10, 10))); image.setVerticalAlignment(SwingConstants.TOP); p.add(image, BorderLayout.WEST); JPanel content = getContentPanel(); content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); p.add(content, BorderLayout.CENTER); return p; } @Override public String getId() { return getClass().getCanonicalName(); } abstract protected JPanel getContentPanel(); } private static class NothingFoundPanel extends AbstractWizardPanel { JCheckBox cbDoNotShowAgain; @Override public boolean isLast() { return true; } @Override public WizardPanel provideNext() { return null; } @Override public void onOkAction() { if (cbDoNotShowAgain.isSelected()) { NPMPlugin.turnOffPermanently(); } } @Override protected JPanel getContentPanel() { JPanel p = new JPanel(); GroupLayout layout = new GroupLayout(p); p.setLayout(layout); HtmlPanel intro = new HtmlPanel("<html>"+ tr("No native password manager could be found!")+"<br>"+ tr("Depending on your Operating System / Distribution, you may have to create a default keyring / wallet first.")+ "</html>"); cbDoNotShowAgain = new JCheckBox("Do not show this wizard again on next start"); layout.setHorizontalGroup( layout.createParallelGroup() .addComponent(intro) .addComponent(cbDoNotShowAgain) ); layout.setVerticalGroup( layout.createSequentialGroup() .addComponent(intro, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) .addComponent(cbDoNotShowAgain) ); return p; } } private static class SelectionPanel extends AbstractWizardPanel implements ActionListener { private NPMType type; private InitializationWizard wizard; private JRadioButton rbManage, rbPlain; public SelectionPanel(NPMType type, InitializationWizard wizard) { this.type = type; this.wizard = wizard; } @Override public boolean isLast() { return (rbPlain != null && rbPlain.isSelected()) || !hasUnprotectedCedentials(); } @Override public WizardPanel provideNext() { return new DeleteOldCredentialsPanel(); } @Override protected JPanel getContentPanel() { JPanel p = new JPanel(); GroupLayout layout = new GroupLayout(p); p.setLayout(layout); HtmlPanel intro = new HtmlPanel("<html><b>"+type.getIntroText()+"</b></html>"); rbManage = new JRadioButton("<html>"+type.getSelectionText()+"</html>"); rbPlain = new JRadioButton("<html>"+tr("No thanks, use JOSM''s plain text preferences storage")+"</html>"); rbManage.addActionListener(this); rbPlain.addActionListener(this); rbManage.setSelected(true); ButtonGroup group = new ButtonGroup(); group.add(rbManage); group.add(rbPlain); layout.setHorizontalGroup( layout.createParallelGroup() .addComponent(intro) .addComponent(rbManage) .addComponent(rbPlain) ); layout.setVerticalGroup( layout.createSequentialGroup() .addComponent(intro, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) .addComponent(rbManage) .addComponent(rbPlain) ); return p; } @Override public void onOkAction() { if (rbManage.isSelected()) { NPMPlugin.selectAndSave(type); } else { wizard.setCanceled(true); NPMPlugin.turnOffPermanently(); } } @Override public void actionPerformed(ActionEvent e) { wizard.updateButtons(); } } private static class DeleteOldCredentialsPanel extends AbstractWizardPanel { private JRadioButton rbClear, rbKeep; @Override public boolean isLast() { return true; } @Override public WizardPanel provideNext() { return null; } @Override public JPanel getContentPanel() { JPanel p = new JPanel(); GroupLayout layout = new GroupLayout(p); p.setLayout(layout); HtmlPanel l = new HtmlPanel(); l.setText("<html><b>"+tr("Found sensitive data that is still saved" + " in JOSM''s preference file (plain text).")+"<b></html>"); rbClear = new JRadioButton("<html>"+tr("Transfer to password manager and remove from preference file")+"</html>"); rbKeep = new JRadioButton("<html>"+tr("No, just keep it")+"</html>"); rbClear.setSelected(true); ButtonGroup group = new ButtonGroup(); group.add(rbClear); group.add(rbKeep); layout.setHorizontalGroup( layout.createParallelGroup() .addComponent(l) .addComponent(rbClear) .addComponent(rbKeep) ); layout.setVerticalGroup( layout.createSequentialGroup() .addComponent(l, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) .addComponent(rbClear) .addComponent(rbKeep) ); return p; } @Override public void onOkAction() { CredentialsManager cm = CredentialsManager.getInstance(); String server_username = Main.pref.get("osm-server.username", null); String server_password = Main.pref.get("osm-server.password", null); if (server_username != null || server_password != null) { try { cm.store(RequestorType.SERVER, OsmApi.getOsmApi().getHost(), new PasswordAuthentication(string(server_username), toCharArray(server_password))); if (rbClear.isSelected()) { Main.pref.put("osm-server.username", null); Main.pref.put("osm-server.password", null); } } catch (CredentialsAgentException ex) { ex.printStackTrace(); } } String proxy_username = Main.pref.get(ProxyPreferencesPanel.PROXY_USER, null); String proxy_password = Main.pref.get(ProxyPreferencesPanel.PROXY_PASS, null); String proxy_host = Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_HOST, null); if (proxy_username != null || proxy_password != null) { try { cm.store(RequestorType.PROXY, proxy_host, new PasswordAuthentication(string(proxy_username), toCharArray(proxy_password))); if (rbClear.isSelected()) { Main.pref.put(ProxyPreferencesPanel.PROXY_USER, null); Main.pref.put(ProxyPreferencesPanel.PROXY_PASS, null); } } catch (CredentialsAgentException ex) { ex.printStackTrace(); } } String oauth_key = Main.pref.get("oauth.access-token.key", null); String oauth_secret = Main.pref.get("oauth.access-token.secret", null); if (oauth_key != null || oauth_secret != null) { try { cm.storeOAuthAccessToken(new OAuthToken(string(oauth_key), string(oauth_secret))); if (rbClear.isSelected()) { Main.pref.put("oauth.access-token.key", null); Main.pref.put("oauth.access-token.secret", null); } } catch (CredentialsAgentException ex) { ex.printStackTrace(); } } } } private final static String NPM = "Native Password Manager Plugin: "; private static NPMType detectNativePasswordManager() { NPMType[] potentialManagers; if (Main.platform instanceof PlatformHookWindows) { potentialManagers = new NPMType[] { NPMType.CRYPT32 }; } else if (Main.platform instanceof PlatformHookOsx) { potentialManagers = new NPMType[] { NPMType.KEYCHAIN }; } else if (Main.platform instanceof PlatformHookUnixoid) { potentialManagers = new NPMType[] { NPMType.GNOME_KEYRING, NPMType.KWALLET }; } else throw new AssertionError(); for (NPMType manager : potentialManagers) { System.out.println(NPM + "Looking for " + manager.getName()); KeyringProvider provider = manager.getProvider(); if (provider.enabled()) { System.out.println(NPM + "Found " + manager.getName()); return manager; } } return null; } private static boolean hasUnprotectedCedentials() { return Main.pref.get("osm-server.username", null) != null || Main.pref.get("osm-server.password", null) != null || Main.pref.get(ProxyPreferencesPanel.PROXY_USER, null) != null || Main.pref.get(ProxyPreferencesPanel.PROXY_PASS, null) != null || Main.pref.get("oauth.access-token.key", null) != null || Main.pref.get("oauth.access-token.secret", null) != null; } public void showDialog() { pack(); setVisible(true); } @Override public void setVisible(boolean visible) { if (visible) { new WindowGeometry( getClass().getName() + ".geometry", WindowGeometry.centerInWindow( getParent(), new Dimension(600,400) ) ).applySafe(this); } else if (!visible && isShowing()){ new WindowGeometry(this).remember(getClass().getName() + ".geometry"); } super.setVisible(visible); } public boolean isCanceled() { return canceled; } protected void setCanceled(boolean canceled) { this.canceled = canceled; } /** * Close the dialog and discard all changes. */ class CancelAction extends AbstractAction { public CancelAction() { putValue(NAME, tr("Cancel")); putValue(SMALL_ICON, ImageProvider.get("cancel")); putValue(SHORT_DESCRIPTION, tr("Close the dialog and discard all changes")); } public void cancel() { setCanceled(true); setVisible(false); } @Override public void actionPerformed(ActionEvent evt) { cancel(); } } /** * Go to the previous page. */ class BackAction extends AbstractAction { public BackAction() { putValue(NAME, tr("Back")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "previous")); putValue(SHORT_DESCRIPTION, tr("Go to the previous page")); } @Override public void actionPerformed(ActionEvent evt) { if (panelIndex <= 0) throw new RuntimeException(); panelIndex--; cardLayout.show(cardPanel, panels.get(panelIndex).getId()); updateButtons(); } } class NextAction extends AbstractAction { public NextAction() { putValue(NAME, tr("Next")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "next")); putValue(SHORT_DESCRIPTION, tr("Proceed and go to the next page")); } @Override public void actionPerformed(ActionEvent evt) { if (panelIndex == panels.size() - 1) { WizardPanel next = panels.get(panelIndex).provideNext(); cardPanel.add(next.getPanel(), next.getId()); panels.add(next); } panelIndex++; cardLayout.show(cardPanel, panels.get(panelIndex).getId()); updateButtons(); } } class FinishAction extends AbstractAction { public FinishAction() { putValue(NAME, tr("Finish")); putValue(SMALL_ICON, ImageProvider.get("ok")); putValue(SHORT_DESCRIPTION, tr("Confirm the setup and close this dialog")); } @Override public void actionPerformed(ActionEvent evt) { for (int i=0; i<=panelIndex; ++i) { if (isCanceled()) { break; } panels.get(i).onOkAction(); } setVisible(false); } } class WindowEventHandler extends WindowAdapter { @Override public void windowClosing(WindowEvent arg0) { new CancelAction().cancel(); } } public static char[] toCharArray(String s) { if (s == null) return new char[0]; else return s.toCharArray(); } public static String string(String s) { if (s == null) return ""; else return s; } }