package org.rstudio.studio.client.rsconnect.ui;
import org.rstudio.core.client.resources.ImageResource2x;
import org.rstudio.core.client.widget.OperationWithInput;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.core.client.widget.WizardPage;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.GlobalDisplay.NewWindowOptions;
import org.rstudio.studio.client.common.Value;
import org.rstudio.studio.client.common.satellite.events.WindowClosedEvent;
import org.rstudio.studio.client.rsconnect.model.NewRSConnectAccountInput;
import org.rstudio.studio.client.rsconnect.model.NewRSConnectAccountResult;
import org.rstudio.studio.client.rsconnect.model.RSConnectAuthUser;
import org.rstudio.studio.client.rsconnect.model.RSConnectPreAuthToken;
import org.rstudio.studio.client.rsconnect.model.RSConnectServerInfo;
import org.rstudio.studio.client.rsconnect.model.RSConnectServerOperations;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.Widget;
public class NewRSConnectAuthPage
extends WizardPage<NewRSConnectAccountInput,
NewRSConnectAccountResult>
implements WindowClosedEvent.Handler
{
public NewRSConnectAuthPage()
{
super("", "", "Verifying Account",
new ImageResource2x(RSConnectResources.INSTANCE.localAccountIcon2x()),
new ImageResource2x(RSConnectResources.INSTANCE.localAccountIconLarge2x()));
// listen for window close events (this page needs to know when the user
// closes the auth dialog
RStudioGinjector.INSTANCE.getEventBus().addHandler(
WindowClosedEvent.TYPE,
this);
waitingForAuth_.addValueChangeHandler(new ValueChangeHandler<Boolean>()
{
@Override
public void onValueChange(ValueChangeEvent<Boolean> waiting)
{
if (setOkButtonVisible_ != null)
setOkButtonVisible_.execute(!waiting.getValue());
}
});
}
@Override
public void focus()
{
}
@Override
public void setIntermediateResult(NewRSConnectAccountResult result)
{
result_ = result;
}
@Override
public void onActivate(final ProgressIndicator indicator)
{
if (waitingForAuth_.getValue() || result_ == null)
return;
// save reference to parent wizard's progress indicator for retries
wizardIndicator_ = indicator;
indicator.onProgress("Checking server connection...");
server_.validateServerUrl(result_.getServerUrl(),
new ServerRequestCallback<RSConnectServerInfo>()
{
@Override
public void onResponseReceived(RSConnectServerInfo info)
{
if (info.isValid())
{
result_.setServerInfo(info);
getPreAuthToken(indicator);
}
else
{
contents_.showError("Server Validation Failed",
"The URL '" + result_.getServerUrl() +
"' does not appear to belong to a valid server. Please " +
"double check the URL, and contact your administrator " +
"if the problem persists.\n\n" +
info.getMessage());
indicator.clearProgress();
}
}
@Override
public void onError(ServerError error)
{
contents_.showError("Error Connecting Account",
"The server couldn't be validated. " +
error.getMessage());
indicator.clearProgress();
}
});
}
@Override
public void onWindowClosed(WindowClosedEvent event)
{
if (event.getName().equals(AUTH_WINDOW_NAME))
{
waitingForAuth_.setValue(false, true);
// check to see if the user successfully authenticated
onAuthCompleted();
}
}
@Override
public void onWizardClosing()
{
// this will cause us to stop polling for auth (if we haven't already)
waitingForAuth_.setValue(false, true);
}
public void setOkButtonVisible(OperationWithInput<Boolean> okButtonVisible)
{
setOkButtonVisible_ = okButtonVisible;
}
@Override
protected Widget createWidget()
{
contents_ = new RSConnectAuthWait();
contents_.setOnTryAgain(new Command()
{
@Override
public void execute()
{
onActivate(wizardIndicator_);
}
});
return contents_;
}
@Override
protected void initialize(NewRSConnectAccountInput initData)
{
server_ = initData.getServer();
display_ = initData.getDisplay();
}
@Override
protected NewRSConnectAccountResult collectInput()
{
return result_;
}
@Override
protected boolean validate(NewRSConnectAccountResult input)
{
return input != null && input.getAuthUser() != null;
}
private void pollForAuthCompleted()
{
Scheduler.get().scheduleFixedDelay(new RepeatingCommand()
{
@Override
public boolean execute()
{
// don't keep polling once auth is complete or window is closed
if (!waitingForAuth_.getValue())
return false;
// avoid re-entrancy--if we're already running a check but it hasn't
// returned for some reason, just wait for it to finish
if (runningAuthCompleteCheck_)
return true;
runningAuthCompleteCheck_ = true;
server_.getUserFromToken(result_.getServerInfo().getUrl(),
result_.getPreAuthToken(),
new ServerRequestCallback<RSConnectAuthUser>()
{
@Override
public void onResponseReceived(RSConnectAuthUser user)
{
runningAuthCompleteCheck_ = false;
// expected if user hasn't finished authenticating yet,
// just wait and try again
if (!user.isValidUser())
return;
// user is valid--cache account info and close the
// window
result_.setAuthUser(user);
waitingForAuth_.setValue(false, true);
if (Desktop.isDesktop())
{
// on the desktop, we can close the window by name
Desktop.getFrame().closeNamedWindow(
AUTH_WINDOW_NAME);
}
onUserAuthVerified();
}
@Override
public void onError(ServerError error)
{
// ignore this error
runningAuthCompleteCheck_ = false;
}
});
return true;
}
}, 1000);
}
private void onAuthCompleted()
{
server_.getUserFromToken(result_.getServerInfo().getUrl(),
result_.getPreAuthToken(),
new ServerRequestCallback<RSConnectAuthUser>()
{
@Override
public void onResponseReceived(RSConnectAuthUser user)
{
if (!user.isValidUser())
{
contents_.showError("Account Not Connected",
"Authentication failed. If you did not cancel " +
"authentication, try again, or contact your server " +
"administrator for assistance.");
}
else
{
result_.setAuthUser(user);
onUserAuthVerified();
}
}
@Override
public void onError(ServerError error)
{
contents_.showError("Account Validation Failed",
"RStudio failed to determine whether the account was " +
"valid. Try again; if the error persists, contact your " +
"server administrator.\n\n" +
result_.getServerInfo().getInfoString() + "\n" +
error.getMessage());
}
});
}
private void onUserAuthVerified()
{
// set the account nickname if we didn't already have one
if (result_.getAccountNickname().length() == 0)
{
if (result_.getAuthUser().getUsername().length() > 0)
{
// if we have a username already, just use it
result_.setAccountNickname(
result_.getAuthUser().getUsername());
}
else
{
// if we don't have any username, guess one based on user's given name
// on the server
result_.setAccountNickname(
result_.getAuthUser().getFirstName().substring(0, 1) +
result_.getAuthUser().getLastName().toLowerCase());
}
}
contents_.showSuccess(result_.getServerName(),
result_.getAccountNickname());
}
private void getPreAuthToken(ProgressIndicator indicator)
{
getPreAuthToken(result_, result_.getServerInfo(), indicator,
new OperationWithInput<NewRSConnectAccountResult>()
{
@Override
public void execute(NewRSConnectAccountResult input)
{
// do nothing if no result returned
if (input == null)
return;
// save intermediate result
result_ = input;
contents_.setClaimLink(result_.getServerInfo().getName(),
result_.getPreAuthToken().getClaimUrl());
// begin waiting for user to complete authentication
waitingForAuth_.setValue(true, true);
contents_.showWaiting();
// prepare a new window with the auth URL loaded
if (canSpawnAuthenticationWindow())
{
NewWindowOptions options = new NewWindowOptions();
options.setName(AUTH_WINDOW_NAME);
options.setAllowExternalNavigation(true);
options.setShowDesktopToolbar(false);
display_.openWebMinimalWindow(
result_.getPreAuthToken().getClaimUrl(),
false,
700, 800, options);
}
else
{
Desktop.getFrame().browseUrl(result_.getPreAuthToken().getClaimUrl());
}
// close the window automatically when authentication finishes
pollForAuthCompleted();
}
});
}
private void getPreAuthToken(
final NewRSConnectAccountResult result,
final RSConnectServerInfo serverInfo,
final ProgressIndicator indicator,
final OperationWithInput<NewRSConnectAccountResult> onResult)
{
indicator.onProgress("Setting up an account...");
server_.getPreAuthToken(serverInfo.getName(),
new ServerRequestCallback<RSConnectPreAuthToken>()
{
@Override
public void onResponseReceived(final RSConnectPreAuthToken token)
{
NewRSConnectAccountResult newResult = result;
newResult.setPreAuthToken(token);
newResult.setServerInfo(serverInfo);
onResult.execute(newResult);
indicator.clearProgress();
}
@Override
public void onError(ServerError error)
{
display_.showErrorMessage("Error Connecting Account",
"The server appears to be valid, but rejected the " +
"request to authorize an account.\n\n"+
serverInfo.getInfoString() + "\n" +
error.getMessage());
indicator.clearProgress();
onResult.execute(null);
}
});
}
private boolean canSpawnAuthenticationWindow()
{
if (!Desktop.isDesktop())
return true;
if (Desktop.getFrame().isCentOS())
return false;
return true;
}
private OperationWithInput<Boolean> setOkButtonVisible_;
private NewRSConnectAccountResult result_;
private RSConnectServerOperations server_;
private GlobalDisplay display_;
private RSConnectAuthWait contents_;
private Value<Boolean> waitingForAuth_ = new Value<Boolean>(false);
private boolean runningAuthCompleteCheck_ = false;
private ProgressIndicator wizardIndicator_;
public final static String AUTH_WINDOW_NAME = "rstudio_rsconnect_auth";
}