f4925c43d71f13c448a3ea47691355bd4c474bb1
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / updateSettings / impl / PluginDownloader.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.updateSettings.impl;
17
18 import com.intellij.ide.IdeBundle;
19 import com.intellij.ide.plugins.*;
20 import com.intellij.ide.startup.StartupActionScriptManager;
21 import com.intellij.ide.util.PropertiesComponent;
22 import com.intellij.openapi.application.Application;
23 import com.intellij.openapi.application.ApplicationInfo;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.application.PathManager;
26 import com.intellij.openapi.application.ex.ApplicationInfoEx;
27 import com.intellij.openapi.application.impl.ApplicationInfoImpl;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.extensions.PluginId;
30 import com.intellij.openapi.progress.ProgressIndicator;
31 import com.intellij.openapi.ui.Messages;
32 import com.intellij.openapi.util.BuildNumber;
33 import com.intellij.openapi.util.io.FileUtil;
34 import com.intellij.openapi.util.text.StringUtil;
35 import com.intellij.util.PathUtil;
36 import com.intellij.util.io.HttpRequests;
37 import com.intellij.util.io.ZipUtil;
38 import org.apache.http.client.utils.URIBuilder;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41
42 import java.io.File;
43 import java.io.IOException;
44 import java.net.*;
45 import java.util.List;
46 import java.util.UUID;
47
48 /**
49  * @author anna
50  * @since 10-Aug-2007
51  */
52 public class PluginDownloader {
53   private static final Logger LOG = Logger.getInstance("#" + PluginDownloader.class.getName());
54
55   private static final String FILENAME = "filename=";
56
57   private final String myPluginId;
58   private final String myPluginUrl;
59   private String myPluginVersion;
60   private String myFileName;
61   private String myPluginName;
62   private BuildNumber myBuildNumber;
63   private boolean myForceHttps;
64
65   private File myFile;
66   private File myOldFile;
67   private String myDescription;
68   private List<PluginId> myDepends;
69   private IdeaPluginDescriptor myDescriptor;
70
71   private PluginDownloader(@NotNull String pluginId,
72                            @NotNull String pluginUrl,
73                            @Nullable String pluginVersion,
74                            @Nullable String fileName,
75                            @Nullable String pluginName,
76                            @Nullable BuildNumber buildNumber) {
77     myPluginId = pluginId;
78     myPluginUrl = pluginUrl;
79     myPluginVersion = pluginVersion;
80     myFileName = fileName;
81     myPluginName = pluginName;
82     myBuildNumber = buildNumber;
83   }
84
85   public String getPluginId() {
86     return myPluginId;
87   }
88
89   public String getPluginVersion() {
90     return myPluginVersion;
91   }
92
93   public String getFileName() {
94     if (myFileName == null) {
95       myFileName = myPluginUrl.substring(myPluginUrl.lastIndexOf('/') + 1);
96     }
97     return myFileName;
98   }
99
100   public String getPluginName() {
101     if (myPluginName == null) {
102       myPluginName = FileUtil.getNameWithoutExtension(getFileName());
103     }
104     return myPluginName;
105   }
106
107   public BuildNumber getBuildNumber() {
108     return myBuildNumber;
109   }
110
111   public String getDescription() {
112     return myDescription;
113   }
114
115   public void setDescription(String description) {
116     myDescription = description;
117   }
118
119   public List<PluginId> getDepends() {
120     return myDepends;
121   }
122
123   public void setDepends(List<PluginId> depends) {
124     myDepends = depends;
125   }
126
127   public IdeaPluginDescriptor getDescriptor() {
128     return myDescriptor;
129   }
130
131   public void setDescriptor(IdeaPluginDescriptor descriptor) {
132     myDescriptor = descriptor;
133   }
134
135   public void setForceHttps(boolean forceHttps) {
136     myForceHttps = forceHttps;
137   }
138
139   public boolean prepareToInstall(@NotNull ProgressIndicator indicator) throws IOException {
140     if (myFile != null) {
141       return true;
142     }
143
144     IdeaPluginDescriptor descriptor = null;
145     if (!Boolean.getBoolean(StartupActionScriptManager.STARTUP_WIZARD_MODE) && PluginManager.isPluginInstalled(PluginId.getId(myPluginId))) {
146       //store old plugins file
147       descriptor = PluginManager.getPlugin(PluginId.getId(myPluginId));
148       LOG.assertTrue(descriptor != null);
149       if (myPluginVersion != null && compareVersionsSkipBroken(descriptor, myPluginVersion) <= 0) {
150         LOG.info("Plugin " + myPluginId + ": current version (max) " + myPluginVersion);
151         return false;
152       }
153       myOldFile = descriptor.getPath();
154     }
155
156     // download plugin
157     String errorMessage = IdeBundle.message("unknown.error");
158     try {
159       myFile = downloadPlugin(indicator);
160     }
161     catch (IOException ex) {
162       myFile = null;
163       LOG.warn(ex);
164       errorMessage = ex.getMessage();
165     }
166     if (myFile == null) {
167       if (ApplicationManager.getApplication() != null) {
168         final String text = IdeBundle.message("error.plugin.was.not.installed", getPluginName(), errorMessage);
169         final String title = IdeBundle.message("title.failed.to.download");
170         ApplicationManager.getApplication().invokeLater(new Runnable() {
171           @Override
172           public void run() {
173             Messages.showErrorDialog(text, title);
174           }
175         });
176       }
177       return false;
178     }
179
180     IdeaPluginDescriptorImpl actualDescriptor = loadDescriptionFromJar(myFile);
181     if (actualDescriptor != null) {
182       if (InstalledPluginsState.getInstance().wasUpdated(actualDescriptor.getPluginId())) {
183         return false; //already updated
184       }
185
186       myPluginVersion = actualDescriptor.getVersion();
187       if (descriptor != null && compareVersionsSkipBroken(descriptor, myPluginVersion) <= 0) {
188         LOG.info("Plugin " + myPluginId + ": current version (max) " + myPluginVersion);
189         return false; //was not updated
190       }
191
192       setDescriptor(actualDescriptor);
193
194       if (PluginManagerCore.isIncompatible(actualDescriptor, myBuildNumber)) {
195         LOG.info("Plugin " + myPluginId + " is incompatible with current installation " +
196                  "(since:" + actualDescriptor.getSinceBuild() + " until:" + actualDescriptor.getUntilBuild() + ")");
197         return false; //host outdated plugins, no compatible plugin for new version
198       }
199     }
200     return true;
201   }
202
203   public static int compareVersionsSkipBroken(IdeaPluginDescriptor descriptor, String newPluginVersion) {
204     int state = StringUtil.compareVersionNumbers(newPluginVersion, descriptor.getVersion());
205     if (PluginManagerCore.isBrokenPlugin(descriptor) && state < 0) {
206       state = 1;
207     }
208     return state;
209   }
210
211   @Nullable
212   public static IdeaPluginDescriptorImpl loadDescriptionFromJar(final File file) throws IOException {
213     IdeaPluginDescriptorImpl descriptor = PluginManagerCore.loadDescriptorFromJar(file);
214     if (descriptor == null) {
215       if (file.getName().endsWith(".zip")) {
216         final File outputDir = FileUtil.createTempDirectory("plugin", "");
217         try {
218           ZipUtil.extract(file, outputDir, null);
219           final File[] files = outputDir.listFiles();
220           if (files != null && files.length == 1) {
221             descriptor = PluginManagerCore.loadDescriptor(files[0], PluginManagerCore.PLUGIN_XML);
222           }
223         }
224         finally {
225           FileUtil.delete(outputDir);
226         }
227       }
228     }
229     return descriptor;
230   }
231
232   public void install() throws IOException {
233     LOG.assertTrue(myFile != null);
234     if (myOldFile != null) {
235       // add command to delete the 'action script' file
236       StartupActionScriptManager.ActionCommand deleteOld = new StartupActionScriptManager.DeleteCommand(myOldFile);
237       StartupActionScriptManager.addActionCommand(deleteOld);
238     }
239     PluginInstaller.install(myFile, getPluginName(), true);
240     InstalledPluginsState.getInstance().onPluginInstall(myDescriptor);
241   }
242
243   @NotNull
244   private File downloadPlugin(@NotNull final ProgressIndicator indicator) throws IOException {
245     File pluginsTemp = new File(PathManager.getPluginTempPath());
246     if (!pluginsTemp.exists() && !pluginsTemp.mkdirs()) {
247       throw new IOException(IdeBundle.message("error.cannot.create.temp.dir", pluginsTemp));
248     }
249     final File file = FileUtil.createTempFile(pluginsTemp, "plugin_", "_download", true, false);
250
251     indicator.checkCanceled();
252     indicator.setText2(IdeBundle.message("progress.downloading.plugin", getPluginName()));
253
254     return HttpRequests.request(myPluginUrl).gzip(false).forceHttps(myForceHttps).connect(new HttpRequests.RequestProcessor<File>() {
255       @Override
256       public File process(@NotNull HttpRequests.Request request) throws IOException {
257         request.saveToFile(file, indicator);
258         if (myFileName == null) {
259           myFileName = guessFileName(request.getConnection(), file);
260         }
261
262         File newFile = new File(file.getParentFile(), myFileName);
263         FileUtil.rename(file, newFile);
264         return newFile;
265       }
266     });
267   }
268
269   @NotNull
270   private String guessFileName(@NotNull URLConnection connection, @NotNull File file) throws IOException {
271     String fileName = null;
272
273     final String contentDisposition = connection.getHeaderField("Content-Disposition");
274     LOG.debug("header: " + contentDisposition);
275
276     if (contentDisposition != null && contentDisposition.contains(FILENAME)) {
277       final int startIdx = contentDisposition.indexOf(FILENAME);
278       final int endIdx = contentDisposition.indexOf(';', startIdx);
279       fileName = contentDisposition.substring(startIdx + FILENAME.length(), endIdx > 0 ? endIdx : contentDisposition.length());
280
281       if (StringUtil.startsWithChar(fileName, '\"') && StringUtil.endsWithChar(fileName, '\"')) {
282         fileName = fileName.substring(1, fileName.length() - 1);
283       }
284     }
285
286     if (fileName == null) {
287       // try to find a filename in an URL
288       final String usedURL = connection.getURL().toString();
289       fileName = usedURL.substring(usedURL.lastIndexOf('/') + 1);
290       if (fileName.length() == 0 || fileName.contains("?")) {
291         fileName = myPluginUrl.substring(myPluginUrl.lastIndexOf('/') + 1);
292       }
293     }
294
295     if (!PathUtil.isValidFileName(fileName)) {
296       FileUtil.delete(file);
297       throw new IOException("Invalid filename returned by a server");
298     }
299
300     return fileName;
301   }
302
303   // creators-converters
304
305   public static PluginDownloader createDownloader(@NotNull IdeaPluginDescriptor descriptor) throws IOException {
306     return createDownloader(descriptor, null, null);
307   }
308
309   @NotNull
310   public static PluginDownloader createDownloader(@NotNull IdeaPluginDescriptor descriptor,
311                                                   @Nullable String host,
312                                                   @Nullable BuildNumber buildNumber) throws IOException {
313     try {
314       PluginId id = descriptor.getPluginId();
315       String url = host != null ? getHostUrl(host, descriptor.getUrl()) : getRepositoryUrl(id, buildNumber);
316       PluginDownloader downloader = new PluginDownloader(id.getIdString(), url, descriptor.getVersion(), null, descriptor.getName(), null);
317       downloader.setDescriptor(descriptor);
318       downloader.setDescription(descriptor.getDescription());
319       downloader.setDepends(((PluginNode)descriptor).getDepends());
320       return downloader;
321     }
322     catch (URISyntaxException e) {
323       throw new IOException(e);
324     }
325   }
326
327   @NotNull
328   private static String getHostUrl(@NotNull String host, @NotNull String pluginUrl) throws URISyntaxException, MalformedURLException {
329     if (new URI(pluginUrl).isAbsolute()) {
330       return pluginUrl;
331     }
332     else {
333       return new URL(new URL(host), pluginUrl).toExternalForm();
334     }
335   }
336
337   @NotNull
338   private static String getRepositoryUrl(@NotNull PluginId pluginId, @Nullable BuildNumber buildNumber) throws URISyntaxException {
339     Application app = ApplicationManager.getApplication();
340     ApplicationInfoEx appInfo = ApplicationInfoImpl.getShadowInstance();
341
342     String buildNumberAsString = buildNumber != null ? buildNumber.asString() :
343                                  app != null ? ApplicationInfo.getInstance().getApiVersion() :
344                                  appInfo.getBuild().asString();
345
346     String uuid = app != null ? UpdateChecker.getInstallationUID(PropertiesComponent.getInstance()) : UUID.randomUUID().toString();
347
348     URIBuilder uriBuilder = new URIBuilder(appInfo.getPluginsDownloadUrl());
349     uriBuilder.addParameter("action", "download");
350     uriBuilder.addParameter("id", pluginId.getIdString());
351     uriBuilder.addParameter("build", buildNumberAsString);
352     uriBuilder.addParameter("uuid", uuid);
353     return uriBuilder.toString();
354   }
355
356   @Nullable
357   public static PluginNode createPluginNode(@Nullable String host, @NotNull PluginDownloader downloader) {
358     IdeaPluginDescriptor descriptor = downloader.getDescriptor();
359     if (descriptor instanceof PluginNode) {
360       return (PluginNode)descriptor;
361     }
362
363     PluginNode node = new PluginNode(PluginId.getId(downloader.getPluginId()));
364     node.setName(downloader.getPluginName());
365     node.setVersion(downloader.getPluginVersion());
366     node.setRepositoryName(host);
367     node.setDownloadUrl(downloader.myPluginUrl);
368     node.setDepends(downloader.getDepends(), null);
369     node.setDescription(downloader.getDescription());
370     return node;
371   }
372 }