1 /*
2  * RSAccountConnector.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.studio.client.rsconnect.ui;
16 
17 import org.rstudio.core.client.command.CommandBinder;
18 import org.rstudio.core.client.command.Handler;
19 import org.rstudio.core.client.widget.Operation;
20 import org.rstudio.core.client.widget.OperationWithInput;
21 import org.rstudio.core.client.widget.ProgressOperationWithInput;
22 import org.rstudio.core.client.widget.ProgressIndicator;
23 import org.rstudio.studio.client.application.events.EventBus;
24 import org.rstudio.studio.client.common.GlobalDisplay;
25 import org.rstudio.studio.client.common.satellite.Satellite;
26 import org.rstudio.studio.client.rsconnect.events.EnableRStudioConnectUIEvent;
27 import org.rstudio.studio.client.rsconnect.model.NewRSConnectAccountResult;
28 import org.rstudio.studio.client.rsconnect.model.NewRSConnectAccountResult.AccountType;
29 import org.rstudio.studio.client.rsconnect.model.RSConnectAccount;
30 import org.rstudio.studio.client.rsconnect.model.RSConnectAuthUser;
31 import org.rstudio.studio.client.rsconnect.model.RSConnectPreAuthToken;
32 import org.rstudio.studio.client.rsconnect.model.RSConnectServerEntry;
33 import org.rstudio.studio.client.rsconnect.model.RSConnectServerInfo;
34 import org.rstudio.studio.client.rsconnect.model.RSConnectServerOperations;
35 import org.rstudio.studio.client.server.ServerError;
36 import org.rstudio.studio.client.server.ServerRequestCallback;
37 import org.rstudio.studio.client.server.Void;
38 import org.rstudio.studio.client.workbench.commands.Commands;
39 import org.rstudio.studio.client.workbench.model.Session;
40 import org.rstudio.studio.client.workbench.model.SessionUtils;
41 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
42 import org.rstudio.studio.client.workbench.prefs.model.UserState;
43 import org.rstudio.studio.client.workbench.prefs.views.PublishingPreferencesPane;
44 import org.rstudio.studio.client.workbench.ui.OptionsLoader;
45 
46 import com.google.gwt.core.client.JsArray;
47 import com.google.gwt.event.logical.shared.CloseEvent;
48 import com.google.gwt.event.logical.shared.CloseHandler;
49 import com.google.gwt.user.client.ui.PopupPanel;
50 import com.google.inject.Inject;
51 import com.google.inject.Provider;
52 import com.google.inject.Singleton;
53 
54 @Singleton
55 public class RSAccountConnector implements EnableRStudioConnectUIEvent.Handler
56 {
57    public interface Binder
58    extends CommandBinder<Commands, RSAccountConnector> {}
59 
60    // possible results of attempting to connect an account
61    enum AccountConnectResult
62    {
63       Incomplete,
64       Successful,
65       Failed
66    }
67 
68    @Inject
RSAccountConnector(RSConnectServerOperations server, GlobalDisplay display, Commands commands, Binder binder, OptionsLoader.Shim optionsLoader, EventBus events, Session session, Provider<UserPrefs> pUiPrefs, Provider<UserState> pState, Satellite satellite)69    public RSAccountConnector(RSConnectServerOperations server,
70          GlobalDisplay display,
71          Commands commands,
72          Binder binder,
73          OptionsLoader.Shim optionsLoader,
74          EventBus events,
75          Session session,
76          Provider<UserPrefs> pUiPrefs,
77          Provider<UserState> pState,
78          Satellite satellite)
79    {
80       server_ = server;
81       display_ = display;
82       optionsLoader_ = optionsLoader;
83       pUserPrefs_ = pUiPrefs;
84       pUserState_ = pState;
85       session_ = session;
86 
87       events.addHandler(EnableRStudioConnectUIEvent.TYPE, this);
88 
89       binder.bind(commands, this);
90 
91       // register satellite callback
92       if (!Satellite.isCurrentWindowSatellite())
93          exportManageAccountsCallback();
94    }
95 
showAccountWizard( boolean forFirstAccount, boolean withCloudOption, final OperationWithInput<Boolean> onCompleted)96    public void showAccountWizard(
97          boolean forFirstAccount,
98          boolean withCloudOption,
99          final OperationWithInput<Boolean> onCompleted)
100    {
101       if (pUserState_.get().enableRsconnectPublishUi().getGlobalValue())
102       {
103          showAccountTypeWizard(forFirstAccount, withCloudOption, onCompleted);
104       }
105       else
106       {
107          showShinyAppsDialog(onCompleted);
108       }
109    }
110 
showReconnectWizard( final RSConnectAccount account, final OperationWithInput<Boolean> onCompleted)111    public void showReconnectWizard(
112          final RSConnectAccount account,
113          final OperationWithInput<Boolean> onCompleted)
114    {
115       server_.getServerUrls(new ServerRequestCallback<JsArray<RSConnectServerEntry>>()
116       {
117          @Override
118          public void onResponseReceived(JsArray<RSConnectServerEntry> entries)
119          {
120             boolean found = false;
121             for (int i = 0; i < entries.length(); i++)
122             {
123                if (entries.get(i).getName().equalsIgnoreCase(
124                      account.getServer()))
125                {
126                   RSConnectReconnectWizard wizard =
127                         new RSConnectReconnectWizard(
128                         server_,
129                         display_,
130                         account,
131                         entries.get(i).getUrl(),
132                         new ProgressOperationWithInput<NewRSConnectAccountResult>()
133                   {
134                      @Override
135                      public void execute(NewRSConnectAccountResult input,
136                            final ProgressIndicator indicator)
137                      {
138                         processDialogResult(input, indicator, onCompleted);
139                      }
140                   });
141                   wizard.showModal();
142                   found = true;
143                   break;
144                }
145             }
146 
147             if (!found)
148             {
149                display_.showErrorMessage("Server Information Not Found",
150                      "RStudio could not retrieve server information for " +
151                      "the selected account.");
152             }
153          }
154 
155          @Override
156          public void onError(ServerError error)
157          {
158             display_.showErrorMessage("Can't Find Servers",
159                   "RStudio could not retrieve server information.");
160          }
161       });
162    }
163 
164    @Handler
onRsconnectManageAccounts()165    public void onRsconnectManageAccounts()
166    {
167       if (Satellite.isCurrentWindowSatellite())
168       {
169          callSatelliteManageAccounts();
170       }
171       else
172       {
173          optionsLoader_.showOptions(PublishingPreferencesPane.class, true);
174       }
175    }
176 
177    // Event handlers ---------------------------------------------------------
178 
179    @Override
onEnableRStudioConnectUI(EnableRStudioConnectUIEvent event)180    public void onEnableRStudioConnectUI(EnableRStudioConnectUIEvent event)
181    {
182       pUserState_.get().enableRsconnectPublishUi().setGlobalValue(event.getEnable());
183       pUserState_.get().writeState();
184    }
185 
186    // Private methods --------------------------------------------------------
187 
showShinyAppsDialog( final OperationWithInput<Boolean> onCompleted)188    private void showShinyAppsDialog(
189          final OperationWithInput<Boolean> onCompleted)
190    {
191       RSConnectCloudDialog dialog = new RSConnectCloudDialog(
192       new ProgressOperationWithInput<NewRSConnectAccountResult>()
193       {
194          @Override
195          public void execute(NewRSConnectAccountResult input,
196                              ProgressIndicator indicator)
197          {
198             processDialogResult(input, indicator, onCompleted);
199          }
200       },
201       new Operation()
202       {
203          @Override
204          public void execute()
205          {
206             onCompleted.execute(false);
207          }
208       });
209       dialog.showModal();
210    }
211 
showAccountTypeWizard( boolean forFirstAccount, boolean withCloudOption, final OperationWithInput<Boolean> onCompleted)212    private void showAccountTypeWizard(
213          boolean forFirstAccount,
214          boolean withCloudOption,
215          final OperationWithInput<Boolean> onCompleted)
216    {
217       // ignore if wizard is already up
218       if (showingWizard_)
219          return;
220 
221       RSConnectAccountWizard wizard = new RSConnectAccountWizard(
222             server_,
223             display_,
224             forFirstAccount,
225             withCloudOption &&
226                SessionUtils.showExternalPublishUi(session_, pUserState_.get()),
227             new ProgressOperationWithInput<NewRSConnectAccountResult>()
228       {
229          @Override
230          public void execute(NewRSConnectAccountResult input,
231                final ProgressIndicator indicator)
232          {
233             processDialogResult(input, indicator, onCompleted);
234          }
235       });
236       wizard.setGlassEnabled(true);
237       wizard.showModal();
238 
239       // remember whether wizard is showing
240       showingWizard_ = true;
241       wizard.addCloseHandler(new CloseHandler<PopupPanel>()
242       {
243          @Override
244          public void onClose(CloseEvent<PopupPanel> arg0)
245          {
246             showingWizard_ = false;
247          }
248       });
249    }
250 
processDialogResult(final NewRSConnectAccountResult input, final ProgressIndicator indicator, final OperationWithInput<Boolean> onCompleted)251    private void processDialogResult(final NewRSConnectAccountResult input,
252          final ProgressIndicator indicator,
253          final OperationWithInput<Boolean> onCompleted)
254    {
255       connectNewAccount(input, indicator,
256             new OperationWithInput<AccountConnectResult>()
257       {
258          @Override
259          public void execute(AccountConnectResult input)
260          {
261             if (input == AccountConnectResult.Failed)
262             {
263                // the connection failed--take down the dialog entirely
264                // (we do this when retrying doesn't make sense)
265                onCompleted.execute(false);
266                indicator.onCompleted();
267             }
268             else if (input == AccountConnectResult.Incomplete)
269             {
270                // the connection didn't finish--take down the progress and
271                // allow retry
272                indicator.clearProgress();
273             }
274             else if (input == AccountConnectResult.Successful)
275             {
276                // successful account connection--mark finished
277                onCompleted.execute(true);
278                indicator.onCompleted();
279             }
280          }
281       });
282    }
283 
connectNewAccount( NewRSConnectAccountResult result, ProgressIndicator indicator, OperationWithInput<AccountConnectResult> onConnected)284    private void connectNewAccount(
285          NewRSConnectAccountResult result,
286          ProgressIndicator indicator,
287          OperationWithInput<AccountConnectResult> onConnected)
288    {
289       if (result.getAccountType() == AccountType.RSConnectCloudAccount)
290       {
291          connectCloudAccount(result, indicator, onConnected);
292       }
293       else
294       {
295          connectLocalAccount(result, indicator, onConnected);
296       }
297    }
298 
connectCloudAccount( final NewRSConnectAccountResult result, final ProgressIndicator indicator, final OperationWithInput<AccountConnectResult> onConnected)299    private void connectCloudAccount(
300          final NewRSConnectAccountResult result,
301          final ProgressIndicator indicator,
302          final OperationWithInput<AccountConnectResult> onConnected)
303    {
304       // get command and substitute rsconnect for shinyapps
305       final String cmd = result.getCloudSecret().replace("shinyapps::",
306                                                          "rsconnect::");
307       if (!cmd.startsWith("rsconnect::setAccountInfo"))
308       {
309          display_.showErrorMessage("Error Connecting Account",
310                "The pasted command should start with " +
311                "rsconnect::setAccountInfo. If you're having trouble, try " +
312                "connecting your account manually; type " +
313                "?rsconnect::setAccountInfo at the R console for help.");
314          onConnected.execute(AccountConnectResult.Incomplete);
315       }
316       indicator.onProgress("Connecting account...");
317       server_.connectRSConnectAccount(cmd,
318             new ServerRequestCallback<Void>()
319       {
320          @Override
321          public void onResponseReceived(Void v)
322          {
323             onConnected.execute(AccountConnectResult.Successful);
324          }
325 
326          @Override
327          public void onError(ServerError error)
328          {
329             display_.showErrorMessage("Error Connecting Account",
330                   "The command '" + cmd + "' failed. You can set up an " +
331                   "account manually by using rsconnect::setAccountInfo; " +
332                   "type ?rsconnect::setAccountInfo at the R console for " +
333                   "more information.");
334             onConnected.execute(AccountConnectResult.Failed);
335          }
336       });
337    }
338 
connectLocalAccount( final NewRSConnectAccountResult result, final ProgressIndicator indicator, final OperationWithInput<AccountConnectResult> onConnected)339    private void connectLocalAccount(
340          final NewRSConnectAccountResult result,
341          final ProgressIndicator indicator,
342          final OperationWithInput<AccountConnectResult> onConnected)
343 
344    {
345       indicator.onProgress("Adding account...");
346       final RSConnectAuthUser user = result.getAuthUser();
347       final RSConnectServerInfo serverInfo = result.getServerInfo();
348       final RSConnectPreAuthToken token = result.getPreAuthToken();
349 
350       server_.registerUserToken(serverInfo.getName(),
351             result.getAccountNickname(),
352             user.getId(), token, new ServerRequestCallback<Void>()
353       {
354          @Override
355          public void onResponseReceived(Void result)
356          {
357             onConnected.execute(AccountConnectResult.Successful);
358          }
359 
360          @Override
361          public void onError(ServerError error)
362          {
363             display_.showErrorMessage("Account Connect Failed",
364                   "Your account was authenticated successfully, but could " +
365                   "not be connected to RStudio. Make sure your installation " +
366                   "of the 'rsconnect' package is correct for the server " +
367                   "you're connecting to.\n\n" +
368                   serverInfo.getInfoString() + "\n" +
369                   error.getMessage());
370             onConnected.execute(AccountConnectResult.Failed);
371          }
372       });
373    }
374 
exportManageAccountsCallback()375    private final native void exportManageAccountsCallback()/*-{
376       var rsAccount = this;
377       $wnd.rsManageAccountsFromRStudioSatellite = $entry(
378          function() {
379             rsAccount.@org.rstudio.studio.client.rsconnect.ui.RSAccountConnector::onRsconnectManageAccounts()();
380          }
381       );
382    }-*/;
383 
callSatelliteManageAccounts()384    private final native void callSatelliteManageAccounts()/*-{
385       $wnd.opener.rsManageAccountsFromRStudioSatellite();
386    }-*/;
387 
388    private final GlobalDisplay display_;
389    private final RSConnectServerOperations server_;
390    private final OptionsLoader.Shim optionsLoader_;
391    private final Provider<UserPrefs> pUserPrefs_;
392    private final Provider<UserState> pUserState_;
393    private final Session session_;
394 
395    private boolean showingWizard_;
396 }
397