Stored XSS vulnerability in Jenkins on new item page
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 display names and IDs of item types shown on the New Item page. This results in a stored cross-site scripting (XSS) vulnerability exploitable by attackers able to specify display names or IDs of item types. As of the publication of this advisory, the Jenkins security team is not aware of any plugins published via the Jenkins project update center that allow doing this. Jenkins 2.275, LTS 2.263.2 escapes display names and IDs of item types shown on the New Item page.
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)