blob: 29f15423e581a22125cf128226d24265ef96692e [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.idea.svn.auth;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.NamedRunnable;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.impl.GenericNotifierImpl;
import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.Consumer;
import com.intellij.util.ThreeState;
import com.intellij.util.net.HttpConfigurable;
import com.intellij.util.proxy.CommonProxy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.*;
import org.jetbrains.idea.svn.api.ClientFactory;
import org.jetbrains.idea.svn.commandLine.SvnBindException;
import org.jetbrains.idea.svn.info.Info;
import org.jetbrains.idea.svn.info.InfoClient;
import org.tmatesoft.svn.core.SVNAuthenticationException;
import org.tmatesoft.svn.core.SVNCancelException;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.auth.SVNAuthentication;
import org.tmatesoft.svn.core.internal.util.SVNURLUtil;
import org.tmatesoft.svn.core.wc.SVNRevision;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FilenameFilter;
import java.net.*;
import java.util.*;
import java.util.List;
import java.util.Timer;
public class SvnAuthenticationNotifier extends GenericNotifierImpl<SvnAuthenticationNotifier.AuthenticationRequest, SVNURL> {
private static final Logger LOG = Logger.getInstance(SvnAuthenticationNotifier.class);
private static final List<String> ourAuthKinds = Arrays.asList(ISVNAuthenticationManager.PASSWORD, ISVNAuthenticationManager.SSH,
ISVNAuthenticationManager.SSL, ISVNAuthenticationManager.USERNAME, "svn.ssl.server", "svn.ssh.server");
private final SvnVcs myVcs;
private final RootsToWorkingCopies myRootsToWorkingCopies;
private final Map<SVNURL, Boolean> myCopiesPassiveResults;
private Timer myTimer;
private volatile boolean myVerificationInProgress;
public SvnAuthenticationNotifier(final SvnVcs svnVcs) {
super(svnVcs.getProject(), svnVcs.getDisplayName(), "Not Logged In to Subversion", NotificationType.ERROR);
myVcs = svnVcs;
myRootsToWorkingCopies = myVcs.getRootsToWorkingCopies();
myCopiesPassiveResults = Collections.synchronizedMap(new HashMap<SVNURL, Boolean>());
myVerificationInProgress = false;
}
public void init() {
if (myTimer != null) {
stop();
}
myTimer = new Timer("SVN authentication timer");
// every 10 minutes
myTimer.schedule(new TimerTask() {
@Override
public void run() {
myCopiesPassiveResults.clear();
}
}, 10000, 10 * 60 * 1000);
}
public void stop() {
myTimer.cancel();
myTimer = null;
}
@Override
protected boolean ask(final AuthenticationRequest obj, String description) {
if (myVerificationInProgress) {
return showAlreadyChecking();
}
myVerificationInProgress = true;
final Ref<Boolean> resultRef = new Ref<Boolean>();
final Runnable checker = new Runnable() {
@Override
public void run() {
try {
final boolean result =
interactiveValidation(obj.myProject, obj.getUrl(), obj.getRealm(), obj.getKind());
log("ask result for: " + obj.getUrl() + " is: " + result);
resultRef.set(result);
if (result) {
onStateChangedToSuccess(obj);
}
}
finally {
myVerificationInProgress = false;
}
}
};
final Application application = ApplicationManager.getApplication();
// also do not show auth if thread does not have progress indicator
if (application.isReadAccessAllowed() || !ProgressManager.getInstance().hasProgressIndicator()) {
application.executeOnPooledThread(checker);
}
else {
checker.run();
return resultRef.get();
}
return false;
}
private boolean showAlreadyChecking() {
final IdeFrame frameFor = WindowManagerEx.getInstanceEx().findFrameFor(myProject);
if (frameFor != null) {
final JComponent component = frameFor.getComponent();
Point point = component.getMousePosition();
if (point == null) {
point = new Point((int)(component.getWidth() * 0.7), 0);
}
SwingUtilities.convertPointToScreen(point, component);
JBPopupFactory.getInstance().createHtmlTextBalloonBuilder("Already checking...", MessageType.WARNING, null).
createBalloon().show(new RelativePoint(point), Balloon.Position.below);
}
return false;
}
private void onStateChangedToSuccess(final AuthenticationRequest obj) {
myCopiesPassiveResults.put(getKey(obj), true);
myVcs.invokeRefreshSvnRoots();
final List<SVNURL> outdatedRequests = new LinkedList<SVNURL>();
final Collection<SVNURL> keys = getAllCurrentKeys();
for (SVNURL key : keys) {
final SVNURL commonURLAncestor = SVNURLUtil.getCommonURLAncestor(key, obj.getUrl());
if ((commonURLAncestor != null) && (! StringUtil.isEmptyOrSpaces(commonURLAncestor.getHost())) &&
(! StringUtil.isEmptyOrSpaces(commonURLAncestor.getPath()))) {
//final AuthenticationRequest currObj = getObj(key);
//if ((currObj != null) && passiveValidation(myVcs.getProject(), key, true, currObj.getRealm(), currObj.getKind())) {
outdatedRequests.add(key);
//}
}
}
log("on state changed ");
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
for (SVNURL key : outdatedRequests) {
removeLazyNotificationByKey(key);
}
}
}, ModalityState.NON_MODAL);
}
@Override
public boolean ensureNotify(AuthenticationRequest obj) {
final SVNURL key = getKey(obj);
myCopiesPassiveResults.remove(key);
/*VcsBalloonProblemNotifier.showOverChangesView(myVcs.getProject(), "You are not authenticated to '" + obj.getRealm() + "'." +
"To login, see pending notifications.", MessageType.ERROR);*/
return super.ensureNotify(obj);
}
@Override
protected boolean onFirstNotification(AuthenticationRequest obj) {
if (ProgressManager.getInstance().hasProgressIndicator()) {
return ask(obj, null); // TODO
} else {
return false;
}
}
@NotNull
@Override
public SVNURL getKey(final AuthenticationRequest obj) {
// !!! wc's URL
return obj.getWcUrl();
}
@Nullable
public SVNURL getWcUrl(final AuthenticationRequest obj) {
if (obj.isOutsideCopies()) return null;
if (obj.getWcUrl() != null) return obj.getWcUrl();
final WorkingCopy copy = myRootsToWorkingCopies.getMatchingCopy(obj.getUrl());
if (copy != null) {
obj.setOutsideCopies(false);
obj.setWcUrl(copy.getUrl());
} else {
obj.setOutsideCopies(true);
}
return copy == null ? null : copy.getUrl();
}
/**
* Bases on presence of notifications!
*/
public ThreeState isAuthenticatedFor(@NotNull VirtualFile vf, @Nullable ClientFactory factory) {
final WorkingCopy wcCopy = myRootsToWorkingCopies.getWcRoot(vf);
if (wcCopy == null) return ThreeState.UNSURE;
// check there's no cancellation yet
final boolean haveCancellation = getStateFor(wcCopy.getUrl());
if (haveCancellation) return ThreeState.NO;
final Boolean keptResult = myCopiesPassiveResults.get(wcCopy.getUrl());
if (Boolean.TRUE.equals(keptResult)) return ThreeState.YES;
if (Boolean.FALSE.equals(keptResult)) return ThreeState.NO;
// check have credentials
final boolean calculatedResult =
factory == null ? passiveValidation(myVcs.getProject(), wcCopy.getUrl()) : passiveValidation(factory, wcCopy.getUrl());
myCopiesPassiveResults.put(wcCopy.getUrl(), calculatedResult);
return calculatedResult ? ThreeState.YES : ThreeState.NO;
}
private static boolean passiveValidation(@NotNull ClientFactory factory, @NotNull SVNURL url) {
Info info = null;
try {
info = factory.create(InfoClient.class, false).doInfo(url, SVNRevision.UNDEFINED, SVNRevision.UNDEFINED);
}
catch (SvnBindException ignore) {
}
return info != null;
}
@NotNull
@Override
protected String getNotificationContent(AuthenticationRequest obj) {
return "<a href=\"\">Click to fix.</a> Not logged In to Subversion '" + obj.getRealm() + "' (" + obj.getUrl().toDecodedString() + ")";
}
public static class AuthenticationRequest {
private final Project myProject;
private final String myKind;
private final SVNURL myUrl;
private final String myRealm;
private SVNURL myWcUrl;
private boolean myOutsideCopies;
private boolean myForceSaving;
public AuthenticationRequest(Project project, String kind, SVNURL url, String realm) {
myProject = project;
myKind = kind;
myUrl = url;
myRealm = realm;
}
public boolean isForceSaving() {
return myForceSaving;
}
public void setForceSaving(boolean forceSaving) {
myForceSaving = forceSaving;
}
public boolean isOutsideCopies() {
return myOutsideCopies;
}
public void setOutsideCopies(boolean outsideCopies) {
myOutsideCopies = outsideCopies;
}
public SVNURL getWcUrl() {
return myWcUrl;
}
public void setWcUrl(SVNURL wcUrl) {
myWcUrl = wcUrl;
}
public String getKind() {
return myKind;
}
public SVNURL getUrl() {
return myUrl;
}
public String getRealm() {
return myRealm;
}
}
static void log(final Throwable t) {
LOG.debug(t);
}
static void log(final String s) {
LOG.debug(s);
}
public static boolean passiveValidation(final Project project, final SVNURL url) {
final SvnConfiguration configuration = SvnConfiguration.getInstance(project);
final SvnAuthenticationManager passiveManager = configuration.getPassiveAuthenticationManager(project);
return validationImpl(project, url, configuration, passiveManager, false, null, null, false);
}
public static boolean interactiveValidation(final Project project, final SVNURL url, final String realm, final String kind) {
final SvnConfiguration configuration = SvnConfiguration.getInstance(project);
final SvnAuthenticationManager passiveManager = configuration.getInteractiveManager(SvnVcs.getInstance(project));
return validationImpl(project, url, configuration, passiveManager, true, realm, kind, true);
}
private static boolean validationImpl(final Project project, final SVNURL url,
final SvnConfiguration configuration, final SvnAuthenticationManager manager,
final boolean checkWrite,
final String realm,
final String kind, boolean interactive) {
// we should also NOT show proxy credentials dialog if at least fixed proxy was used, so
Proxy proxyToRelease = null;
if (! interactive && configuration.isIsUseDefaultProxy()) {
final HttpConfigurable instance = HttpConfigurable.getInstance();
if (instance.USE_HTTP_PROXY && instance.PROXY_AUTHENTICATION && (StringUtil.isEmptyOrSpaces(instance.PROXY_LOGIN) ||
StringUtil.isEmptyOrSpaces(instance.getPlainProxyPassword()))) {
return false;
}
if (instance.USE_PROXY_PAC) {
final List<Proxy> select;
try {
select = CommonProxy.getInstance().select(new URI(url.toString()));
}
catch (URISyntaxException e) {
LOG.info("wrong URL: " + url.toString());
return false;
}
if (select != null && ! select.isEmpty()) {
for (Proxy proxy : select) {
if (HttpConfigurable.isRealProxy(proxy) && Proxy.Type.HTTP.equals(proxy.type())) {
final InetSocketAddress address = (InetSocketAddress)proxy.address();
final PasswordAuthentication password =
HttpConfigurable.getInstance().getGenericPassword(address.getHostName(), address.getPort());
if (password == null) {
CommonProxy.getInstance().noAuthentication("http", address.getHostName(), address.getPort());
proxyToRelease = proxy;
}
}
}
}
}
}
SvnInteractiveAuthenticationProvider.clearCallState();
try {
// start svnkit authentication cycle
SvnVcs.getInstance(project).getSvnKitManager().createWCClient(manager).doInfo(url, SVNRevision.UNDEFINED, SVNRevision.HEAD);
//SvnVcs.getInstance(project).getInfo(url, SVNRevision.HEAD, manager);
} catch (SVNAuthenticationException e) {
log(e);
return false;
} catch (SVNCancelException e) {
log(e); // auth canceled
return false;
} catch (final SVNException e) {
if (e.getErrorMessage().getErrorCode().isAuthentication()) {
log(e);
return false;
}
LOG.info("some other exc", e);
if (interactive) {
showAuthenticationFailedWithHotFixes(project, configuration, e);
}
return false; /// !!!! any exception means user should be notified that authorization failed
} finally {
if (! interactive && configuration.isIsUseDefaultProxy() && proxyToRelease != null) {
final InetSocketAddress address = (InetSocketAddress)proxyToRelease.address();
CommonProxy.getInstance().noAuthentication("http", address.getHostName(), address.getPort());
}
}
if (! checkWrite) {
return true;
}
/*if (passive) {
return SvnInteractiveAuthenticationProvider.wasCalled();
}*/
if (SvnInteractiveAuthenticationProvider.wasCalled() && SvnInteractiveAuthenticationProvider.wasCancelled()) return false;
if (SvnInteractiveAuthenticationProvider.wasCalled()) return true;
final SvnVcs svnVcs = SvnVcs.getInstance(project);
final SvnInteractiveAuthenticationProvider provider = new SvnInteractiveAuthenticationProvider(svnVcs, manager);
final SVNAuthentication svnAuthentication = provider.requestClientAuthentication(kind, url, realm, null, null, true);
if (svnAuthentication != null) {
configuration.acknowledge(kind, realm, svnAuthentication);
try {
configuration.getAuthenticationManager(svnVcs).acknowledgeAuthentication(true, kind, realm, null, svnAuthentication);
}
catch (SVNException e) {
LOG.info(e);
}
return true;
}
return false;
}
private static void showAuthenticationFailedWithHotFixes(final Project project,
final SvnConfiguration configuration,
final SVNException e) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
VcsBalloonProblemNotifier.showOverChangesView(project, "Authentication failed: " + e.getMessage(), MessageType.ERROR,
new NamedRunnable(
SvnBundle.message("confirmation.title.clear.authentication.cache")) {
@Override
public void run() {
clearAuthenticationCache(project, null, configuration
.getConfigurationDirectory());
}
},
new NamedRunnable(SvnBundle.message("action.title.select.configuration.directory")) {
@Override
public void run() {
SvnConfigurable
.selectConfigurationDirectory(configuration.getConfigurationDirectory(),
new Consumer<String>() {
@Override
public void consume(String s) {
configuration
.setConfigurationDirParameters(false, s);
}
}, project, null);
}
}
);
}
}, ModalityState.NON_MODAL, project.getDisposed());
}
public static void clearAuthenticationCache(@NotNull final Project project, final Component component, final String configDirPath) {
if (configDirPath != null) {
int result;
if (component == null) {
result = Messages.showYesNoDialog(project, SvnBundle.message("confirmation.text.delete.stored.authentication.information"),
SvnBundle.message("confirmation.title.clear.authentication.cache"),
Messages.getWarningIcon());
} else {
result = Messages.showYesNoDialog(component, SvnBundle.message("confirmation.text.delete.stored.authentication.information"),
SvnBundle.message("confirmation.title.clear.authentication.cache"),
Messages.getWarningIcon());
}
if (result == Messages.YES) {
SvnConfiguration.RUNTIME_AUTH_CACHE.clear();
clearAuthenticationDirectory(SvnConfiguration.getInstance(project));
}
}
}
public static void clearAuthenticationDirectory(@NotNull SvnConfiguration configuration) {
final File authDir = new File(configuration.getConfigurationDirectory(), "auth");
if (authDir.exists()) {
final Runnable process = new Runnable() {
@Override
public void run() {
final ProgressIndicator ind = ProgressManager.getInstance().getProgressIndicator();
if (ind != null) {
ind.setIndeterminate(true);
ind.setText("Clearing stored credentials in " + authDir.getAbsolutePath());
}
final File[] files = authDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(@NotNull File dir, @NotNull String name) {
return ourAuthKinds.contains(name);
}
});
for (File dir : files) {
if (ind != null) {
ind.setText("Deleting " + dir.getAbsolutePath());
}
FileUtil.delete(dir);
}
}
};
final Application application = ApplicationManager.getApplication();
if (application.isUnitTestMode() || !application.isDispatchThread()) {
process.run();
}
else {
ProgressManager.getInstance()
.runProcessWithProgressSynchronously(process, "button.text.clear.authentication.cache", false, configuration.getProject());
}
}
}
}