diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index c781ceef06d7..70391d2e42dd 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -51,7 +51,7 @@ jobs: private_key: ${{ secrets.JENKINS_CHANGELOG_UPDATER_PRIVATE_KEY }} repository: jenkins-infra/jenkins.io - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Publish jenkins.io changelog draft diff --git a/.github/workflows/publish-release-artifact.yml b/.github/workflows/publish-release-artifact.yml index 7333a12285e8..222b2b5d2ffb 100644 --- a/.github/workflows/publish-release-artifact.yml +++ b/.github/workflows/publish-release-artifact.yml @@ -14,7 +14,7 @@ jobs: project-version: ${{ steps.set-version.outputs.project-version }} is-lts: ${{ steps.set-version.outputs.is-lts }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 11 uses: actions/setup-java@v3 with: diff --git a/bom/pom.xml b/bom/pom.xml index 537d08f11a36..ea33fc828776 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -39,7 +39,7 @@ THE SOFTWARE. 9.5 - 2.0.7 + 2.0.9 1802.v9e2750160d01 2.4.21 @@ -64,7 +64,7 @@ THE SOFTWARE. org.springframework.security spring-security-bom - 5.8.5 + 5.8.6 pom import @@ -189,12 +189,12 @@ THE SOFTWARE. org.apache.ant ant - 1.10.13 + 1.10.14 org.apache.commons commons-compress - 1.23.0 + 1.24.0 org.codehaus.groovy diff --git a/core/src/main/java/hudson/init/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java index be3312a1ff8a..95c31b94bf53 100644 --- a/core/src/main/java/hudson/init/InitStrategy.java +++ b/core/src/main/java/hudson/init/InitStrategy.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; @@ -64,7 +65,11 @@ private void listPluginFiles(PluginManager pm, String extension, Collection pluginFiles = new ArrayList<>(); + pluginFiles.addAll(List.of(files)); + pluginFiles.sort(Comparator.comparing(File::getName)); + + all.addAll(pluginFiles); } /** @@ -76,15 +81,16 @@ private void listPluginFiles(PluginManager pm, String extension, Collection r) { String hplProperty = SystemProperties.getString("hudson.bundled.plugins"); if (hplProperty != null) { + List pluginFiles = new ArrayList<>(); for (String hplLocation : hplProperty.split(",")) { File hpl = new File(hplLocation.trim()); if (hpl.exists()) { - r.add(hpl); + pluginFiles.add(hpl); } else if (hpl.getName().contains("*")) { try { new DirScanner.Glob(hpl.getName(), null).scan(hpl.getParentFile(), new FileVisitor() { @Override public void visit(File f, String relativePath) throws IOException { - r.add(f); + pluginFiles.add(f); } }); } catch (IOException x) { @@ -94,6 +100,8 @@ protected void getBundledPluginsFromProperty(final List r) { LOGGER.warning("bundled plugin " + hplLocation + " does not exist"); } } + pluginFiles.sort(Comparator.comparing(File::getName)); + r.addAll(pluginFiles); } } diff --git a/core/src/main/java/hudson/model/Label.java b/core/src/main/java/hudson/model/Label.java index 43604d408aac..b0c32f212a09 100644 --- a/core/src/main/java/hudson/model/Label.java +++ b/core/src/main/java/hudson/model/Label.java @@ -592,10 +592,16 @@ public static Set parse(@CheckForNull String labels) { final Set r = new TreeSet<>(); labels = fixNull(labels); if (labels.length() > 0) { - final QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(labels); - while (tokenizer.hasMoreTokens()) - r.add(Jenkins.get().getLabelAtom(tokenizer.nextToken())); + Jenkins j = Jenkins.get(); + LabelAtom labelAtom = j.tryGetLabelAtom(labels); + if (labelAtom == null) { + final QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(labels); + while (tokenizer.hasMoreTokens()) + r.add(j.getLabelAtom(tokenizer.nextToken())); + } else { + r.add(labelAtom); } + } return r; } diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java index 519d89c10970..2d34e6e90ade 100644 --- a/core/src/main/java/hudson/model/Node.java +++ b/core/src/main/java/hudson/model/Node.java @@ -68,6 +68,7 @@ import net.sf.json.JSONObject; import org.jvnet.localizer.Localizable; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.accmod.restrictions.ProtectedExternally; import org.kohsuke.stapler.BindInterceptor; import org.kohsuke.stapler.Stapler; @@ -298,6 +299,17 @@ public OfflineCause getTemporaryOfflineCause() { public TagCloud getLabelCloud() { return new TagCloud<>(getAssignedLabels(), Label::getTiedJobCount); } + + /** + * @return An immutable set of LabelAtom associated with the current node label. + */ + @NonNull + @Restricted(NoExternalUse.class) + protected Set getLabelAtomSet() { + // Default implementation doesn't cache, since we can't hook on label updates. + return Collections.unmodifiableSet(Label.parse(getLabelString())); + } + /** * Returns the possibly empty set of labels that are assigned to this node, * including the automatic {@link #getSelfLabel() self label}, manually @@ -305,13 +317,13 @@ public TagCloud getLabelCloud() { * {@link LabelFinder} extension point. * * This method has a side effect of updating the hudson-wide set of labels - * and should be called after events that will change that - e.g. a agent + * and should be called after events that will change that - e.g. an agent * connecting. */ @Exported public Set getAssignedLabels() { - Set r = Label.parse(getLabelString()); + Set r = new HashSet<>(getLabelAtomSet()); r.add(getSelfLabel()); r.addAll(getDynamicLabels()); return Collections.unmodifiableSet(r); diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java index 22eb63cd2fc6..fe69bac0af93 100644 --- a/core/src/main/java/hudson/model/Slave.java +++ b/core/src/main/java/hudson/model/Slave.java @@ -36,6 +36,7 @@ import hudson.Util; import hudson.cli.CLI; import hudson.model.Descriptor.FormException; +import hudson.model.labels.LabelAtom; import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.Which; @@ -60,6 +61,7 @@ import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.jar.JarFile; @@ -179,6 +181,7 @@ protected Slave(@NonNull String name, String remoteFS, ComputerLauncher launcher this.name = name; this.remoteFS = remoteFS; this.launcher = launcher; + this.labelAtomSet = Collections.unmodifiableSet(Label.parse(label)); } /** @@ -193,7 +196,7 @@ protected Slave(@NonNull String name, String nodeDescription, String remoteFS, i this.numExecutors = numExecutors; this.mode = mode; this.remoteFS = Util.fixNull(remoteFS).trim(); - this.label = Util.fixNull(labelString).trim(); + this.labelAtomSet = Collections.unmodifiableSet(Label.parse(labelString)); this.launcher = launcher; this.retentionStrategy = retentionStrategy; getAssignedLabels(); // compute labels now @@ -308,6 +311,10 @@ public DescribableList, NodePropertyDescriptor> getNodePropertie @DataBoundSetter public void setNodeProperties(List> properties) throws IOException { + if (nodeProperties == null) { + warnPlugin(); + nodeProperties = new DescribableList<>(this); + } nodeProperties.replaceBy(properties); } @@ -328,11 +335,33 @@ public String getLabelString() { @Override @DataBoundSetter public void setLabelString(String labelString) throws IOException { - this.label = Util.fixNull(labelString).trim(); + _setLabelString(labelString); // Compute labels now. getAssignedLabels(); } + private void _setLabelString(String labelString) { + this.label = Util.fixNull(labelString).trim(); + this.labelAtomSet = Collections.unmodifiableSet(Label.parse(label)); + } + + @CheckForNull // should be @NonNull, but we've seen plugins overriding readResolve() without calling super. + private transient Set labelAtomSet; + + @Override + protected Set getLabelAtomSet() { + if (labelAtomSet == null) { + warnPlugin(); + this.labelAtomSet = Collections.unmodifiableSet(Label.parse(label)); + } + return labelAtomSet; + } + + private void warnPlugin() { + LOGGER.log(Level.WARNING, () -> getClass().getName() + " or one of its superclass overrides readResolve() without calling super implementation." + + "Please file an issue against the plugin implementing it: " + Jenkins.get().getPluginManager().whichPlugin(getClass())); + } + @Override public Callable getClockDifferenceCallable() { return new GetClockDifference1(); @@ -574,6 +603,7 @@ public int hashCode() { protected Object readResolve() { if (nodeProperties == null) nodeProperties = new DescribableList<>(this); + _setLabelString(label); return this; } diff --git a/core/src/main/java/hudson/security/SecurityRealm.java b/core/src/main/java/hudson/security/SecurityRealm.java index ca62a4d3fe0e..a134755fe95e 100644 --- a/core/src/main/java/hudson/security/SecurityRealm.java +++ b/core/src/main/java/hudson/security/SecurityRealm.java @@ -647,15 +647,27 @@ public static String getFrom() { from = request.getParameter("from"); } + // On the 404 error page, use the session attribute it sets + if (request != null && request.getRequestURI().equals(request.getContextPath() + "/404")) { + final HttpSession session = request.getSession(false); + if (session != null) { + final Object attribute = session.getAttribute("from"); + if (attribute != null) { + from = attribute.toString(); + } + } + } + // If entry point was not found, try to deduce it from the request URI - // except pages related to login process + // except pages related to login process and the 404 error page if (from == null && request != null && request.getRequestURI() != null - && !request.getRequestURI().equals("/loginError") - && !request.getRequestURI().equals("/login")) { - - from = request.getRequestURI(); + // The custom login page makes the next two lines obsolete, but safer to have them. + && !request.getRequestURI().equals(request.getContextPath() + "/loginError") + && !request.getRequestURI().equals(request.getContextPath() + "/login") + && !request.getRequestURI().equals(request.getContextPath() + "/404")) { + from = request.getRequestURI(); } // If deduced entry point isn't deduced yet or the content is a blank value diff --git a/core/src/main/java/hudson/util/ProcessTree.java b/core/src/main/java/hudson/util/ProcessTree.java index 8f995738683b..4776d8a02d26 100644 --- a/core/src/main/java/hudson/util/ProcessTree.java +++ b/core/src/main/java/hudson/util/ProcessTree.java @@ -29,7 +29,6 @@ import static java.util.logging.Level.FINER; import static java.util.logging.Level.FINEST; -import com.google.common.primitives.Ints; import com.sun.jna.LastErrorException; import com.sun.jna.Memory; import com.sun.jna.Native; @@ -750,7 +749,7 @@ abstract static class Unix extends Local { @CheckForNull @Override public OSProcess get(@NonNull Process proc) { - return get(Ints.checkedCast(proc.pid())); + return get(Math.toIntExact(proc.pid())); } @Override diff --git a/core/src/main/java/jenkins/ErrorAttributeFilter.java b/core/src/main/java/jenkins/ErrorAttributeFilter.java new file mode 100644 index 000000000000..be67bdab7588 --- /dev/null +++ b/core/src/main/java/jenkins/ErrorAttributeFilter.java @@ -0,0 +1,41 @@ +package jenkins; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.springframework.security.core.Authentication; + +/** + * Record the current user authentication for later impersonation if the response is 404 Not Found. + * + * @see Jenkins#generateNotFoundResponse(org.kohsuke.stapler.StaplerRequest, org.kohsuke.stapler.StaplerResponse) + */ +@Restricted(NoExternalUse.class) +public class ErrorAttributeFilter implements Filter { + + public static final String USER_ATTRIBUTE = "jenkins.ErrorAttributeFilter.user"; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + final Authentication authentication = Jenkins.getAuthentication2(); + servletRequest.setAttribute(USER_ATTRIBUTE, authentication); + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() { + // Otherwise the PCT fails + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Otherwise the PCT fails + } +} diff --git a/core/src/main/java/jenkins/appearance/AppearanceCategory.java b/core/src/main/java/jenkins/appearance/AppearanceCategory.java new file mode 100644 index 000000000000..529c68cdebb5 --- /dev/null +++ b/core/src/main/java/jenkins/appearance/AppearanceCategory.java @@ -0,0 +1,53 @@ +/* + * The MIT License + * + * Copyright (c) 2023, Tim Jacomb + * + * 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 jenkins.appearance; + +import hudson.Extension; +import jenkins.model.GlobalConfigurationCategory; + +/** + *

Global configuration of appearance configuration.

+ * + *

This should be used for Plugins that contribute to the look and feel of Jenkins. + * Theming, header and footer changes, information density are all good examples. + * API plugins for UI components that are used by other plugins also fit into that, e.g. source code display.

+ * + *

Configuration specific to a single plugin that is not related to the overall look and feel of Jenkins may not belong here.

+ * + *

If a plugin has a single global configuration it should separate appearance and general configuration to different classes.

+ * + */ +@Extension +public class AppearanceCategory extends GlobalConfigurationCategory { + @Override + public String getShortDescription() { + return Messages.AppearanceCategory_DisplayName(); + } + + @Override + public String getDisplayName() { + return Messages.AppearanceCategory_Description(); + } +} diff --git a/core/src/main/java/jenkins/appearance/AppearanceGlobalConfiguration.java b/core/src/main/java/jenkins/appearance/AppearanceGlobalConfiguration.java new file mode 100644 index 000000000000..35075425b90f --- /dev/null +++ b/core/src/main/java/jenkins/appearance/AppearanceGlobalConfiguration.java @@ -0,0 +1,110 @@ +/* + * The MIT License + * + * Copyright (c) 2023, Tim Jacomb + * + * 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 jenkins.appearance; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.Functions; +import hudson.model.Descriptor; +import hudson.model.ManagementLink; +import hudson.util.FormApply; +import java.io.IOException; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.servlet.ServletException; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.verb.POST; + +@Extension +public class AppearanceGlobalConfiguration extends ManagementLink { + + private static final Logger LOGGER = Logger.getLogger(AppearanceGlobalConfiguration.class.getName()); + + @Restricted(NoExternalUse.class) + public static final Predicate FILTER = input -> input.getCategory() instanceof AppearanceCategory; + + @Override + public String getIconFileName() { + if (Functions.getSortedDescriptorsForGlobalConfigByDescriptor(FILTER).isEmpty()) { + return null; + } + + return "symbol-brush-outline"; + } + + @Override + public String getDisplayName() { + return Messages.AppearanceGlobalConfiguration_DisplayName(); + } + + @Override + public String getDescription() { + return Messages.AppearanceGlobalConfiguration_Description(); + } + + @Override + public String getUrlName() { + return "appearance"; + } + + @NonNull + @Override + public Category getCategory() { + return Category.CONFIGURATION; + } + + @POST + public synchronized void doConfigure(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException { + boolean result = configure(req, req.getSubmittedForm()); + LOGGER.log(Level.FINE, "appearance saved: " + result); + FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null); + } + + private boolean configure(StaplerRequest req, JSONObject json) throws Descriptor.FormException, IOException { + Jenkins j = Jenkins.get(); + j.checkPermission(Jenkins.MANAGE); + + boolean result = true; + for (Descriptor d : Functions.getSortedDescriptorsForGlobalConfigByDescriptor(FILTER)) { + result &= configureDescriptor(req, json, d); + } + j.save(); + + return result; + } + + private boolean configureDescriptor(StaplerRequest req, JSONObject json, Descriptor d) throws Descriptor.FormException { + String name = d.getJsonSafeClassName(); + JSONObject js = json.has(name) ? json.getJSONObject(name) : new JSONObject(); // if it doesn't have the property, the method returns invalid null object. + json.putAll(js); + return d.configure(req, js); + } +} diff --git a/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java b/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java index eb9685f54ba0..cc1d5214e0ae 100644 --- a/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java +++ b/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java @@ -28,6 +28,7 @@ import hudson.Extension; import hudson.diagnosis.ReverseProxySetupMonitor; import hudson.model.AdministrativeMonitor; +import hudson.model.ManageJenkinsAction; import hudson.model.PageDecorator; import hudson.util.HudsonIsLoading; import hudson.util.HudsonIsRestarting; @@ -53,9 +54,6 @@ public class AdministrativeMonitorsDecorator extends PageDecorator { private final Collection ignoredJenkinsRestOfUrls = new ArrayList<>(); public AdministrativeMonitorsDecorator() { - // redundant - ignoredJenkinsRestOfUrls.add("manage"); - // otherwise this would be added to every internal context menu building request ignoredJenkinsRestOfUrls.add("contextMenu"); @@ -165,6 +163,11 @@ public Collection getMonitorsToDisplay() { return null; } + // Don't show on Manage Jenkins + if (o instanceof ManageJenkinsAction) { + return null; + } + // don't show for some URLs served directly by Jenkins if (o instanceof Jenkins) { String url = a.getRestOfUrl(); diff --git a/core/src/main/java/jenkins/management/Badge.java b/core/src/main/java/jenkins/management/Badge.java index 67528ad56b32..6ee8fb25c493 100644 --- a/core/src/main/java/jenkins/management/Badge.java +++ b/core/src/main/java/jenkins/management/Badge.java @@ -48,6 +48,10 @@ * A badge might display the same information as an {@link AdministrativeMonitor}. While an {@link AdministrativeMonitor} * can be disabled, a badge will always be shown. E.g. the badge of {@link OldDataMonitor.ManagementLinkImpl} always shows the number of old data entries. * + *

+ * A badge can also be used in a {@code } to show information on the right of the link in the sidepanel, + * e.g. to show number of available plugin updates. + * * @since 2.385 */ @ExportedBean diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 9bf94cf9389a..d021e5975ec1 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -262,6 +262,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import jenkins.AgentProtocol; +import jenkins.ErrorAttributeFilter; import jenkins.ExtensionComponentSet; import jenkins.ExtensionRefreshException; import jenkins.InitReactorRunner; @@ -275,6 +276,7 @@ import jenkins.security.ConfidentialStore; import jenkins.security.MasterToSlaveCallable; import jenkins.security.RedactSecretJsonInErrorMessageSanitizer; +import jenkins.security.ResourceDomainConfiguration; import jenkins.security.SecurityListener; import jenkins.security.stapler.DoActionFilter; import jenkins.security.stapler.StaplerDispatchValidator; @@ -930,7 +932,7 @@ protected Jenkins(File root, ServletContext context, PluginManager pluginManager Trigger.timer = new java.util.Timer("Jenkins cron thread"); queue = new Queue(LoadBalancer.CONSISTENT_HASH); - + labelAtomSet = Collections.unmodifiableSet(Label.parse(label)); try { dependencyGraph = DependencyGraph.EMPTY; } catch (InternalError e) { @@ -1088,6 +1090,7 @@ protected Object readResolve() { /* deserializing without a value set means we need to migrate */ nodeRenameMigrationNeeded = true; } + _setLabelString(label); return this; } @@ -1468,6 +1471,14 @@ public boolean hasPeople() { } public Api getApi() { + /* Do not show "REST API" link in footer when on 404 error page */ + final StaplerRequest req = Stapler.getCurrentRequest(); + if (req != null) { + final Object attribute = req.getAttribute("javax.servlet.error.message"); + if (attribute != null) { + return null; + } + } return new Api(this); } @@ -2114,6 +2125,20 @@ public Label getLabel(String expr) { } } + /** + * Returns the label atom of the given name, only if it already exists. + * @return non-null if the label atom already exists. + */ + @Restricted(NoExternalUse.class) + public @Nullable LabelAtom tryGetLabelAtom(@NonNull String name) { + Label label = labels.get(name); + if (label instanceof LabelAtom) { + return (LabelAtom) label; + } + return null; + } + + /** * Gets all the active labels in the current system. */ @@ -2126,6 +2151,14 @@ public Set

See also:{@link #getUnprotectedRootActions}. */ private static final Set ALWAYS_READABLE_PATHS = new HashSet<>(Arrays.asList( + "404", // Web method + "_404", // .jelly + "_404_simple", // .jelly "login", // .jelly "loginError", // .jelly "logout", // #doLogout diff --git a/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java b/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java index 01ac4ca37242..9656db78c05a 100644 --- a/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java +++ b/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java @@ -25,6 +25,8 @@ import org.apache.commons.jelly.XMLOutput; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; +import org.jenkins.ui.symbol.Symbol; +import org.jenkins.ui.symbol.SymbolRequest; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.stapler.HttpResponse; @@ -436,8 +438,18 @@ public MenuItem withStockIcon(String icon) { } public MenuItem withIconClass(String iconClass) { - Icon iconByClass = IconSet.icons.getIconByClassSpec(iconClass + " icon-md"); - this.icon = iconByClass == null ? null : iconByClass.getQualifiedUrl(getResourceUrl()); + if (iconClass != null && iconClass.startsWith("symbol-")) { + this.icon = iconClass; + this.iconXml = Symbol.get(new SymbolRequest.Builder() + .withName(iconClass.split(" ")[0].substring(7)) + .withPluginName(Functions.extractPluginNameFromIconSrc(iconClass)) + .withClasses("icon-md") + .build() + ); + } else { + Icon iconByClass = IconSet.icons.getIconByClassSpec(iconClass + " icon-md"); + this.icon = iconByClass == null ? null : iconByClass.getQualifiedUrl(getResourceUrl()); + } return this; } diff --git a/core/src/main/java/jenkins/model/SimplePageDecorator.java b/core/src/main/java/jenkins/model/SimplePageDecorator.java index 7a0639ee774a..29e08679506c 100644 --- a/core/src/main/java/jenkins/model/SimplePageDecorator.java +++ b/core/src/main/java/jenkins/model/SimplePageDecorator.java @@ -35,6 +35,25 @@ *

* This class provides a few hooks to augment the HTML of the login page. * + *

+ *
simple-branding.jelly
+ *
+ * This view contributes to the branding section, usually located on the left side of the login/register pages. + *
+ *
simple-footer.jelly
+ *
+ * This view contributes to the footer section, located below the login/register form. + *
+ *
simple-head.jelly
+ *
+ * This view contributes to the head section. + *
+ *
simple-header.jelly
+ *
+ * This view contributes to the header section just above the login/register form. + *
+ *
+ * * @since 2.128 */ public class SimplePageDecorator extends Descriptor implements ExtensionPoint, Describable { diff --git a/core/src/main/java/jenkins/security/ResourceDomainFilter.java b/core/src/main/java/jenkins/security/ResourceDomainFilter.java index eda15fe3e7c9..b88fc7045ff6 100644 --- a/core/src/main/java/jenkins/security/ResourceDomainFilter.java +++ b/core/src/main/java/jenkins/security/ResourceDomainFilter.java @@ -25,6 +25,7 @@ package jenkins.security; import hudson.Extension; +import hudson.Functions; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; @@ -49,14 +50,14 @@ public class ResourceDomainFilter implements HttpServletFilter { private static final Logger LOGGER = Logger.getLogger(ResourceDomainFilter.class.getName()); - private static final Set ALLOWED_PATHS = new HashSet<>(Arrays.asList("/" + ResourceDomainRootAction.URL, "/favicon.ico", "/favicon.svg", "/robots.txt")); + private static final Set ALLOWED_PATHS = new HashSet<>(Arrays.asList("/" + ResourceDomainRootAction.URL, "/favicon.ico", "/favicon.svg", "/apple-touch-icon.png", "/mask-icon.svg", "/robots.txt", "/images/rage.svg")); public static final String ERROR_RESPONSE = "Jenkins serves only static files on this domain."; @Override public boolean handle(HttpServletRequest req, HttpServletResponse rsp) throws IOException, ServletException { if (ResourceDomainConfiguration.isResourceRequest(req)) { String path = req.getPathInfo(); - if (!path.startsWith("/" + ResourceDomainRootAction.URL + "/") && !ALLOWED_PATHS.contains(path)) { + if (!path.startsWith("/" + ResourceDomainRootAction.URL + "/") && !ALLOWED_PATHS.contains(path) && !isAllowedPathWithResourcePrefix(path)) { LOGGER.fine(() -> "Rejecting request to " + req.getRequestURL() + " from " + req.getRemoteAddr() + " on resource domain"); rsp.sendError(404, ERROR_RESPONSE); return true; @@ -66,4 +67,7 @@ public boolean handle(HttpServletRequest req, HttpServletResponse rsp) throws IO return false; } + private static boolean isAllowedPathWithResourcePrefix(String path) { + return path.startsWith(Functions.getResourcePath()) && ALLOWED_PATHS.contains(path.substring(Functions.getResourcePath().length())); + } } diff --git a/core/src/main/java/jenkins/security/ResourceDomainRootAction.java b/core/src/main/java/jenkins/security/ResourceDomainRootAction.java index a910800ea859..4de14ef1cfcb 100644 --- a/core/src/main/java/jenkins/security/ResourceDomainRootAction.java +++ b/core/src/main/java/jenkins/security/ResourceDomainRootAction.java @@ -74,6 +74,8 @@ @Restricted(NoExternalUse.class) public class ResourceDomainRootAction implements UnprotectedRootAction { + private static final String RESOURCE_DOMAIN_ROOT_ACTION_ERROR = "jenkins.security.ResourceDomainRootAction.error"; + private static final Logger LOGGER = Logger.getLogger(ResourceDomainRootAction.class.getName()); public static final String URL = "static-files"; @@ -104,12 +106,14 @@ public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException if (ResourceDomainConfiguration.isResourceRequest(req)) { rsp.sendError(404, ResourceDomainFilter.ERROR_RESPONSE); } else { + req.setAttribute(RESOURCE_DOMAIN_ROOT_ACTION_ERROR, true); rsp.sendError(404, "Cannot handle requests to this URL unless on Jenkins resource URL."); } } public Object getDynamic(String id, StaplerRequest req, StaplerResponse rsp) throws Exception { if (!ResourceDomainConfiguration.isResourceRequest(req)) { + req.setAttribute(RESOURCE_DOMAIN_ROOT_ACTION_ERROR, true); rsp.sendError(404, "Cannot handle requests to this URL unless on Jenkins resource URL."); return null; } diff --git a/core/src/main/java/jenkins/telemetry/impl/SecurityConfiguration.java b/core/src/main/java/jenkins/telemetry/impl/SecurityConfiguration.java new file mode 100644 index 000000000000..a59a34bcbf4c --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/SecurityConfiguration.java @@ -0,0 +1,79 @@ +/* + * The MIT License + * + * Copyright (c) 2023, CloudBees, Inc. + * + * 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 jenkins.telemetry.impl; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.TcpSlaveAgentListener; +import hudson.security.csrf.CrumbIssuer; +import java.time.LocalDate; +import jenkins.model.Jenkins; +import jenkins.security.apitoken.ApiTokenPropertyConfiguration; +import jenkins.telemetry.Telemetry; +import net.sf.json.JSONObject; + +@Extension +public class SecurityConfiguration extends Telemetry { + @NonNull + @Override + public String getDisplayName() { + return "Basic information about security-related settings"; + } + + @NonNull + @Override + public LocalDate getStart() { + return LocalDate.of(2023, 8, 1); + } + + @NonNull + @Override + public LocalDate getEnd() { + return LocalDate.of(2023, 12, 1); + } + + @Override + public JSONObject createContent() { + final Jenkins j = Jenkins.get(); + final JSONObject o = new JSONObject(); + o.put("components", buildComponentInformation()); + + o.put("authorizationStrategy", j.getAuthorizationStrategy().getClass().getName()); + o.put("securityRealm", j.getSecurityRealm().getClass().getName()); + final CrumbIssuer crumbIssuer = j.getCrumbIssuer(); + o.put("crumbIssuer", crumbIssuer == null ? null : crumbIssuer.getClass().getName()); + o.put("markupFormatter", j.getMarkupFormatter().getClass().getName()); + final TcpSlaveAgentListener tcpSlaveAgentListener = j.getTcpSlaveAgentListener(); + o.put("inboundAgentListener", tcpSlaveAgentListener == null ? null : tcpSlaveAgentListener.configuredPort != -1); + + final ApiTokenPropertyConfiguration apiTokenPropertyConfiguration = ExtensionList.lookupSingleton(ApiTokenPropertyConfiguration.class); + o.put("apiTokenCreationOfLegacyTokenEnabled", apiTokenPropertyConfiguration.isCreationOfLegacyTokenEnabled()); + o.put("apiTokenTokenGenerationOnCreationEnabled", apiTokenPropertyConfiguration.isTokenGenerationOnCreationEnabled()); + o.put("apiTokenUsageStatisticsEnabled", apiTokenPropertyConfiguration.isUsageStatisticsEnabled()); + + return o; + } +} diff --git a/core/src/main/resources/hudson/PluginManager/available.jelly b/core/src/main/resources/hudson/PluginManager/available.jelly index 212deb493567..b0fa8f8fe5a3 100644 --- a/core/src/main/resources/hudson/PluginManager/available.jelly +++ b/core/src/main/resources/hudson/PluginManager/available.jelly @@ -32,8 +32,6 @@ THE SOFTWARE. - -
- - diff --git a/core/src/main/resources/hudson/PluginManager/sidepanel.jelly b/core/src/main/resources/hudson/PluginManager/sidepanel.jelly index 6c45382c9e03..21755b32072d 100644 --- a/core/src/main/resources/hudson/PluginManager/sidepanel.jelly +++ b/core/src/main/resources/hudson/PluginManager/sidepanel.jelly @@ -28,6 +28,7 @@ THE SOFTWARE. + diff --git a/core/src/main/resources/hudson/PluginManager/updates.jelly b/core/src/main/resources/hudson/PluginManager/updates.jelly index 6bf1982d89fe..25402731273b 100644 --- a/core/src/main/resources/hudson/PluginManager/updates.jelly +++ b/core/src/main/resources/hudson/PluginManager/updates.jelly @@ -35,8 +35,6 @@ THE SOFTWARE. - -
- + - + + +
${%description}
+ - + diff --git a/core/src/main/resources/hudson/model/ComputerSet/configure.properties b/core/src/main/resources/hudson/model/ComputerSet/configure.properties new file mode 100644 index 000000000000..e2e74cba5ece --- /dev/null +++ b/core/src/main/resources/hudson/model/ComputerSet/configure.properties @@ -0,0 +1 @@ +description=Jenkins monitors each attached node for disk space, free temp space, free swap, clock time/sync, and response time. Nodes will be taken offline if any of these values go outside of the configured threshold. diff --git a/core/src/main/resources/hudson/model/ComputerSet/index.jelly b/core/src/main/resources/hudson/model/ComputerSet/index.jelly index 6c295c2e933b..ff8a8e288075 100644 --- a/core/src/main/resources/hudson/model/ComputerSet/index.jelly +++ b/core/src/main/resources/hudson/model/ComputerSet/index.jelly @@ -32,13 +32,19 @@ THE SOFTWARE. - - - - - ${%New Node} - - + + + + + ${%New Node} + + + + + + ${%Node Monitoring} + +

- - - - - - +

diff --git a/core/src/main/resources/hudson/model/UpdateCenter/update-center.js b/core/src/main/resources/hudson/model/UpdateCenter/update-center.js index aa426cb9adfa..90441d4f005f 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/update-center.js +++ b/core/src/main/resources/hudson/model/UpdateCenter/update-center.js @@ -3,7 +3,7 @@ Behaviour.specify( "scheduleRestartCheckbox", 0, function (el) { - el.addEventListener("click", function () { + el.addEventListener("change", function () { var form = document.getElementById("scheduleRestart"); form.action = el.checked ? "safeRestart" : "cancelRestart"; crumb.appendToForm(form); diff --git a/core/src/main/resources/hudson/model/View/newJobButtonBar_tr.properties b/core/src/main/resources/hudson/model/View/newJobButtonBar_tr.properties new file mode 100644 index 000000000000..2d84d0b0982c --- /dev/null +++ b/core/src/main/resources/hudson/model/View/newJobButtonBar_tr.properties @@ -0,0 +1 @@ +OK=Tamam diff --git a/core/src/main/resources/hudson/model/View/newJob_tr.properties b/core/src/main/resources/hudson/model/View/newJob_tr.properties index 340c20aed4c3..198d62e95937 100644 --- a/core/src/main/resources/hudson/model/View/newJob_tr.properties +++ b/core/src/main/resources/hudson/model/View/newJob_tr.properties @@ -20,8 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -#JobName=İş adı -#CopyExisting=Varolan bir işi kopyala -#Copy\ from=Kopyalanacak iş -CopyExisting=Varolandan kopyala {0} -JobName=isim +NewJob=Yeni {0} +ItemName.help=Zorunlu alan +ItemName.label=Öğe adını girin +ItemName.validation.required=Bu alan boş olamaz, lütfen geçerli bir ad girin +CopyOption.placeholder=Yazmaya başlayınca otomatik tamamlanır +CopyOption.description=Mevcut öğelerden birini kopyalamak istiyorsanız bu seçeneği kullanabilirsiniz: +CopyOption.label=Kopyalanacak öğe diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly index 1e550e3a1497..52e13b720fc7 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly @@ -48,19 +48,16 @@ THE SOFTWARE. - +