Merge remote-tracking branch 'origin/master' into prendota/plugin-manager-new-protocol
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / updateSettings / impl / PluginDownloader.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.updateSettings.impl;
3
4 import com.intellij.ide.IdeBundle;
5 import com.intellij.ide.plugins.*;
6 import com.intellij.ide.plugins.marketplace.MarketplaceRequests;
7 import com.intellij.ide.startup.StartupActionScriptManager;
8 import com.intellij.openapi.application.*;
9 import com.intellij.openapi.application.impl.ApplicationInfoImpl;
10 import com.intellij.openapi.diagnostic.Logger;
11 import com.intellij.openapi.extensions.PluginId;
12 import com.intellij.openapi.progress.ProgressIndicator;
13 import com.intellij.openapi.ui.Messages;
14 import com.intellij.openapi.util.BuildNumber;
15 import com.intellij.openapi.util.io.FileUtil;
16 import com.intellij.openapi.util.text.StringUtil;
17 import com.intellij.util.PathUtil;
18 import com.intellij.util.Urls;
19 import com.intellij.util.io.HttpRequests;
20 import com.intellij.util.text.VersionComparatorUtil;
21 import org.jetbrains.annotations.NotNull;
22 import org.jetbrains.annotations.Nullable;
23
24 import javax.swing.*;
25 import java.io.File;
26 import java.io.IOException;
27 import java.net.URI;
28 import java.net.URISyntaxException;
29 import java.net.URL;
30 import java.net.URLConnection;
31 import java.util.*;
32
33 /**
34  * @author anna
35  */
36 public final class PluginDownloader {
37   private static final Logger LOG = Logger.getInstance(PluginDownloader.class);
38
39   private static final String FILENAME = "filename=";
40
41   private final PluginId myPluginId;
42   private final String myPluginName;
43   private final @Nullable String myProductCode;
44   private final Date myReleaseDate;
45   private final int myReleaseVersion;
46   private final boolean myLicenseOptional;
47   private final String myDescription;
48   private final List<PluginId> myDepends;
49
50   private final String myPluginUrl;
51   private final BuildNumber myBuildNumber;
52
53   private String myPluginVersion;
54   private IdeaPluginDescriptor myDescriptor;
55   private File myFile;
56   private File myOldFile;
57
58   private boolean myShownErrors;
59
60   private PluginDownloader(@NotNull IdeaPluginDescriptor descriptor, @NotNull String url, @Nullable BuildNumber buildNumber) {
61     myPluginId = descriptor.getPluginId();
62     myPluginName = descriptor.getName();
63     myProductCode = descriptor.getProductCode();
64     myReleaseDate = descriptor.getReleaseDate();
65     myReleaseVersion = descriptor.getReleaseVersion();
66     myLicenseOptional = descriptor.isLicenseOptional();
67     myDescription = descriptor.getDescription();
68     myDepends = descriptor instanceof PluginNode ? ((PluginNode)descriptor).getDepends() : Arrays.asList(descriptor.getDependentPluginIds());
69
70     myPluginUrl = url;
71     myBuildNumber = buildNumber;
72
73     myPluginVersion = descriptor.getVersion();
74     myDescriptor = descriptor;
75   }
76
77   /**
78    * @deprecated Use {@link #getId()}
79    */
80   @NotNull
81   @Deprecated
82   public String getPluginId() {
83     return myPluginId.getIdString();
84   }
85
86   @NotNull
87   public PluginId getId() {
88     return myPluginId;
89   }
90
91   public String getPluginVersion() {
92     return myPluginVersion;
93   }
94
95   @NotNull
96   public String getPluginName() {
97     return myPluginName != null ? myPluginName : myPluginId.getIdString();
98   }
99
100   @Nullable
101   public String getProductCode() {
102     return myProductCode;
103   }
104
105   public Date getReleaseDate() {
106     return myReleaseDate;
107   }
108
109   public int getReleaseVersion() {
110     return myReleaseVersion;
111   }
112
113   public boolean isLicenseOptional() {
114     return myLicenseOptional;
115   }
116
117   @Nullable
118   public BuildNumber getBuildNumber() {
119     return myBuildNumber;
120   }
121
122   @NotNull
123   public IdeaPluginDescriptor getDescriptor() {
124     return myDescriptor;
125   }
126
127   public File getFile() {
128     return myFile;
129   }
130
131   public boolean isShownErrors() {
132     return myShownErrors;
133   }
134
135   public boolean prepareToInstall(@NotNull ProgressIndicator indicator) throws IOException {
136     return prepareToInstallAndLoadDescriptor(indicator) != null;
137   }
138
139   @Nullable
140   public IdeaPluginDescriptorImpl prepareToInstallAndLoadDescriptor(@NotNull ProgressIndicator indicator) throws IOException {
141     myShownErrors = false;
142
143     if (myFile != null) {
144       IdeaPluginDescriptorImpl actualDescriptor = PluginManager.loadDescriptorFromArtifact(myFile.toPath(), myBuildNumber);
145       myDescriptor = actualDescriptor;
146       return actualDescriptor;
147     }
148
149     IdeaPluginDescriptor descriptor = null;
150     if (!Boolean.getBoolean(StartupActionScriptManager.STARTUP_WIZARD_MODE) &&
151         PluginManagerCore.isPluginInstalled(myPluginId)) {
152       //store old plugins file
153       descriptor = PluginManagerCore.getPlugin(myPluginId);
154       LOG.assertTrue(descriptor != null);
155       if (myPluginVersion != null && compareVersionsSkipBrokenAndIncompatible(descriptor, myPluginVersion) <= 0) {
156         LOG.info("Plugin " + myPluginId + ": current version (max) " + myPluginVersion);
157         return null;
158       }
159       myOldFile = descriptor.isBundled() ? null : descriptor.getPath();
160     }
161
162     // download plugin
163     String errorMessage = null;
164     try {
165       myFile = downloadPlugin(indicator);
166     }
167     catch (IOException ex) {
168       myFile = null;
169       LOG.warn(ex);
170       errorMessage = ex.getMessage();
171     }
172     if (myFile == null) {
173       Application app = ApplicationManager.getApplication();
174       if (app != null) {
175         myShownErrors = true;
176         if (errorMessage == null) {
177           errorMessage = IdeBundle.message("unknown.error");
178         }
179         String text = IdeBundle.message("error.plugin.was.not.installed", getPluginName(), errorMessage);
180         String title = IdeBundle.message("title.failed.to.download");
181         app.invokeLater(() -> Messages.showErrorDialog(text, title), ModalityState.any());
182       }
183       return null;
184     }
185
186     IdeaPluginDescriptorImpl actualDescriptor = PluginManager.loadDescriptorFromArtifact(myFile.toPath(), myBuildNumber);
187     if (actualDescriptor != null) {
188       InstalledPluginsState state = InstalledPluginsState.getInstanceIfLoaded();
189       if (state != null && state.wasUpdated(actualDescriptor.getPluginId())) {
190         return null; //already updated
191       }
192
193       myPluginVersion = actualDescriptor.getVersion();
194       if (descriptor != null && compareVersionsSkipBrokenAndIncompatible(descriptor, myPluginVersion) <= 0) {
195         LOG.info("Plugin " + myPluginId + ": current version (max) " + myPluginVersion);
196         return null; //was not updated
197       }
198
199       myDescriptor = actualDescriptor;
200
201       if (PluginManagerCore.isIncompatible(actualDescriptor, myBuildNumber)) {
202         LOG.info("Plugin " + myPluginId + " is incompatible with current installation " +
203                  "(since:" + actualDescriptor.getSinceBuild() + " until:" + actualDescriptor.getUntilBuild() + ")");
204         return null; //host outdated plugins, no compatible plugin for new version
205       }
206     }
207
208     return actualDescriptor;
209   }
210
211   public static int compareVersionsSkipBrokenAndIncompatible(@NotNull IdeaPluginDescriptor existingPlugin, String newPluginVersion) {
212     int state = VersionComparatorUtil.compare(newPluginVersion, existingPlugin.getVersion());
213     if (state < 0 && (PluginManagerCore.isBrokenPlugin(existingPlugin) || PluginManagerCore.isIncompatible(existingPlugin))) {
214       state = 1;
215     }
216     return state;
217   }
218
219   public void install() throws IOException {
220     if (myFile == null) {
221       throw new IOException("Plugin '" + getPluginName() + "' was not successfully downloaded");
222     }
223
224     PluginInstaller.installAfterRestart(myFile, true, myOldFile, myDescriptor);
225
226     InstalledPluginsState state = InstalledPluginsState.getInstanceIfLoaded();
227     if (state != null) {
228       state.onPluginInstall(myDescriptor, PluginManagerCore.isPluginInstalled(myDescriptor.getPluginId()), true);
229     }
230     else {
231       InstalledPluginsState.addPreInstalledPlugin(myDescriptor);
232     }
233   }
234
235   public boolean tryInstallWithoutRestart(@Nullable JComponent ownerComponent) {
236     final IdeaPluginDescriptorImpl descriptorImpl = (IdeaPluginDescriptorImpl)myDescriptor;
237     if (!DynamicPlugins.allowLoadUnloadWithoutRestart(descriptorImpl)) return false;
238
239     if (myOldFile != null) {
240       IdeaPluginDescriptor installedPlugin = PluginManagerCore.getPlugin(myDescriptor.getPluginId());
241       if (installedPlugin == null) {
242         return false;
243       }
244       IdeaPluginDescriptorImpl installedPluginDescriptor = PluginEnabler.tryLoadFullDescriptor((IdeaPluginDescriptorImpl)installedPlugin);
245       if (installedPluginDescriptor == null || !DynamicPlugins.unloadPlugin(installedPluginDescriptor)) {
246         return false;
247       }
248     }
249
250     PluginInstaller.installAndLoadDynamicPlugin(myFile, ownerComponent, descriptorImpl);
251     return true;
252   }
253
254   @NotNull
255   private File downloadPlugin(@NotNull ProgressIndicator indicator) throws IOException {
256     File pluginsTemp = new File(PathManager.getPluginTempPath());
257     if (!pluginsTemp.exists() && !pluginsTemp.mkdirs()) {
258       throw new IOException(IdeBundle.message("error.cannot.create.temp.dir", pluginsTemp));
259     }
260
261     indicator.checkCanceled();
262     indicator.setText2(IdeBundle.message("progress.downloading.plugin", getPluginName()));
263
264     File file = FileUtil.createTempFile(pluginsTemp, "plugin_", "_download", true, false);
265     return HttpRequests.request(myPluginUrl).gzip(false).productNameAsUserAgent().connect(request -> {
266       request.saveToFile(file, indicator);
267
268       String fileName = guessFileName(request.getConnection(), file);
269       File newFile = new File(file.getParentFile(), fileName);
270       FileUtil.rename(file, newFile);
271       return newFile;
272     });
273   }
274
275   @NotNull
276   private String guessFileName(@NotNull URLConnection connection, @NotNull File file) throws IOException {
277     String fileName = null;
278
279     final String contentDisposition = connection.getHeaderField("Content-Disposition");
280     LOG.debug("header: " + contentDisposition);
281
282     if (contentDisposition != null && contentDisposition.contains(FILENAME)) {
283       final int startIdx = contentDisposition.indexOf(FILENAME);
284       final int endIdx = contentDisposition.indexOf(';', startIdx);
285       fileName = contentDisposition.substring(startIdx + FILENAME.length(), endIdx > 0 ? endIdx : contentDisposition.length());
286
287       if (StringUtil.startsWithChar(fileName, '\"') && StringUtil.endsWithChar(fileName, '\"')) {
288         fileName = fileName.substring(1, fileName.length() - 1);
289       }
290     }
291
292     if (fileName == null) {
293       // try to find a filename in an URL
294       final String usedURL = connection.getURL().toString();
295       LOG.debug("url: " + usedURL);
296       fileName = usedURL.substring(usedURL.lastIndexOf('/') + 1);
297       if (fileName.length() == 0 || fileName.contains("?")) {
298         fileName = myPluginUrl.substring(myPluginUrl.lastIndexOf('/') + 1);
299       }
300     }
301
302     if (!PathUtil.isValidFileName(fileName)) {
303       LOG.debug("fileName: " + fileName);
304       FileUtil.delete(file);
305       throw new IOException("Invalid filename returned by a server");
306     }
307
308     return fileName;
309   }
310
311   // creators-converters
312   public static PluginDownloader createDownloader(@NotNull IdeaPluginDescriptor descriptor) throws IOException {
313     return createDownloader(descriptor, null, null);
314   }
315
316   @NotNull
317   public static PluginDownloader createDownloader(
318     @NotNull IdeaPluginDescriptor descriptor,
319     @Nullable String host,
320     @Nullable BuildNumber buildNumber
321   ) throws IOException {
322     String url;
323     try {
324       if (host != null && descriptor instanceof PluginNode) {
325         url = ((PluginNode)descriptor).getDownloadUrl();
326         if (!new URI(url).isAbsolute()) {
327           url = new URL(new URL(host), url).toExternalForm();
328         }
329       }
330       else {
331         final Map<String, String> parameters = new HashMap<>();
332         parameters.put("id", descriptor.getPluginId().getIdString());
333         parameters.put("build", getBuildNumberForDownload(buildNumber));
334         parameters.put("uuid", PermanentInstallationID.get());
335         url = Urls
336           .newFromEncoded(ApplicationInfoImpl.getShadowInstance().getPluginsDownloadUrl())
337           .addParameters(parameters)
338           .toExternalForm();
339       }
340     }
341     catch (URISyntaxException e) {
342       throw new IOException(e);
343     }
344     return new PluginDownloader(descriptor, url, buildNumber);
345   }
346
347   @NotNull
348   public static String getBuildNumberForDownload(@Nullable BuildNumber buildNumber) {
349     return buildNumber != null ? buildNumber.asString() : MarketplaceRequests.getBuildForPluginRepositoryRequests();
350   }
351
352   @NotNull
353   public static PluginNode createPluginNode(@Nullable String host, @NotNull PluginDownloader downloader) {
354     IdeaPluginDescriptor descriptor = downloader.getDescriptor();
355     if (descriptor instanceof PluginNode) {
356       return (PluginNode)descriptor;
357     }
358
359     PluginNode node = new PluginNode(downloader.myPluginId);
360     node.setName(downloader.getPluginName());
361     node.setProductCode(downloader.getProductCode());
362     node.setReleaseDate(downloader.getReleaseDate());
363     node.setReleaseVersion(downloader.getReleaseVersion());
364     node.setLicenseOptional(downloader.isLicenseOptional());
365     node.setVersion(downloader.getPluginVersion());
366     node.setRepositoryName(host);
367     node.setDownloadUrl(downloader.myPluginUrl);
368     node.setDepends(downloader.myDepends, null);
369     node.setDescription(downloader.myDescription);
370     return node;
371   }
372 }