1 /*
2  * RSConnect.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;
16 
17 import java.util.ArrayList;
18 import java.util.List;
19 
20 import com.google.gwt.aria.client.Roles;
21 import org.rstudio.core.client.CommandWithArg;
22 import org.rstudio.core.client.JsArrayUtil;
23 import org.rstudio.core.client.StringUtil;
24 import org.rstudio.core.client.command.CommandBinder;
25 import org.rstudio.core.client.dom.WindowEx;
26 import org.rstudio.core.client.files.FileSystemItem;
27 import org.rstudio.core.client.js.JsObject;
28 import org.rstudio.core.client.resources.ImageResource2x;
29 import org.rstudio.core.client.widget.ModalDialogBase;
30 import org.rstudio.core.client.widget.ModalDialogTracker;
31 import org.rstudio.core.client.widget.ProgressIndicator;
32 import org.rstudio.core.client.widget.ProgressOperation;
33 import org.rstudio.core.client.widget.ProgressOperationWithInput;
34 import org.rstudio.core.client.widget.ThemedButton;
35 import org.rstudio.core.client.widget.images.MessageDialogImages;
36 import org.rstudio.studio.client.application.Desktop;
37 import org.rstudio.studio.client.application.events.EventBus;
38 import org.rstudio.studio.client.common.FilePathUtils;
39 import org.rstudio.studio.client.common.GlobalDisplay;
40 import org.rstudio.studio.client.common.dependencies.DependencyManager;
41 import org.rstudio.studio.client.common.rpubs.RPubsUploader;
42 import org.rstudio.studio.client.common.rpubs.model.RPubsServerOperations;
43 import org.rstudio.studio.client.common.rpubs.ui.RPubsUploadDialog;
44 import org.rstudio.studio.client.common.satellite.Satellite;
45 import org.rstudio.studio.client.quarto.model.QuartoConfig;
46 import org.rstudio.studio.client.rsconnect.events.RSConnectActionEvent;
47 import org.rstudio.studio.client.rsconnect.events.RSConnectDeployInitiatedEvent;
48 import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentCancelledEvent;
49 import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentCompletedEvent;
50 import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentFailedEvent;
51 import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentStartedEvent;
52 import org.rstudio.studio.client.rsconnect.model.PlotPublishMRUList;
53 import org.rstudio.studio.client.rsconnect.model.QmdPublishDetails;
54 import org.rstudio.studio.client.rsconnect.model.RSConnectApplicationInfo;
55 import org.rstudio.studio.client.rsconnect.model.RSConnectDeploymentRecord;
56 import org.rstudio.studio.client.rsconnect.model.RSConnectDirectoryState;
57 import org.rstudio.studio.client.rsconnect.model.RSConnectLintResults;
58 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishInput;
59 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishResult;
60 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishSettings;
61 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishSource;
62 import org.rstudio.studio.client.rsconnect.model.RSConnectServerOperations;
63 import org.rstudio.studio.client.rsconnect.model.RenderedDocPreview;
64 import org.rstudio.studio.client.rsconnect.model.RmdPublishDetails;
65 import org.rstudio.studio.client.rsconnect.ui.RSAccountConnector;
66 import org.rstudio.studio.client.rsconnect.ui.RSConnectDeployDialog;
67 import org.rstudio.studio.client.rsconnect.ui.RSConnectPublishWizard;
68 import org.rstudio.studio.client.server.ServerError;
69 import org.rstudio.studio.client.server.ServerRequestCallback;
70 import org.rstudio.studio.client.workbench.commands.Commands;
71 import org.rstudio.studio.client.workbench.events.SessionInitEvent;
72 import org.rstudio.studio.client.workbench.model.ClientState;
73 import org.rstudio.studio.client.workbench.model.Session;
74 import org.rstudio.studio.client.workbench.model.SessionUtils;
75 import org.rstudio.studio.client.workbench.model.helper.JSObjectStateValue;
76 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
77 import org.rstudio.studio.client.workbench.prefs.model.UserState;
78 import org.rstudio.studio.client.workbench.views.source.model.SourceServerOperations;
79 
80 import com.google.gwt.core.client.JavaScriptObject;
81 import com.google.gwt.core.client.JsArray;
82 import com.google.gwt.core.client.JsArrayString;
83 import com.google.gwt.dom.client.Style.Unit;
84 import com.google.gwt.event.dom.client.ClickEvent;
85 import com.google.gwt.event.dom.client.ClickHandler;
86 import com.google.gwt.user.client.ui.HTML;
87 import com.google.gwt.user.client.ui.HorizontalPanel;
88 import com.google.gwt.user.client.ui.Image;
89 import com.google.gwt.user.client.ui.Widget;
90 import com.google.inject.Inject;
91 import com.google.inject.Provider;
92 import com.google.inject.Singleton;
93 
94 @Singleton
95 public class RSConnect implements SessionInitEvent.Handler,
96                                   RSConnectActionEvent.Handler,
97                                   RSConnectDeployInitiatedEvent.Handler,
98                                   RSConnectDeploymentCompletedEvent.Handler,
99                                   RSConnectDeploymentFailedEvent.Handler,
100                                   RSConnectDeploymentCancelledEvent.Handler
101 {
102    public interface Binder
103            extends CommandBinder<Commands, RSConnect> {}
104 
105    @Inject
RSConnect(EventBus events, Commands commands, Session session, GlobalDisplay display, DependencyManager dependencyManager, Binder binder, RSConnectServerOperations server, SourceServerOperations sourceServer, RPubsServerOperations rpubsServer, RSAccountConnector connector, Provider<UserPrefs> pUserPrefs, Provider<UserState> pUserState, PlotPublishMRUList plotMru)106    public RSConnect(EventBus events,
107                     Commands commands,
108                     Session session,
109                     GlobalDisplay display,
110                     DependencyManager dependencyManager,
111                     Binder binder,
112                     RSConnectServerOperations server,
113                     SourceServerOperations sourceServer,
114                     RPubsServerOperations rpubsServer,
115                     RSAccountConnector connector,
116                     Provider<UserPrefs> pUserPrefs,
117                     Provider<UserState> pUserState,
118                     PlotPublishMRUList plotMru)
119    {
120       commands_ = commands;
121       display_ = display;
122       dependencyManager_ = dependencyManager;
123       session_ = session;
124       server_ = server;
125       sourceServer_ = sourceServer;
126       rpubsServer_ = rpubsServer;
127       events_ = events;
128       connector_ = connector;
129       pUserPrefs_ = pUserPrefs;
130       pUserState_ = pUserState;
131       plotMru_ = plotMru;
132 
133       binder.bind(commands, this);
134 
135       events.addHandler(SessionInitEvent.TYPE, this);
136       events.addHandler(RSConnectActionEvent.TYPE, this);
137       events.addHandler(RSConnectDeployInitiatedEvent.TYPE, this);
138       events.addHandler(RSConnectDeploymentCompletedEvent.TYPE, this);
139       events.addHandler(RSConnectDeploymentFailedEvent.TYPE, this);
140       events.addHandler(RSConnectDeploymentCancelledEvent.TYPE, this);
141 
142       // satellite windows don't get session init events, so initialize the
143       // session here
144       if (Satellite.isCurrentWindowSatellite())
145       {
146          ensureSessionInit();
147       }
148 
149       exportNativeCallbacks();
150    }
151 
152    @Override
onSessionInit(SessionInitEvent sie)153    public void onSessionInit(SessionInitEvent sie)
154    {
155       ensureSessionInit();
156    }
157 
158    @Override
onRSConnectAction(final RSConnectActionEvent event)159    public void onRSConnectAction(final RSConnectActionEvent event)
160    {
161       // ignore if we're already waiting for a dependency check
162       if (depsPending_)
163          return;
164 
165       // see if we have the requisite R packages
166       depsPending_ = true;
167       dependencyManager_.withRSConnect(
168          "Publishing content",
169          event.getContentType() == CONTENT_TYPE_DOCUMENT ||
170          event.getContentType() == CONTENT_TYPE_WEBSITE ||
171          event.getContentType() == CONTENT_TYPE_QUARTO_WEBSITE ,
172          null, new CommandWithArg<Boolean>() {
173             @Override
174             public void execute(Boolean succeeded)
175             {
176                if (succeeded)
177                   handleRSConnectAction(event);
178 
179                depsPending_ = false;
180             }
181          });
182    }
183 
supportedRPubsDocExtension(String filename)184    private boolean supportedRPubsDocExtension(String filename)
185    {
186       if (StringUtil.isNullOrEmpty(filename))
187          return false;
188 
189       String extension = FileSystemItem.getExtensionFromPath(filename).toLowerCase();
190       return StringUtil.equals(extension, ".html") ||
191              StringUtil.equals(extension, ".htm") ||
192              StringUtil.equals(extension, ".nb.html");
193    }
194 
publishAsRPubs(RSConnectActionEvent event)195    private void publishAsRPubs(RSConnectActionEvent event)
196    {
197       // If previously published but the rendered file is now missing, give a warning instead
198       // of trying to republish.
199       if (event.getFromPrevious() != null &&
200             !StringUtil.isNullOrEmpty(event.getFromPrevious().getBundleId()) &&
201             StringUtil.isNullOrEmpty(event.getHtmlFile()))
202       {
203          display_.showErrorMessage("Republish Document",
204                "Only rendered documents can be republished to RPubs. " +
205                "To republish this document, click Knit or Preview to render it to HTML, then " +
206                "click the Republish button above the rendered document.");
207          return;
208       }
209 
210       // If we don't have an html file, can't publish to RPubs, e.g. create a generic markdown
211       // file (.md), don't preview it, and try to publish it to RPubs. Also, prevent publishing
212       // unsupported output formats; relatively easy to get in this state; e.g. Knit and
213       // publish HTML to RPubs, then Knit as PDF and try to republish.
214       if (StringUtil.isNullOrEmpty(event.getHtmlFile()) ||
215             (event.getContentType() == CONTENT_TYPE_DOCUMENT &&
216                   !supportedRPubsDocExtension(event.getHtmlFile())))
217       {
218          showUnsupportedRPubsFormatMessage();
219          return;
220       }
221 
222       String ctx = "Publish " + contentTypeDesc(event.getContentType());
223       RPubsUploadDialog dlg = new RPubsUploadDialog(
224             "Publish Wizard",
225             ctx,
226             event.getFromPreview() != null ?
227                   event.getFromPreview().getSourceFile() : null,
228             event.getHtmlFile(),
229             event.getFromPrevious() == null ?
230                   "" : event.getFromPrevious().getBundleId(),
231             false);
232       dlg.showModal();
233    }
234 
showPublishUI(final RSConnectActionEvent event)235    private void showPublishUI(final RSConnectActionEvent event)
236    {
237       final RSConnectPublishInput input = new RSConnectPublishInput(event);
238 
239       // set these inside the wizard input so we don't need to pass around
240       // session/prefs
241       input.setConnectUIEnabled(
242             pUserState_.get().enableRsconnectPublishUi().getGlobalValue());
243       input.setExternalUIEnabled(
244             session_.getSessionInfo().getAllowExternalPublish());
245       input.setDescription(event.getDescription());
246 
247       if (event.getFromPrevious() != null)
248       {
249          switch (event.getContentType())
250          {
251          case CONTENT_TYPE_APP:
252          case CONTENT_TYPE_APP_SINGLE:
253             publishAsCode(event, null, true);
254             break;
255          case CONTENT_TYPE_PRES:
256          case CONTENT_TYPE_PLOT:
257          case CONTENT_TYPE_HTML:
258          case CONTENT_TYPE_DOCUMENT:
259          case CONTENT_TYPE_WEBSITE:
260             if (event.getFromPrevious().getServer() == "rpubs.com")
261             {
262                publishAsRPubs(event);
263             }
264             else
265             {
266                fillInputFromDoc(input, event.getPath(),
267                      new CommandWithArg<RSConnectPublishInput>()
268                {
269                   @Override
270                   public void execute(RSConnectPublishInput arg)
271                   {
272                      if (arg == null)
273                         return;
274 
275                      boolean isQuarto = false;
276                      if (event.getFromPreview() != null)
277                      {
278                         isQuarto = event.getFromPreview().isQuarto();
279                      }
280 
281                      if (event.getFromPrevious().getAsStatic())
282                         publishAsFiles(event,
283                               new RSConnectPublishSource(event.getPath(),
284                                     event.getHtmlFile(),
285                                     arg.getWebsiteDir(),
286                                     arg.getWebsiteOutputDir(),
287                                     arg.isSelfContained(),
288                                     true,
289                                     arg.isShiny(),
290                                     isQuarto,
291                                     arg.getDescription(),
292                                     event.getContentType()));
293                      else
294                         publishAsCode(event, arg.getWebsiteDir(),
295                               arg.isShiny());
296                   }
297                });
298                }
299                break;
300             case CONTENT_TYPE_QUARTO_WEBSITE:
301                // Quarto website publishing metadata is extracted from the active Quarto project
302                QuartoConfig config = session_.getSessionInfo().getQuartoConfig();
303                FileSystemItem projectDir = FileSystemItem.createDir(config.project_dir);
304                String websiteOutputDir = projectDir.completePath(config.project_output_dir);
305 
306                if (event.getFromPrevious().getAsStatic())
307                {
308                   publishAsFiles(event,
309                      new RSConnectPublishSource(event.getPath(),
310                         config.project_dir,
311                         config.project_dir,
312                         websiteOutputDir,
313                         input.isSelfContained(),
314                         true, // isStatic
315                         false, // isShiny
316                         true, // isQuarto
317                         input.getDescription(),
318                         event.getContentType()));
319                }
320                else
321                {
322                   publishAsCode(event, config.project_dir, false /* isShiny */);
323                }
324                break;
325 
326 
327             case CONTENT_TYPE_PLUMBER_API:
328                publishAsCode(event, null, false);
329                break;
330          }
331       }
332       else
333       {
334          // plots and HTML are implicitly self-contained
335          if (event.getContentType() == CONTENT_TYPE_PLOT ||
336              event.getContentType() == CONTENT_TYPE_HTML ||
337              event.getContentType() == CONTENT_TYPE_PRES)
338          {
339             input.setIsSelfContained(true);
340          }
341 
342          // if R Markdown, get info on what we're publishing from the server
343          if (event.getFromPreview() != null)
344          {
345             input.setSourceRmd(FileSystemItem.createFile(
346                   event.getFromPreview().getSourceFile()));
347             fillInputFromDoc(input, event.getFromPreview().getSourceFile(),
348                   new CommandWithArg<RSConnectPublishInput>()
349             {
350                @Override
351                public void execute(RSConnectPublishInput arg)
352                {
353                   showPublishUI(arg);
354                }
355             });
356          }
357          else if (event.getContentType() == RSConnect.CONTENT_TYPE_QUARTO_WEBSITE)
358          {
359             QuartoConfig config = session_.getSessionInfo().getQuartoConfig();
360             FileSystemItem projectDir = FileSystemItem.createDir(config.project_dir);
361 
362             // fill publish input from session
363             input.setIsQuarto(true);
364             input.setWebsiteDir(config.project_dir);
365             input.setWebsiteOutputDir(projectDir.completePath(config.project_output_dir));
366             showPublishUI(input);
367          }
368          else
369          {
370             showPublishUI(input);
371          }
372       }
373    }
374 
showPublishUI(RSConnectPublishInput input)375    private void showPublishUI(RSConnectPublishInput input)
376    {
377       final RSConnectActionEvent event = input.getOriginatingEvent();
378       if (input.getContentType() == CONTENT_TYPE_PLOT ||
379           input.getContentType() == CONTENT_TYPE_HTML ||
380           input.getContentType() == CONTENT_TYPE_PRES)
381       {
382          if (!input.isConnectUIEnabled() && input.isExternalUIEnabled())
383          {
384             publishAsRPubs(event);
385          }
386          else if (input.isConnectUIEnabled() && input.isExternalUIEnabled())
387          {
388             publishWithWizard(input);
389          }
390          else if (input.isConnectUIEnabled() && !input.isExternalUIEnabled())
391          {
392             publishAsStatic(input);
393          }
394       }
395       else if (input.isWebsiteContentType() ||
396                (input.getContentType() == CONTENT_TYPE_DOCUMENT && input.isWebsiteRmd()))
397       {
398          if (input.hasDocOutput() || input.isWebsiteContentType())
399          {
400             publishWithWizard(input);
401          }
402          else
403          {
404             publishAsCode(event, input.getWebsiteDir(), false);
405          }
406       }
407       else if (input.getContentType() == CONTENT_TYPE_DOCUMENT)
408       {
409          if (input.isShiny())
410          {
411             if (input.isMultiRmd())
412             {
413                // multiple Shiny doc
414                publishWithWizard(input);
415             }
416             else
417             {
418                // single Shiny doc
419                publishAsCode(event, input.getWebsiteDir(), true);
420             }
421          }
422          else
423          {
424             if (input.isConnectUIEnabled())
425             {
426                // need to disambiguate between code/output and/or
427                // single/multi page
428                publishWithWizard(input);
429             }
430             else if (!input.isSelfContained())
431             {
432                // we should generally hide the button in this case
433                display_.showErrorMessage("Content Not Publishable",
434                      "Only self-contained documents can currently be " +
435                      "published to RPubs.");
436             }
437             else
438             {
439                // RStudio Connect is disabled, go straight to RPubs
440                publishAsRPubs(event);
441             }
442          }
443       }
444       else if (input.getContentType() == CONTENT_TYPE_APP ||
445                input.getContentType() == CONTENT_TYPE_APP_SINGLE)
446       {
447          publishAsCode(event, null, true);
448       }
449       else if (input.getContentType() == CONTENT_TYPE_PLUMBER_API)
450       {
451          if (!input.isConnectUIEnabled())
452          {
453             display_.showErrorMessage("API Not Publishable",
454                      "Publishing to RStudio Connect is disabled in the Publishing options.");
455          }
456          else
457          {
458             publishAsCode(event, null, false);
459          }
460       }
461    }
462 
publishAsCode(RSConnectActionEvent event, String websiteDir, boolean isShiny)463    private void publishAsCode(RSConnectActionEvent event, String websiteDir, boolean isShiny)
464    {
465       boolean isAPI = event.getContentType() == CONTENT_TYPE_PLUMBER_API;
466 
467       RSConnectPublishSource source = null;
468       if (event.getContentType() == CONTENT_TYPE_APP ||
469           event.getContentType() == CONTENT_TYPE_APP_SINGLE ||
470           isAPI)
471       {
472          if (StringUtil.getExtension(event.getPath()).equalsIgnoreCase("r"))
473          {
474             FileSystemItem rFile = FileSystemItem.createFile(event.getPath());
475 
476             // use the directory for the deployment record when publishing APIs or
477             // directory-based apps; use the file itself when publishing
478             // single-file apps
479             source = new RSConnectPublishSource(rFile.getParentPathString(),
480                   event.getContentType() == CONTENT_TYPE_APP_SINGLE ?
481                         rFile.getName() :
482                         rFile.getParentPathString(),
483                         isAPI);
484 
485          }
486          else
487          {
488             source = new RSConnectPublishSource(event.getPath(),
489                   event.getPath(),
490                   isAPI);
491          }
492       }
493       else
494       {
495          source = new RSConnectPublishSource(event.getPath(), websiteDir,
496             false, false, isShiny,
497             event.getContentType() == RSConnect.CONTENT_TYPE_QUARTO_WEBSITE, null, event.getContentType());
498       }
499 
500       // detect quarto
501       if (event.getFromPreview() != null)
502       {
503          source.setIsQuarto(event.getFromPreview().isQuarto());
504       }
505 
506       publishAsFiles(event, source);
507    }
508 
publishAsStatic(RSConnectPublishInput input)509    private void publishAsStatic(RSConnectPublishInput input)
510    {
511       RSConnectPublishSource source = null;
512       if (input.getContentType() == RSConnect.CONTENT_TYPE_DOCUMENT ||
513           input.isWebsiteContentType())
514       {
515          source = new RSConnectPublishSource(
516                      input.getOriginatingEvent().getFromPreview(),
517                      input.getWebsiteDir(),
518                      input.isSelfContained(),
519                      true,
520                      input.isShiny(),
521                      input.getDescription());
522       }
523       else
524       {
525          source = new RSConnectPublishSource(
526                input.getOriginatingEvent().getHtmlFile(),
527                input.getWebsiteDir(),
528                input.isSelfContained(),
529                true,
530                input.isShiny(),
531                input.isQuarto(),
532                input.getDescription(),
533                input.getContentType());
534       }
535       publishAsFiles(input.getOriginatingEvent(), source);
536    }
537 
publishAsFiles(RSConnectActionEvent event, RSConnectPublishSource source)538    private void publishAsFiles(RSConnectActionEvent event,
539          RSConnectPublishSource source)
540    {
541       RSConnectDeployDialog dialog =
542             new RSConnectDeployDialog(
543                       event.getContentType(),
544                       server_, this, display_,
545                       source,
546                       event.getFromPrevious());
547       dialog.showModal();
548    }
549 
publishWithWizard(final RSConnectPublishInput input)550    private void publishWithWizard(final RSConnectPublishInput input)
551    {
552       RSConnectPublishWizard wizard =
553             new RSConnectPublishWizard(input,
554                   new ProgressOperationWithInput<RSConnectPublishResult>()
555             {
556                @Override
557                public void execute(RSConnectPublishResult result,
558                      ProgressIndicator indicator)
559                {
560                   switch (result.getPublishType())
561                   {
562                   case RSConnectPublishResult.PUBLISH_STATIC:
563                   case RSConnectPublishResult.PUBLISH_CODE:
564                      // always launch the browser--the wizard implies we're
565                      // doing a first-time publish, and we may need to do some
566                      // post-publish configuration
567                      fireRSConnectPublishEvent(result, true);
568                      indicator.onCompleted();
569                      break;
570                   case RSConnectPublishResult.PUBLISH_RPUBS:
571                      uploadToRPubs(input, result, indicator);
572                      break;
573                   }
574                }
575             });
576       wizard.showModal();
577    }
578 
579    @Override
onRSConnectDeployInitiated( final RSConnectDeployInitiatedEvent event)580    public void onRSConnectDeployInitiated(
581          final RSConnectDeployInitiatedEvent event)
582    {
583       // shortcut: when deploying static content we don't need to do any linting
584       if (event.getSettings().getAsStatic())
585       {
586          doDeployment(event);
587          return;
588       }
589 
590       // get lint results for the file or directory being deployed, as
591       // appropriate
592       server_.getLintResults(event.getSource().getDeployKey(),
593             new ServerRequestCallback<RSConnectLintResults>()
594       {
595          @Override
596          public void onResponseReceived(RSConnectLintResults results)
597          {
598             if (results.getErrorMessage().length() > 0)
599             {
600                display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
601                      "Lint Failed",
602                      "The content you tried to publish could not be checked " +
603                      "for errors. Do you want to proceed? \n\n" +
604                      results.getErrorMessage(), false,
605                      new ProgressOperation()
606                      {
607                         @Override
608                         public void execute(ProgressIndicator indicator)
609                         {
610                            // "Publish Anyway"
611                            doDeployment(event);
612                            indicator.onCompleted();
613                         }
614                      },
615                      new ProgressOperation()
616                      {
617                         @Override
618                         public void execute(ProgressIndicator indicator)
619                         {
620                            // "Cancel"
621                            indicator.onCompleted();
622                         }
623                      },
624                      "Publish Anyway", "Cancel", false);
625             }
626             else if (results.hasLint())
627             {
628                display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
629                      "Publish Content Issues Found",
630                      "Some issues were found in your content, which may " +
631                      "prevent it from working correctly after publishing. " +
632                      "Do you want to review these issues or publish anyway? "
633                      , false,
634                      new ProgressOperation()
635                      {
636                         @Override
637                         public void execute(ProgressIndicator indicator)
638                         {
639                            // "Review Issues" -- we automatically show the
640                            // markers so they're already behind the dialog.
641                            indicator.onCompleted();
642                         }
643                      },
644                      new ProgressOperation() {
645                         @Override
646                         public void execute(ProgressIndicator indicator)
647                         {
648                            // "Publish Anyway"
649                            doDeployment(event);
650                            indicator.onCompleted();
651                         }
652                      },
653                      "Review Issues", "Publish Anyway", true);
654             }
655             else
656             {
657                // no lint and no errors -- good to go for deployment
658                doDeployment(event);
659             }
660          }
661 
662          @Override
663          public void onError(ServerError error)
664          {
665             // we failed to lint, which is not encouraging, but we don't want to
666             // fail the whole deployment lest a balky linter prevent people from
667             // getting their work published, so forge on ahead.
668             doDeployment(event);
669          }
670       });
671    }
672 
673    @Override
onRSConnectDeploymentCompleted( RSConnectDeploymentCompletedEvent event)674    public void onRSConnectDeploymentCompleted(
675          RSConnectDeploymentCompletedEvent event)
676    {
677       if (launchBrowser_ && event.succeeded())
678       {
679          display_.openWindow(event.getUrl());
680       }
681    }
682 
683    @Override
onRSConnectDeploymentCancelled( RSConnectDeploymentCancelledEvent event)684    public void onRSConnectDeploymentCancelled(
685          RSConnectDeploymentCancelledEvent event)
686    {
687       display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
688             "Stop deployment?",
689             "Do you want to stop the deployment process? If the server has already " +
690             "received the content, it will still be published.",
691             false, // include cancel
692             () -> {
693                 server_.cancelPublish(new ServerRequestCallback<Boolean>()
694                 {
695                   @Override
696                   public void onError(ServerError error)
697                   {
698                      display_.showErrorMessage("Error Stopping Deployment",
699                            error.getMessage());
700                   }
701 
702                   @Override
703                   public void onResponseReceived(Boolean result)
704                   {
705                      if (!result)
706                      {
707                        display_.showErrorMessage("Could not cancel deployment",
708                              "The deployment could not be cancelled; it is not running, or termination failed.");
709                      }
710                   }
711                 });
712             },
713             null,
714             null,
715             "Stop deployment",
716             "Cancel",
717             false);
718    }
719 
720    @Override
onRSConnectDeploymentFailed( final RSConnectDeploymentFailedEvent event)721    public void onRSConnectDeploymentFailed(
722          final RSConnectDeploymentFailedEvent event)
723    {
724       String failedPath = event.getData().getPath();
725       // if this looks like an API call, process the path to get the 'bare'
726       // server URL
727       int pos = failedPath.indexOf("__api__");
728       if (pos < 1)
729       {
730          // if not, just get the host
731          pos = failedPath.indexOf("/", 10) + 1;
732       }
733       if (pos > 0)
734       {
735          failedPath = failedPath.substring(0, pos);
736       }
737       final String serverUrl = failedPath;
738 
739       new ModalDialogBase(Roles.getAlertdialogRole())
740       {
741          @Override
742          protected Widget createMainWidget()
743          {
744             setText("Publish Failed");
745             addOkButton(new ThemedButton("OK", new ClickHandler()
746             {
747                @Override
748                public void onClick(ClickEvent arg0)
749                {
750                   closeDialog();
751                }
752             }));
753             HorizontalPanel panel = new HorizontalPanel();
754             Image errorImage =
755                   new Image(new ImageResource2x(MessageDialogImages.INSTANCE.dialog_error2x()));
756             errorImage.getElement().getStyle().setMarginTop(1, Unit.EM);
757             errorImage.getElement().getStyle().setMarginRight(1, Unit.EM);
758             panel.add(errorImage);
759             panel.add(new HTML("<p>Your content could not be published because " +
760                   "of a problem on the server.</p>" +
761                   "<p>More information may be available on the server's home " +
762                   "page:</p>" +
763                   "<p><a href=\"" + serverUrl + "\">" + serverUrl + "</a>" +
764                   "</p>" +
765                   "<p>If the error persists, contact the server's "  +
766                   "administrator.</p>" +
767                   "<p><small>Error code: " + event.getData().getHttpStatus() +
768                   "</small></p>"));
769             return panel;
770          }
771       }.showModal();
772    }
773 
ensureSessionInit()774    public void ensureSessionInit()
775    {
776       if (sessionInited_)
777          return;
778 
779       // "Manage accounts" can be invoked any time we're permitted to
780       // publish
781       commands_.rsconnectManageAccounts().setVisible(
782             SessionUtils.showPublishUi(session_, pUserState_.get()));
783 
784       // This object keeps track of the most recent deployment we made of each
785       // directory, and is used to default directory deployments to last-used
786       // settings.
787       new JSObjectStateValue(
788             "rsconnect",
789             "rsconnectDirectories",
790             ClientState.PERSISTENT,
791             session_.getSessionInfo().getClientState(),
792             false)
793        {
794           @Override
795           protected void onInit(JsObject value)
796           {
797              dirState_ = (RSConnectDirectoryState) (value == null ?
798                    RSConnectDirectoryState.create() :
799                    value.cast());
800           }
801 
802           @Override
803           protected JsObject getValue()
804           {
805              dirStateDirty_ = false;
806              return (JsObject) (dirState_ == null ?
807                    RSConnectDirectoryState.create().cast() :
808                    dirState_.cast());
809           }
810 
811           @Override
812           protected boolean hasChanged()
813           {
814              return dirStateDirty_;
815           }
816        };
817 
818        sessionInited_ = true;
819    }
820 
deployFromSatellite( String sourceFile, String deployDir, String deployFile, String websiteDir, String description, JsArrayString deployFiles, JsArrayString additionalFiles, JsArrayString ignoredFiles, boolean isSelfContained, boolean isShiny, boolean asMultiple, boolean asStatic, boolean isQuarto, boolean launch, JavaScriptObject record)821    public static native void deployFromSatellite(
822          String sourceFile,
823          String deployDir,
824          String deployFile,
825          String websiteDir,
826          String description,
827          JsArrayString deployFiles,
828          JsArrayString additionalFiles,
829          JsArrayString ignoredFiles,
830          boolean isSelfContained,
831          boolean isShiny,
832          boolean asMultiple,
833          boolean asStatic,
834          boolean isQuarto,
835          boolean launch,
836          JavaScriptObject record) /*-{
837       $wnd.opener.deployToRSConnect(sourceFile, deployDir, deployFile,
838                                     websiteDir, description, deployFiles,
839                                     additionalFiles, ignoredFiles, isSelfContained,
840                                     isShiny, asMultiple, asStatic, isQuarto, launch,
841                                     record);
842    }-*/;
843 
844 
showRSConnectUI()845    public static boolean showRSConnectUI()
846    {
847       return true;
848    }
849 
contentTypeDesc(int contentType)850    public static String contentTypeDesc(int contentType)
851    {
852       switch(contentType)
853       {
854       case RSConnect.CONTENT_TYPE_APP:
855       case RSConnect.CONTENT_TYPE_APP_SINGLE:
856          return "Application";
857       case RSConnect.CONTENT_TYPE_PLOT:
858          return "Plot";
859       case RSConnect.CONTENT_TYPE_HTML:
860          return "HTML";
861       case RSConnect.CONTENT_TYPE_DOCUMENT:
862          return "Document";
863       case RSConnect.CONTENT_TYPE_PRES:
864          return "Presentation";
865       case RSConnect.CONTENT_TYPE_WEBSITE:
866          return "Website";
867       case RSConnect.CONTENT_TYPE_PLUMBER_API:
868          return "API";
869       case RSConnect.CONTENT_TYPE_QUARTO_WEBSITE:
870          return "Quarto Website";
871       }
872       return "Content";
873    }
874 
fireRSConnectPublishEvent(RSConnectPublishResult result, boolean launchBrowser)875    public void fireRSConnectPublishEvent(RSConnectPublishResult result,
876          boolean launchBrowser)
877    {
878       if (Satellite.isCurrentWindowSatellite())
879       {
880          // in a satellite window, call back to the main window to do a
881          // deployment
882          RSConnect.deployFromSatellite(
883                result.getSource().getSourceFile(),
884                result.getSource().getDeployDir(),
885                result.getSource().getDeployFile(),
886                result.getSource().getWebsiteDir(),
887                result.getSource().getDescription(),
888                JsArrayUtil.toJsArrayString(
889                      result.getSettings().getDeployFiles()),
890                JsArrayUtil.toJsArrayString(
891                      result.getSettings().getAdditionalFiles()),
892                JsArrayUtil.toJsArrayString(
893                      result.getSettings().getIgnoredFiles()),
894                result.getSource().isSelfContained(),
895                result.getSource().isShiny(),
896                result.getSettings().getAsMultiple(),
897                result.getSettings().getAsStatic(),
898                result.getSource().isQuarto(),
899                launchBrowser,
900                RSConnectDeploymentRecord.create(result.getAppName(),
901                      result.getAppTitle(), result.getAppId(), result.getAccount(), ""));
902 
903          // we can't raise the main window if we aren't in desktop mode, so show
904          // a dialog to guide the user there
905          if (!Desktop.hasDesktopFrame())
906          {
907             display_.showMessage(GlobalDisplay.MSG_INFO, "Deployment Started",
908                   "RStudio is deploying " + result.getAppName() + ". " +
909                   "Check the Deploy console tab in the main window for " +
910                   "status updates. ");
911          }
912       }
913       else
914       {
915          // in the main window, initiate the deployment directly
916          events_.fireEvent(new RSConnectDeployInitiatedEvent(
917                result.getSource(),
918                result.getSettings(),
919                launchBrowser,
920                RSConnectDeploymentRecord.create(result.getAppName(),
921                      result.getAppTitle(), result.getAppId(), result.getAccount(), "")));
922       }
923    }
924 
925    // Private methods ---------------------------------------------------------
showUnsupportedRPubsFormatMessage()926    private void showUnsupportedRPubsFormatMessage()
927    {
928       display_.showErrorMessage("Unsupported Document Format",
929             "Only documents rendered to HTML can be published to RPubs. " +
930             "To publish this document, click Knit or Preview to render it to HTML, then " +
931             "click the Publish button above the rendered document.");
932    }
933 
uploadToRPubs(RSConnectPublishInput input, RSConnectPublishResult result, final ProgressIndicator indicator)934    private void uploadToRPubs(RSConnectPublishInput input,
935          RSConnectPublishResult result,
936          final ProgressIndicator indicator)
937    {
938       if (input.getContentType() == CONTENT_TYPE_DOCUMENT)
939       {
940          if (!input.hasDocOutput())
941          {
942             display_.showErrorMessage("Publish Document",
943                   "Only rendered documents can be published to RPubs. " +
944                   "To publish this document, click Knit or Preview to render it to HTML, then " +
945                   "click the Publish button above the rendered document.");
946             indicator.onCompleted();
947             return;
948          }
949          else if (!supportedRPubsDocExtension(input.getDocOutput()))
950          {
951             showUnsupportedRPubsFormatMessage();
952             indicator.onCompleted();
953             return;
954          }
955       }
956 
957       RPubsUploader uploader = new RPubsUploader(rpubsServer_, display_,
958             events_, "rpubs-" + rpubsCount_++);
959       String contentType = contentTypeDesc(input.getContentType());
960       indicator.onProgress("Uploading " + contentType);
961       uploader.setOnUploadComplete(new CommandWithArg<Boolean>()
962       {
963          @Override
964          public void execute(Boolean arg)
965          {
966             indicator.onCompleted();
967          }
968       });
969       uploader.performUpload(contentType,
970             input.getSourceRmd() == null ? null :
971                input.getSourceRmd().getPath(),
972             input.getOriginatingEvent().getHtmlFile(),
973             input.getOriginatingEvent().getFromPrevious() == null ? "" :
974                input.getOriginatingEvent().getFromPrevious().getBundleId(),
975             false);
976    }
977 
handleRSConnectAction(RSConnectActionEvent event)978    private void handleRSConnectAction(RSConnectActionEvent event)
979    {
980       if (event.getAction() == RSConnectActionEvent.ACTION_TYPE_DEPLOY)
981       {
982          // ignore this request if there's already a modal up
983          if (ModalDialogTracker.numModalsShowing() > 0)
984             return;
985 
986          // show publish UI appropriate to the type of content being deployed
987          showPublishUI(event);
988       }
989       else if (event.getAction() == RSConnectActionEvent.ACTION_TYPE_CONFIGURE)
990       {
991          configureShinyApp(FilePathUtils.dirFromFile(event.getPath()));
992       }
993    }
994 
doDeployment(final RSConnectDeployInitiatedEvent event)995    private void doDeployment(final RSConnectDeployInitiatedEvent event)
996    {
997       server_.publishContent(event.getSource(),
998                              event.getRecord().getAccountName(),
999                              event.getRecord().getServer(),
1000                              event.getRecord().getName(),
1001                              event.getRecord().getTitle(),
1002                              event.getRecord().getAppId(),
1003                              event.getSettings(),
1004       new ServerRequestCallback<Boolean>()
1005       {
1006          @Override
1007          public void onResponseReceived(Boolean status)
1008          {
1009             if (status)
1010             {
1011                dirState_.addDeployment(event.getSource().getDeployDir(),
1012                      event.getRecord());
1013                dirStateDirty_ = true;
1014                if (event.getSource().getContentCategory() ==
1015                      RSConnect.CONTENT_CATEGORY_PLOT)
1016                {
1017                   plotMru_.addPlotMruEntry(event.getRecord().getAccountName(),
1018                         event.getRecord().getServer(),
1019                         event.getRecord().getName(),
1020                         event.getRecord().getTitle());
1021                }
1022                launchBrowser_ = event.getLaunchBrowser();
1023                events_.fireEvent(new RSConnectDeploymentStartedEvent(
1024                      event.getSource().isWebsiteRmd() ? "" :
1025                        event.getSource().getDeployKey(),
1026                      event.getSource().getDescription()));
1027             }
1028             else
1029             {
1030                display_.showErrorMessage("Deployment In Progress",
1031                      "Another deployment is currently in progress; only one " +
1032                      "deployment can be performed at a time.");
1033             }
1034          }
1035 
1036          @Override
1037          public void onError(ServerError error)
1038          {
1039             display_.showErrorMessage("Error Deploying Application",
1040                   "Could not deploy application '" +
1041                   event.getRecord().getName() +
1042                   "': " + error.getMessage());
1043          }
1044       });
1045    }
1046 
1047    // Manage, step 1: create a list of apps deployed from this directory
configureShinyApp(final String dir)1048    private void configureShinyApp(final String dir)
1049    {
1050       server_.getRSConnectDeployments(dir,
1051             "",
1052             new ServerRequestCallback<JsArray<RSConnectDeploymentRecord>>()
1053       {
1054          @Override
1055          public void onResponseReceived(
1056                JsArray<RSConnectDeploymentRecord> records)
1057          {
1058             configureShinyApp(dir, records);
1059          }
1060          @Override
1061          public void onError(ServerError error)
1062          {
1063             display_.showErrorMessage("Error Configuring Application",
1064                   "Could not determine application deployments for '" +
1065                    dir + "':" + error.getMessage());
1066          }
1067       });
1068    }
1069 
1070    // Manage, step 2: Get the status of the applications from the server
configureShinyApp(final String dir, JsArray<RSConnectDeploymentRecord> records)1071    private void configureShinyApp(final String dir,
1072          JsArray<RSConnectDeploymentRecord> records)
1073    {
1074       if (records.length() == 0)
1075       {
1076          display_.showMessage(GlobalDisplay.MSG_INFO, "No Deployments Found",
1077                "No application deployments were found for '" + dir + "'");
1078          return;
1079       }
1080 
1081       // If we know the most recent deployment of the directory, act on that
1082       // deployment by default
1083       final ArrayList<RSConnectDeploymentRecord> recordList = new ArrayList<>();
1084       RSConnectDeploymentRecord lastRecord = dirState_.getLastDeployment(dir);
1085       if (lastRecord != null)
1086       {
1087          recordList.add(lastRecord);
1088       }
1089       for (int i = 0; i < records.length(); i++)
1090       {
1091          RSConnectDeploymentRecord record = records.get(i);
1092          if (lastRecord == null)
1093          {
1094             recordList.add(record);
1095          }
1096          else
1097          {
1098             if (record.getUrl() == lastRecord.getUrl())
1099                recordList.set(0, record);
1100          }
1101       }
1102 
1103       // We need to further filter the list by deployments that are
1104       // eligible for termination (i.e. are currently running)
1105       server_.getRSConnectAppList(recordList.get(0).getAccountName(),
1106             recordList.get(0).getServer(),
1107             new ServerRequestCallback<JsArray<RSConnectApplicationInfo>>()
1108       {
1109          @Override
1110          public void onResponseReceived(JsArray<RSConnectApplicationInfo> apps)
1111          {
1112             configureShinyApp(dir, apps, recordList);
1113          }
1114          @Override
1115          public void onError(ServerError error)
1116          {
1117             display_.showErrorMessage("Error Listing Applications",
1118                   error.getMessage());
1119          }
1120       });
1121    }
1122 
1123    // Manage, step 3: compare the deployments and apps active on the server
1124    // until we find a running app from the current directory
configureShinyApp(String dir, JsArray<RSConnectApplicationInfo> apps, List<RSConnectDeploymentRecord> records)1125    private void configureShinyApp(String dir,
1126          JsArray<RSConnectApplicationInfo> apps,
1127          List<RSConnectDeploymentRecord> records)
1128    {
1129       for (int i = 0; i < records.size(); i++)
1130       {
1131          for (int j = 0; j < apps.length(); j++)
1132          {
1133             RSConnectApplicationInfo candidate = apps.get(j);
1134             if (candidate.getName() == records.get(i).getName())
1135             {
1136                // show the management ui
1137                display_.openWindow(candidate.getConfigUrl());
1138                return;
1139             }
1140          }
1141       }
1142       display_.showMessage(GlobalDisplay.MSG_INFO,
1143             "No Running Deployments Found", "No applications deployed from '" +
1144              dir + "' appear to be running.");
1145    }
1146 
exportNativeCallbacks()1147    private final native void exportNativeCallbacks() /*-{
1148       var thiz = this;
1149       $wnd.deployToRSConnect = $entry(
1150          function(sourceFile, deployDir, deployFile, websiteDir, description, deployFiles, additionalFiles, ignoredFiles, isSelfContained, isShiny, asMultiple, asStatic, isQuarto, launch, record) {
1151             thiz.@org.rstudio.studio.client.rsconnect.RSConnect::deployToRSConnect(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/gwt/core/client/JsArrayString;Lcom/google/gwt/core/client/JsArrayString;Lcom/google/gwt/core/client/JsArrayString;ZZZZZZLcom/google/gwt/core/client/JavaScriptObject;)(sourceFile, deployDir, deployFile, websiteDir, description, deployFiles, additionalFiles, ignoredFiles, isSelfContained, isShiny, asMultiple, asStatic, isQuarto, launch, record);
1152          }
1153       );
1154    }-*/;
1155 
deployToRSConnect(String sourceFile, String deployDir, String deployFile, String websiteDir, String description, JsArrayString deployFiles, JsArrayString additionalFiles, JsArrayString ignoredFiles, boolean isSelfContained, boolean isShiny, boolean asMultiple, boolean asStatic, boolean isQuarto, boolean launch, JavaScriptObject jsoRecord)1156    private void deployToRSConnect(String sourceFile,
1157                                   String deployDir,
1158                                   String deployFile,
1159                                   String websiteDir,
1160                                   String description,
1161                                   JsArrayString deployFiles,
1162                                   JsArrayString additionalFiles,
1163                                   JsArrayString ignoredFiles,
1164                                   boolean isSelfContained,
1165                                   boolean isShiny,
1166                                   boolean asMultiple,
1167                                   boolean asStatic,
1168                                   boolean isQuarto,
1169                                   boolean launch,
1170                                   JavaScriptObject jsoRecord)
1171    {
1172       // this can be invoked by a satellite, so bring the main frame to the
1173       // front if we can
1174       if (Desktop.hasDesktopFrame())
1175          Desktop.getFrame().bringMainFrameToFront();
1176       else
1177          WindowEx.get().focus();
1178 
1179       ArrayList<String> deployFilesList =
1180             JsArrayUtil.fromJsArrayString(deployFiles);
1181       ArrayList<String> additionalFilesList =
1182             JsArrayUtil.fromJsArrayString(additionalFiles);
1183       ArrayList<String> ignoredFilesList =
1184             JsArrayUtil.fromJsArrayString(ignoredFiles);
1185 
1186       RSConnectDeploymentRecord record = jsoRecord.cast();
1187       events_.fireEvent(new RSConnectDeployInitiatedEvent(
1188             new RSConnectPublishSource(sourceFile, deployDir, deployFile,
1189                   websiteDir, isSelfContained, asStatic, isShiny, isQuarto, description),
1190             new RSConnectPublishSettings(deployFilesList,
1191                   additionalFilesList, ignoredFilesList, asMultiple, asStatic),
1192             launch, record));
1193    }
1194 
fillInputFromDoc(final RSConnectPublishInput input, final String docPath, final CommandWithArg<RSConnectPublishInput> onComplete)1195    private void fillInputFromDoc(final RSConnectPublishInput input,
1196          final String docPath,
1197          final CommandWithArg<RSConnectPublishInput> onComplete)
1198    {
1199       boolean isQuarto = false;
1200       if (input.getOriginatingEvent() != null &&
1201           input.getOriginatingEvent().getFromPreview() != null)
1202       {
1203          isQuarto = input.getOriginatingEvent().getFromPreview().isQuarto();
1204       }
1205 
1206       if (isQuarto)
1207       {
1208          // Quarto metadata lookup can take a couple of seconds; ensure the
1209          // user can see some progress while we're doing it
1210          final ProgressIndicator indicator = display_.getProgressIndicator("Error");
1211          indicator.onProgress("Preparing for Publish...");
1212 
1213          server_.quartoPublishDetails(
1214             docPath,
1215             new ServerRequestCallback<QmdPublishDetails>()
1216             {
1217                @Override
1218                public void onResponseReceived(QmdPublishDetails details)
1219                {
1220                   indicator.onCompleted();
1221                   RenderedDocPreview previewParams = input.getOriginatingEvent().getFromPreview();
1222                   if (previewParams != null)
1223                   {
1224                      if (StringUtil.isNullOrEmpty(details.website_output_dir))
1225                         previewParams.setOutputFile(details.output_file);
1226                      else
1227                         previewParams.setOutputFile(details.website_output_dir);
1228                      previewParams.setWebsiteDir(details.website_dir);
1229                   }
1230                   input.setIsMultiRmd(false);
1231                   input.setIsQuarto(true);
1232                   input.setIsShiny(details.is_shiny_qmd);
1233                   input.setIsSelfContained(details.is_self_contained);
1234                   input.setHasConnectAccount(details.has_connect_account);
1235                   input.setWebsiteDir(details.website_dir);
1236                   input.setWebsiteOutputDir(details.website_output_dir);
1237 
1238                   onComplete.execute(input);
1239                }
1240 
1241                @Override
1242                public void onError(ServerError error)
1243                {
1244                   indicator.onError(error.getMessage());
1245                   onComplete.execute(null);
1246                }
1247             }
1248          );
1249       }
1250       else
1251       {
1252          server_.getRmdPublishDetails(
1253             docPath,
1254             new ServerRequestCallback<RmdPublishDetails>()
1255             {
1256                @Override
1257                public void onResponseReceived(RmdPublishDetails details)
1258                {
1259                   input.setIsMultiRmd(details.is_multi_rmd);
1260                   input.setIsShiny(details.is_shiny_rmd);
1261                   input.setIsSelfContained(details.is_self_contained);
1262                   input.setIsQuarto(false);
1263                   input.setHasConnectAccount(details.has_connect_account);
1264                   input.setWebsiteDir(details.website_dir);
1265                   input.setWebsiteOutputDir(details.website_output_dir);
1266                   if (StringUtil.isNullOrEmpty(input.getDescription()))
1267                   {
1268                      if (!StringUtil.isNullOrEmpty(details.title))
1269                      {
1270                         // set the description from the document title, if we
1271                         // have it
1272                         input.setDescription(details.title);
1273                      }
1274                      else
1275                      {
1276                         // set the description from the document name
1277                         input.setDescription(
1278                               FilePathUtils.fileNameSansExtension(docPath));
1279                      }
1280                   }
1281                   onComplete.execute(input);
1282                }
1283 
1284                @Override
1285                public void onError(ServerError error)
1286                {
1287                   // this is unlikely since the RPC does little work, but
1288                   // we can't offer the right choices in the wizard if we
1289                   // don't know what we're working with.
1290                   display_.showErrorMessage("Could Not Publish",
1291                      error.getMessage());
1292                   onComplete.execute(null);
1293                }
1294             });
1295       }
1296    }
1297 
1298    private final Commands commands_;
1299    private final GlobalDisplay display_;
1300    private final Session session_;
1301    private final RSConnectServerOperations server_;
1302    private final RPubsServerOperations rpubsServer_;
1303    private final SourceServerOperations sourceServer_;
1304    private final DependencyManager dependencyManager_;
1305    private final EventBus events_;
1306    private final RSAccountConnector connector_;
1307    private final Provider<UserPrefs> pUserPrefs_;
1308    private final Provider<UserState> pUserState_;
1309    private final PlotPublishMRUList plotMru_;
1310 
1311    private boolean launchBrowser_ = false;
1312    private boolean sessionInited_ = false;
1313    private boolean depsPending_ = false;
1314    private String lastDeployedServer_ = "";
1315 
1316    // incremented on each RPubs publish (to provide a unique context)
1317    private static int rpubsCount_ = 0;
1318 
1319    private RSConnectDirectoryState dirState_;
1320    private boolean dirStateDirty_ = false;
1321 
1322    public final static String CLOUD_SERVICE_NAME = "ShinyApps.io";
1323 
1324    // No/unknown content type
1325    public final static int CONTENT_TYPE_NONE           = 0;
1326 
1327    // A single HTML file representing a plot
1328    public final static int CONTENT_TYPE_PLOT           = 1;
1329 
1330    // A document (.Rmd, .md, etc.),
1331    public final static int CONTENT_TYPE_DOCUMENT       = 2;
1332 
1333    // A Shiny application
1334    public final static int CONTENT_TYPE_APP            = 3;
1335 
1336    // A single-file Shiny application
1337    public final static int CONTENT_TYPE_APP_SINGLE     = 4;
1338 
1339    // Standalone HTML (from HTML widgets/viewer pane, etc.)
1340    public final static int CONTENT_TYPE_HTML           = 5;
1341 
1342    // A .Rpres presentation
1343    public final static int CONTENT_TYPE_PRES           = 6;
1344 
1345    // A page in an R Markdown website
1346    public final static int CONTENT_TYPE_WEBSITE        = 7;
1347 
1348    // Plumber API
1349    public final static int CONTENT_TYPE_PLUMBER_API    = 8;
1350 
1351    // A Quarto website
1352    public final static int CONTENT_TYPE_QUARTO_WEBSITE = 9;
1353 
1354    public final static String CONTENT_CATEGORY_PLOT = "plot";
1355    public final static String CONTENT_CATEGORY_SITE = "site";
1356    public final static String CONTENT_CATEGORY_API = "api";
1357 }
1358