diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index bdacd598fe..d70df7c5c6 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -145,6 +145,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; +import java.util.function.Function; import java.util.function.Supplier; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -2238,6 +2239,32 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas } + /** + * {@link AdministrativeMonitor} that checks if there are any plugins that are deprecated. + * + * @since TODO + */ + @Restricted(NoExternalUse.class) + @Symbol("pluginDeprecation") + @Extension + public static final class PluginDeprecationMonitor extends AdministrativeMonitor { + + @Override + public String getDisplayName() { + return Messages.PluginManager_PluginDeprecationMonitor_DisplayName(); + } + + public boolean isActivated() { + return !getDeprecatedPlugins().isEmpty(); + } + + public Map getDeprecatedPlugins() { + return Jenkins.get().getPluginManager().getPlugins().stream() + .filter(PluginWrapper::isDeprecated) + .collect(Collectors.toMap(Function.identity(), it -> it.getDeprecations().get(0).url)); + } + } + @Restricted(DoNotUse.class) public String unscientific(double d) { return String.format(Locale.US, "%15.4f", d); @@ -2254,7 +2281,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas @Restricted(DoNotUse.class) // Used from table.jelly public boolean isMetaLabel(String label) { - return "adopt-this-plugin".equals(label); + return "adopt-this-plugin".equals(label) || "deprecated".equals(label); } @Restricted(DoNotUse.class) // Used from table.jelly diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index f1ab74fb5f..0442248c2d 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -364,6 +364,21 @@ public class PluginWrapper implements Comparable, ModelObject { return dependencies.stream().anyMatch(d -> !d.optional); } + /** + * Is this plugin deprecated? + * + * @return {@code true} if and only if an update site reports deprecations for this plugin. + * @since TODO + */ + @Restricted(NoExternalUse.class) + public boolean isDeprecated() { + /* + While better handled in UpdateSite.Plugin that only exists for plugins actively being published. + We have no good model for plugin metadata from update sites for plugins not being published. + */ + return !getDeprecations().isEmpty(); + } + @ExportedBean public static final class Dependency { @Exported @@ -553,8 +568,11 @@ public class PluginWrapper implements Comparable, ModelObject { /** * Returns a one-line descriptive name of this plugin. + * + * @deprecated For most purposes, use {@link #getDisplayName()}. */ @Exported + @Deprecated public String getLongName() { String name = manifest.getMainAttributes().getValue("Long-Name"); if(name!=null) return name; @@ -1284,6 +1302,20 @@ public class PluginWrapper implements Comparable, ModelObject { return ExtensionList.lookupSingleton(UpdateSiteWarningsMonitor.class).getActivePluginWarningsByPlugin().getOrDefault(this, Collections.emptyList()); } + @Restricted(NoExternalUse.class) + public List getDeprecations() { + /* Would be much nicer to go through getInfoFromAllSites but that only works for currently published plugins */ + List deprecations = new ArrayList<>(); + for (UpdateSite site : Jenkins.get().getUpdateCenter().getSites()) { + for (Map.Entry entry : site.getData().getDeprecations().entrySet()) { + if (entry.getKey().equals(this.shortName)) { + deprecations.add(entry.getValue()); + } + } + } + return deprecations; + } + private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName()); /** diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 7a15a896dd..6938e62a2e 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -45,13 +45,17 @@ import java.net.URL; import java.net.URLEncoder; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.UUID; @@ -555,6 +559,13 @@ public class UpdateSite { */ private final Set warnings = new HashSet<>(); + /** + * Mapping of plugin IDs to deprecation notices + * + * @since TODO + */ + private final Map deprecations = new HashMap<>(); + /** * If this is non-null, Jenkins is going to check the connectivity to this URL to make sure * the network connection is up. Null to skip the check. @@ -581,6 +592,24 @@ public class UpdateSite { } } + JSONObject deprecations = o.optJSONObject("deprecations"); + if (deprecations != null) { + for (Iterator it = deprecations.keys(); it.hasNext(); ) { + try { + String pluginId = it.next().toString(); + JSONObject entry = deprecations.getJSONObject(pluginId); // additional level of indirection to support future extensibility + if (entry != null) { + String referenceUrl = entry.getString("url"); + if (referenceUrl != null) { + this.deprecations.put(pluginId, new Deprecation(referenceUrl)); + } + } + } catch (RuntimeException ex) { + LOGGER.log(Level.WARNING, "Failed to parse JSON for deprecation", ex); + } + } + } + for(Map.Entry e : (Set>)o.getJSONObject("plugins").entrySet()) { Plugin p = new Plugin(sourceId, e.getValue()); // JENKINS-33308 - include implied dependencies for older plugins that may need them @@ -593,6 +622,16 @@ public class UpdateSite { } } plugins.put(Util.intern(e.getKey()), p); + + // compatibility with update sites that have no separate 'deprecated' top-level entry. + // Also do this even if there are deprecations to potentially allow limiting the top-level entry to overridden URLs. + if (p.categories != null) { + if (Arrays.asList(p.categories).contains("deprecated")) { + if (!this.deprecations.containsKey(p.name)) { + this.deprecations.put(p.name, new Deprecation(p.wiki)); + } + } + } } connectionCheckUrl = (String)o.get("connectionCheckUrl"); @@ -608,6 +647,16 @@ public class UpdateSite { return this.warnings; } + /** + * Returns the deprecations provided by the update site + * @return the deprecations provided by the update site + * @since TODO + */ + @Restricted(NoExternalUse.class) + public Map getDeprecations() { + return this.deprecations; + } + /** * Is there a new version of the core? */ @@ -790,6 +839,37 @@ public class UpdateSite { } } + /** + * Represents a deprecation of a certain component. Jenkins project policy determines exactly what it means. + * + * @since TODO + */ + @Restricted(NoExternalUse.class) + public static final class Deprecation { + /** + * URL for this deprecation. + * + * Jenkins will show a link to this URL when displaying the deprecation message. + */ + public final String url; + public Deprecation(String url) { + this.url = url; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Deprecation that = (Deprecation) o; + return Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(url); + } + } + /** * Represents a warning about a certain component, mostly related to known security issues. * @@ -1106,7 +1186,15 @@ public class UpdateSite { } + @Restricted(NoExternalUse.class) + public boolean isDeprecated() { + return getDeprecation() != null; + } + @Restricted(NoExternalUse.class) + public UpdateSite.Deprecation getDeprecation() { + return Jenkins.get().getUpdateCenter().getSite(sourceId).getData().getDeprecations().get(this.name); + } public String getDisplayName() { String displayName; diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index 109bcdeb4f..a336263006 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -58,6 +58,7 @@ PluginManager.ConfigureUpdateCenterPermission.Description=\ Deprecated - Please use the Overall/Administer permission instead PluginManager.PluginCycleDependenciesMonitor.DisplayName=Cyclic Dependencies Detector PluginManager.PluginUpdateMonitor.DisplayName=Invalid Plugin Configuration +PluginManager.PluginDeprecationMonitor.DisplayName=Deprecated Plugin Monitor PluginManager.CheckUpdateServerError=There were errors checking the update sites: {0} PluginManager.UpdateSiteError=Error checking update sites for {0} attempt(s). Last exception was: {1} PluginManager.UpdateSiteChangeLogLevel=Change the log level of {0} logger to WARNING or below to see more information and the error message of every attempt diff --git a/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly b/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly new file mode 100644 index 0000000000..1b4dc8e8a9 --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly @@ -0,0 +1,36 @@ + + + + +
+
+
${%DeprecatedPlugins}
+ +
${e.key.displayName}
+
+
+

${%DeprecatedPluginsDescription}

+
+
diff --git a/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.properties b/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.properties new file mode 100644 index 0000000000..e093ad7210 --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.properties @@ -0,0 +1,27 @@ +# The MIT License +# +# Copyright (c) 2019, Joseph Petersen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +DeprecatedPlugins=The following installed plugins are deprecated +DeprecatedPluginsDescription=\ + In general, this means that these plugins are either obsolete, no longer being developed, or may no longer work.
\ + See the linked web pages for further information about the cause for the deprecation, and suggestions on how to proceed. diff --git a/core/src/main/resources/hudson/PluginManager/installed.jelly b/core/src/main/resources/hudson/PluginManager/installed.jelly index 6ae63b7143..5cd04e5c6b 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.jelly +++ b/core/src/main/resources/hudson/PluginManager/installed.jelly @@ -125,6 +125,12 @@ THE SOFTWARE. + +
+ + ${%deprecationWarning(p.getDeprecations().get(0).url)} +
+
${%adoptThisPlugin} diff --git a/core/src/main/resources/hudson/PluginManager/installed.properties b/core/src/main/resources/hudson/PluginManager/installed.properties index c77d4015dd..15f4137a46 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.properties +++ b/core/src/main/resources/hudson/PluginManager/installed.properties @@ -24,6 +24,7 @@ requires.restart=This Jenkins instance requires a restart. Changing the state of detached-disable=This plugin may not be safe to disable detached-uninstall=This plugin may not be safe to uninstall detached-possible-dependents=Its functionality was at one point moved out of Jenkins core, and another plugin with a core dependency predating the split may be relying on it implicitly. +deprecationWarning=This plugin is deprecated. In general, this means that it is either obsolete, no longer being developed, or may no longer work. Learn more. securityWarning=\ Warning: The currently installed plugin version may not be safe to use. Please review the following security notices: adoptThisPlugin=\ diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly index e66edecd33..e9af990eb7 100644 --- a/core/src/main/resources/hudson/PluginManager/table.jelly +++ b/core/src/main/resources/hudson/PluginManager/table.jelly @@ -161,6 +161,11 @@ THE SOFTWARE.
+ +
+ ${%deprecationWarning(p.getDeprecation().url)} +
+
${%adoptThisPlugin} diff --git a/core/src/main/resources/hudson/PluginManager/table.properties b/core/src/main/resources/hudson/PluginManager/table.properties index 75df8c7994..37dee4a7e0 100644 --- a/core/src/main/resources/hudson/PluginManager/table.properties +++ b/core/src/main/resources/hudson/PluginManager/table.properties @@ -62,3 +62,4 @@ unavailable=Unavailable loading=Plugin list is loading... instructions=Use the search field above to search for available plugins. +deprecationWarning=This plugin is deprecated. In general, this means that it is either obsolete, no longer being developed, or may no longer work. Learn more. diff --git a/test/src/test/java/hudson/PluginManagerTest.java b/test/src/test/java/hudson/PluginManagerTest.java index cdd99fe5d9..a0839d5c1a 100644 --- a/test/src/test/java/hudson/PluginManagerTest.java +++ b/test/src/test/java/hudson/PluginManagerTest.java @@ -40,8 +40,11 @@ import java.io.IOException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.Future; import jenkins.ClassLoaderReflectionToolkit; @@ -612,6 +615,64 @@ public class PluginManagerTest { assertTrue(ExtensionList.lookup(GlobalConfiguration.class).stream().anyMatch(gc -> "io.jenkins.plugins.MyGlobalConfiguration".equals(gc.getClass().getName()))); } + /* + credentials - present in update-center.json, not deprecated + htmlpublisher - not in update-center.json, not deprecated + icon-shim, present in update-center.json, deprecated via label and top-level list + token-macro, present in update-center.json, deprecated via label only + variant, not in update-center.json, deprecated via top-level list + */ + @Test + @Issue("JENKINS-59136") + @WithPlugin({"credentials.hpi", "htmlpublisher.jpi", "icon-shim.hpi", "token-macro.hpi", "variant.hpi"}) + public void testDeprecationNotices() throws Exception { + PersistedList sites = r.jenkins.getUpdateCenter().getSites(); + sites.clear(); + URL url = PluginManagerTest.class.getResource("/plugins/deprecations-update-center.json"); + UpdateSite site = new UpdateSite(UpdateCenter.ID_DEFAULT, url.toString()); + sites.add(site); + + assertEquals(FormValidation.ok(), site.updateDirectlyNow(false)); + assertNotNull(site.getData()); + + assertTrue(ExtensionList.lookupSingleton(PluginManager.PluginDeprecationMonitor.class).isActivated()); + + final PluginManager pm = Jenkins.get().getPluginManager(); + + final PluginWrapper credentials = pm.getPlugin("credentials"); + Objects.requireNonNull(credentials); + assertFalse(credentials.isDeprecated()); + assertTrue(credentials.getDeprecations().isEmpty()); + + final PluginWrapper htmlpublisher = pm.getPlugin("htmlpublisher"); + Objects.requireNonNull(htmlpublisher); + assertFalse(htmlpublisher.isDeprecated()); + assertTrue(htmlpublisher.getDeprecations().isEmpty()); + + final PluginWrapper iconShim = pm.getPlugin("icon-shim"); + Objects.requireNonNull(iconShim); + assertTrue(iconShim.isDeprecated()); + List deprecations = iconShim.getDeprecations(); + assertEquals(1, deprecations.size()); + assertEquals("https://jenkins.io/deprecations/icon-shim/", deprecations.get(0).url); + assertEquals("https://wiki.jenkins-ci.org/display/JENKINS/Icon+Shim+Plugin", iconShim.getInfo().wiki); + + final PluginWrapper tokenMacro = pm.getPlugin("token-macro"); + Objects.requireNonNull(tokenMacro); + assertTrue(tokenMacro.isDeprecated()); + deprecations = tokenMacro.getDeprecations(); + assertEquals(1, deprecations.size()); + assertEquals("https://wiki.jenkins-ci.org/display/JENKINS/Token+Macro+Plugin", deprecations.get(0).url); + + final PluginWrapper variant = pm.getPlugin("variant"); + Objects.requireNonNull(variant); + assertTrue(variant.isDeprecated()); + deprecations = variant.getDeprecations(); + assertEquals(1, deprecations.size()); + assertEquals("https://jenkins.io/deprecations/variant/", deprecations.get(0).url); + assertNull(variant.getInfo()); + } + @Issue("JENKINS-62622") @Test @WithPlugin("legacy.hpi") diff --git a/test/src/test/resources/plugins/deprecations-update-center.json b/test/src/test/resources/plugins/deprecations-update-center.json new file mode 100644 index 0000000000..9fab1d35e8 --- /dev/null +++ b/test/src/test/resources/plugins/deprecations-update-center.json @@ -0,0 +1,81 @@ +updateCenter.post( +{"connectionCheckUrl":"http://www.google.com/", + "core": { + "buildDate": "Dec 31, 1969", + "name": "core", + "url": "jenkins.war", + "version": "23" + }, + "id": "default", + "plugins": { + "credentials": { + "buildDate": "Apr 04, 2016", + "dependencies": [ + { + "name": "icon-shim", + "optional": false, + "version":"2.0.2" + } + ], + "developers": [], + "excerpt": "This plugin allows you to store credentials in Jenkins.", + "gav": "org.jenkins-ci.plugins:credentials:1.27", + "labels": [], + "name": "credentials", + "previousTimestamp": "2016-03-24T01:19:58.00Z", + "previousVersion": "1.26", + "releaseTimestamp": "2016-04-04T16:26:54.00Z", + "requiredCore": "1.565", + "scm": "github.com", + "sha1": "XWJpfL8B2x7qrISJg7IY2ZTPxFk=", + "title": "Credentials Plugin", + "url": "credentials.hpi", + "version": "1.27", + "wiki": "https://wiki.jenkins-ci.org/display/JENKINS/Credentials+Plugin" + }, + "icon-shim": { + "buildDate": "Feb 23, 2016", + "dependencies": [], + "developers": [], + "excerpt": "This plugin allows other Jenkins plugins to take advantage of the tag. ", + "gav": "org.jenkins-ci.plugins.icon-shim:icon-shim:2.0.3", + "labels": ["deprecated"], + "name": "icon-shim", + "previousTimestamp": "2016-01-09T20:01:12.00Z", + "previousVersion": "2.0.2", + "releaseTimestamp": "2016-02-23T23:08:18.00Z", + "requiredCore": "1.609", + "scm": "github.com", + "sha1": "umtSUbNwIuqU6JGEQcBxqFt1X24=", + "title": "Icon Shim Plugin", + "url": "icon-shim.hpi", + "version": "2.0.3", + "wiki": "https://wiki.jenkins-ci.org/display/JENKINS/Icon+Shim+Plugin" + }, + "token-macro": { + "buildDate": "Dec 14, 2015", + "dependencies": [], + "developers": [], + "excerpt": "This plugin adds reusable macro expansion capability for other plugins to use", + "gav": "org.jenkins-ci.plugins:token-macro:1.12.1", + "labels": ["deprecated"], + "name": "token-macro", + "previousTimestamp": "2015-12-11T08:27:18.00Z", + "previousVersion": "1.12", + "releaseTimestamp": "2015-12-14T18:07:56.00Z", + "requiredCore": "1.609.2", + "scm": "github.com", + "sha1": "TV61PGo+od75URp3maLGUFJ768k=", + "title": "Token Macro Plugin", + "url": "token-macro.hpi", + "version": "1.9", + "wiki": "https://wiki.jenkins-ci.org/display/JENKINS/Token+Macro+Plugin" + } + }, + "deprecations": { + "icon-shim": { "url": "https://jenkins.io/deprecations/icon-shim/" }, + "variant": { "url": "https://jenkins.io/deprecations/variant/" } + }, + "updateCenterVersion": 1 +} +);