diff --git a/platform/built-in-server/src/org/jetbrains/ide/InstallPluginService.kt b/platform/built-in-server/src/org/jetbrains/ide/InstallPluginService.kt new file mode 100644 index 000000000000..d952c0121225 --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/ide/InstallPluginService.kt @@ -0,0 +1,147 @@ +// Copyright 2000-2019 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. +package org.jetbrains.ide + +import com.intellij.openapi.application.Application +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ApplicationNamesInfo +import com.intellij.openapi.application.ex.ApplicationInfoEx +import com.intellij.openapi.application.impl.ApplicationInfoImpl +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser +import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.AppIcon +import com.intellij.util.PlatformUtils +import com.intellij.util.Urls +import com.intellij.util.io.HttpRequests +import com.intellij.util.io.origin +import com.intellij.util.net.NetUtils +import com.intellij.util.text.nullize +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.http.FullHttpRequest +import io.netty.handler.codec.http.HttpRequest +import io.netty.handler.codec.http.QueryStringDecoder +import java.io.OutputStream +import java.net.URI +import java.net.URISyntaxException + +internal class InstallPluginService : RestService() { + override fun getServiceName() = "installPlugin" + + override fun isAccessible(request: HttpRequest) = true + + var isAvailable = true + + override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? { + val pluginId = getStringParameter("pluginId", urlDecoder) + val action = getStringParameter("action", urlDecoder) + + if (pluginId.isNullOrBlank()) { + return productInfo(request, context) + } + + return when (action) { + "checkCompatibility" -> checkCompatibility(request, context, pluginId) + "install" -> installPlugin(request, context, pluginId) + else -> productInfo(request, context) + } + } + + //TODO: replace with `PluginDownloader.getLastCompatiblePluginUpdate` from IDEA-CR-56320 + private fun getBuildNumberForDownload(): String { + val app: Application? = ApplicationManager.getApplication() + val appInfo = ApplicationInfoImpl.getShadowInstance() + return if (app != null) ApplicationInfo.getInstance().apiVersion else appInfo.build.asString() + } + + private fun checkCompatibility(request: FullHttpRequest, + context: ChannelHandlerContext, + pluginId: String): Nothing? { + //check if there is an update for this IDE with this ID. TODO: replace with `getLastCompatiblePluginUpdate` from IDEA-CR-56320 + val url = Urls + .newFromEncoded(ApplicationInfoImpl.getShadowInstance().pluginManagerUrl.trimEnd('/') + "/api/getCompatibleUpdates") + .addParameters(mapOf( + "build" to getBuildNumberForDownload(), + "pluginId" to pluginId, + "max" to "1" + )) + val compatibleUpdateExists = HttpRequests.request(url).readString() != "[]" + + val out = BufferExposingByteArrayOutputStream() + + val writer = createJsonWriter(out) + writer.beginObject() + writer.name("compatible").value(compatibleUpdateExists) + writer.endObject() + writer.close() + + send(out, request, context) + return null + } + + private fun installPlugin(request: FullHttpRequest, + context: ChannelHandlerContext, + pluginId: String): Nothing? { + if (isAvailable) { + isAvailable = false + val effectiveProject = getLastFocusedOrOpenedProject() ?: ProjectManager.getInstance().defaultProject + PluginId.findId(pluginId)?.let { + ApplicationManager.getApplication().invokeLater(Runnable { + AppIcon.getInstance().requestAttention(effectiveProject, true) + PluginsAdvertiser.installAndEnable(setOf(it)) { } + isAvailable = true + }, effectiveProject.disposed) + } + } + + sendOk(request, context) + return null + } + + private fun productInfo(request: FullHttpRequest, + context: ChannelHandlerContext): Nothing? { + val out = BufferExposingByteArrayOutputStream() + + writeIDEInfo(out) + + send(out, request, context) + return null + } + + private fun writeIDEInfo(out: OutputStream) { + val writer = createJsonWriter(out) + writer.beginObject() + + var appName = ApplicationInfoEx.getInstanceEx().fullApplicationName + val build = ApplicationInfo.getInstance().build + + if (!PlatformUtils.isIdeaUltimate()) { + val productName = ApplicationNamesInfo.getInstance().productName + appName = appName.replace("$productName ($productName)", productName) + appName = StringUtil.trimStart(appName, "JetBrains ") + } + + writer.name("name").value(appName) + writer.name("buildNumber").value(build.asString()) + writer.endObject() + writer.close() + } + + override fun isHostTrusted(request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean { + val origin = request.origin + val originHost = try { + if (origin == null) null else URI(origin).host.nullize() + } + catch (ignored: URISyntaxException) { + return false + } + + return (originHost != null && + (originHost == "plugins.jetbrains.com" || + originHost.endsWith(".dev.marketplace.intellij.net") || + NetUtils.isLocalhost(originHost))) || + super.isHostTrusted(request, urlDecoder) + } +} \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/built-in-server.xml b/platform/platform-resources/src/META-INF/built-in-server.xml index 1cc11142a072..ae29c9d07cb0 100644 --- a/platform/platform-resources/src/META-INF/built-in-server.xml +++ b/platform/platform-resources/src/META-INF/built-in-server.xml @@ -29,6 +29,7 @@ +