[plugins] IJPL-249806 Missing project-trust gate lets Plugins Advertiser query Marketplace pre-trust

(cherry picked from commit 00b015bb260b66f11815270962173175dfbbf3bf)

IJ-CR-214792

GitOrigin-RevId: 6ae16ea77313cb5e6ea32b563fc125c9c67a3b2b
This commit is contained in:
Yuriy Artamonov
2026-08-07 09:39:08 +00:00
committed by intellij-monorepo-bot
parent 8118f673a3
commit 6748fee73d
5 changed files with 132 additions and 129 deletions
@@ -1896,7 +1896,7 @@ plugins.advertiser.action.try.ultimate=Try {0}
plugins.advertiser.action.ignore.ultimate=Dismiss
plugins.advertiser.no.suggested.plugins=No suggested plugins found
plugins.advertiser.no.compatible.plugins.found=No compatible plugins found
plugins.advertiser.check.progress=Checking suggested plugins for project
plugins.advertiser.check.progress=Checking Suggested Plugins for Project
plugins.advertiser.ultimate.features.detected=Features covered by Ultimate Subscription {0} are detected
plugins.advertiser.missing.feature={0,choice,1#Plugin|2#Plugins} {4} supporting {1} ''{2}'' {0,choice,1#is|2#are} currently {3,choice,0#disabled|1#not installed}.
plugins.advertiser.missing.features={0,choice,1#Plugin|2#Plugins} {3} supporting features ({1}) {0,choice,1#is|2#are} currently {2,choice,0#disabled|1#not installed}.
@@ -4,6 +4,7 @@ package com.intellij.ide.plugins.advertiser
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceAsync
import com.intellij.platform.settings.CacheTag
import com.intellij.platform.settings.Setting
import com.intellij.platform.settings.objectSerializer
@@ -21,6 +22,7 @@ import org.jetbrains.annotations.ApiStatus
class PluginFeatureCacheService {
companion object {
fun getInstance(): PluginFeatureCacheService = service()
suspend fun getInstanceAsync(): PluginFeatureCacheService = serviceAsync()
}
private val factory = settingDescriptorFactory(PluginManagerCore.CORE_ID)
@@ -2,23 +2,24 @@
package com.intellij.openapi.updateSettings.impl.pluginsAdvertisement
import com.intellij.ide.IdeBundle
import com.intellij.ide.trustedProjects.TrustedProjects
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
internal class CheckSuggestedPluginsAction : DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
runWithModalProgressBlocking(project, IdeBundle.message("plugins.advertiser.check.progress")) {
PluginsAdvertiserStartupActivity().checkSuggestedPlugins(project = project, includeIgnored = true)
}
}
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.project != null
e.presentation.isEnabledAndVisible = e.project?.let { TrustedProjects.isProjectTrusted(it) } ?: false
}
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun actionPerformed(e: AnActionEvent) {
val project = e.project?.takeIf { TrustedProjects.isProjectTrusted(it) } ?: return
runWithModalProgressBlocking(project, IdeBundle.message("plugins.advertiser.check.progress")) {
checkSuggestedPlugins(project = project, includeIgnored = true)
}
}
}
@@ -10,6 +10,7 @@ import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.ide.plugins.advertiser.PluginData
import com.intellij.ide.plugins.marketplace.MarketplaceRequests
import com.intellij.ide.plugins.pluginRequiresUltimatePluginButItsDisabled
import com.intellij.ide.trustedProjects.TrustedProjects
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
@@ -36,6 +37,7 @@ import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import com.intellij.ui.EditorNotifications
import com.intellij.ui.HyperlinkLabel
import com.intellij.util.application
import fleet.util.Either
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -56,14 +58,14 @@ import javax.swing.JLabel
class PluginAdvertiserEditorNotificationProvider : EditorNotificationProvider, DumbAware {
override fun collectNotificationData(project: Project, file: VirtualFile): Function<in FileEditor, out JComponent?>? {
val app = ApplicationManager.getApplication()
if (app.isUnitTestMode || app.isHeadlessEnvironment || tryUltimateIsDisabled()) {
if (!TrustedProjects.isProjectTrusted(project)) return null
if (application.isHeadlessEnvironment || tryUltimateIsDisabled()) {
return null
}
val providedSuggestion = SUGGESTION_EP_NAME.extensionList.asSequence()
.mapNotNull { it.getSuggestion(project, file) }
.firstOrNull()
val providedSuggestion = SUGGESTION_EP_NAME.extensionList
.firstNotNullOfOrNull { it.getSuggestion(project, file) }
val suggestionChoice = getSuggestionData(project = project,
activeProductCode = service<ApplicationInfo>().build.productCode,
@@ -307,8 +309,7 @@ private fun logSuggestionShown(project: Project, pluginIds: Collection<PluginId>
}
private fun logSuggestedProducts(project: Project, suggestedIdes: List<SuggestedIde>) {
for (ide in suggestedIdes) {
val productCode = ide.productCode
for ((_, productCode) in suggestedIdes) {
if (!loggedIdeSuggestions.contains(productCode)) {
FUSEventSource.EDITOR.logIdeSuggested(project, productCode)
loggedIdeSuggestions.add(productCode)
@@ -332,7 +333,7 @@ private fun getSuggestionData(
}
}
private fun getSuggestionDataByDetector(project: Project, suggestion: PluginAdvertisedByFileContent): AdvertiserSuggestion? {
private fun getSuggestionDataByDetector(project: Project, suggestion: PluginAdvertisedByFileContent): AdvertiserSuggestion {
val implementationName = "${FILE_HANDLER_KIND}:${suggestion.fileHandler.id}"
return AdvertiserSuggestion(
@@ -12,116 +12,43 @@ import com.intellij.ide.plugins.advertiser.PluginFeatureMap
import com.intellij.ide.plugins.marketplace.MarketplaceRequests
import com.intellij.ide.plugins.newui.PluginNodeModelBuilderFactory
import com.intellij.ide.plugins.newui.PluginUiModel
import com.intellij.ide.trustedProjects.TrustedProjects
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.diagnostic.rethrowControlFlowException
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.registry.RegistryManager
import com.intellij.ui.EditorNotifications
import com.intellij.util.SystemProperties
import com.intellij.util.io.computeDetached
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.coroutineContext
private val isTestingMode : Boolean by lazy {
private fun isTestingMode(): Boolean {
val application = ApplicationManager.getApplication()
application.isUnitTestMode || application.isHeadlessEnvironment || SystemProperties.getBooleanProperty("idea.is.playback", false)
return application.isHeadlessEnvironment || SystemProperties.getBooleanProperty("idea.is.playback", false)
}
internal class PluginsAdvertiserStartupActivity : ProjectActivity {
suspend fun checkSuggestedPlugins(project: Project, includeIgnored: Boolean) {
if (isTestingMode) {
return
}
val customPlugins = computeDetached { RepositoryHelper.loadPluginsFromCustomRepositories(null, PluginNodeModelBuilderFactory) }
coroutineContext.ensureActive()
val extensionService = PluginFeatureCacheService.getInstance()
val oldExtensions = extensionService.extensions.get()
val pluginAdvertiserService = PluginAdvertiserService.getInstance(project)
pluginAdvertiserService.collectDependencyUnknownFeatures(includeIgnored)
val unknownFeatures = UnknownFeaturesCollector.getInstance(project).unknownFeatures
if (oldExtensions != null && unknownFeatures.isEmpty()) {
if (includeIgnored) {
coroutineContext.ensureActive()
withContext(Dispatchers.EDT) {
getPluginSuggestionNotificationGroup()
.createNotification(IdeBundle.message("plugins.advertiser.no.suggested.plugins"), NotificationType.INFORMATION)
.setDisplayId("advertiser.no.plugins")
.notify(project)
}
}
}
try {
val customPluginIds = customPlugins.map { it.pluginId.idString }.toSet()
if (oldExtensions == null
|| oldExtensions.isOutdated
|| includeIgnored) {
@Suppress("DEPRECATION")
extensionService.extensions.set(PluginFeatureMap(
featureMap = getFeatureMapFromMarketPlace(customPluginIds = customPluginIds, featureType = "com.intellij.fileTypeFactory"),
lastUpdateTime = if (oldExtensions != null) System.currentTimeMillis() else 0L,
))
coroutineContext.ensureActive()
EditorNotifications.getInstance(project).updateAllNotifications()
}
val oldDependencies = extensionService.dependencies.get()
if (oldDependencies == null
|| oldDependencies.isOutdated
|| includeIgnored) {
extensionService.dependencies.set(PluginFeatureMap(
featureMap = getFeatureMapFromMarketPlace(customPluginIds = customPluginIds, featureType = DEPENDENCY_SUPPORT_FEATURE),
lastUpdateTime = if (oldDependencies != null) System.currentTimeMillis() else 0L,
))
}
// update information about file handlers when forced
if (includeIgnored && PluginAdvertiserExtensionsStateService.getInstance().updateCompatibleFileHandlers()) {
EditorNotifications.getInstance(project).updateAllNotifications()
}
coroutineContext.ensureActive()
if (unknownFeatures.isNotEmpty()) {
pluginAdvertiserService.run(
customPlugins = customPlugins,
unknownFeatures = unknownFeatures,
includeIgnored = includeIgnored
)
}
notifyUnbundledPlugins(project)
}
catch (e: CancellationException) {
throw e
}
catch (e: Exception) {
if (e !is ControlFlowException) {
thisLogger().info(e)
}
init {
if (isTestingMode()) {
throw ExtensionNotApplicableException.create()
}
}
override suspend fun execute(project: Project) {
if (!Registry.`is`("ide.show.plugin.suggestions.on.open", true)) {
if (!RegistryManager.getInstanceAsync().`is`("ide.show.plugin.suggestions.on.open")) {
thisLogger().debug("Plugin advertiser plugin suggestions disabled on open")
return
}
@@ -129,41 +56,113 @@ internal class PluginsAdvertiserStartupActivity : ProjectActivity {
}
}
internal fun findSuggestedPlugins(project: Project, customRepositories: Map<String, List<PluginUiModel>>): List<PluginUiModel> {
return runBlockingMaybeCancellable {
if (isTestingMode) {
return@runBlockingMaybeCancellable emptyList()
private val LOG = logger<PluginsAdvertiserStartupActivity>()
internal suspend fun checkSuggestedPlugins(project: Project, includeIgnored: Boolean) {
if (!TrustedProjects.isProjectTrusted(project)) return
LOG.debug("Checking plugin advertiser plugin suggestions")
val customPlugins = computeDetached { RepositoryHelper.loadPluginsFromCustomRepositories(null, PluginNodeModelBuilderFactory) }
currentCoroutineContext().ensureActive()
val extensionService = PluginFeatureCacheService.getInstanceAsync()
val oldExtensions = extensionService.extensions.get()
val pluginAdvertiserService = PluginAdvertiserService.getInstance(project)
pluginAdvertiserService.collectDependencyUnknownFeatures(includeIgnored)
val unknownFeatures = project.serviceAsync<UnknownFeaturesCollector>().unknownFeatures
if (oldExtensions != null && unknownFeatures.isEmpty()) {
if (includeIgnored) {
currentCoroutineContext().ensureActive()
withContext(Dispatchers.EDT) {
getPluginSuggestionNotificationGroup()
.createNotification(IdeBundle.message("plugins.advertiser.no.suggested.plugins"), NotificationType.INFORMATION)
.setDisplayId("advertiser.no.plugins")
.notify(project)
}
}
}
val customPlugins = ArrayList<PluginUiModel>()
for (value in customRepositories.values) {
customPlugins.addAll(value)
}
val pluginAdvertiserService = project.serviceAsync<PluginAdvertiserService>()
pluginAdvertiserService.collectDependencyUnknownFeatures(true)
try {
val customPluginIds = customPlugins.map { it.pluginId.idString }.toSet()
val extensionService = serviceAsync<PluginFeatureCacheService>()
val oldDependencies = extensionService.dependencies.get()
extensionService.dependencies.set(PluginFeatureMap(
getFeatureMapFromMarketPlace(customPluginIds, DEPENDENCY_SUPPORT_FEATURE),
if (oldDependencies != null) System.currentTimeMillis() else 0L,
))
val unknownFeatures = UnknownFeaturesCollector.getInstance(project).unknownFeatures
if (unknownFeatures.isNotEmpty()) {
return@runBlockingMaybeCancellable pluginAdvertiserService.fetch(customPlugins, unknownFeatures, true)
if (oldExtensions == null
|| oldExtensions.isOutdated
|| includeIgnored) {
@Suppress("DEPRECATION")
extensionService.extensions.set(PluginFeatureMap(
featureMap = getFeatureMapFromMarketPlace(customPluginIds = customPluginIds, featureType = "com.intellij.fileTypeFactory"),
lastUpdateTime = if (oldExtensions != null) System.currentTimeMillis() else 0L,
))
currentCoroutineContext().ensureActive()
EditorNotifications.getInstance(project).updateAllNotifications()
}
return@runBlockingMaybeCancellable emptyList()
val oldDependencies = extensionService.dependencies.get()
if (oldDependencies == null
|| oldDependencies.isOutdated
|| includeIgnored) {
extensionService.dependencies.set(PluginFeatureMap(
featureMap = getFeatureMapFromMarketPlace(customPluginIds = customPluginIds, featureType = DEPENDENCY_SUPPORT_FEATURE),
lastUpdateTime = if (oldDependencies != null) System.currentTimeMillis() else 0L,
))
}
// update information about file handlers when forced
if (includeIgnored && PluginAdvertiserExtensionsStateService.getInstance().updateCompatibleFileHandlers()) {
EditorNotifications.getInstance(project).updateAllNotifications()
}
currentCoroutineContext().ensureActive()
if (unknownFeatures.isNotEmpty()) {
pluginAdvertiserService.run(
customPlugins = customPlugins,
unknownFeatures = unknownFeatures,
includeIgnored = includeIgnored
)
}
notifyUnbundledPlugins(project)
}
catch (e: Exception) {
rethrowControlFlowException(e)
LOG.warn(e)
}
}
private suspend fun getFeatureMapFromMarketPlace(customPluginIds: Set<String>, featureType: String): Map<String, PluginDataSet> {
if (isTestingMode) {
return emptyMap()
internal suspend fun findSuggestedPlugins(project: Project, customRepositories: Map<String, List<PluginUiModel>>): List<PluginUiModel> {
if (isTestingMode()) return emptyList()
val customPlugins = ArrayList<PluginUiModel>()
for (value in customRepositories.values) {
customPlugins.addAll(value)
}
val pluginAdvertiserService = project.serviceAsync<PluginAdvertiserService>()
pluginAdvertiserService.collectDependencyUnknownFeatures(true)
val customPluginIds = customPlugins.map { it.pluginId.idString }.toSet()
val extensionService = serviceAsync<PluginFeatureCacheService>()
val oldDependencies = extensionService.dependencies.get()
extensionService.dependencies.set(PluginFeatureMap(
getFeatureMapFromMarketPlace(customPluginIds, DEPENDENCY_SUPPORT_FEATURE),
if (oldDependencies != null) System.currentTimeMillis() else 0L,
))
val unknownFeatures = UnknownFeaturesCollector.getInstance(project).unknownFeatures
if (unknownFeatures.isNotEmpty()) {
return pluginAdvertiserService.fetch(customPlugins, unknownFeatures, true)
}
return emptyList()
}
private suspend fun getFeatureMapFromMarketPlace(customPluginIds: Set<String>, featureType: String): Map<String, PluginDataSet> {
if (isTestingMode()) return emptyMap()
val params = mapOf("featureType" to featureType)
val features = MarketplaceRequests.getInstance().getFeatures(params)
return features