/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Yahoo! Inc., Erik Ramfelt, Tom Huybrechts * * 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. */ package hudson; import com.google.common.collect.Sets; import hudson.PluginManager.PluginInstanceStore; import hudson.model.AdministrativeMonitor; import hudson.model.Api; import hudson.model.ModelObject; import hudson.model.UpdateCenter; import hudson.model.UpdateSite; import hudson.util.VersionNumber; import io.jenkins.lib.versionnumber.JavaSpecificationVersion; import jenkins.YesNoMaybe; import jenkins.model.Jenkins; import jenkins.security.UpdateSiteWarningsMonitor; import jenkins.util.java.JavaUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.LogFactory; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import static hudson.PluginWrapper.PluginDisableStatus.ALREADY_DISABLED; import static hudson.PluginWrapper.PluginDisableStatus.DISABLED; import static hudson.PluginWrapper.PluginDisableStatus.ERROR_DISABLING; import static hudson.PluginWrapper.PluginDisableStatus.NOT_DISABLED_DEPENDANTS; import static hudson.PluginWrapper.PluginDisableStatus.NO_SUCH_PLUGIN; import static java.util.logging.Level.WARNING; import jenkins.plugins.DetachedPluginsUtil; import static org.apache.commons.io.FilenameUtils.getBaseName; /** * Represents a Jenkins plug-in and associated control information * for Jenkins to control {@link Plugin}. * *

* A plug-in is packaged into a jar file whose extension is {@code ".jpi"} (or {@code ".hpi"} for backward compatibility), * A plugin needs to have a special manifest entry to identify what it is. * *

* At the runtime, a plugin has two distinct state axis. *

    *
  1. Enabled/Disabled. If enabled, Jenkins is going to use it * next time Jenkins runs. Otherwise the next run will ignore it. *
  2. Activated/Deactivated. If activated, that means Jenkins is using * the plugin in this session. Otherwise it's not. *
*

* For example, an activated but disabled plugin is still running but the next * time it won't. * * @author Kohsuke Kawaguchi */ @ExportedBean public class PluginWrapper implements Comparable, ModelObject { /** * A plugin won't be loaded unless his declared dependencies are present and match the required minimal version. * This can be set to false to disable the version check (legacy behaviour) */ private static final boolean ENABLE_PLUGIN_DEPENDENCIES_VERSION_CHECK = Boolean.parseBoolean(System.getProperty(PluginWrapper.class.getName()+"." + "dependenciesVersionCheck.enabled", "true")); /** * {@link PluginManager} to which this belongs to. */ public final PluginManager parent; /** * Plugin manifest. * Contains description of the plugin. */ private final Manifest manifest; /** * {@link ClassLoader} for loading classes from this plugin. * Null if disabled. */ public final ClassLoader classLoader; /** * Base URL for loading static resources from this plugin. * Null if disabled. The static resources are mapped under * {@code CONTEXTPATH/plugin/SHORTNAME/}. */ public final URL baseResourceURL; /** * Used to control enable/disable setting of the plugin. * If this file exists, plugin will be disabled. */ private final File disableFile; /** * A .jpi file, an exploded plugin directory, or a .jpl file. */ private final File archive; /** * Short name of the plugin. The artifact Id of the plugin. * This is also used in the URL within Jenkins, so it needs * to remain stable even when the *.jpi file name is changed * (like Maven does.) */ private final String shortName; /** * True if this plugin is activated for this session. * The snapshot of {@code disableFile.exists()} as of the start up. */ private final boolean active; private boolean hasCycleDependency = false; private final List dependencies; private final List optionalDependencies; public List getDependencyErrors() { return Collections.unmodifiableList(new ArrayList<>(dependencyErrors.keySet())); } @Restricted(NoExternalUse.class) // Jelly use public List getOriginalDependencyErrors() { Predicate> p = Map.Entry::getValue; return dependencyErrors.entrySet().stream().filter(p.negate()).map(Map.Entry::getKey).collect(Collectors.toList()); } @Restricted(NoExternalUse.class) // Jelly use public boolean hasOriginalDependencyErrors() { return !getOriginalDependencyErrors().isEmpty(); } @Restricted(NoExternalUse.class) // Jelly use public List getDerivedDependencyErrors() { return dependencyErrors.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey).collect(Collectors.toList()); } @Restricted(NoExternalUse.class) // Jelly use public boolean hasDerivedDependencyErrors() { return !getDerivedDependencyErrors().isEmpty(); } /** * A String error message, and a boolean indicating whether it's an original error (false) or downstream from an original one (true) */ private final transient Map dependencyErrors = new HashMap<>(0); /** * Is this plugin bundled in jenkins.war? */ /*package*/ boolean isBundled; /** * List of plugins that depend on this plugin. */ private Set dependents = Collections.emptySet(); /** * List of plugins that depend optionally on this plugin. */ private Set optionalDependents = Collections.emptySet(); /** * The core can depend on a plugin if it is bundled. Sometimes it's the only thing that * depends on the plugin e.g. UI support library bundle plugin. */ private static Set CORE_ONLY_DEPENDANT = Collections.singleton("jenkins-core"); /** * Set the list of components that depend on this plugin. * @param dependents The list of components that depend on this plugin. */ public void setDependents(@NonNull Set dependents) { this.dependents = dependents; } /** * @deprecated Please use {@link #setDependents}. */ @Deprecated public void setDependants(@NonNull Set dependents) { setDependents(dependents); } /** * Set the list of components that depend optionally on this plugin. * @param optionalDependents The list of components that depend optionally on this plugin. */ public void setOptionalDependents(@NonNull Set optionalDependents) { this.optionalDependents = optionalDependents; } /** * @deprecated Please use {@link #setOptionalDependents}. */ @Deprecated public void setOptionalDependants(@NonNull Set optionalDependents) { setOptionalDependents(dependents); } /** * Get the list of components that depend on this plugin. * Note that the list will include elements of {@link #getOptionalDependents}. * @return The list of components that depend on this plugin. */ public @NonNull Set getDependents() { if (isBundled && dependents.isEmpty()) { return CORE_ONLY_DEPENDANT; } else { return dependents; } } /** * @deprecated Please use {@link #getDependents}. */ @Deprecated public @NonNull Set getDependants() { return getDependents(); } /** * Like {@link #getDependents} but excluding optional dependencies. * @since 2.181 */ public @NonNull Set getMandatoryDependents() { Set s = new HashSet<>(dependents); s.removeAll(optionalDependents); return s; } /** * @return The list of components that depend optionally on this plugin. */ public @NonNull Set getOptionalDependents() { return optionalDependents; } /** * @deprecated Please use {@link #getOptionalDependents}. */ @Deprecated public @NonNull Set getOptionalDependants() { return getOptionalDependents(); } /** * Does this plugin have anything that depends on it. * Note that optional dependents are included. * @return {@code true} if something (Jenkins core, or another plugin) depends on this * plugin, otherwise {@code false}. */ public boolean hasDependents() { return (isBundled || !dependents.isEmpty()); } /** * Like {@link #hasDependents} but excluding optional dependencies. * @since 2.181 */ public boolean hasMandatoryDependents() { if (isBundled) { return true; } return dependents.stream().anyMatch(d -> !optionalDependents.contains(d)); } /** * @deprecated Please use {@link #hasDependents}. */ @Deprecated public boolean hasDependants() { return hasDependents(); } /** * Does this plugin have anything that depends optionally on it. * @return {@code true} if something (Jenkins core, or another plugin) depends optionally on this * plugin, otherwise {@code false}. */ public boolean hasOptionalDependents() { return !optionalDependents.isEmpty(); } /** * @deprecated Please use {@link #hasOptionalDependents}. */ @Deprecated public boolean hasOptionalDependants() { return hasOptionalDependents(); } /** * Does this plugin depend on any other plugins. * Note that this include optional dependencies. * @return {@code true} if this plugin depends on other plugins, otherwise {@code false}. */ public boolean hasDependencies() { return !dependencies.isEmpty(); } /** * Like {@link #hasDependencies} but omitting optional dependencies. * @since 2.181 */ public boolean hasMandatoryDependencies() { 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 2.246 */ @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 public final String shortName; @Exported public final String version; @Exported public final boolean optional; public Dependency(String s) { int idx = s.indexOf(':'); if(idx==-1) throw new IllegalArgumentException("Illegal dependency specifier "+s); this.shortName = Util.intern(s.substring(0,idx)); String version = Util.intern(s.substring(idx+1)); boolean isOptional = false; String[] osgiProperties = version.split("[;]"); for (int i = 1; i < osgiProperties.length; i++) { String osgiProperty = osgiProperties[i].trim(); if (osgiProperty.equalsIgnoreCase("resolution:=optional")) { isOptional = true; break; } } this.optional = isOptional; if (isOptional) { this.version = osgiProperties[0]; } else { this.version = version; } } @Override public String toString() { return shortName + " (" + version + ")" + (optional ? " optional" : ""); } } /** * @param archive * A .jpi archive file jar file, or a .jpl linked plugin. * @param manifest * The manifest for the plugin * @param baseResourceURL * A URL pointing to the resources for this plugin * @param classLoader * a classloader that loads classes from this plugin and its dependencies * @param disableFile * if this file exists on startup, the plugin will not be activated * @param dependencies a list of mandatory dependencies * @param optionalDependencies a list of optional dependencies */ public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL baseResourceURL, ClassLoader classLoader, File disableFile, List dependencies, List optionalDependencies) { this.parent = parent; this.manifest = manifest; this.shortName = Util.intern(computeShortName(manifest, archive.getName())); this.baseResourceURL = baseResourceURL; this.classLoader = classLoader; this.disableFile = disableFile; this.active = !disableFile.exists(); this.dependencies = dependencies; this.optionalDependencies = optionalDependencies; for (Dependency d : optionalDependencies) { assert d.optional : d + " included among optionalDependencies of " + shortName + " but was not marked optional"; } this.archive = archive; } @Override public String getDisplayName() { return StringUtils.removeStart(getLongName(), "Jenkins "); } public Api getApi() { Jenkins.get().checkAnyPermission(Jenkins.SYSTEM_READ, Jenkins.MANAGE); return new Api(this); } /** * Returns the URL of the index page jelly script. */ public URL getIndexPage() { // In the current impl dependencies are checked first, so the plugin itself // will add the last entry in the getResources result. URL idx = null; try { Enumeration en = classLoader.getResources("index.jelly"); while (en.hasMoreElements()) idx = en.nextElement(); } catch (IOException ignore) { } // In case plugin has dependencies but is missing its own index.jelly, // check that result has this plugin's artifactId in it: return idx != null && idx.toString().contains(shortName) ? idx : null; } static String computeShortName(Manifest manifest, String fileName) { // use the name captured in the manifest, as often plugins // depend on the specific short name in its URLs. String n = manifest.getMainAttributes().getValue("Short-Name"); if(n!=null) return n; // maven seems to put this automatically, so good fallback to check. n = manifest.getMainAttributes().getValue("Extension-Name"); if(n!=null) return n; // otherwise infer from the file name, since older plugins don't have // this entry. return getBaseName(fileName); } /** * Gets all dependencies of this plugin on other plugins. * Note that the list will usually include the members of {@link #getOptionalDependencies} * (missing optional dependencies will however be omitted). */ @Exported public List getDependencies() { return dependencies; } /** * Like {@link #getDependencies} but omits optional dependencies. * @since 2.181 */ public List getMandatoryDependencies() { return dependencies.stream().filter(d -> !d.optional).collect(Collectors.toList()); } public List getOptionalDependencies() { return optionalDependencies; } /** * Returns the short name suitable for URL. */ @Exported public String getShortName() { return shortName; } /** * Gets the instance of {@link Plugin} contributed by this plugin. * @return Plugin instace or {@code null} if it is not present in the plugin instance store. */ public @CheckForNull Plugin getPlugin() { PluginInstanceStore pis = Jenkins.lookup(PluginInstanceStore.class); return pis != null ? pis.store.get(this) : null; } /** * Gets the instance of {@link Plugin} contributed by this plugin. * @throws Exception no plugin in the {@link PluginInstanceStore} */ public @NonNull Plugin getPluginOrFail() throws Exception { Plugin plugin = getPlugin(); if (plugin == null) { throw new Exception("Cannot find the plugin instance: " + shortName); } return plugin; } /** * Gets the URL that shows more information about this plugin. * @return * null if this information is unavailable. * @since 1.283 */ @Exported public String getUrl() { // first look in update center metadata List siteMetadataList = getInfoFromAllSites(); String firstSiteUrl = null; if (!siteMetadataList.isEmpty()) { firstSiteUrl = siteMetadataList.get(0).wiki; if (allUrlsMatch(firstSiteUrl, siteMetadataList)) { return firstSiteUrl; } } // if update sites give different / empty results, // use manifest (since maven-hpi-plugin 1.30) String url = manifest.getMainAttributes().getValue("Url"); if (url != null) { return url; } return firstSiteUrl; } private boolean allUrlsMatch(String url, List uiList) { return uiList.stream().allMatch(k -> k.wiki != null && k.wiki.equals(url)); } @Override public String toString() { return "Plugin:" + getShortName(); } /** * 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; return shortName; } /** * Does this plugin supports dynamic loading? */ @Exported public YesNoMaybe supportsDynamicLoad() { String v = manifest.getMainAttributes().getValue("Support-Dynamic-Loading"); if (v==null) return YesNoMaybe.MAYBE; return Boolean.parseBoolean(v) ? YesNoMaybe.YES : YesNoMaybe.NO; } /** * Returns the version number of this plugin */ @Exported public String getVersion() { return getVersionOf(manifest); } private String getVersionOf(Manifest manifest) { String v = manifest.getMainAttributes().getValue("Plugin-Version"); if(v!=null) return v; // plugins generated before maven-hpi-plugin 1.3 should still have this attribute v = manifest.getMainAttributes().getValue("Implementation-Version"); if(v!=null) return v; return "???"; } /** * Returns the required Jenkins core version of this plugin. * @return the required Jenkins core version of this plugin. * @since 2.16 */ @Exported public @CheckForNull String getRequiredCoreVersion() { String v = manifest.getMainAttributes().getValue("Jenkins-Version"); if (v!= null) return v; v = manifest.getMainAttributes().getValue("Hudson-Version"); if (v!= null) return v; return null; } /** * Returns the minimum Java version of this plugin, as specified in the plugin metadata. * Generally coming from the {@code java.level} extracted as MANIFEST's metadata with * this addition on the plugins' parent pom. * * @see maven-hpi-plugin#PR-75. * * @since 2.158 */ @Exported public @CheckForNull String getMinimumJavaVersion() { return manifest.getMainAttributes().getValue("Minimum-Java-Version"); } /** * Returns the version number of this plugin */ public VersionNumber getVersionNumber() { return new VersionNumber(getVersion()); } /** * Returns true if the version of this plugin is older than the given version. */ public boolean isOlderThan(VersionNumber v) { try { return getVersionNumber().compareTo(v) < 0; } catch (IllegalArgumentException e) { // if we can't figure out our current version, it probably means it's very old, // since the version information is missing only from the very old plugins return true; } } /** * Terminates the plugin. */ public void stop() { Plugin plugin = getPlugin(); if (plugin != null) { try { LOGGER.log(Level.FINE, "Stopping {0}", shortName); plugin.stop(); } catch (Throwable t) { LOGGER.log(WARNING, "Failed to shut down " + shortName, t); } } else { LOGGER.log(Level.FINE, "Could not find Plugin instance to stop for {0}", shortName); } // Work around a bug in commons-logging. // See http://www.szegedi.org/articles/memleak.html LogFactory.release(classLoader); } public void releaseClassLoader() { if (classLoader instanceof Closeable) try { ((Closeable) classLoader).close(); } catch (IOException e) { LOGGER.log(WARNING, "Failed to shut down classloader",e); } } /** * Enables this plugin next time Jenkins runs. */ public void enable() throws IOException { if (!disableFile.exists()) { LOGGER.log(Level.FINEST, "Plugin {0} has been already enabled. Skipping the enable() operation", getShortName()); return; } if(!disableFile.delete()) throw new IOException("Failed to delete "+disableFile); } /** * Disables this plugin next time Jenkins runs. As it doesn't check anything, it's recommended to use the method * {@link #disable(PluginDisableStrategy)} */ @Deprecated //see https://issues.jenkins-ci.org/browse/JENKINS-27177 public void disable() throws IOException { disableWithoutCheck(); } /** * Disable a plugin wihout checking any dependency. Only add the disable file. */ private void disableWithoutCheck() throws IOException { try (OutputStream os = Files.newOutputStream(disableFile.toPath())) { // creates an empty file } catch (InvalidPathException e) { throw new IOException(e); } } /** * Disable this plugin using a strategy. * @param strategy strategy to use * @return an object representing the result of the disablement of this plugin and its dependents plugins. */ public @NonNull PluginDisableResult disable(@NonNull PluginDisableStrategy strategy) { PluginDisableResult result = new PluginDisableResult(shortName); if (!this.isEnabled()) { result.setMessage(Messages.PluginWrapper_Already_Disabled(shortName)); result.setStatus(ALREADY_DISABLED); return result; } // Act as a flag indicating if this plugin, finally, can be disabled. If there is a not-disabled-dependent // plugin, this one couldn't be disabled. String aDependentNotDisabled = null; // List of dependents plugins to 'check'. 'Check' means disable for mandatory or all strategies, or review if // this dependent-mandatory plugin is enabled in order to return an error for the NONE strategy. Set dependentsToCheck = dependentsToCheck(strategy); // Review all the dependents and add to the plugin result what happened with its dependents for (String dependent : dependentsToCheck) { PluginWrapper dependentPlugin = parent.getPlugin(dependent); // The dependent plugin doesn't exist, add an error to the report if (dependentPlugin == null) { PluginDisableResult dependentStatus = new PluginDisableResult(dependent, NO_SUCH_PLUGIN, Messages.PluginWrapper_NoSuchPlugin(dependent)); result.addDependentDisableStatus(dependentStatus); // If the strategy is none and there is some enabled dependent plugin, the plugin cannot be disabled. If // this dependent plugin is not enabled, continue searching for one enabled. } else if (strategy.equals(PluginDisableStrategy.NONE)) { if (dependentPlugin.isEnabled()) { aDependentNotDisabled = dependent; break; // in this case, we don't need to continue with the rest of its dependents } // If the strategy is not none and this dependent plugin is not enabled, add it as already disabled } else if (!dependentPlugin.isEnabled()) { PluginDisableResult dependentStatus = new PluginDisableResult(dependent, ALREADY_DISABLED, Messages.PluginWrapper_Already_Disabled(dependent)); result.addDependentDisableStatus(dependentStatus); // If the strategy is not none and this dependent plugin is enabled, disable it } else { // As there is no cycles in the plugin dependencies, the recursion shouldn't be infinite. The // strategy used is the same for its dependents plugins PluginDisableResult dependentResult = dependentPlugin.disable(strategy); PluginDisableStatus dependentStatus = dependentResult.status; // If something wrong happened, flag this dependent plugin to set the plugin later as not-disabled due // to its dependents plugins. if (ERROR_DISABLING.equals(dependentStatus) || NOT_DISABLED_DEPENDANTS.equals(dependentStatus)) { aDependentNotDisabled = dependent; break; // we found a dependent plugin enabled, stop looking for dependent plugins to disable. } result.addDependentDisableStatus(dependentResult); } } // If there is no enabled-dependent plugin, disable this plugin and add it to the result if (aDependentNotDisabled == null) { try { this.disableWithoutCheck(); result.setMessage(Messages.PluginWrapper_Plugin_Disabled(shortName)); result.setStatus(DISABLED); } catch (IOException io) { result.setMessage(Messages.PluginWrapper_Error_Disabling(shortName, io.toString())); result.setStatus(ERROR_DISABLING); } // if there is yet some not disabled dependent plugin (only possible with none strategy), this plugin cannot // be disabled. } else { result.setMessage(Messages.PluginWrapper_Plugin_Has_Dependent(shortName, aDependentNotDisabled, strategy)); result.setStatus(NOT_DISABLED_DEPENDANTS); } return result; } private Set dependentsToCheck(PluginDisableStrategy strategy) { Set dependentsToCheck; switch (strategy) { case ALL: // getDependents returns all the dependent plugins, mandatory or optional. dependentsToCheck = this.getDependents(); break; default: // It includes MANDATORY, NONE: // with NONE, the process only fail if mandatory dependent plugins exists // As of getDependents has all the dependents, we get the difference between them and only the optionals dependentsToCheck = Sets.difference(this.getDependents(), this.getOptionalDependents()); } return dependentsToCheck; } /** * Returns true if this plugin is enabled for this session. */ @Exported public boolean isActive() { return active && !hasCycleDependency(); } public boolean hasCycleDependency(){ return hasCycleDependency; } public void setHasCycleDependency(boolean hasCycle){ hasCycleDependency = hasCycle; } /** * Is this plugin bundled in the WAR? * Normally false as noted in {@link PluginManager#loadBundledPlugins}: * this does not apply to “detached” plugins. */ @Exported public boolean isBundled() { return isBundled; } /** * If true, the plugin is going to be activated next time * Jenkins runs. */ @Exported public boolean isEnabled() { return !disableFile.exists(); } public Manifest getManifest() { return manifest; } public void setPlugin(Plugin plugin) { Jenkins.lookup(PluginInstanceStore.class).store.put(this,plugin); plugin.wrapper = this; } public String getPluginClass() { return manifest.getMainAttributes().getValue("Plugin-Class"); } public boolean hasLicensesXml() { try { new URL(baseResourceURL,"WEB-INF/licenses.xml").openStream().close(); return true; } catch (IOException e) { return false; } } /** * Makes sure that all the dependencies exist, and then accept optional dependencies * as real dependencies. * * @throws IOException * thrown if one or several mandatory dependencies doesn't exists. */ /*package*/ void resolvePluginDependencies() throws IOException { if (ENABLE_PLUGIN_DEPENDENCIES_VERSION_CHECK) { String requiredCoreVersion = getRequiredCoreVersion(); if (requiredCoreVersion == null) { LOGGER.warning(shortName + " doesn't declare required core version."); } else { VersionNumber actualVersion = Jenkins.getVersion(); if (actualVersion.isOlderThan(new VersionNumber(requiredCoreVersion))) { versionDependencyError(Messages.PluginWrapper_obsoleteCore(Jenkins.getVersion().toString(), requiredCoreVersion), Jenkins.getVersion().toString(), requiredCoreVersion); } } String minimumJavaVersion = getMinimumJavaVersion(); if (minimumJavaVersion != null) { JavaSpecificationVersion actualVersion = JavaUtils.getCurrentJavaRuntimeVersionNumber(); if (actualVersion.isOlderThan(new JavaSpecificationVersion(minimumJavaVersion))) { versionDependencyError(Messages.PluginWrapper_obsoleteJava(actualVersion.toString(), minimumJavaVersion), actualVersion.toString(), minimumJavaVersion); } } } // make sure dependencies exist for (Dependency d : dependencies) { PluginWrapper dependency = parent.getPlugin(d.shortName); if (dependency == null) { PluginWrapper failedDependency = NOTICE.getPlugin(d.shortName); if (failedDependency != null) { dependencyErrors.put(Messages.PluginWrapper_failed_to_load_dependency(failedDependency.getLongName(), failedDependency.getVersion()), true); break; } else { dependencyErrors.put(Messages.PluginWrapper_missing(d.shortName, d.version), false); } } else { if (dependency.isActive()) { if (isDependencyObsolete(d, dependency)) { versionDependencyError(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version); } } else { if (isDependencyObsolete(d, dependency)) { versionDependencyError(Messages.PluginWrapper_disabledAndObsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version); } else { dependencyErrors.put(Messages.PluginWrapper_disabled(dependency.getLongName()), false); } } } } // add the optional dependencies that exists for (Dependency d : optionalDependencies) { PluginWrapper dependency = parent.getPlugin(d.shortName); if (dependency != null && dependency.isActive()) { if (isDependencyObsolete(d, dependency)) { versionDependencyError(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version); } else { dependencies.add(d); } } } if (!dependencyErrors.isEmpty()) { NOTICE.addPlugin(this); StringBuilder messageBuilder = new StringBuilder(); messageBuilder.append(Messages.PluginWrapper_failed_to_load_plugin(getLongName(), getVersion())).append(System.lineSeparator()); for (Iterator iterator = dependencyErrors.keySet().iterator(); iterator.hasNext(); ) { String dependencyError = iterator.next(); messageBuilder.append(" - ").append(dependencyError); if (iterator.hasNext()) { messageBuilder.append(System.lineSeparator()); } } throw new IOException(messageBuilder.toString()); } } private boolean isDependencyObsolete(Dependency d, PluginWrapper dependency) { return ENABLE_PLUGIN_DEPENDENCIES_VERSION_CHECK && dependency.getVersionNumber().isOlderThan(new VersionNumber(d.version)); } /** * Called when there appears to be a core or plugin version which is too old for a stated dependency. * Normally records an error in {@link #dependencyErrors}. * But if one or both versions {@link #isSnapshot}, just issue a warning (JENKINS-52665). */ private void versionDependencyError(String message, String actual, String minimum) { if (isSnapshot(actual) || isSnapshot(minimum)) { LOGGER.log(WARNING, "Suppressing dependency error in {0} v{1}: {2}", new Object[] {getLongName(), getVersion(), message}); } else { dependencyErrors.put(message, false); } } /** * Similar to {@code org.apache.maven.artifact.ArtifactUtils.isSnapshot}. */ static boolean isSnapshot(@NonNull String version) { return version.contains("-SNAPSHOT") || version.matches(".+-[0-9]{8}.[0-9]{6}-[0-9]+"); } /** * If the plugin has {@link #getUpdateInfo() an update}, * returns the {@link hudson.model.UpdateSite.Plugin} object. * * @return * This method may return null — for example, * the user may have installed a plugin locally developed. */ public UpdateSite.Plugin getUpdateInfo() { UpdateCenter uc = Jenkins.get().getUpdateCenter(); UpdateSite.Plugin p = uc.getPlugin(getShortName(), getVersionNumber()); if(p!=null && p.isNewerThan(getVersion())) return p; return null; } /** * returns the {@link hudson.model.UpdateSite.Plugin} object, or null. */ public UpdateSite.Plugin getInfo() { UpdateCenter uc = Jenkins.get().getUpdateCenter(); UpdateSite.Plugin p = uc.getPlugin(getShortName(), getVersionNumber()); if (p != null) return p; return uc.getPlugin(getShortName()); } private List getInfoFromAllSites() { UpdateCenter uc = Jenkins.get().getUpdateCenter(); return uc.getPluginFromAllSites(getShortName(), getVersionNumber()); } /** * Returns true if this plugin has update in the update center. * *

* This method is conservative in the sense that if the version number is incomprehensible, * it always returns false. */ @Exported public boolean hasUpdate() { return getUpdateInfo()!=null; } @Exported @Deprecated // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ public boolean isPinned() { return false; } /** * Returns true if this plugin is deleted. * * The plugin continues to function in this session, but in the next session it'll disappear. */ @Exported public boolean isDeleted() { return !archive.exists(); } /** * Same as {@link DetachedPluginsUtil#isDetachedPlugin}. * @since 2.185 */ @Exported public boolean isDetached() { return DetachedPluginsUtil.isDetachedPlugin(shortName); } @Restricted(NoExternalUse.class) public boolean hasImpliedDependents() { if (!isDetached()) { return false; } for (PluginWrapper p : Jenkins.get().getPluginManager().getPlugins()) { for (Dependency dependency : DetachedPluginsUtil.getImpliedDependencies(p.shortName, p.getRequiredCoreVersion())) { if (dependency.shortName.equals(shortName)) { return true; } } } return false; } /** * Get list of implied dependencies. * @since TODO */ @Restricted(NoExternalUse.class) public @NonNull Set getImpliedDependents() { if (!isDetached()) { return Collections.emptySet(); } Set implied = new HashSet<>(); for (PluginWrapper p : Jenkins.get().getPluginManager().getPlugins()) { for (Dependency dependency : DetachedPluginsUtil.getImpliedDependencies(p.shortName, p.getRequiredCoreVersion())) { if (dependency.shortName.equals(shortName)) { implied.add(p.shortName); } } } return implied; } /** * Sort by short name. */ @Override public int compareTo(PluginWrapper pw) { return shortName.compareToIgnoreCase(pw.shortName); } /** * returns true if backup of previous version of plugin exists */ @Exported public boolean isDowngradable() { return getBackupFile().exists(); } /** * Where is the backup file? */ public File getBackupFile() { return new File(Jenkins.get().getRootDir(),"plugins/"+getShortName() + ".bak"); } /** * returns the version of the backed up plugin, * or null if there's no back up. */ @Exported public String getBackupVersion() { File backup = getBackupFile(); if (backup.exists()) { try { try (JarFile backupPlugin = new JarFile(backup)) { return backupPlugin.getManifest().getMainAttributes().getValue("Plugin-Version"); } } catch (IOException e) { LOGGER.log(WARNING, "Failed to get backup version from " + backup, e); return null; } } else { return null; } } /** * Checks if this plugin is pinned and that's forcing us to use an older version than the bundled one. */ @Deprecated // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ public boolean isPinningForcingOldVersion() { return false; } @Extension public static final PluginWrapperAdministrativeMonitor NOTICE = new PluginWrapperAdministrativeMonitor(); /** * Administrative Monitor for failed plugins */ public static final class PluginWrapperAdministrativeMonitor extends AdministrativeMonitor { private final Map plugins = new HashMap<>(); void addPlugin(PluginWrapper plugin) { plugins.put(plugin.shortName, plugin); } @Override public boolean isActivated() { return !plugins.isEmpty(); } @Restricted(DoNotUse.class) // Jelly public boolean hasAnyDerivedDependencyErrors() { return plugins.values().stream().anyMatch(PluginWrapper::hasDerivedDependencyErrors); } @Override public String getDisplayName() { return Messages.PluginWrapper_PluginWrapperAdministrativeMonitor_DisplayName(); } public Collection getPlugins() { return plugins.values(); } public PluginWrapper getPlugin(String shortName) { return plugins.get(shortName); } /** * Depending on whether the user said "dismiss" or "correct", send him to the right place. */ public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException { if(req.hasParameter("correct")) { rsp.sendRedirect(req.getContextPath()+"/pluginManager"); } } public static PluginWrapperAdministrativeMonitor get() { return AdministrativeMonitor.all().get(PluginWrapperAdministrativeMonitor.class); } } /** * The result of the disablement of a plugin and its dependents plugins. */ public static class PluginDisableResult { private String plugin; private PluginDisableStatus status; private String message; private Set dependentsDisableStatus = new HashSet<>(); public PluginDisableResult(String plugin) { this.plugin = plugin; } public PluginDisableResult(String plugin, PluginDisableStatus status, String message) { this.plugin = plugin; this.status = status; this.message = message; } public String getPlugin() { return plugin; } public PluginDisableStatus getStatus() { return status; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PluginDisableResult that = (PluginDisableResult) o; return Objects.equals(plugin, that.plugin); } @Override public int hashCode() { return Objects.hash(plugin); } public void setStatus(PluginDisableStatus status) { this.status = status; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Set getDependentsDisableStatus() { return dependentsDisableStatus; } public void addDependentDisableStatus(PluginDisableResult dependentDisableStatus) { dependentsDisableStatus.add(dependentDisableStatus); } } /** * An enum to hold the status of a disabling action against a plugin. To do it more reader-friendly. */ public enum PluginDisableStatus { NO_SUCH_PLUGIN, DISABLED, ALREADY_DISABLED, NOT_DISABLED_DEPENDANTS, ERROR_DISABLING } /** * The strategies defined for disabling a plugin. */ public enum PluginDisableStrategy { NONE, MANDATORY, ALL; @Override public String toString() { return this.name().toLowerCase(); } } // // // Action methods // // @RequirePOST public HttpResponse doMakeEnabled() throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); enable(); return HttpResponses.ok(); } @RequirePOST public HttpResponse doMakeDisabled() throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); disable(); return HttpResponses.ok(); } @RequirePOST @Deprecated public HttpResponse doPin() throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ LOGGER.log(WARNING, "Call to pin plugin has been ignored. Plugin name: " + shortName); return HttpResponses.ok(); } @RequirePOST @Deprecated public HttpResponse doUnpin() throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ LOGGER.log(WARNING, "Call to unpin plugin has been ignored. Plugin name: " + shortName); return HttpResponses.ok(); } @RequirePOST public HttpResponse doDoUninstall() throws IOException { Jenkins jenkins = Jenkins.get(); jenkins.checkPermission(Jenkins.ADMINISTER); archive.delete(); // Redo who depends on who. jenkins.getPluginManager().resolveDependentPlugins(); return HttpResponses.redirectViaContextPath("/pluginManager/installed"); // send back to plugin manager } @Restricted(DoNotUse.class) // Jelly public List getActiveWarnings() { 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<>(); final UpdateCenter updateCenter = Jenkins.get().getUpdateCenter(); if (updateCenter.isSiteDataReady()) { for (UpdateSite site : updateCenter.getSites()) { final UpdateSite.Data data = site.getData(); if (data != null) { for (Map.Entry entry : data.getDeprecations().entrySet()) { if (entry.getKey().equals(this.shortName)) { deprecations.add(entry.getValue()); } } } } } return deprecations; } private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName()); /** * Name of the plugin manifest file (to help find where we parse them.) */ public static final String MANIFEST_FILENAME = "META-INF/MANIFEST.MF"; }