diff --git a/plugins/git4idea/src/git4idea/jgit/GitHttpAdapter.java b/plugins/git4idea/src/git4idea/jgit/GitHttpAdapter.java index 248f997941dc..1e1b2f14d95c 100644 --- a/plugins/git4idea/src/git4idea/jgit/GitHttpAdapter.java +++ b/plugins/git4idea/src/git4idea/jgit/GitHttpAdapter.java @@ -15,8 +15,15 @@ */ package git4idea.jgit; +import com.intellij.ide.passwordSafe.PasswordSafe; +import com.intellij.ide.passwordSafe.PasswordSafeException; +import com.intellij.ide.passwordSafe.config.PasswordSafeSettings; +import com.intellij.ide.passwordSafe.impl.PasswordSafeImpl; +import com.intellij.ide.passwordSafe.impl.PasswordSafeProvider; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; import git4idea.push.GitSimplePushResult; +import git4idea.remote.GitRememberedInputs; import git4idea.repo.GitRepository; import git4idea.update.GitFetchResult; import org.eclipse.jgit.api.FetchCommand; @@ -195,7 +202,17 @@ public final class GitHttpAdapter { private static GeneralResult callWithAuthRetry(@NotNull MyRunnable command, GitHttpCredentialsProvider provider) throws InvalidRemoteException, IOException { for (int i = 0; i < 3; i++) { try { + AuthData authData = getUsernameAndPassword(provider.getProject(), provider.getUrl()); + if (authData != null) { + provider.fillAuthDataIfNotFilled(authData.getLogin(), authData.getPassword()); + } + if (i == 0) { + provider.setAlwaysShowDialog(false); // if username and password are supplied, no need to show the dialog + } else { + provider.setAlwaysShowDialog(true); // unless these values fail authentication + } command.run(); + rememberPassword(provider); return GeneralResult.SUCCESS; } catch (JGitInternalException e) { if (!authError(e)) { @@ -207,7 +224,98 @@ public final class GitHttpAdapter { } return GeneralResult.NOT_AUTHORIZED; } + + private static void rememberPassword(@NotNull GitHttpCredentialsProvider credentialsProvider) { + if (!credentialsProvider.wasDialogShown()) { // the dialog is not shown => everything is already stored + return; + } + final PasswordSafeImpl passwordSafe = (PasswordSafeImpl)PasswordSafe.getInstance(); + if (passwordSafe.getSettings().getProviderType() == PasswordSafeSettings.ProviderType.DO_NOT_STORE) { + return; + } + String login = credentialsProvider.getUserName(); + if (login == null || credentialsProvider.getPassword() == null) { + return; + } + + String url = adjustHttpUrl(credentialsProvider.getUrl()); + String key = keyForUrlAndLogin(url, login); + try { + // store in memory always + storePassword(passwordSafe.getMemoryProvider(), credentialsProvider, key); + if (credentialsProvider.isRememberPassword()) { + storePassword(passwordSafe.getMasterKeyProvider(), credentialsProvider, key); + } + GitRememberedInputs.getInstance().addUrl(url, login); + } + catch (PasswordSafeException e) { + LOG.info("Couldn't store the password for key [" + key + "]", e); + } + } + + private static void storePassword(PasswordSafeProvider passwordProvider, GitHttpCredentialsProvider credentialsProvider, String key) throws PasswordSafeException { + passwordProvider.storePassword(credentialsProvider.getProject(), GitHttpCredentialsProvider.class, key, credentialsProvider.getPassword()); + } + + @Nullable + private static AuthData getUsernameAndPassword(Project project, String url) { + url = adjustHttpUrl(url); + String userName = GitRememberedInputs.getInstance().getUserNameForUrl(url); + if (userName == null) { + return null; + } + String key = keyForUrlAndLogin(url, userName); + final PasswordSafeImpl passwordSafe = (PasswordSafeImpl)PasswordSafe.getInstance(); + try { + String password = passwordSafe.getMemoryProvider().getPassword(project, GitHttpCredentialsProvider.class, key); + if (password == null) { + password = passwordSafe.getMasterKeyProvider().getPassword(project, GitHttpCredentialsProvider.class, key); + } + return password != null ? new AuthData(userName, password) : null; + } + catch (PasswordSafeException e) { + LOG.info("Couldn't store the password for key [" + key + "]", e); + return null; + } + } + private static class AuthData { + private final String myLogin; + private final String myPassword; + + private AuthData(@NotNull String login, @NotNull String password) { + myPassword = password; + myLogin = login; + } + + @NotNull + public String getLogin() { + return myLogin; + } + + @NotNull + public String getPassword() { + return myPassword; + } + } + + + /** + * If url is HTTPS, store it as HTTP in the password database, not to make user enter and remember same credentials twice. + */ + @NotNull + private static String adjustHttpUrl(@NotNull String url) { + if (url.startsWith("https")) { + return url.replaceFirst("https", "http"); + } + return url; + } + + @NotNull + private static String keyForUrlAndLogin(@NotNull String stringUrl, @NotNull String login) { + return login + ":" + stringUrl; + } + private static boolean authError(@NotNull JGitInternalException e) { Throwable cause = e.getCause(); return (cause instanceof TransportException && cause.getMessage().contains("not authorized")); diff --git a/plugins/git4idea/src/git4idea/jgit/GitHttpCredentialsProvider.java b/plugins/git4idea/src/git4idea/jgit/GitHttpCredentialsProvider.java index 49f93726db25..61fdadc3167a 100644 --- a/plugins/git4idea/src/git4idea/jgit/GitHttpCredentialsProvider.java +++ b/plugins/git4idea/src/git4idea/jgit/GitHttpCredentialsProvider.java @@ -33,9 +33,17 @@ import java.util.regex.Pattern; */ public class GitHttpCredentialsProvider extends CredentialsProvider { + private static final Pattern HTTP_URL_PATTERN = Pattern.compile("http(?:s?)://(?:([\\S^@\\.]*)@)?.*"); + private final Project myProject; private final String myRemoteUrl; + private boolean myCancelled; + private boolean myRememberPassword; + private String myPassword; + private String myUserName; + private boolean myShowDialog; + private boolean myDialogShown; public GitHttpCredentialsProvider(@NotNull Project project, @NotNull String remoteUrl) { myProject = project; @@ -75,16 +83,33 @@ public class GitHttpCredentialsProvider extends CredentialsProvider { if (userNameItem != null || passwordItem != null) { String username = getUserNameFromUrl(myRemoteUrl); - final AuthDialog dialog = new AuthDialog(myProject, "Login required", "Login to " + myRemoteUrl, username, null, true); - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - dialog.show(); - } - }); - boolean ok = dialog.isOK(); + String password = null; + if (username == null) { // username is not in the url => reading pre-filled value from the password storage + username = myUserName; + password = myPassword; + } else if (username.equals(myUserName)) { // username is in url => read password only if it is for the same user + password = myPassword; + } + + final AuthDialog dialog = new AuthDialog(myProject, "Login required", "Login to " + myRemoteUrl, username, password, false); + boolean ok; + if (username != null && password != null && !myShowDialog) { + ok = true; + myDialogShown = false; + } else { + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + dialog.show(); + } + }); + ok = dialog.isOK(); + myDialogShown = true; + } + if (!ok) { myCancelled = true; + myRememberPassword = false; // in case of re-usage of the provider } else { if (userNameItem != null) { userNameItem.setValue(dialog.getUsername()); @@ -92,14 +117,56 @@ public class GitHttpCredentialsProvider extends CredentialsProvider { if (passwordItem != null) { passwordItem.setValue(dialog.getPassword().toCharArray()); } + myRememberPassword = dialog.isRememberPassword(); + myPassword = dialog.getPassword(); + myUserName = dialog.getUsername(); } return ok; } return true; } - private static final Pattern HTTP_URL_PATTERN = Pattern.compile("http(?:s?)://(?:([\\S^@\\.]*)@)?.*"); - + public boolean isRememberPassword() { + return myRememberPassword; + } + + @NotNull + public Project getProject() { + return myProject; + } + + @Nullable + public String getPassword() { + return myPassword; + } + + @Nullable + public String getUserName() { + return myUserName; + } + + @NotNull + public String getUrl() { + return myRemoteUrl; + } + + public void fillAuthDataIfNotFilled(@NotNull String login, @NotNull String password) { + if (myUserName == null) { + myUserName = login; + myPassword = password; + } else if (myPassword != null) { + myPassword = password; + } + } + + public void setAlwaysShowDialog(boolean showDialog) { + myShowDialog = showDialog; + } + + public boolean wasDialogShown() { + return myDialogShown; + } + @Nullable private static String getUserNameFromUrl(@NotNull String url) { Matcher matcher = HTTP_URL_PATTERN.matcher(url); diff --git a/plugins/git4idea/src/git4idea/remote/GitRememberedInputs.java b/plugins/git4idea/src/git4idea/remote/GitRememberedInputs.java index d1a6e49e814f..ecb62c385694 100644 --- a/plugins/git4idea/src/git4idea/remote/GitRememberedInputs.java +++ b/plugins/git4idea/src/git4idea/remote/GitRememberedInputs.java @@ -19,6 +19,8 @@ import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -34,6 +36,16 @@ public class GitRememberedInputs implements PersistentStateComponent visitedUrls = new ArrayList(); + public String cloneParentDir = ""; + } + + public static class UrlAndUserName { + public String url; + public String userName; + } + public static GitRememberedInputs getInstance() { return ServiceManager.getService(GitRememberedInputs.class); } @@ -48,24 +60,42 @@ public class GitRememberedInputs implements PersistentStateComponent getVisitedUrls() { - return myState.myVisitedUrls; + List urls = new ArrayList(myState.visitedUrls.size()); + for (UrlAndUserName urlAndUserName : myState.visitedUrls) { + urls.add(urlAndUserName.url); + } + return urls; } public String getCloneParentDir() { - return myState.myCloneParentDir; + return myState.cloneParentDir; } public void setCloneParentDir(String cloneParentDir) { - myState.myCloneParentDir = cloneParentDir; + myState.cloneParentDir = cloneParentDir; } - public static class State { - public List myVisitedUrls = new ArrayList(); - public String myCloneParentDir = ""; - } }