diff --git a/.gitpod/Dockerfile b/.gitpod/Dockerfile index 6b71c6707163..f2475612b107 100644 --- a/.gitpod/Dockerfile +++ b/.gitpod/Dockerfile @@ -1,6 +1,6 @@ FROM gitpod/workspace-full -ARG MAVEN_VERSION=3.9.6 +ARG MAVEN_VERSION=3.9.7 RUN brew install gh && \ bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && sdk install maven ${MAVEN_VERSION} && sdk default maven ${MAVEN_VERSION}" diff --git a/ath.sh b/ath.sh index e39c053ef66e..3ab8d159b66e 100644 --- a/ath.sh +++ b/ath.sh @@ -6,7 +6,7 @@ set -o xtrace cd "$(dirname "$0")" # https://github.com/jenkinsci/acceptance-test-harness/releases -export ATH_VERSION=5814.vdc5d6d484b_40 +export ATH_VERSION=5858.v4b_c4e3b_16099 if [[ $# -eq 0 ]]; then export JDK=17 diff --git a/bom/pom.xml b/bom/pom.xml index 91fac96bf44b..273355a332b2 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -38,8 +38,9 @@ THE SOFTWARE. The module contains dependencies that are used by a specific Jenkins version + 2.0.0-M2 2.0.13 - 1860.vb_89682cf8d96 + 1870.v48cc46ef5fee 2.4.21 @@ -55,7 +56,7 @@ THE SOFTWARE. org.springframework spring-framework-bom - 5.3.34 + 5.3.36 pom import @@ -119,11 +120,6 @@ THE SOFTWARE. commons-collections 3.2.2 - - commons-fileupload - commons-fileupload - 1.5 - commons-io commons-io @@ -149,6 +145,11 @@ THE SOFTWARE. jenkins-stapler-support 1.1 + + jakarta.servlet + jakarta.servlet-api + 4.0.4 + jakarta.servlet.jsp.jstl jakarta.servlet.jsp.jstl-api @@ -189,6 +190,41 @@ THE SOFTWARE. commons-compress 1.26.1 + + org.apache.commons + commons-fileupload2 + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-core + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-distribution + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-jakarta-servlet5 + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-jakarta-servlet6 + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-javax + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-portlet + ${commons-fileupload2.version} + org.codehaus.groovy groovy-all @@ -340,7 +376,7 @@ THE SOFTWARE. commons-logging commons-logging - 1.3.1 + 1.3.2 provided diff --git a/core/pom.xml b/core/pom.xml index faef477b9cd2..61c48e489794 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -160,10 +160,6 @@ THE SOFTWARE. commons-collections commons-collections - - commons-fileupload - commons-fileupload - commons-io commons-io @@ -284,6 +280,14 @@ THE SOFTWARE. + + org.apache.commons + commons-fileupload2-core + + + org.apache.commons + commons-fileupload2-javax + org.codehaus.groovy groovy-all @@ -439,7 +443,6 @@ THE SOFTWARE. jakarta.servlet jakarta.servlet-api - 4.0.4 provided diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index 10b0d179fdd7..d9b37f0fac29 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -52,6 +52,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -235,7 +236,11 @@ private static Manifest loadLinkedManifest(File archive) throws IOException { dependencyLoader = getBaseClassLoader(atts, dependencyLoader); return new PluginWrapper(pluginManager, archive, manifest, baseResourceURL, - createClassLoader(paths, dependencyLoader, atts), disableFile, dependencies, optionalDependencies); + createClassLoader(computeClassLoaderName(manifest, archive), paths, dependencyLoader, atts), disableFile, dependencies, optionalDependencies); + } + + private static String computeClassLoaderName(Manifest mf, File archive) { + return "PluginClassLoader for " + PluginWrapper.computeShortName(mf, archive.getName()); } private void fix(Attributes atts, List optionalDependencies) { @@ -263,15 +268,28 @@ public static List getImpliedDependencies(String plugi return DetachedPluginsUtil.getImpliedDependencies(pluginName, jenkinsVersion); } - @Deprecated + /** + * @deprecated since TODO use {@link #createClassLoader(String, List, ClassLoader, Attributes)} + */ + @Deprecated(since = "TODO") protected ClassLoader createClassLoader(List paths, ClassLoader parent) throws IOException { return createClassLoader(paths, parent, null); } /** - * Creates the classloader that can load all the specified jar files and delegate to the given parent. + * @deprecated since TODO use {@link #createClassLoader(String, List, ClassLoader, Attributes)} */ + @Deprecated(since="TODO") protected ClassLoader createClassLoader(List paths, ClassLoader parent, Attributes atts) throws IOException { + // generate a legacy id so at least we can track to something + return createClassLoader("unidentified-" + UUID.randomUUID(), paths, parent, atts); + } + + /** + * Creates a classloader that can load all the specified jar files and delegate to the given parent. + * @since TODO + */ + protected ClassLoader createClassLoader(String name, List paths, ClassLoader parent, Attributes atts) throws IOException { boolean usePluginFirstClassLoader = atts != null && Boolean.parseBoolean(atts.getValue("PluginFirstClassLoader")); @@ -285,9 +303,9 @@ protected ClassLoader createClassLoader(List paths, ClassLoader parent, At } URLClassLoader2 classLoader; if (usePluginFirstClassLoader) { - classLoader = new PluginFirstClassLoader2(urls.toArray(new URL[0]), parent); + classLoader = new PluginFirstClassLoader2(name, urls.toArray(new URL[0]), parent); } else { - classLoader = new URLClassLoader2(urls.toArray(new URL[0]), parent); + classLoader = new URLClassLoader2(name, urls.toArray(new URL[0]), parent); } return classLoader; } @@ -561,7 +579,7 @@ static final class DependencyClassLoader extends ClassLoader { } DependencyClassLoader(ClassLoader parent, File archive, List dependencies, PluginManager pluginManager) { - super(parent); + super("dependency ClassLoader for " + archive.getPath(), parent); this._for = archive; this.dependencies = List.copyOf(dependencies); this.pluginManager = pluginManager; diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 3525b931ef78..44a39b22836f 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -31,8 +31,6 @@ import static hudson.Util.fixEmptyAndTrim; import com.google.common.annotations.VisibleForTesting; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -61,7 +59,6 @@ import hudson.util.ExceptionCatchingThreadFactory; import hudson.util.FileVisitor; import hudson.util.FormValidation; -import hudson.util.HeadBufferingStream; import hudson.util.IOUtils; import hudson.util.NamingThreadFactory; import hudson.util.io.Archiver; @@ -80,6 +77,7 @@ import java.io.OutputStreamWriter; import java.io.RandomAccessFile; import java.io.Serializable; +import java.io.UncheckedIOException; import java.io.Writer; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -123,6 +121,8 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import jenkins.MasterToSlaveFileCallable; import jenkins.SlaveToMasterFileCallable; import jenkins.model.Jenkins; @@ -130,14 +130,14 @@ import jenkins.util.ContextResettingExecutorService; import jenkins.util.SystemProperties; import jenkins.util.VirtualFile; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; -import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload2.core.FileItem; import org.apache.commons.io.input.CountingInputStream; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.types.FileSet; +import org.apache.tools.tar.TarEntry; +import org.apache.tools.tar.TarInputStream; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipFile; import org.jenkinsci.remoting.RoleChecker; @@ -887,15 +887,8 @@ public OutputStream compress(OutputStream out) { }, GZIP { @Override - public InputStream extract(InputStream _in) throws IOException { - HeadBufferingStream in = new HeadBufferingStream(_in, SIDE_BUFFER_SIZE); - try { - return new GZIPInputStream(in, 8192, true); - } catch (IOException e) { - // various people reported "java.io.IOException: Not in GZIP format" here, so diagnose this problem better - in.fillSide(); - throw new IOException(e.getMessage() + "\nstream=" + Util.toHexString(in.getSideBuffer()), e); - } + public InputStream extract(InputStream in) throws IOException { + return new GZIPInputStream(new BufferedInputStream(in)); } @Override @@ -1166,7 +1159,9 @@ public void copyFrom(FilePath src) throws IOException, InterruptedException { public void copyFrom(FileItem file) throws IOException, InterruptedException { if (channel == null) { try { - file.write(new File(remote)); + file.write(Paths.get(remote)); + } catch (UncheckedIOException e) { + throw e.getCause(); } catch (IOException e) { throw e; } catch (Exception e) { @@ -1180,6 +1175,14 @@ public void copyFrom(FileItem file) throws IOException, InterruptedException { } } + /** + * @deprecated use {@link #copyFrom(FileItem)} + */ + @Deprecated + public void copyFrom(org.apache.commons.fileupload.FileItem file) throws IOException, InterruptedException { + copyFrom(file.toFileUpload2FileItem()); + } + /** * Code that gets executed on the machine where the {@link FilePath} is local. * Used to act on {@link FilePath}. @@ -2460,7 +2463,7 @@ private OffsetPipeSecureFileCallable(Pipe p, long offset) { @Override public Void invoke(File f, VirtualChannel channel) throws IOException { try (OutputStream os = p.getOut(); - OutputStream out = new java.util.zip.GZIPOutputStream(os, 8192); + OutputStream out = new GZIPOutputStream(os, 8192); RandomAccessFile raf = new RandomAccessFile(f, "r")) { raf.seek(offset); byte[] buf = new byte[8192]; @@ -3068,14 +3071,13 @@ private static void readFromTar(String name, File baseDir, InputStream in) throw /** * Reads from a tar stream and stores obtained files to the base dir. - * Supports large files > 10 GB since 1.627 when this was migrated to use commons-compress. + * Supports large files > 10 GB since 1.627. */ private static void readFromTar(String name, File baseDir, InputStream in, Charset filenamesEncoding) throws IOException { - // TarInputStream t = new TarInputStream(in); - try (TarArchiveInputStream t = new TarArchiveInputStream(in, filenamesEncoding.name())) { - TarArchiveEntry te; - while ((te = t.getNextTarEntry()) != null) { + try (TarInputStream t = new TarInputStream(in, filenamesEncoding.name())) { + TarEntry te; + while ((te = t.getNextEntry()) != null) { File f = new File(baseDir, te.getName()); if (!f.toPath().normalize().startsWith(baseDir.toPath())) { throw new IOException( diff --git a/core/src/main/java/hudson/PluginFirstClassLoader2.java b/core/src/main/java/hudson/PluginFirstClassLoader2.java index 974939d53acb..30618835c8a7 100644 --- a/core/src/main/java/hudson/PluginFirstClassLoader2.java +++ b/core/src/main/java/hudson/PluginFirstClassLoader2.java @@ -25,8 +25,9 @@ public class PluginFirstClassLoader2 extends URLClassLoader2 { registerAsParallelCapable(); } - public PluginFirstClassLoader2(@NonNull URL[] urls, @NonNull ClassLoader parent) { - super(Objects.requireNonNull(urls), Objects.requireNonNull(parent)); + + public PluginFirstClassLoader2(String name, @NonNull URL[] urls, @NonNull ClassLoader parent) { + super(name, Objects.requireNonNull(urls), Objects.requireNonNull(parent)); } /** diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 1fd5358454a6..5d80c6e623e4 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -139,10 +139,12 @@ import jenkins.util.xml.RestrictiveEntityResolver; import net.sf.json.JSONArray; import net.sf.json.JSONObject; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.fileupload2.core.DiskFileItem; +import org.apache.commons.fileupload2.core.DiskFileItemFactory; +import org.apache.commons.fileupload2.core.FileItem; +import org.apache.commons.fileupload2.core.FileUploadException; +import org.apache.commons.fileupload2.javax.JavaxServletDiskFileUpload; +import org.apache.commons.fileupload2.javax.JavaxServletFileUpload; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; @@ -1099,7 +1101,7 @@ protected void copyBundledPlugin(URL src, String fileName) throws IOException { } /*package*/ static @CheckForNull Manifest parsePluginManifest(URL bundledJpi) { - try (URLClassLoader cl = new URLClassLoader(new URL[]{bundledJpi})) { + try (URLClassLoader cl = new URLClassLoader("Temporary classloader for parsing " + bundledJpi.toString(), new URL[]{bundledJpi}, ClassLoader.getSystemClassLoader())) { InputStream in = null; try { URL res = cl.findResource(PluginWrapper.MANIFEST_FILENAME); @@ -1832,13 +1834,21 @@ static class FileUploadPluginCopier implements PluginCopier { } @Override - public void copy(File target) throws Exception { - fileItem.write(target); + public void copy(File target) throws IOException { + try { + fileItem.write(Util.fileToPath(target)); + } catch (UncheckedIOException e) { + throw e.getCause(); + } } @Override public void cleanup() { - fileItem.delete(); + try { + fileItem.delete(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } } @@ -1873,8 +1883,8 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl String fileName = ""; PluginCopier copier; File tmpDir = Files.createTempDirectory("uploadDir").toFile(); - ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD, tmpDir)); - List items = upload.parseRequest(req); + JavaxServletFileUpload upload = new JavaxServletDiskFileUpload(DiskFileItemFactory.builder().setFile(tmpDir).get()); + List items = upload.parseRequest(req); String string = items.get(1).getString(); if (string != null && !string.isBlank()) { // this is a URL deployment @@ -2337,7 +2347,7 @@ public static final class UberClassLoader extends ClassLoader { } public UberClassLoader(List activePlugins) { - super(PluginManager.class.getClassLoader()); + super("UberClassLoader", PluginManager.class.getClassLoader()); this.activePlugins = activePlugins; } diff --git a/core/src/main/java/hudson/console/AnnotatedLargeText.java b/core/src/main/java/hudson/console/AnnotatedLargeText.java index 8e0a59bee2af..4e0d3b9908af 100644 --- a/core/src/main/java/hudson/console/AnnotatedLargeText.java +++ b/core/src/main/java/hudson/console/AnnotatedLargeText.java @@ -28,8 +28,6 @@ import static java.lang.Math.abs; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; import edu.umd.cs.findbugs.annotations.CheckReturnValue; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.remoting.ObjectInputStreamEx; @@ -45,6 +43,8 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; diff --git a/core/src/main/java/hudson/console/ConsoleNote.java b/core/src/main/java/hudson/console/ConsoleNote.java index 20ded06e9862..633a5ed54743 100644 --- a/core/src/main/java/hudson/console/ConsoleNote.java +++ b/core/src/main/java/hudson/console/ConsoleNote.java @@ -24,8 +24,6 @@ package hudson.console; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.ExtensionPoint; import hudson.Functions; @@ -50,6 +48,8 @@ import java.util.Base64; import java.util.Collection; import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import jenkins.model.Jenkins; import jenkins.security.HMACConfidentialKey; import jenkins.util.JenkinsJVM; diff --git a/core/src/main/java/hudson/console/HyperlinkNote.java b/core/src/main/java/hudson/console/HyperlinkNote.java index 93e2ff40f493..1e582dab2b4a 100644 --- a/core/src/main/java/hudson/console/HyperlinkNote.java +++ b/core/src/main/java/hudson/console/HyperlinkNote.java @@ -88,10 +88,8 @@ static String encodeTo(String url, String text, BiFunction build) { return new BuildWrapper() { @@ -241,11 +259,120 @@ private File getFileParameterFolderUnderBuild(AbstractBuild build) { /** * Default implementation from {@link File}. + * + * @deprecated use {@link FileItemImpl2} */ - public static final class FileItemImpl implements FileItem { - private final File file; + @Deprecated + public static final class FileItemImpl implements org.apache.commons.fileupload.FileItem { + private final FileItem delegate; public FileItemImpl(File file) { + if (file == null) { + throw new NullPointerException("file"); + } + this.delegate = new FileItemImpl2(file); + } + + @Override + public InputStream getInputStream() throws IOException { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getInputStream(); + } + + @Override + public String getContentType() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getContentType(); + } + + @Override + @SuppressFBWarnings(value = "FILE_UPLOAD_FILENAME", justification = "for compatibility") + public String getName() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getName(); + } + + @Override + public boolean isInMemory() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).isInMemory(); + } + + @Override + public long getSize() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getSize(); + } + + @Override + public byte[] get() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).get(); + } + + @Override + public String getString(String encoding) throws UnsupportedEncodingException { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getString(encoding); + } + + @Override + public String getString() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getString(); + } + + @Override + public void write(File to) throws Exception { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).write(to); + } + + @Override + public void delete() { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).delete(); + } + + @Override + public String getFieldName() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getFieldName(); + } + + @Override + public void setFieldName(String name) { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).setFieldName(name); + } + + @Override + public boolean isFormField() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).isFormField(); + } + + @Override + public void setFormField(boolean state) { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).setFormField(state); + } + + @Override + @Deprecated + public OutputStream getOutputStream() throws IOException { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getOutputStream(); + } + + @Override + public org.apache.commons.fileupload.FileItemHeaders getHeaders() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getHeaders(); + } + + @Override + public void setHeaders(org.apache.commons.fileupload.FileItemHeaders headers) { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).setHeaders(headers); + } + + @Override + public FileItem toFileUpload2FileItem() { + return delegate; + } + } + + /** + * Default implementation from {@link File}. + */ + public static final class FileItemImpl2 implements FileItem { + private final File file; + + public FileItemImpl2(File file) { if (file == null) { throw new NullPointerException("file"); } @@ -287,8 +414,12 @@ public byte[] get() { } @Override - public String getString(String encoding) throws UnsupportedEncodingException { - return new String(get(), encoding); + public String getString(Charset toCharset) throws IOException { + try { + return new String(get(), toCharset); + } catch (UncheckedIOException e) { + throw e.getCause(); + } } @Override @@ -297,17 +428,19 @@ public String getString() { } @Override - public void write(File to) throws Exception { - new FilePath(file).copyTo(new FilePath(to)); + public FileItem write(Path to) throws IOException { + try { + new FilePath(file).copyTo(new FilePath(to.toFile())); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return this; } @Override - public void delete() { - try { - Files.deleteIfExists(file.toPath()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + public FileItem delete() throws IOException { + Files.deleteIfExists(Util.fileToPath(file)); + return this; } @Override @@ -316,7 +449,8 @@ public String getFieldName() { } @Override - public void setFieldName(String name) { + public FileItem setFieldName(String name) { + return this; } @Override @@ -325,7 +459,8 @@ public boolean isFormField() { } @Override - public void setFormField(boolean state) { + public FileItem setFormField(boolean state) { + return this; } @Override @@ -336,11 +471,12 @@ public OutputStream getOutputStream() throws IOException { @Override public FileItemHeaders getHeaders() { - return new FileItemHeadersImpl(); + return FileItemFactory.AbstractFileItemBuilder.newFileItemHeaders(); } @Override - public void setHeaders(FileItemHeaders headers) { + public FileItemHeadersProvider setHeaders(FileItemHeaders headers) { + return this; } } } diff --git a/core/src/main/java/hudson/model/ItemGroupMixIn.java b/core/src/main/java/hudson/model/ItemGroupMixIn.java index 2a6b264db9de..faa214dee621 100644 --- a/core/src/main/java/hudson/model/ItemGroupMixIn.java +++ b/core/src/main/java/hudson/model/ItemGroupMixIn.java @@ -290,6 +290,7 @@ public synchronized TopLevelItem createProjectFromXML(String name, InputStream x add(result); + result.onCreatedFromScratch(); ItemListener.fireOnCreated(result); Jenkins.get().rebuildDependencyGraphAsync(); diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index 8c4e3183100a..92f1d374fb08 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -626,11 +626,11 @@ public Collection getOverrides() { } /** - * @deprecated see {@link LazyBuildMixIn#createHistoryWidget()} + * @deprecated Remove any override, history widget is now created via {@link jenkins.widgets.WidgetFactory} implementation. */ @Deprecated(forRemoval = true, since = "2.410") protected HistoryWidget createHistoryWidget() { - return new HistoryWidget(this, getBuilds(), HISTORY_ADAPTER); + throw new IllegalStateException("HistoryWidget is now created via WidgetFactory implementation"); } public static final HistoryWidget.Adapter HISTORY_ADAPTER = new Adapter<>() { diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index a2061b162b0b..324a5eff3cb9 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -33,7 +33,6 @@ import static java.util.logging.Level.SEVERE; import static java.util.logging.Level.WARNING; -import com.jcraft.jzlib.GZIPInputStream; import com.thoughtworks.xstream.XStream; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; @@ -106,6 +105,7 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import jenkins.model.ArtifactManager; diff --git a/core/src/main/java/hudson/model/UsageStatistics.java b/core/src/main/java/hudson/model/UsageStatistics.java index 9fc052cd1009..341f135c52f1 100644 --- a/core/src/main/java/hudson/model/UsageStatistics.java +++ b/core/src/main/java/hudson/model/UsageStatistics.java @@ -26,7 +26,6 @@ import static java.util.concurrent.TimeUnit.DAYS; -import com.jcraft.jzlib.GZIPOutputStream; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; @@ -56,6 +55,7 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.GZIPOutputStream; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; diff --git a/core/src/main/java/hudson/util/CompressedFile.java b/core/src/main/java/hudson/util/CompressedFile.java index d7f42ca7a9ae..cd08f838a250 100644 --- a/core/src/main/java/hudson/util/CompressedFile.java +++ b/core/src/main/java/hudson/util/CompressedFile.java @@ -24,8 +24,6 @@ package hudson.util; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; import hudson.Util; import java.io.File; import java.io.FileNotFoundException; @@ -43,6 +41,8 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; /** * Represents write-once read-many file that can be optionally compressed diff --git a/core/src/main/java/hudson/util/MaskingClassLoader.java b/core/src/main/java/hudson/util/MaskingClassLoader.java index 74bbbe1a9863..dfad754fc170 100644 --- a/core/src/main/java/hudson/util/MaskingClassLoader.java +++ b/core/src/main/java/hudson/util/MaskingClassLoader.java @@ -59,7 +59,7 @@ public MaskingClassLoader(ClassLoader parent, String... masks) { } public MaskingClassLoader(ClassLoader parent, Collection masks) { - super(parent); + super("Masking ClassLoader of " + parent.getName(), parent); this.masksClasses = List.copyOf(masks); /* diff --git a/core/src/main/java/hudson/util/MultipartFormDataParser.java b/core/src/main/java/hudson/util/MultipartFormDataParser.java index 8633a77e1534..f173cff9802b 100644 --- a/core/src/main/java/hudson/util/MultipartFormDataParser.java +++ b/core/src/main/java/hudson/util/MultipartFormDataParser.java @@ -27,17 +27,21 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.fileupload.FileCountLimitExceededException; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileUploadBase; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.fileupload2.core.DiskFileItem; +import org.apache.commons.fileupload2.core.DiskFileItemFactory; +import org.apache.commons.fileupload2.core.FileItem; +import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException; +import org.apache.commons.fileupload2.core.FileUploadException; +import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException; +import org.apache.commons.fileupload2.core.FileUploadSizeException; +import org.apache.commons.fileupload2.javax.JavaxServletDiskFileUpload; +import org.apache.commons.fileupload2.javax.JavaxServletFileUpload; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -52,7 +56,7 @@ public class MultipartFormDataParser implements AutoCloseable { /** * Limits the number of form fields that can be processed in one multipart/form-data request. - * Used to set {@link org.apache.commons.fileupload.servlet.ServletFileUpload#setFileCountMax(long)}. + * Used to set {@link org.apache.commons.fileupload2.javax.JavaxServletFileUpload#setFileCountMax(long)}. * Despite the name, this applies to all form fields, not just actual file attachments. * Set to {@code -1} to disable limits. */ @@ -60,7 +64,7 @@ public class MultipartFormDataParser implements AutoCloseable { /** * Limits the size (in bytes) of individual fields that can be processed in one multipart/form-data request. - * Used to set {@link org.apache.commons.fileupload.servlet.ServletFileUpload#setFileSizeMax(long)}. + * Used to set {@link org.apache.commons.fileupload2.javax.JavaxServletFileUpload#setFileSizeMax(long)}. * Despite the name, this applies to all form fields, not just actual file attachments. * Set to {@code -1} to disable limits. */ @@ -68,7 +72,7 @@ public class MultipartFormDataParser implements AutoCloseable { /** * Limits the total request size (in bytes) that can be processed in one multipart/form-data request. - * Used to set {@link org.apache.commons.fileupload.servlet.ServletFileUpload#setSizeMax(long)}. + * Used to set {@link org.apache.commons.fileupload2.javax.JavaxServletFileUpload#setSizeMax(long)}. * Set to {@code -1} to disable limits. */ private static /* nonfinal for Jenkins script console */ long FILEUPLOAD_MAX_SIZE = Long.getLong(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_SIZE", -1); @@ -82,20 +86,20 @@ public MultipartFormDataParser(HttpServletRequest request, int maxParts, long ma throw new ServletException("Error creating temporary directory", e); } tmpDir.deleteOnExit(); - ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD, tmpDir)); + JavaxServletFileUpload upload = new JavaxServletDiskFileUpload(DiskFileItemFactory.builder().setFile(tmpDir).get()); upload.setFileCountMax(maxParts); upload.setFileSizeMax(maxPartSize); upload.setSizeMax(maxSize); try { for (FileItem fi : upload.parseRequest(request)) byName.put(fi.getFieldName(), fi); - } catch (FileCountLimitExceededException e) { + } catch (FileUploadFileCountLimitException e) { throw new ServletException("File upload field count limit exceeded. Consider setting the Java system property " + MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILES to a value greater than " + FILEUPLOAD_MAX_FILES + ", or to -1 to disable this limit.", e); - } catch (FileUploadBase.FileSizeLimitExceededException e) { + } catch (FileUploadByteCountLimitException e) { throw new ServletException("File upload field size limit exceeded. Consider setting the Java system property " + MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILE_SIZE to a value greater than " + FILEUPLOAD_MAX_FILE_SIZE + ", or to -1 to disable this limit.", e); - } catch (FileUploadBase.SizeLimitExceededException e) { + } catch (FileUploadSizeException e) { throw new ServletException("File upload total size limit exceeded. Consider setting the Java system property " + MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_SIZE to a value greater than " + FILEUPLOAD_MAX_SIZE + ", or to -1 to disable this limit.", e); } catch (FileUploadException e) { @@ -118,17 +122,30 @@ public String get(String key) { return fi.getString(); } - public FileItem getFileItem(String key) { + public FileItem getFileItem2(String key) { return byName.get(key); } + /** + * @deprecated use {@link #getFileItem2(String)} + */ + @Deprecated + public org.apache.commons.fileupload.FileItem getFileItem(String key) { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(getFileItem2(key)); + } + /** * If any file is created on the disk, delete them all. * Even if this method is not called, the resource will be still cleaned up later by GC. */ public void cleanUp() { - for (FileItem item : byName.values()) - item.delete(); + for (FileItem item : byName.values()) { + try { + item.delete(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } /** Alias for {@link #cleanUp}. */ diff --git a/core/src/main/java/hudson/util/io/TarArchiver.java b/core/src/main/java/hudson/util/io/TarArchiver.java index 00b3d1b55d12..597b4ff47339 100644 --- a/core/src/main/java/hudson/util/io/TarArchiver.java +++ b/core/src/main/java/hudson/util/io/TarArchiver.java @@ -36,10 +36,10 @@ import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.attribute.BasicFileAttributes; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; -import org.apache.commons.compress.archivers.tar.TarConstants; -import org.apache.commons.compress.utils.BoundedInputStream; +import org.apache.commons.io.input.BoundedInputStream; +import org.apache.tools.tar.TarConstants; +import org.apache.tools.tar.TarEntry; +import org.apache.tools.tar.TarOutputStream; /** * {@link FileVisitor} that creates a tar archive. @@ -48,17 +48,17 @@ */ final class TarArchiver extends Archiver { private final byte[] buf = new byte[8192]; - private final TarArchiveOutputStream tar; + private final TarOutputStream tar; TarArchiver(OutputStream out, Charset filenamesEncoding) { - tar = new TarArchiveOutputStream(out, filenamesEncoding.name()); - tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR); - tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU); + tar = new TarOutputStream(out, filenamesEncoding.name()); + tar.setBigNumberMode(TarOutputStream.BIGNUMBER_STAR); + tar.setLongFileMode(TarOutputStream.LONGFILE_GNU); } @Override public void visitSymlink(File link, String target, String relativePath) throws IOException { - TarArchiveEntry e = new TarArchiveEntry(relativePath, TarConstants.LF_SYMLINK); + TarEntry e = new TarEntry(relativePath, TarConstants.LF_SYMLINK); try { int mode = IOUtils.mode(link); if (mode != -1) { @@ -70,8 +70,8 @@ public void visitSymlink(File link, String target, String relativePath) throws I e.setLinkName(target); - tar.putArchiveEntry(e); - tar.closeArchiveEntry(); + tar.putNextEntry(e); + tar.closeEntry(); entriesWritten++; } @@ -88,7 +88,7 @@ public void visit(File file, String relativePath) throws IOException { BasicFileAttributes basicFileAttributes = Files.readAttributes(Util.fileToPath(file), BasicFileAttributes.class); if (basicFileAttributes.isDirectory()) relativePath += '/'; - TarArchiveEntry te = new TarArchiveEntry(relativePath); + TarEntry te = new TarEntry(relativePath); int mode = IOUtils.mode(file); if (mode != -1) te.setMode(mode); te.setModTime(basicFileAttributes.lastModifiedTime().toMillis()); @@ -98,7 +98,7 @@ public void visit(File file, String relativePath) throws IOException { size = basicFileAttributes.size(); te.setSize(size); } - tar.putArchiveEntry(te); + tar.putNextEntry(te); try { if (!basicFileAttributes.isDirectory()) { // ensure we don't write more bytes than the declared when we created the entry @@ -118,7 +118,7 @@ public void visit(File file, String relativePath) throws IOException { } } } finally { // always close the entry - tar.closeArchiveEntry(); + tar.closeEntry(); } entriesWritten++; } diff --git a/core/src/main/java/jenkins/agents/CloudSet.java b/core/src/main/java/jenkins/agents/CloudSet.java index e69a3f9add3c..addefceb83ff 100644 --- a/core/src/main/java/jenkins/agents/CloudSet.java +++ b/core/src/main/java/jenkins/agents/CloudSet.java @@ -242,9 +242,13 @@ private void handleNewCloudPage(Descriptor descriptor, String name, Stapl */ @POST public synchronized void doDoCreate(StaplerRequest req, StaplerResponse rsp, - @QueryParameter String type) throws IOException, ServletException, Descriptor.FormException { + @QueryParameter String cloudDescriptorName) throws IOException, ServletException, Descriptor.FormException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); - Cloud cloud = Cloud.all().find(type).newInstance(req, req.getSubmittedForm()); + Descriptor cloudDescriptor = Cloud.all().findByName(cloudDescriptorName); + if (cloudDescriptor == null) { + throw new Failure(String.format("No cloud type ā€˜%sā€™ is known", cloudDescriptorName)); + } + Cloud cloud = cloudDescriptor.newInstance(req, req.getSubmittedForm()); if (!Jenkins.get().clouds.add(cloud)) { LOGGER.log(Level.WARNING, () -> "Creating duplicate cloud name " + cloud.name + ". Plugin " + Jenkins.get().getPluginManager().whichPlugin(cloud.getClass()) + " should be updated to support user provided name."); } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index b08ac6c8941d..7fe515fddf0e 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -4492,7 +4492,7 @@ public void doDoFingerprintCheck(StaplerRequest req, StaplerResponse rsp) throws rsp.sendError(HttpServletResponse.SC_FORBIDDEN, "No crumb found"); } rsp.sendRedirect2(req.getContextPath() + "/fingerprint/" + - Util.getDigestOf(p.getFileItem("name").getInputStream()) + '/'); + Util.getDigestOf(p.getFileItem2("name").getInputStream()) + '/'); } } diff --git a/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java b/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java index 570048c5327f..203ea976bccd 100644 --- a/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java +++ b/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java @@ -39,7 +39,6 @@ import hudson.model.RunMap; import hudson.model.listeners.ItemListener; import hudson.model.queue.SubTask; -import hudson.widgets.BuildHistoryWidget; import hudson.widgets.HistoryWidget; import java.io.File; import java.io.IOException; @@ -297,10 +296,11 @@ public List getEstimatedDurationCandidates() { } /** - * Suitable for {@link Job#createHistoryWidget}. + * @deprecated Remove any code calling this method, history widget is now created via {@link jenkins.widgets.WidgetFactory} implementation. */ + @Deprecated(forRemoval = true, since = "TODO") public final HistoryWidget createHistoryWidget() { - return new BuildHistoryWidget(asJob(), builds, Job.HISTORY_ADAPTER); + throw new IllegalStateException("HistoryWidget is now created via WidgetFactory implementation"); } /** diff --git a/core/src/main/java/jenkins/monitor/JavaVersionRecommendationAdminMonitor.java b/core/src/main/java/jenkins/monitor/JavaVersionRecommendationAdminMonitor.java index 86093a634617..4b3642bb059c 100644 --- a/core/src/main/java/jenkins/monitor/JavaVersionRecommendationAdminMonitor.java +++ b/core/src/main/java/jenkins/monitor/JavaVersionRecommendationAdminMonitor.java @@ -78,7 +78,16 @@ public class JavaVersionRecommendationAdminMonitor extends AdministrativeMonitor static { NavigableMap supportedVersions = new TreeMap<>(); - supportedVersions.put(11, LocalDate.of(2024, 9, 30)); // Temurin: 2024-10-31 + // Adjust Java 11 end of life date for weekly and LTS + if (Jenkins.VERSION.split("[.]").length > 2) { + // LTS will require Java 17 or newer beginning 30 Oct 2024 + // https://groups.google.com/g/jenkinsci-dev/c/gsXAqOQQEPc/m/VT9IBYdmAQAJ + supportedVersions.put(11, LocalDate.of(2024, 10, 30)); // Temurin: 2024-10-31 + } else { + // Weekly will require Java 17 or newer beginning 18 Jun 2024 + // https://groups.google.com/g/jenkinsci-dev/c/gsXAqOQQEPc/m/4fn4Un1iAwAJ + supportedVersions.put(11, LocalDate.of(2024, 6, 18)); // Temurin: 2024-10-31 + } supportedVersions.put(17, LocalDate.of(2026, 3, 31)); // Temurin: 2027-10-31 supportedVersions.put(21, LocalDate.of(2027, 9, 30)); // Temurin: 2029-09-30 SUPPORTED_JAVA_VERSIONS = Collections.unmodifiableNavigableMap(supportedVersions); diff --git a/core/src/main/java/jenkins/util/URLClassLoader2.java b/core/src/main/java/jenkins/util/URLClassLoader2.java index 72611bc49432..3fdedcbeada0 100644 --- a/core/src/main/java/jenkins/util/URLClassLoader2.java +++ b/core/src/main/java/jenkins/util/URLClassLoader2.java @@ -17,14 +17,43 @@ public class URLClassLoader2 extends URLClassLoader implements JenkinsClassLoade registerAsParallelCapable(); } + /** + * @deprecated use {@link URLClassLoader2#URLClassLoader2(String, URL[])} + */ + @Deprecated(since = "TODO") public URLClassLoader2(URL[] urls) { super(urls); } + /** + * @deprecated use {@link URLClassLoader2#URLClassLoader2(String, URL[], ClassLoader)} + */ + @Deprecated(since = "TODO") public URLClassLoader2(URL[] urls, ClassLoader parent) { super(urls, parent); } + /** + * Create a new {@link URLClassLoader2} with the given name and URLS and the {@link #getSystemClassLoader()} as its parent. + * @param name name of this classloader. + * @param urls the list of URLS to find classes in. + * @since TODO + */ + public URLClassLoader2(String name, URL[] urls) { + super(name, urls, getSystemClassLoader()); + } + + /** + * Create a new {@link URLClassLoader2} with the given name, URLS parent. + * @param name name of this classloader. + * @param urls the list of URLS to find classes in. + * @param parent the parent to search for classes before we look in the {@code urls} + * @since TODO + */ + public URLClassLoader2(String name, URL[] urls, ClassLoader parent) { + super(name, urls, parent); + } + @Override public void addURL(URL url) { super.addURL(url); diff --git a/core/src/main/java/org/jenkins/ui/symbol/Symbol.java b/core/src/main/java/org/jenkins/ui/symbol/Symbol.java index 9973acc18da5..9a07556d7c71 100644 --- a/core/src/main/java/org/jenkins/ui/symbol/Symbol.java +++ b/core/src/main/java/org/jenkins/ui/symbol/Symbol.java @@ -8,13 +8,30 @@ import hudson.Util; import java.io.IOException; import java.io.InputStream; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; +import org.apache.tools.ant.filters.StringInputStream; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; /** * Helper class to load symbols from Jenkins core or plugins. @@ -88,12 +105,37 @@ private static String loadSymbol(String namespace, String name) { LOGGER.log(Level.FINE, "Failed to load symbol " + name, e); } } - return markup.replaceAll("().*?()", "$1$2") - .replaceAll(" "The given src for the svg is not a valid xml document"); + return PLACEHOLDER_SVG; + } + + return markup; } @CheckForNull diff --git a/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.jelly b/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.jelly index 5ccac430ecd2..cb20ebf8dfe7 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.jelly +++ b/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.jelly @@ -24,7 +24,7 @@ THE SOFTWARE. -
+
${%PluginCycles}
diff --git a/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly b/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly index 45c4cead0a44..1f5ec400bd3c 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly +++ b/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly @@ -24,7 +24,7 @@ THE SOFTWARE. -
+
${%DeprecatedPlugins}
diff --git a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly index 73182ec483e4..b1f9ba193d49 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly +++ b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly @@ -24,7 +24,7 @@ THE SOFTWARE. -
+
${%RequiredPluginUpdates}
diff --git a/core/src/main/resources/hudson/PluginManager/advanced.jelly b/core/src/main/resources/hudson/PluginManager/advanced.jelly index 8d0e6abcd22e..81948afa6e80 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced.jelly +++ b/core/src/main/resources/hudson/PluginManager/advanced.jelly @@ -37,7 +37,7 @@ THE SOFTWARE. -
+
${%proxyMovedBlurb(rootURL+"/manage/configure")}
diff --git a/core/src/main/resources/hudson/PluginManager/installed.jelly b/core/src/main/resources/hudson/PluginManager/installed.jelly index c22c9ddeeba1..3c3ee43e0193 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.jelly +++ b/core/src/main/resources/hudson/PluginManager/installed.jelly @@ -54,7 +54,7 @@ THE SOFTWARE. data-is-restart-required="${app.updateCenter.isRestartRequiredForCompletion()}" /> -
${%Warning}: ${%requires.restart}
+
${%Warning}: ${%requires.restart}