Stored XSS vulnerability in Jenkins button labels
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
Details
Jenkins 2.274 and earlier, LTS 2.263.1 and earlier does not escape button labels in the Jenkins UI. This results in a cross-site scripting vulnerability exploitable by attackers with the ability to control button labels. An example of buttons with a user-controlled label are the buttons of the Pipeline `input` step. Jenkins 2.275, LTS 2.263.2 escapes button labels in the Jenkins UI.
The fix
[SECURITY-2035][SECURITY-2171]
core/src/main/resources/hudson/tools/ToolInstallation/global.jelly+1 −0
@@ -26,6 +26,7 @@ THE SOFTWARE.<f:section title="${descriptor.displayName}"><d:taglib uri="local"><d:tag name="section">+<j:set var="escapeEntryTitleAndDescription" value="true"/><f:entry title="${%title(descriptor.displayName)}" description="${%description(descriptor.displayName)}"><f:repeatable name="tool" var="instance" items="${descriptor.installations}"add="${%label.add(descriptor.displayName)}" header="${descriptor.displayName}" enableTopButton="true"
core/src/main/resources/lib/form/hetero-list/hetero-list.js+6 −1
@@ -26,7 +26,12 @@ Behaviour.specify("DIV.hetero-list-container", 'hetero-list', -100, function(e)var name = n.getAttribute("name");var tooltip = n.getAttribute("tooltip");var descriptorId = n.getAttribute("descriptorId");-menu.options[i] = new Option(n.getAttribute("title"),""+i);+// YUI Menu interprets this <option> text node as HTML, so let's escape it again!+var title = n.getAttribute("title");+if (title) {+title = title.escapeHTML();+}+menu.options[i] = new Option(title,""+i);templates.push({html:n.innerHTML, name:name, tooltip:tooltip,descriptorId:descriptorId});i++;});
test/src/test/java/hudson/model/ViewSEC2171Test.java+215 −0
@@ -0,0 +1,215 @@+/*+* The MIT License+*+* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, 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.model;++import com.gargoylesoftware.htmlunit.html.HtmlPage;+import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.Issue;+import org.jvnet.hudson.test.JenkinsRule;+import org.jvnet.hudson.test.TestExtension;++import javax.annotation.CheckForNull;++import java.util.Arrays;+import java.util.List;++import static org.hamcrest.CoreMatchers.containsString;+import static org.hamcrest.CoreMatchers.hasItem;+import static org.hamcrest.CoreMatchers.instanceOf;+import static org.hamcrest.CoreMatchers.not;+import static org.hamcrest.CoreMatchers.nullValue;+import static org.hamcrest.core.Is.is;+import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertThat;++/**+* @author Kohsuke Kawaguchi+*/+//TODO to be merged back into ViewTest after the security release+public class ViewSEC2171Test {++@Rule public JenkinsRule j = new JenkinsRule();++@Test+@Issue("SECURITY-2171")+public void newJob_xssPreventedInId() throws Exception {+CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);+customizableTLID.customId = "regularclass\" onclick=alert(123) other=\"";+customizableTLID.customDisplayName = "DN-xss-id";++JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("view/all/newJob");++Object result = page.executeJavaScript("Array.from(document.querySelectorAll('.label')).filter(el => el.innerText.indexOf('" + customizableTLID.customDisplayName + "') !== -1)[0].parentElement.parentElement").getJavaScriptResult();+assertThat(result, instanceOf(HTMLElement.class));+HTMLElement resultElement = (HTMLElement) result;+assertThat(resultElement.getAttribute("onclick", null), nullValue());+}++@Test+@Issue("SECURITY-2171")+public void newJob_xssPreventedInDisplayName() throws Exception {+CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);+customizableTLID.customId = "xss-dn";+customizableTLID.customDisplayName = "DN <img src=x onerror=console.warn(123)>";++JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("view/all/newJob");++Object result = page.executeJavaScript("document.querySelector('.xss-dn .label').innerHTML").getJavaScriptResult();+assertThat(result, instanceOf(String.class));+String resultString = (String) result;+assertThat(resultString, not(containsString("<")));+}++@Test+public void newJob_descriptionSupportsHtml() throws Exception {+CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);+customizableTLID.customId = "html-desc";+customizableTLID.customDescription = "Super <strong>looong</strong> description";++JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("view/all/newJob");++Object result = page.executeJavaScript("document.querySelector('.html-desc .desc strong')").getJavaScriptResult();+assertThat(result, instanceOf(HTMLElement.class));+assertThat(((HTMLElement) result).getTagName(), is("STRONG"));+}++@Test+@Issue("SECURITY-2171")+public void newJob_xssPreventedInGetIconFilePathPattern() throws Exception {+CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);+customizableTLID.customId = "xss-ifpp";+customizableTLID.customIconClassName = null;+customizableTLID.customIconFilePathPattern = "\"><img src=x onerror=\"alert(123)";++JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("view/all/newJob");++Object resultIconChildrenCount = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .icon').children.length").getJavaScriptResult();+assertThat(resultIconChildrenCount, instanceOf(Integer.class));+int resultIconChildrenCountInt = (int) resultIconChildrenCount;+assertEquals(1, resultIconChildrenCountInt);++Object resultImgAttributesCount = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .icon img').attributes.length").getJavaScriptResult();+assertThat(resultImgAttributesCount, instanceOf(Integer.class));+int resultImgAttributesCountInt = (int) resultImgAttributesCount;+assertEquals(1, resultImgAttributesCountInt);+}++@Test+public void newJob_iconClassName() throws Exception {+JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("view/all/newJob");++Object resultClassNames = page.executeJavaScript("document.querySelector('.hudson_model_FreeStyleProject .icon img').className").getJavaScriptResult();+assertThat(resultClassNames, instanceOf(String.class));+String resultClassNamesString = (String) resultClassNames;+List<String> resultClassNamesList = Arrays.asList(resultClassNamesString.split(" "));+assertThat(resultClassNamesList, hasItem("icon-xlg"));+assertThat(resultClassNamesList, hasItem("icon-freestyle-project"));++Object resultSrc = page.executeJavaScript("document.querySelector('.hudson_model_FreeStyleProject .icon img').src").getJavaScriptResult();+assertThat(resultSrc, instanceOf(String.class));+String resultSrcString = (String) resultSrc;+assertThat(resultSrcString, containsString("48x48"));+assertThat(resultSrcString, containsString("freestyleproject.png"));+}++@Test+public void newJob_twoLetterIcon() throws Exception {+CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class);+customizableTLID.customId = "two-letters-desc";+customizableTLID.customDisplayName = "Two words";+customizableTLID.customIconClassName = null;+customizableTLID.customIconFilePathPattern = null;++JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("view/all/newJob");++Object result = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .default-icon')").getJavaScriptResult();+assertThat(result, instanceOf(HTMLElement.class));+HTMLElement resultHtml = (HTMLElement) result;+HTMLElement spanA = (HTMLElement) resultHtml.getFirstElementChild();+HTMLElement spanB = (HTMLElement) resultHtml.getLastElementChild();+assertThat(spanA.getClassName_js(), is("a"));+assertThat(spanA.getInnerText(), is("T"));+assertThat(spanB.getClassName_js(), is("b"));+assertThat(spanB.getInnerText(), is("w"));+}++@TestExtension+public static class CustomizableTLID extends TopLevelItemDescriptor {++public String customId = "ID-not-yet-defined";+public String customDisplayName = "DisplayName-not-yet-defined";+public String customDescription = "Description-not-yet-defined";+public String customIconFilePathPattern = "IconFilePathPattern-not-yet-defined";+public String customIconClassName = "IconClassName-not-yet-defined";++public CustomizableTLID() {+super(FreeStyleProject.class);+}++@Override+public String getId() {+return customId;+}++@Override+public String getDisplayName() {+return customDisplayName;+}++@Override+public String getDescription() {+return customDescription;+}++@Override+public @CheckForNull String getIconFilePathPattern() {+return customIconFilePathPattern;+}++@Override+public String getIconClassName() {+return customIconClassName;+}++@Override+public TopLevelItem newInstance(ItemGroup parent, String name) {+throw new UnsupportedOperationException();+}++}+}
test/src/test/java/lib/form/HeteroListSEC2035Test.java+283 −0
@@ -0,0 +1,283 @@+/*+* The MIT License+*+* Copyright (c) 2020, 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 lib.form;++import com.gargoylesoftware.htmlunit.html.DomElement;+import com.gargoylesoftware.htmlunit.html.HtmlButton;+import com.gargoylesoftware.htmlunit.html.HtmlElementUtil;+import com.gargoylesoftware.htmlunit.html.HtmlPage;+import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLAnchorElement;+import hudson.ExtensionList;+import hudson.model.Describable;+import hudson.model.Descriptor;+import hudson.model.UnprotectedRootAction;+import hudson.tools.ToolDescriptor;+import hudson.tools.ToolInstallation;+import hudson.tools.ToolInstaller;+import hudson.tools.ToolProperty;+import hudson.util.FormValidation;+import net.sourceforge.htmlunit.corejs.javascript.NativeArray;+import org.jenkinsci.Symbol;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.Issue;+import org.jvnet.hudson.test.JenkinsRule;+import org.jvnet.hudson.test.TestExtension;++import javax.annotation.CheckForNull;+import java.io.File;+import java.util.Arrays;+import java.util.Collections;+import java.util.List;+import java.util.Optional;++import static org.hamcrest.CoreMatchers.containsString;+import static org.hamcrest.CoreMatchers.instanceOf;+import static org.hamcrest.CoreMatchers.not;+import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertThat;+import static org.junit.Assert.assertTrue;++//TODO rename to HeteroListTest after security release+public class HeteroListSEC2035Test {+@Rule+public JenkinsRule j = new JenkinsRule();++@Test+@Issue("SECURITY-2035")+public void xssPrevented_heteroList_usingDescriptorDisplayName() throws Exception {+JenkinsRule.WebClient wc = j.createWebClient();++RootActionImpl rootAction = ExtensionList.lookupSingleton(RootActionImpl.class);+TestItemDescribable.DynamicDisplayNameDescriptor dynamic = ExtensionList.lookupSingleton(TestItemDescribable.DynamicDisplayNameDescriptor.class);+rootAction.descriptorList = Arrays.asList(dynamic);++dynamic.displayName = "Display<strong>Name</strong>";++HtmlPage page = wc.goTo("root");++page.executeJavaScript("document.querySelector('.hetero-list-add').click();");+Object result = page.executeJavaScript("document.querySelector('.yuimenuitem a')").getJavaScriptResult();+assertThat(result, instanceOf(HTMLAnchorElement.class));+HTMLAnchorElement menuItem = (HTMLAnchorElement) result;+String menuItemContent = menuItem.getInnerHTML();+assertThat(menuItemContent, not(containsString("<")));+}++// correspond to the hardening of escapeEntryTitleAndDescription+@Test+@Issue("SECURITY-2035")+public void xssPrevented_usingToolInstallation_withJustDisplayName() throws Exception {+JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("configureTools/");++// check the displayName+Object resultDN = page.executeJavaScript(+"var settingFields = document.querySelectorAll('.setting-name');" ++"var children = Array.from(settingFields).filter(b => b.textContent.indexOf('XSS:') !== -1)[0].children;" ++"Array.from(children).filter(c => c.tagName === 'IMG')"+).getJavaScriptResult();+assertThat(resultDN, instanceOf(NativeArray.class));+NativeArray resultDNNA = (NativeArray) resultDN;+assertEquals(0, resultDNNA.size());++// check the description+Object resultDesc = page.executeJavaScript(+"var settingFields = document.querySelectorAll('.setting-description');" ++"var children = Array.from(settingFields).filter(b => b.textContent.indexOf('XSS:') !== -1)[0].children;" ++"Array.from(children).filter(c => c.tagName === 'IMG')"+).getJavaScriptResult();+assertThat(resultDesc, instanceOf(NativeArray.class));+NativeArray resultDescNA = (NativeArray) resultDesc;+assertEquals(0, resultDescNA.size());+}++@Test+@Issue("SECURITY-2035")+public void xssPrevented_usingToolInstallation_repeatableAddExisting() throws Exception {+JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("configureTools/");++// the existing add button can already trigger an XSS+Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('Add XSS') !== -1)[0].innerHTML").getJavaScriptResult();+assertThat(result, instanceOf(String.class));+String resultString = (String) result;+assertThat(resultString, not(containsString("<")));+}++// only possible after a partial fix+@Test+public void xssPrevented_usingToolInstallation_repeatableAddAfterClick() throws Exception {+JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("configureTools/");++Optional<DomElement> addXssButtonRawOptional = page.getElementsByTagName("button").stream().filter(e -> e.getTextContent().contains("Add XSS")).findFirst();+assertTrue(addXssButtonRawOptional.isPresent());+assertThat(addXssButtonRawOptional.get(), instanceOf(HtmlButton.class));++HtmlButton addXssButton = (HtmlButton) addXssButtonRawOptional.get();+HtmlElementUtil.click(addXssButton);++// checking only the newly created button (at the top of the panel), hence the [0]+Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('Add XSS') !== -1)[0].innerHTML").getJavaScriptResult();+assertThat(result, instanceOf(String.class));+String resultString = (String) result;+assertThat(resultString, not(containsString("<")));+}++@Test+@Issue("SECURITY-2035")+public void xssPrevented_usingToolInstallation_repeatableAddWithExistingUsingInstallationsButton() throws Exception {+Xss.DescriptorImpl xssDescriptor = ExtensionList.lookupSingleton(Xss.DescriptorImpl.class);+xssDescriptor.installations = new Xss[]{ new Xss("name1", "home1", null) };++JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("configureTools/");++// XSS: [img] installations...+Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('XSS:') !== -1)[0].innerHTML").getJavaScriptResult();+assertThat(result, instanceOf(String.class));+String resultString = (String) result;+assertThat(resultString, not(containsString("<")));+}++@Test+@Issue("SECURITY-2035")+public void xssPrevented_usingToolInstallation_repeatableAddWithExistingAfterOpening() throws Exception {+Xss.DescriptorImpl xssDescriptor = ExtensionList.lookupSingleton(Xss.DescriptorImpl.class);+xssDescriptor.installations = new Xss[]{ new Xss("name1", "home1", null) };++JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("configureTools/");++// Passing the installation button+page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('XSS:') !== -1)[0].click()");++// Looking for all the buttons displayed, at this point there is one Add, one Delete and the second Add.+// Both add are generated through different code.+// While keeping away the installations... advanced button as it's covered in its own test+Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('XSS') !== -1 && b.textContent.indexOf('...') === -1).map(b => b.innerHTML)").getJavaScriptResult();+assertThat(result, instanceOf(List.class));+List resultArray = (List) result;+for (int i = 0; i < resultArray.size(); i++) {+assertThat((String) resultArray.get(i), not(containsString("<")));+}++// "delete" then "add" makes us coming back in scenario covered by xssUsingToolInstallationRepeatableAdd+}++@Test+@Issue("SECURITY-2035")+public void xssPrevented_usingToolInstallation_repeatableDelete() throws Exception {+JenkinsRule.WebClient wc = j.createWebClient();++HtmlPage page = wc.goTo("configureTools/");++// we could also re-use the same method as used in xssUsingToolInstallationRepeatableAdd+page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('Add XSS') !== -1)[0].click()");++Object result = page.executeJavaScript("Array.from(document.querySelectorAll('button')).filter(b => b.textContent.indexOf('Delete XSS') !== -1)[0].innerHTML").getJavaScriptResult();+assertThat(result, instanceOf(String.class));+String resultString = (String) result;+assertThat(resultString, not(containsString("<")));+}++public static class TestItemDescribable implements Describable<TestItemDescribable> {+public Descriptor<TestItemDescribable> getDescriptor() {+return ExtensionList.lookupSingleton(DynamicDisplayNameDescriptor.class);+}++@TestExtension+public static class DynamicDisplayNameDescriptor extends Descriptor<TestItemDescribable> {+public String displayName = "NotYetDefined";++@Override+public String getDisplayName() {+return displayName;+}+}+}++@TestExtension+public static class RootActionImpl implements UnprotectedRootAction {+public List<Descriptor<?>> descriptorList;++@CheckForNull+public String getIconFileName() {+return null;+}++@CheckForNull+public String getDisplayName() {+return null;+}++@CheckForNull+public String getUrlName() {+return "root";+}+}++public static final class Xss extends ToolInstallation {++public Xss(String name, String home, List<? extends ToolProperty<?>> properties) {+super(name, home, properties);+}++@TestExtension+@Symbol("tool-xss")+public static class DescriptorImpl extends ToolDescriptor<Xss> {+private Xss[] installations = new Xss[0];++public String getDisplayName() {+return "XSS: <img src=x onerror=console.warn('" + getClass().getName() +"') />";+}++public @Override Xss[] getInstallations() {+return installations;+}++public @Override void setInstallations(Xss... xsses) {+this.installations = xsses;+}++@Override+public List<? extends ToolInstaller> getDefaultInstallers() {+return Collections.emptyList();+}++/**+* Checks if the JAVA_HOME is a valid JAVA_HOME path.+*/+@Override protected FormValidation checkHomeDirectory(File value) {+return FormValidation.ok();+}+}+}+}
test/src/test/resources/lib/form/HeteroListSEC2035Test/RootActionImpl/index.jelly+8 −0
@@ -0,0 +1,8 @@+<?jelly escape-by-default='true'?>+<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form">+<l:layout title="Testing the effect of validateButton">+<l:main-panel>+<f:hetero-list name="no-care" descriptors="${it.descriptorList}"/>+</l:main-panel>+</l:layout>+</j:jelly>
war/src/main/js/add-item.js+63 −22
@@ -160,9 +160,30 @@ $.when(getItems()).done(function(data) {}function drawItem(elem) {-var desc = checkForLink(elem.description);-var $item = $(['<li tabindex="0" role="radio" aria-checked="false" class="', cleanClassName(elem.class), '"><label><input type="radio" name="mode" value="',-elem.class ,'"/> <span class="label">', elem.displayName, '</span></label></li>'].join('')).append(['<div class="desc">', desc, '</div>'].join('')).append(drawIcon(elem));+var item = document.createElement('li');+item.tabIndex = 0;+item.className = cleanClassName(elem.class);+item.setAttribute('role', 'radio');+item.setAttribute('aria-checked', 'false');++var label = item.appendChild(document.createElement('label'));++var radio = label.appendChild(document.createElement('input'));+radio.type = 'radio';+radio.name = 'mode';+radio.value = elem.class;++var displayName = label.appendChild(document.createElement('span'));+displayName.className = 'label';++displayName.appendChild(document.createTextNode(elem.displayName));++var desc = item.appendChild(document.createElement('div'));+desc.className = 'desc';+desc.innerHTML = checkForLink(elem.description);++var iconDiv = drawIcon(elem);+item.appendChild(iconDiv);function select(e) {e.preventDefault();@@ -183,32 +204,41 @@ $.when(getItems()).done(function(data) {}}-$item.click(select);--$item.keypress(function(e) {-switch (e.which) {-case 13:-case 32:-$(this).trigger('click');-e.stopPropagation();-break;+item.addEventListener('click', select);+item.addEventListener('keydown', function (evt) {+if (evt.code === 'Space' || evt.code === 'Enter') {+this.click();+evt.stopPropagation();}});-return $item;+return item;}function drawIcon(elem) {-var $icn;+var iconDiv = document.createElement('div');if (elem.iconClassName && elem.iconQualifiedUrl) {-$icn = $('<div class="icon">');-$(['<img class="', elem.iconClassName, ' icon-xlg" src="', elem.iconQualifiedUrl, '">'].join('')).appendTo($icn);+iconDiv.className = 'icon';++var img1 = document.createElement('img');+img1.className = elem.iconClassName + ' icon-xlg';+img1.src = elem.iconQualifiedUrl;+iconDiv.appendChild(img1);++// Example for Freestyle project+// <div class="icon"><img class="icon-freestyle-project icon-xlg" src="/jenkins/static/108b2346/images/48x48/freestyleproject.png"></div>} else if (elem.iconFilePathPattern) {-$icn = $('<div class="icon">');+iconDiv.className = 'icon';+var iconFilePath = jRoot + '/' + elem.iconFilePathPattern.replace(":size", "48x48");-$(['<img src="', iconFilePath, '">'].join('')).appendTo($icn);++var img2 = document.createElement('img');+img2.src = iconFilePath;+iconDiv.appendChild(img2);++// Example for Maven project+// <div class="icon"><img src="/jenkins/plugin/maven-plugin/images/48x48/mavenmoduleset.png"></div>} else {-$icn = $('<div class="default-icon">');var colors = ['c-49728B','c-335061','c-D33833','c-6D6B6D', 'c-6699CC'];var desc = elem.description || '';var name = elem.displayName;@@ -216,10 +246,21 @@ $.when(getItems()).done(function(data) {var aName = name.split(' ');var a = name.substring(0,1);var b = ((aName.length === 1) ? name.substring(1,2) : aName[1].substring(0,1));-$(['<span class="a">',a,'</span><span class="b">',b,'</span>'].join('')).appendTo($icn);-$icn.addClass(colorClass);++var spanFakeImgA = document.createElement('span');+spanFakeImgA.className = "a";+spanFakeImgA.innerText = a;+iconDiv.appendChild(spanFakeImgA);+var spanFakeImgB = document.createElement('span');+spanFakeImgB.className = "b";+spanFakeImgB.innerText = b;+iconDiv.appendChild(spanFakeImgB);+iconDiv.className = colorClass + ' default-icon';++// Example for MockFolder+// <div class="default-icon c-49728B"><span class="a">M</span><span class="b">o</span></div>}-return $icn;+return iconDiv;}// The main panel content is hidden by default via an inline style. We're ready to remove that now.
war/src/main/webapp/scripts/hudson-behavior.js+9 −1
@@ -611,7 +611,15 @@ function makeButton(e,onclick) {var h = e.onclick;var clsName = e.className;var n = e.name;-var btn = new YAHOO.widget.Button(e,{});++var attributes = {};+// YUI Button class interprets value attribute of <input> as HTML+// similar to how the child nodes of a <button> are treated as HTML.+// in standard HTML, we wouldn't expect the former case, yet here we are!+if (e.tagName === 'INPUT') {+attributes.label = e.value.escapeHTML();+}+var btn = new YAHOO.widget.Button(e, attributes);if(onclick!=null)btn.addListener("click",onclick);if(h!=null)