From d15db8aad9951624e4789f22d0c76b3fa9307387 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 4 Sep 2025 19:58:16 -0500 Subject: [PATCH 01/16] Backport of CSP-related changes Backport from several Mojarra 5 commits Backport Faces 5 TCK test of CSP for coverage in 4.0 --- impl/pom.xml | 36 +- .../resource/ResourceHandlerImpl.java | 57 ++ .../view/FaceletViewHandlingStrategy.java | 35 +- .../sun/faces/config/WebConfiguration.java | 6 +- .../sun/faces/facelets/tag/ui/UIDebug.java | 16 +- .../sun/faces/renderkit/RenderKitUtils.java | 234 ++++-- .../renderkit/html_basic/ButtonRenderer.java | 28 +- .../html_basic/CheckboxRenderer.java | 12 +- .../html_basic/CommandLinkRenderer.java | 23 +- .../html_basic/CommandScriptRenderer.java | 10 +- .../renderkit/html_basic/MenuRenderer.java | 6 +- .../OutcomeTargetButtonRenderer.java | 43 +- .../renderkit/html_basic/RadioRenderer.java | 20 +- .../renderkit/html_basic/ScriptRenderer.java | 6 + .../renderkit/html_basic/SecretRenderer.java | 12 +- .../SelectManyCheckboxListRenderer.java | 15 +- .../renderkit/html_basic/TextRenderer.java | 14 +- .../html_basic/TextareaRenderer.java | 11 +- .../html_basic/WebsocketRenderer.java | 15 +- .../faces/application/ResourceHandler.java | 99 ++- .../application/ResourceHandlerWrapper.java | 17 + .../jakarta.faces/faces-uncompressed.js | 398 ++++++--- .../html_basic/TestResponseWriter.java | 9 +- impl/src/test/ts/spec/faces.util.test.ts | 7 +- impl/src/test/ts/spec/mojarra.test.ts | 1 + impl/src/test/ts/test-setup.ts | 2 +- .../org/eclipse/mojarra/test/base/BaseIT.java | 5 +- test/csp/pom.xml | 63 ++ test/csp/src/main/webapp/WEB-INF/beans.xml | 0 .../src/main/webapp/WEB-INF/faces-config.xml | 24 + test/csp/src/main/webapp/WEB-INF/web.xml | 44 + test/csp/src/main/webapp/spec1590.xhtml | 58 ++ .../faces/test/util/selenium/BaseITNG.java | 153 ++++ .../util/selenium/ChromeDevtoolsDriver.java | 789 ++++++++++++++++++ .../faces/test/util/selenium/DriverPool.java | 89 ++ .../test/util/selenium/ExtendedTextInput.java | 164 ++++ .../test/util/selenium/ExtendedWebDriver.java | 124 +++ .../tck/faces/test/util/selenium/WebPage.java | 391 +++++++++ .../eclipse/mojarra/test/csp/Spec1590IT.java | 154 ++++ .../issue5676/src/main/webapp/issue5676.xhtml | 1 + test/pom.xml | 3 +- 41 files changed, 2840 insertions(+), 354 deletions(-) create mode 100644 test/csp/pom.xml create mode 100644 test/csp/src/main/webapp/WEB-INF/beans.xml create mode 100644 test/csp/src/main/webapp/WEB-INF/faces-config.xml create mode 100644 test/csp/src/main/webapp/WEB-INF/web.xml create mode 100644 test/csp/src/main/webapp/spec1590.xhtml create mode 100644 test/csp/src/test/java/ee/jakarta/tck/faces/test/util/selenium/BaseITNG.java create mode 100644 test/csp/src/test/java/ee/jakarta/tck/faces/test/util/selenium/ChromeDevtoolsDriver.java create mode 100644 test/csp/src/test/java/ee/jakarta/tck/faces/test/util/selenium/DriverPool.java create mode 100644 test/csp/src/test/java/ee/jakarta/tck/faces/test/util/selenium/ExtendedTextInput.java create mode 100644 test/csp/src/test/java/ee/jakarta/tck/faces/test/util/selenium/ExtendedWebDriver.java create mode 100644 test/csp/src/test/java/ee/jakarta/tck/faces/test/util/selenium/WebPage.java create mode 100644 test/csp/src/test/java/org/eclipse/mojarra/test/csp/Spec1590IT.java diff --git a/impl/pom.xml b/impl/pom.xml index 7bb676b89c..2de12463f7 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -280,40 +280,36 @@ + - net.alchim31.maven - yuicompressor-maven-plugin - 1.5.1 + com.github.blutorange + closure-compiler-maven-plugin + 2.30.0 minify-faces-js generate-sources - compress + minify - true - true - - - - ${project.basedir}/src/main/resources/META-INF/resources/jakarta.faces/faces-uncompressed.js - - ${project.build.directory}/generated-resources/yui/faces.js - - + UTF-8 + ${project.basedir}/src/main/resources/META-INF/resources/ + ${project.build.directory}/classes/META-INF/resources/ + jakarta.faces + jakarta.faces - *.js + faces-uncompressed.js - ${project.build.directory}/generated-resources/yui - ${project.build.directory}/generated-resources/yui/META-INF/resources/jakarta.faces/ + faces.js + true + NO_TRANSPILE - - + org.apache.maven.plugins diff --git a/impl/src/main/java/com/sun/faces/application/resource/ResourceHandlerImpl.java b/impl/src/main/java/com/sun/faces/application/resource/ResourceHandlerImpl.java index 104e635394..c12cef07a0 100644 --- a/impl/src/main/java/com/sun/faces/application/resource/ResourceHandlerImpl.java +++ b/impl/src/main/java/com/sun/faces/application/resource/ResourceHandlerImpl.java @@ -30,6 +30,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; import static jakarta.servlet.http.MappingMatch.EXTENSION; import static java.lang.Boolean.FALSE; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.logging.Level.FINE; import static java.util.logging.Level.WARNING; @@ -40,8 +41,11 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Map; +import java.security.SecureRandom; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; @@ -49,6 +53,8 @@ import com.sun.faces.application.ApplicationAssociate; import com.sun.faces.config.WebConfiguration; +import com.sun.faces.renderkit.html_basic.ScriptRenderer; +import com.sun.faces.renderkit.html_basic.StylesheetRenderer; import com.sun.faces.util.FacesLogger; import com.sun.faces.util.RequestStateManager; import com.sun.faces.util.Util; @@ -67,10 +73,14 @@ public class ResourceHandlerImpl extends ResourceHandler { // Log instance for this class private static final Logger LOGGER = FacesLogger.APPLICATION.getLogger(); + private static final String CURRENT_NONCE = ResourceHandlerImpl.class.getName() + ".currentNonce"; + ResourceManager manager; List excludePatterns; private long creationTime; private long maxAge; + private boolean cspEnabled; + private SecureRandom secureRandom; private WebConfiguration webconfig; // ------------------------------------------------------------ Constructors @@ -85,6 +95,11 @@ public ResourceHandlerImpl() { manager = ApplicationAssociate.getInstance(extContext).getResourceManager(); initExclusions(extContext.getApplicationMap()); initMaxAge(); + cspEnabled = webconfig.isSet(WebConfiguration.WebContextInitParameter.CspNonceEnabled); + if (cspEnabled) { + secureRandom = new SecureRandom(); + secureRandom.nextBytes(new byte[1]); + } } // ------------------------------------------- Methods from Resource Handler @@ -235,6 +250,21 @@ public String getRendererTypeForResourceName(String resourceName) { return rendererType; } + private static String getResourceType(String contentType) { + if (contentType == null) { + return null; + } + + final String type = contentType.toLowerCase(); + if (type.equals(ScriptRenderer.DEFAULT_CONTENT_TYPE)) { + return "script"; + } else if (type.equals(StylesheetRenderer.DEFAULT_CONTENT_TYPE)) { + return "style"; + } + + return null; + } + /** * @see jakarta.faces.application.ResourceHandler#handleResourceRequest(jakarta.faces.context.FacesContext) */ @@ -385,6 +415,33 @@ private void send304(FacesContext ctx) { ctx.getExternalContext().setResponseStatus(SC_NOT_MODIFIED); } + @Override + public String getCurrentNonce(FacesContext context) { + if (cspEnabled) { + var requestMap = context.getAttributes(); + var nonce = (String) requestMap.get(CURRENT_NONCE); + + if (nonce == null) { + var viewMap = context.getViewRoot().getViewMap(true); + nonce = (String) viewMap.get(CURRENT_NONCE); + + if (nonce == null) { + byte[] bytes = new byte[32]; + secureRandom.nextBytes(bytes); + nonce = Base64.getEncoder().encodeToString(bytes); + } + + requestMap.put(CURRENT_NONCE, nonce); + viewMap.put(CURRENT_NONCE, nonce); + } + + return nonce; + } + + return null; + } + + // ------------------------------------------------- Package Private Methods /** diff --git a/impl/src/main/java/com/sun/faces/application/view/FaceletViewHandlingStrategy.java b/impl/src/main/java/com/sun/faces/application/view/FaceletViewHandlingStrategy.java index 52959eb996..612c6931c8 100644 --- a/impl/src/main/java/com/sun/faces/application/view/FaceletViewHandlingStrategy.java +++ b/impl/src/main/java/com/sun/faces/application/view/FaceletViewHandlingStrategy.java @@ -173,6 +173,10 @@ public class FaceletViewHandlingStrategy extends ViewHandlingStrategy { private Cache metadataCache; private Map> contractMappings; + private static final String NONCE_EXPRESSION = "#{nonce}"; + private String cspHeader; + private boolean dynamicCspHeader; + // ------------------------------------------------------------ Constructors public FaceletViewHandlingStrategy() { @@ -424,6 +428,12 @@ public void renderView(FacesContext ctx, UIViewRoot viewToRender) throws IOExcep FormOmittedChecker.check(ctx); } + // Add CSP header if necessary + String nonce = ctx.getApplication().getResourceHandler().getCurrentNonce(ctx); + if (nonce != null) { + ctx.getExternalContext().addResponseHeader("Content-Security-Policy", evaluateCspHeader(ctx, nonce)); + } + // Render the XML declaration to the response String xmlDecl = getXMLDECLFromFacesContextAttributes(ctx); if (xmlDecl != null) { @@ -913,11 +923,11 @@ protected ResponseWriter createResponseWriter(FacesContext context) throws IOExc // See also ViewHandler#apply(). String defaultContentType = (String) context.getAttributes().get("facelets.ContentType"); - // Get the or otherwise Facelets default encoding of UTF-8 as default encoding. + // Get the or otherwise Facelets default encoding of UTF-8 as default encoding. // See also SAXCompiler#doCompile() and EncodingHandler#apply(). String defaultEncoding = (String) context.getAttributes().get(FACELETS_ENCODING_KEY); - // Create a dummy ResponseWriter with a bogus writer, so we can figure out what + // Create a dummy ResponseWriter with a bogus writer, so we can figure out what // content type and default encoding the ResponseWriter is ultimately going to need. ResponseWriter initWriter = renderKit.createResponseWriter(NullWriter.INSTANCE, defaultContentType, defaultEncoding); @@ -1975,4 +1985,25 @@ private void markInitialStateIfNotMarked(UIComponent component) { } } } + + private String evaluateCspHeader(FacesContext context, String nonce) { + if (cspHeader == null) { + cspHeader = //WebConfiguration.WebContextInitParameter.CspPolicy. + WebConfiguration.getInstance(context.getExternalContext()).getOptionValue(WebConfiguration.WebContextInitParameter.CspPolicy); + + if (!cspHeader.contains(NONCE_EXPRESSION)) { + throw new IllegalArgumentException("The context parameter " + WebConfiguration.WebContextInitParameter.CspPolicy.name() + " must include the expression '" + NONCE_EXPRESSION + "'"); + } + + dynamicCspHeader = cspHeader.replace(NONCE_EXPRESSION, "").contains("#{"); + } + + var header = cspHeader.replace(NONCE_EXPRESSION, nonce); + + if (dynamicCspHeader) { + header = context.getApplication().evaluateExpressionGet(context, header, String.class); + } + + return header; + } } diff --git a/impl/src/main/java/com/sun/faces/config/WebConfiguration.java b/impl/src/main/java/com/sun/faces/config/WebConfiguration.java index d91c3b4931..d0a2d71527 100644 --- a/impl/src/main/java/com/sun/faces/config/WebConfiguration.java +++ b/impl/src/main/java/com/sun/faces/config/WebConfiguration.java @@ -114,7 +114,7 @@ public class WebConfiguration { private FaceletsConfiguration faceletsConfig; private boolean hasFlows; - + private String specificationVersion; // ------------------------------------------------------------ Constructors @@ -136,7 +136,7 @@ private WebConfiguration(ServletContext servletContext) { getOptionValue(WebContextInitParameter.ResourceExcludes, " "); getOptionValue(WebContextInitParameter.FaceletsViewMappings, ";"); getOptionValue(WebContextInitParameter.FaceletsSuffix, " "); - + specificationVersion = getClass().getPackage().getSpecificationVersion(); } @@ -785,6 +785,8 @@ public enum WebContextInitParameter { WebAppResourcesDirectory(ResourceHandler.WEBAPP_RESOURCES_DIRECTORY_PARAM_NAME, "/resources"), WebAppContractsDirectory(ResourceHandler.WEBAPP_CONTRACTS_DIRECTORY_PARAM_NAME, "/contracts"), ExceptionTypesToIgnoreInLogging("com.sun.faces.exceptionTypesToIgnoreInLogging", ""), + CspNonceEnabled(ResourceHandler.ENABLE_CSP_NONCE_PARAM_NAME, ""), + CspPolicy(ResourceHandler.CSP_POLICY_PARAM_NAME, ResourceHandler.DEFAULT_CSP_POLICY), ; private String defaultValue; diff --git a/impl/src/main/java/com/sun/faces/facelets/tag/ui/UIDebug.java b/impl/src/main/java/com/sun/faces/facelets/tag/ui/UIDebug.java index 5a5d436f16..8b3637107d 100644 --- a/impl/src/main/java/com/sun/faces/facelets/tag/ui/UIDebug.java +++ b/impl/src/main/java/com/sun/faces/facelets/tag/ui/UIDebug.java @@ -22,16 +22,15 @@ import java.util.List; import java.util.Map; -import com.sun.faces.facelets.util.DevTools; -import com.sun.faces.facelets.util.FastWriter; -import com.sun.faces.renderkit.RenderKitUtils; -import com.sun.faces.renderkit.html_basic.ScriptRenderer; - import jakarta.faces.component.UIComponentBase; import jakarta.faces.context.FacesContext; import jakarta.faces.context.ResponseWriter; import jakarta.servlet.http.HttpServletResponse; +import com.sun.faces.facelets.util.DevTools; +import com.sun.faces.facelets.util.FastWriter; +import com.sun.faces.renderkit.RenderKitUtils; + /** * @author Jacob Hookom */ @@ -103,14 +102,9 @@ public void encodeBegin(FacesContext facesContext) throws IOException { ResponseWriter writer = facesContext.getResponseWriter(); writer.startElement("span", this); writer.writeAttribute("id", getClientId(facesContext), "id"); - writer.startElement("script", this); - if (!RenderKitUtils.isOutputHtml5Doctype(facesContext)) { - writer.writeAttribute("type", ScriptRenderer.DEFAULT_CONTENT_TYPE, "type"); - } + RenderKitUtils.renderScript(facesContext, this, null, sb.toString()); - writer.writeText(sb.toString(), this, null); - writer.endElement("script"); writer.endElement("span"); } } diff --git a/impl/src/main/java/com/sun/faces/renderkit/RenderKitUtils.java b/impl/src/main/java/com/sun/faces/renderkit/RenderKitUtils.java index a1bbd394cd..ef2b711bac 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/RenderKitUtils.java +++ b/impl/src/main/java/com/sun/faces/renderkit/RenderKitUtils.java @@ -21,6 +21,7 @@ import static com.sun.faces.renderkit.RenderKitUtils.PredefinedPostbackParameter.PARTIAL_EVENT_PARAM; import static jakarta.faces.application.ResourceHandler.FACES_SCRIPT_LIBRARY_NAME; import static jakarta.faces.application.ResourceHandler.FACES_SCRIPT_RESOURCE_NAME; +import static java.util.stream.Collectors.toList; import java.io.IOException; import java.io.Writer; @@ -30,9 +31,11 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -41,6 +44,7 @@ import com.sun.faces.config.WebConfiguration; import com.sun.faces.el.ELUtils; import com.sun.faces.facelets.util.DevTools; +import com.sun.faces.renderkit.html_basic.ScriptRenderer; import com.sun.faces.util.FacesLogger; import com.sun.faces.util.RequestStateManager; import com.sun.faces.util.Util; @@ -143,6 +147,8 @@ public class RenderKitUtils { */ private static final String ATTRIBUTES_THAT_ARE_SET_KEY = UIComponentBase.class.getName() + ".attributesThatAreSet"; + private static final String BEHAVIOR_EVENT_ATTRIBUTE_PREFIX = "on"; + /** * UIViewRoot attribute key of a boolean value which remembers whether the view will be rendered with a HTML5 doctype. */ @@ -332,33 +338,33 @@ public static void renderPassThruAttributes(FacesContext context, ResponseWriter behaviors = Collections.emptyMap(); } - if (canBeOptimized(component, behaviors)) { - List setAttributes = (List) component.getAttributes().get(ATTRIBUTES_THAT_ARE_SET_KEY); - if (setAttributes != null) { + List setAttributes = (List) component.getAttributes().get(ATTRIBUTES_THAT_ARE_SET_KEY); + + if (setAttributes != null && canBeOptimized(component, behaviors)) { renderPassThruAttributesOptimized(context, writer, component, attributes, setAttributes, behaviors); - } } else { // this block should only be hit by custom components leveraging // the RI's rendering code, or in cases where we have behaviors // attached to multiple events. We make no assumptions and loop // through - renderPassThruAttributesUnoptimized(context, writer, component, attributes, behaviors); + renderPassThruAttributesUnoptimized(context, writer, component, attributes, setAttributes, behaviors); } } - // Renders the onchange handler for input components. Handles + // Renders the onchange event listener for input components. Handles // chaining together the user-provided onchange handler with // any Behavior scripts. - public static void renderOnchange(FacesContext context, UIComponent component, boolean incExec) throws IOException { + public static void renderOnchangeEventListener(FacesContext context, UIComponent component, boolean incExec) throws IOException { final String handlerName = "onchange"; final Object userHandler = component.getAttributes().get(handlerName); String behaviorEventName = "valueChange"; + String domEventName = "change"; if (component instanceof ClientBehaviorHolder) { Map behaviors = ((ClientBehaviorHolder) component).getClientBehaviors(); - if (null != behaviors && behaviors.containsKey("change")) { - behaviorEventName = "change"; + if (null != behaviors && behaviors.containsKey(domEventName)) { + behaviorEventName = domEventName; } } @@ -369,19 +375,20 @@ public static void renderOnchange(FacesContext context, UIComponent component, b params = new LinkedList<>(); params.add(new ClientBehaviorContext.Parameter("incExec", true)); } - renderHandler(context, component, params, handlerName, userHandler, behaviorEventName, null, false, incExec); + renderHandler(context, component, null, params, handlerName, userHandler, behaviorEventName, "change", null, false, incExec, true); } - // Renders onclick handler for SelectRaidio and SelectCheckbox - public static void renderSelectOnclick(FacesContext context, UIComponent component, boolean incExec) throws IOException { + // Renders onclick event listener for SelectRadio and SelectCheckbox + public static void renderSelectOnclickEventListener(FacesContext context, UIComponent component, String clientId, boolean incExec) throws IOException { final String handlerName = "onclick"; final Object userHandler = component.getAttributes().get(handlerName); String behaviorEventName = "valueChange"; + String domEventName = "click"; if (component instanceof ClientBehaviorHolder) { Map behaviors = ((ClientBehaviorHolder) component).getClientBehaviors(); - if (null != behaviors && behaviors.containsKey("click")) { - behaviorEventName = "click"; + if (null != behaviors && behaviors.containsKey(domEventName)) { + behaviorEventName = domEventName; } } @@ -392,44 +399,44 @@ public static void renderSelectOnclick(FacesContext context, UIComponent compone params = new LinkedList<>(); params.add(new ClientBehaviorContext.Parameter("incExec", true)); } - renderHandler(context, component, params, handlerName, userHandler, behaviorEventName, null, false, incExec); + renderHandler(context, component, clientId, params, handlerName, userHandler, behaviorEventName, domEventName, null, false, incExec, true); } - // Renders the onclick handler for command buttons. Handles + // Renders the onclick event listener for command buttons. Handles // chaining together the user-provided onclick handler, any // Behavior scripts, plus the default button submit script. - public static void renderOnclick(FacesContext context, UIComponent component, Collection params, String submitTarget, + public static void renderOnclickEventListener(FacesContext context, UIComponent component, Collection params, String submitTarget, boolean needsSubmit) throws IOException { - final String handlerName = "onclick"; final Object userHandler = component.getAttributes().get(handlerName); String behaviorEventName = "action"; + String domEventName = "click"; if (component instanceof ClientBehaviorHolder) { Map> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors(); - boolean mixed = null != behaviors && behaviors.containsKey("click") && behaviors.containsKey("action"); + boolean mixed = null != behaviors && behaviors.containsKey(domEventName) && behaviors.containsKey(behaviorEventName); if (mixed) { - behaviorEventName = "click"; - List clickBehaviors = behaviors.get("click"); - List actionBehaviors = behaviors.get("action"); + List actionBehaviors = behaviors.get(behaviorEventName); + behaviorEventName = domEventName; + List clickBehaviors = behaviors.get(domEventName); clickBehaviors.addAll(actionBehaviors); actionBehaviors.clear(); - } else if (null != behaviors && behaviors.containsKey("click")) { - behaviorEventName = "click"; + } else if (null != behaviors && behaviors.containsKey(domEventName)) { + behaviorEventName = domEventName; } } - renderHandler(context, component, params, handlerName, userHandler, behaviorEventName, submitTarget, needsSubmit, false); + renderHandler(context, component, null, params, handlerName, userHandler, behaviorEventName, domEventName, submitTarget, needsSubmit, false, true); + } - // Renders the script function for command scripts. + // Renders the script element with the function for command scripts. public static void renderFunction(FacesContext context, UIComponent component, Collection params, String submitTarget) throws IOException { ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context, component, "action", submitTarget, params); AjaxBehavior behavior = (AjaxBehavior) context.getApplication().createBehavior(AjaxBehavior.BEHAVIOR_ID); mapAttributes(component, behavior, "execute", "render", "onerror", "onevent", "resetValues"); - - context.getResponseWriter().append(behavior.getScript(behaviorContext)); + renderScript(context, component, null, behavior.getScript(behaviorContext)); } private static void mapAttributes(UIComponent component, AjaxBehavior behavior, String... attributeNames) { @@ -623,7 +630,19 @@ private static void renderPassThruAttributesOptimized(FacesContext context, Resp Attribute attr = knownAttributes[index]; if (isBehaviorEventAttribute(attr, behaviorEventName)) { - renderHandler(context, component, null, name, value, behaviorEventName, null, false, false); + renderHandler(context, component, null, null, name, value, behaviorEventName, behaviorEventName, null, false, false, false); + + renderedBehavior = true; + } else { + writer.writeAttribute(prefixAttribute(name, isXhtml), value, name); + } + } + } + else if (isBehaviorEventAttribute(name)) { + Object value = attrMap.get(name); + if (value != null && shouldRenderAttribute(value)) { + if (name.substring(2).equals(behaviorEventName)) { + renderHandler(context, component, null, null, name, value, behaviorEventName, behaviorEventName, null, false, false, false); renderedBehavior = true; } else { @@ -637,18 +656,29 @@ private static void renderPassThruAttributesOptimized(FacesContext context, Resp // attribute rendering. Need to manually render it out now. if (behaviorEventName != null && !renderedBehavior) { + List behaviorAttributes = setAttributes.stream().filter(RenderKitUtils::isBehaviorEventAttribute).collect(toList()); + + for (String attrName : behaviorAttributes) { + String eventName = attrName.substring(2); + if (behaviorEventName.equals(eventName)) { + renderPassthruAttribute(context, writer, component, behaviors, isXhtml, attrMap, attrName, behaviorEventName); + return; + } + } + // Note that we can optimize this search by providing // an event name -> Attribute inverse look up map. // This would change the search time from O(n) to O(1). - for (int i = 0; i < knownAttributes.length; i++) { - Attribute attr = knownAttributes[i]; - String[] events = attr.getEvents(); + for (Attribute attribute : knownAttributes) { + String attrName = attribute.getName(); + String[] events = attribute.getEvents(); if (events != null && events.length > 0 && behaviorEventName.equals(events[0])) { - - renderHandler(context, component, null, attr.getName(), null, behaviorEventName, null, false, false); + renderHandler(context, component, null, null, attrName, null, behaviorEventName, behaviorEventName, null, false, false, false); + return; } } + renderPassthruAttribute(context, writer, component, behaviors, isXhtml, attrMap, BEHAVIOR_EVENT_ATTRIBUTE_PREFIX + behaviorEventName, behaviorEventName); } } @@ -660,35 +690,59 @@ private static void renderPassThruAttributesOptimized(FacesContext context, Resp * @param writer the current writer * @param component the component whose attributes we're rendering * @param knownAttributes an array of pass-through attributes supported by this component + * @param setAttributes a List of attributes that have been set on the provided component * @param behaviors the non-null behaviors map for this request. * @throws IOException if an error occurs during the write */ private static void renderPassThruAttributesUnoptimized(FacesContext context, ResponseWriter writer, UIComponent component, Attribute[] knownAttributes, - Map> behaviors) throws IOException { + List setAttributes, Map> behaviors) throws IOException { boolean isXhtml = RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType()); Map attrMap = component.getAttributes(); + Set behaviorEventNames = new LinkedHashSet<>(behaviors.size() + 2); + + behaviorEventNames.addAll(behaviors.keySet()); + + if (setAttributes != null) { + setAttributes.stream().filter(RenderKitUtils::isBehaviorEventAttribute).map(a -> a.substring(BEHAVIOR_EVENT_ATTRIBUTE_PREFIX.length())).forEach(behaviorEventNames::add); + } for (Attribute attribute : knownAttributes) { String attrName = attribute.getName(); String[] events = attribute.getEvents(); - boolean hasBehavior = events != null && events.length > 0 && behaviors.containsKey(events[0]); + String eventName = events != null && events.length > 0 ? events[0] : null; + renderPassthruAttribute(context, writer, component, behaviors, isXhtml, attrMap, attrName, eventName); + behaviorEventNames.remove(eventName); + } + + for (String eventName : behaviorEventNames) { + renderPassthruAttribute(context, writer, component, behaviors, isXhtml, attrMap, BEHAVIOR_EVENT_ATTRIBUTE_PREFIX + eventName, eventName); + } + } - Object value = attrMap.get(attrName); + private static void renderPassthruAttribute(FacesContext context, ResponseWriter writer, UIComponent component, + Map> behaviors, boolean isXhtml, Map attrMap, String attrName, + String eventName) throws IOException { + boolean hasBehavior = eventName != null && behaviors.containsKey(eventName); - if (value != null && shouldRenderAttribute(value) && !hasBehavior) { - writer.writeAttribute(prefixAttribute(attrName, isXhtml), value, attrName); - } else if (hasBehavior) { + Object value = attrMap.get(attrName); - // If we've got a behavior for this attribute, - // we may need to chain scripts together, so use - // renderHandler(). - renderHandler(context, component, null, attrName, value, events[0], null, false, false); - } + if (value != null && shouldRenderAttribute(value) && !hasBehavior) { + writer.writeAttribute(prefixAttribute(attrName, isXhtml), value, attrName); + } else if (hasBehavior) { + + // If we've got a behavior for this attribute, + // we may need to chain scripts together, so use + // renderEventListener(). + renderHandler(context, component, null, null, attrName, value, eventName, eventName, null, false, false, false); } } + public static boolean isBehaviorEventAttribute(String name) { + return name.startsWith(BEHAVIOR_EVENT_ATTRIBUTE_PREFIX) && name.length() > 2; + } + /** *

* Determines if an attribute should be rendered based on the specified #attributeVal. @@ -1492,7 +1546,7 @@ private static String getSubmitHandler(FacesContext context, UIComponent compone builder.append("')"); if (preventDefault) { - builder.append(";return false"); + builder.append(";event.preventDefault()"); } return builder.toString(); @@ -1530,10 +1584,10 @@ private static String getChainedHandler(FacesContext context, UIComponent compon builder.append(")"); // If we're submitting (either via a behavior, or by rendering - // a submit script), we need to return false to prevent the - // default button/link action. + // a submit script), we need to prevent the + // default button/link action event. if (submitting && ("action".equals(behaviorEventName) || "click".equals(behaviorEventName))) { - builder.append(";return false"); + builder.append(";event.preventDefault()"); } return builder.toString(); @@ -1554,7 +1608,7 @@ private static String getSingleBehaviorHandler(FacesContext context, UIComponent script = getSubmitHandler(context, component, params, submitTarget, preventDefault); } } else if (preventDefault) { - script = script + ";return false"; + script = script + ";event.preventDefault()"; } return script; @@ -1583,15 +1637,16 @@ private static boolean isSubmitting(ClientBehavior behavior) { * @param handlerValue the user-specified value for the handler attribute * @param behaviorEventName the name of the behavior event that corresponds to this handler (eg. "action" or * "mouseover"). + * @param domEventName the name of the DOM event that corresponds to this handler (eg. "click" or + * "change"). * @param needsSubmit indicates whether the mojarra.cljs() "submit" script is required by the component. Most * components do not need this, either because they submit themselves (eg. commandButton), or because they do not * perform submits (eg. non-command components). This flag is mainly here for the commandLink case, where we need to * render the submit script to make the link submit. */ - private static void renderHandler(FacesContext context, UIComponent component, Collection params, String handlerName, - Object handlerValue, String behaviorEventName, String submitTarget, boolean needsSubmit, boolean includeExec) throws IOException { + private static void renderHandler(FacesContext context, UIComponent component, String clientId, Collection params, String handlerName, + Object handlerValue, String behaviorEventName, String domEventName, String submitTarget, boolean needsSubmit, boolean includeExec, boolean asEventListener) throws IOException { - ResponseWriter writer = context.getResponseWriter(); String userHandler = getNonEmptyUserHandler(handlerValue); List behaviors = getClientBehaviors(component, behaviorEventName); @@ -1605,27 +1660,70 @@ private static void renderHandler(FacesContext context, UIComponent component, C } String handler = null; switch (getHandlerType(behaviors, params, userHandler, needsSubmit, includeExec)) { + case USER_HANDLER_ONLY: + handler = userHandler; + break; + + case SINGLE_BEHAVIOR_ONLY: + handler = getSingleBehaviorHandler(context, component, behaviors.get(0), params, behaviorEventName, submitTarget, needsSubmit); + break; + + case SUBMIT_ONLY: + handler = getSubmitHandler(context, component, params, submitTarget, true); + break; + + case CHAIN: + handler = getChainedHandler(context, component, behaviors, params, behaviorEventName, userHandler, submitTarget, needsSubmit); + break; + default: + assert false; + } + + if (handler != null) { + if (asEventListener) { + addEventListener(context, component, clientId, domEventName, handler); + } + else { + context.getResponseWriter().writeAttribute(handlerName, handler, null); + } + } + } + + public static void addEventListener(FacesContext context, UIComponent component, String clientId, String domEventName, String function) throws IOException { + StringBuilder script = new StringBuilder("mojarra.ael('") + .append(clientId != null ? clientId : component.getClientId(context)) + .append("','") + .append(domEventName) + .append("',function(event){" + function + "})"); + + if (context.getPartialViewContext().isAjaxRequest()) { + context.getPartialViewContext().getEvalScripts().add(script.toString()); + } + else { + renderScript(context, component, null, script.toString()); + } + } - case USER_HANDLER_ONLY: - handler = userHandler; - break; + public static void renderScript(FacesContext context, UIComponent component, String clientId, String script) throws IOException { + ResponseWriter writer = context.getResponseWriter(); - case SINGLE_BEHAVIOR_ONLY: - handler = getSingleBehaviorHandler(context, component, behaviors.get(0), params, behaviorEventName, submitTarget, needsSubmit); - break; + writer.startElement("script", component); - case SUBMIT_ONLY: - handler = getSubmitHandler(context, component, params, submitTarget, true); - break; + if (clientId != null) { + writer.writeAttribute("id", clientId, "id"); + } + + if (!RenderKitUtils.isOutputHtml5Doctype(context)) { + writer.writeAttribute("type", ScriptRenderer.DEFAULT_CONTENT_TYPE, "type"); + } - case CHAIN: - handler = getChainedHandler(context, component, behaviors, params, behaviorEventName, userHandler, submitTarget, needsSubmit); - break; - default: - assert false; + String nonce = context.getApplication().getResourceHandler().getCurrentNonce(context); + if (nonce != null) { + writer.writeAttribute("nonce", nonce, "nonce"); } - writer.writeAttribute(handlerName, handler, null); + writer.writeText(script, component, null); + writer.endElement("script"); } // Determines the type of handler to render based on what sorts of diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/ButtonRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/ButtonRenderer.java index e851f26c04..fb70584027 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/ButtonRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/ButtonRenderer.java @@ -18,19 +18,12 @@ package com.sun.faces.renderkit.html_basic; -import static com.sun.faces.renderkit.RenderKitUtils.PredefinedPostbackParameter.BEHAVIOR_SOURCE_PARAM; - import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.logging.Level; -import com.sun.faces.RIConstants; -import com.sun.faces.renderkit.Attribute; -import com.sun.faces.renderkit.AttributeManager; -import com.sun.faces.renderkit.RenderKitUtils; - import jakarta.faces.component.UICommand; import jakarta.faces.component.UIComponent; import jakarta.faces.component.behavior.ClientBehavior; @@ -39,6 +32,11 @@ import jakarta.faces.context.ResponseWriter; import jakarta.faces.event.ActionEvent; +import com.sun.faces.RIConstants; +import com.sun.faces.renderkit.Attribute; +import com.sun.faces.renderkit.AttributeManager; +import com.sun.faces.renderkit.RenderKitUtils; + /** * ButtonRenderer is a class that renders the current value of UICommand as a Button. */ @@ -98,11 +96,6 @@ public void encodeBegin(FacesContext context, UIComponent component) throws IOEx * when we decide how to do script injection. */ - Collection params = getBehaviorParameters(component); - if (!params.isEmpty() && (type.equals("submit") || type.equals("button"))) { - RenderKitUtils.renderFacesJsIfNecessary(context); - } - String imageSrc = (String) component.getAttributes().get("image"); writer.startElement("input", component); writeIdAttributeIfNecessary(context, writer, component); @@ -134,8 +127,6 @@ else if (writer.getContentType().equals(RIConstants.XHTML_CONTENT_TYPE)) { writer.writeAttribute("class", styleClass, "styleClass"); } - RenderKitUtils.renderOnclick(context, component, params, null, false); - // PENDING(edburns): Prior to i_spec_1111, this element // was rendered unconditionally @@ -155,6 +146,15 @@ public void encodeEnd(FacesContext context, UIComponent component) throws IOExce if (component.getChildCount() > 0) { context.getResponseWriter().endElement("input"); } + + String type = getButtonType(component); + Collection params = getBehaviorParameters(component); + + if (!params.isEmpty() && (type.equals("submit") || type.equals("button"))) { + RenderKitUtils.renderFacesJsIfNecessary(context); + } + + RenderKitUtils.renderOnclickEventListener(context, component, params, null, false); } // --------------------------------------------------------- Private Methods diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/CheckboxRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/CheckboxRenderer.java index 6f064512ee..1c5ff9bf6d 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/CheckboxRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/CheckboxRenderer.java @@ -22,15 +22,15 @@ import java.util.Map; import java.util.logging.Level; -import com.sun.faces.renderkit.Attribute; -import com.sun.faces.renderkit.AttributeManager; -import com.sun.faces.renderkit.RenderKitUtils; - import jakarta.faces.component.UIComponent; import jakarta.faces.context.FacesContext; import jakarta.faces.context.ResponseWriter; import jakarta.faces.convert.ConverterException; +import com.sun.faces.renderkit.Attribute; +import com.sun.faces.renderkit.AttributeManager; +import com.sun.faces.renderkit.RenderKitUtils; + /** * CheckboxRenderer is a class that renders the current value of UISelectBoolean as a checkbox. */ @@ -104,10 +104,10 @@ protected void getEndTextToRender(FacesContext context, UIComponent component, S RenderKitUtils.renderPassThruAttributes(context, writer, component, ATTRIBUTES, getNonOnClickSelectBehaviors(component)); RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component); - RenderKitUtils.renderSelectOnclick(context, component, false); - writer.endElement("input"); + RenderKitUtils.renderSelectOnclickEventListener(context, component, null, false); + } // --------------------------------------------------------- Private Methods diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/CommandLinkRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/CommandLinkRenderer.java index 40b40fdb97..927b608f20 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/CommandLinkRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/CommandLinkRenderer.java @@ -24,10 +24,6 @@ import java.util.Map; import java.util.logging.Level; -import com.sun.faces.renderkit.Attribute; -import com.sun.faces.renderkit.AttributeManager; -import com.sun.faces.renderkit.RenderKitUtils; - import jakarta.faces.component.UICommand; import jakarta.faces.component.UIComponent; import jakarta.faces.component.behavior.ClientBehavior; @@ -36,6 +32,10 @@ import jakarta.faces.context.ResponseWriter; import jakarta.faces.event.ActionEvent; +import com.sun.faces.renderkit.Attribute; +import com.sun.faces.renderkit.AttributeManager; +import com.sun.faces.renderkit.RenderKitUtils; + /** * CommandLinkRenderer is a class that renders the current value of UICommand as a HyperLink that * acts like a Button. @@ -158,6 +158,12 @@ protected void renderAsActive(FacesContext context, UIComponent command) throws RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, command); + writeCommonLinkAttributes(writer, command); + + // render the current value as link text. + writeValue(command, writer); + writer.flush(); + String target = (String) command.getAttributes().get("target"); if (target != null) { target = target.trim(); @@ -166,14 +172,7 @@ protected void renderAsActive(FacesContext context, UIComponent command) throws } Collection params = getBehaviorParameters(command); - RenderKitUtils.renderOnclick(context, command, params, target, true); - - writeCommonLinkAttributes(writer, command); - - // render the current value as link text. - writeValue(command, writer); - writer.flush(); - + RenderKitUtils.renderOnclickEventListener(context, command, params, target, true); } // --------------------------------------------------------- Private Methods diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/CommandScriptRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/CommandScriptRenderer.java index 01e3940566..778909a759 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/CommandScriptRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/CommandScriptRenderer.java @@ -22,8 +22,6 @@ import java.util.logging.Level; import java.util.regex.Pattern; -import com.sun.faces.renderkit.RenderKitUtils; - import jakarta.faces.component.UICommand; import jakarta.faces.component.UIComponent; import jakarta.faces.component.html.HtmlCommandScript; @@ -32,6 +30,8 @@ import jakarta.faces.event.ActionEvent; import jakarta.faces.event.PhaseId; +import com.sun.faces.renderkit.RenderKitUtils; + /** * CommandScriptRenderer is a class that renders the current value of UICommand as a Script that acts * like an Ajax Button. @@ -90,11 +90,6 @@ public void encodeBegin(FacesContext context, UIComponent component) throws IOEx writer.startElement("span", commandScript); writer.writeAttribute("id", clientId, "id"); - writer.startElement("script", commandScript); - - if (!RenderKitUtils.isOutputHtml5Doctype(context)) { - writer.writeAttribute("type", ScriptRenderer.DEFAULT_CONTENT_TYPE, "type"); - } RenderKitUtils.renderFunction(context, component, getBehaviorParameters(commandScript), clientId); } @@ -110,7 +105,6 @@ public void encodeEnd(FacesContext context, UIComponent component) throws IOExce ResponseWriter writer = context.getResponseWriter(); assert writer != null; - writer.endElement("script"); writer.endElement("span"); } diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/MenuRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/MenuRenderer.java index 55ad92daaf..4aa6969dc6 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/MenuRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/MenuRenderer.java @@ -20,7 +20,7 @@ import static com.sun.faces.RIConstants.NO_VALUE; import static com.sun.faces.renderkit.RenderKitUtils.getSelectItems; -import static com.sun.faces.renderkit.RenderKitUtils.renderOnchange; +import static com.sun.faces.renderkit.RenderKitUtils.renderOnchangeEventListener; import static com.sun.faces.renderkit.RenderKitUtils.renderPassThruAttributes; import static com.sun.faces.renderkit.RenderKitUtils.renderXHTMLStyleBooleanAttributes; import static com.sun.faces.util.MessageUtils.CONVERSION_ERROR_MESSAGE_ID; @@ -683,12 +683,12 @@ protected void renderSelect(FacesContext context, UIComponent component) throws renderPassThruAttributes(context, writer, component, ATTRIBUTES, getNonOnChangeBehaviors(component)); renderXHTMLStyleBooleanAttributes(writer, component); - renderOnchange(context, component, false); - // Now, write the buffered option content writer.write(bufferedWriter.toString()); writer.endElement("select"); + + renderOnchangeEventListener(context, component, false); } protected Integer getSizeAttribute(UIComponent component) { diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/OutcomeTargetButtonRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/OutcomeTargetButtonRenderer.java index 6c53763a4e..29c21b7e72 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/OutcomeTargetButtonRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/OutcomeTargetButtonRenderer.java @@ -18,6 +18,11 @@ import java.io.IOException; +import jakarta.faces.application.NavigationCase; +import jakarta.faces.component.UIComponent; +import jakarta.faces.context.FacesContext; +import jakarta.faces.context.ResponseWriter; + import com.sun.faces.RIConstants; import com.sun.faces.renderkit.Attribute; import com.sun.faces.renderkit.AttributeManager; @@ -25,11 +30,6 @@ import com.sun.faces.util.MessageUtils; import com.sun.faces.util.Util; -import jakarta.faces.application.NavigationCase; -import jakarta.faces.component.UIComponent; -import jakarta.faces.context.FacesContext; -import jakarta.faces.context.ResponseWriter; - public class OutcomeTargetButtonRenderer extends OutcomeTargetRenderer { private static final Attribute[] ATTRIBUTES = AttributeManager.getAttributes(AttributeManager.Key.OUTCOMETARGETBUTTON); @@ -68,20 +68,6 @@ else if (writer.getContentType().equals(RIConstants.XHTML_CONTENT_TYPE)) { String label = getLabel(component); - if (!Util.componentIsDisabled(component)) { - NavigationCase navCase = getNavigationCase(context, component); - - if (navCase == null) { - // QUESTION should this only be added in development mode? - label += MessageUtils.getExceptionMessageString(MessageUtils.OUTCOME_TARGET_BUTTON_NO_MATCH); - writer.writeAttribute("disabled", "true", "disabled"); - } else { - String hrefVal = getEncodedTargetURL(context, component, navCase); - hrefVal += getFragment(component); - writer.writeAttribute("onclick", getOnclick(component, hrefVal), "onclick"); - } - } - // value should be used even for image type for accessibility (e.g., images disabled in browser) writer.writeAttribute("value", label, "value"); @@ -92,6 +78,16 @@ else if (writer.getContentType().equals(RIConstants.XHTML_CONTENT_TYPE)) { renderPassThruAttributes(context, writer, component, ATTRIBUTES, null); + if (!Util.componentIsDisabled(component)) { + NavigationCase navCase = getNavigationCase(context, component); + + if (navCase == null) { + // QUESTION should this only be added in development mode? + label += MessageUtils.getExceptionMessageString(MessageUtils.OUTCOME_TARGET_BUTTON_NO_MATCH); + writer.writeAttribute("disabled", "true", "disabled"); + } + } + if (component.getChildCount() == 0) { writer.endElement("input"); } @@ -107,6 +103,15 @@ public void encodeEnd(FacesContext context, UIComponent component) throws IOExce context.getResponseWriter().endElement("input"); } + if (!Util.componentIsDisabled(component)) { + NavigationCase navCase = getNavigationCase(context, component); + + if (navCase != null) { + String hrefVal = getEncodedTargetURL(context, component, navCase); + hrefVal += getFragment(component); + RenderKitUtils.addEventListener(context, component, null,"click", getOnclick(component, hrefVal)); + } + } } // ------------------------------------------------------- Protected Methods diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/RadioRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/RadioRenderer.java index b1ed88b0fd..100a3075b4 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/RadioRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/RadioRenderer.java @@ -31,14 +31,6 @@ import java.util.Map; import java.util.logging.Level; -import com.sun.faces.RIConstants; -import com.sun.faces.renderkit.Attribute; -import com.sun.faces.renderkit.AttributeManager; -import com.sun.faces.renderkit.RenderKitUtils; -import com.sun.faces.renderkit.SelectItemsIterator; -import com.sun.faces.util.RequestStateManager; -import com.sun.faces.util.Util; - import jakarta.el.ELException; import jakarta.el.ValueExpression; import jakarta.faces.component.UIComponent; @@ -55,6 +47,14 @@ import jakarta.faces.event.PostAddToViewEvent; import jakarta.faces.model.SelectItem; +import com.sun.faces.RIConstants; +import com.sun.faces.renderkit.Attribute; +import com.sun.faces.renderkit.AttributeManager; +import com.sun.faces.renderkit.RenderKitUtils; +import com.sun.faces.renderkit.SelectItemsIterator; +import com.sun.faces.util.RequestStateManager; +import com.sun.faces.util.Util; + /** * ReadoRenderer is a class that renders the current value of UISelectOne or UISelectMany * component as a list of radio buttons @@ -314,9 +314,9 @@ protected void renderInput(FacesContext context, ResponseWriter writer, UICompon RenderKitUtils.renderPassThruAttributes(context, writer, component, ATTRIBUTES, getNonOnClickSelectBehaviors(component)); RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component); - RenderKitUtils.renderSelectOnclick(context, component, false); - writer.endElement("input"); + + RenderKitUtils.renderSelectOnclickEventListener(context, component, clientId, false); } protected void renderLabel(ResponseWriter writer, UIComponent component, String forClientId, SelectItem curItem, OptionComponentInfo optionInfo) diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/ScriptRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/ScriptRenderer.java index 72b1d40f2b..3d716ca8c6 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/ScriptRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/ScriptRenderer.java @@ -40,6 +40,12 @@ protected void startInlineElement(FacesContext context, ResponseWriter writer, U if (!RenderKitUtils.isOutputHtml5Doctype(context)) { writer.writeAttribute("type", DEFAULT_CONTENT_TYPE, "type"); } + + String nonce = context.getApplication().getResourceHandler().getCurrentNonce(context); + + if (nonce != null) { + writer.writeAttribute("nonce", nonce, "nonce"); + } } @Override diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/SecretRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/SecretRenderer.java index 4a0e930f79..30959a02b0 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/SecretRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/SecretRenderer.java @@ -20,14 +20,14 @@ import java.io.IOException; -import com.sun.faces.renderkit.Attribute; -import com.sun.faces.renderkit.AttributeManager; -import com.sun.faces.renderkit.RenderKitUtils; - import jakarta.faces.component.UIComponent; import jakarta.faces.context.FacesContext; import jakarta.faces.context.ResponseWriter; +import com.sun.faces.renderkit.Attribute; +import com.sun.faces.renderkit.AttributeManager; +import com.sun.faces.renderkit.RenderKitUtils; + /** * SecretRenderer is a class that renders the current value of UIInput component as a password field. */ @@ -81,8 +81,6 @@ protected void getEndTextToRender(FacesContext context, UIComponent component, S RenderKitUtils.renderPassThruAttributes(context, writer, component, ATTRIBUTES, getNonOnChangeBehaviors(component)); RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component); - RenderKitUtils.renderOnchange(context, component, false); - String styleClass; if (null != (styleClass = (String) component.getAttributes().get("styleClass"))) { writer.writeAttribute("class", styleClass, "styleClass"); @@ -90,6 +88,8 @@ protected void getEndTextToRender(FacesContext context, UIComponent component, S writer.endElement("input"); + RenderKitUtils.renderOnchangeEventListener(context, component, false); + } } // end of class SecretRenderer diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/SelectManyCheckboxListRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/SelectManyCheckboxListRenderer.java index 2656959082..91ff7baa61 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/SelectManyCheckboxListRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/SelectManyCheckboxListRenderer.java @@ -24,11 +24,6 @@ import java.io.IOException; import java.util.Iterator; -import com.sun.faces.renderkit.Attribute; -import com.sun.faces.renderkit.AttributeManager; -import com.sun.faces.renderkit.RenderKitUtils; -import com.sun.faces.util.RequestStateManager; - import jakarta.faces.component.UIComponent; import jakarta.faces.component.UINamingContainer; import jakarta.faces.component.ValueHolder; @@ -38,6 +33,11 @@ import jakarta.faces.model.SelectItem; import jakarta.faces.model.SelectItemGroup; +import com.sun.faces.renderkit.Attribute; +import com.sun.faces.renderkit.AttributeManager; +import com.sun.faces.renderkit.RenderKitUtils; +import com.sun.faces.util.RequestStateManager; + /** * SelectManyCheckboxListRenderer is a class that renders the current value of UISelectMany component * as a list of checkboxes. @@ -275,9 +275,10 @@ protected void renderOption(FacesContext context, UIComponent component, Convert RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component); - RenderKitUtils.renderSelectOnclick(context, component, false); - writer.endElement("input"); + + RenderKitUtils.renderSelectOnclickEventListener(context, component, idString, false); + writer.startElement("label", component); writer.writeAttribute("for", idString, "for"); diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/TextRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/TextRenderer.java index de230a9786..4e88f7041c 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/TextRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/TextRenderer.java @@ -23,11 +23,6 @@ import java.util.HashMap; import java.util.Map; -import com.sun.faces.config.WebConfiguration; -import com.sun.faces.renderkit.Attribute; -import com.sun.faces.renderkit.AttributeManager; -import com.sun.faces.renderkit.RenderKitUtils; - import jakarta.faces.application.FacesMessage; import jakarta.faces.application.ProjectStage; import jakarta.faces.component.UIComponent; @@ -38,6 +33,11 @@ import jakarta.faces.context.FacesContext; import jakarta.faces.context.ResponseWriter; +import com.sun.faces.config.WebConfiguration; +import com.sun.faces.renderkit.Attribute; +import com.sun.faces.renderkit.AttributeManager; +import com.sun.faces.renderkit.RenderKitUtils; + /** * TextRenderer is a class that renders the current value of UIInput or UIOutput * component as a input field or static text. @@ -136,10 +136,10 @@ protected void getEndTextToRender(FacesContext context, UIComponent component, S RenderKitUtils.renderPassThruAttributes(context, writer, component, attributes, getNonOnChangeBehaviors(component)); RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component); - RenderKitUtils.renderOnchange(context, component, false); - writer.endElement("input"); + RenderKitUtils.renderOnchangeEventListener(context, component, hasPassthroughAttributes); + } else if (isOutput = component instanceof UIOutput) { if (styleClass != null || style != null || dir != null || lang != null || title != null || hasPassthroughAttributes || (shouldWriteIdAttribute = shouldWriteIdAttribute(component))) { diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/TextareaRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/TextareaRenderer.java index a1668035e6..9d304063f3 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/TextareaRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/TextareaRenderer.java @@ -20,14 +20,14 @@ import java.io.IOException; -import com.sun.faces.renderkit.Attribute; -import com.sun.faces.renderkit.AttributeManager; -import com.sun.faces.renderkit.RenderKitUtils; - import jakarta.faces.component.UIComponent; import jakarta.faces.context.FacesContext; import jakarta.faces.context.ResponseWriter; +import com.sun.faces.renderkit.Attribute; +import com.sun.faces.renderkit.AttributeManager; +import com.sun.faces.renderkit.RenderKitUtils; + /** * TextareaRenderer is a class that renders the current value of UIInput component as a Textarea. */ @@ -63,8 +63,6 @@ protected void getEndTextToRender(FacesContext context, UIComponent component, S RenderKitUtils.renderPassThruAttributes(context, writer, component, ATTRIBUTES); RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component); - RenderKitUtils.renderOnchange(context, component, false); - if (component.getAttributes().containsKey("com.sun.faces.addNewLineAtStart") && "true".equalsIgnoreCase((String) component.getAttributes().get("com.sun.faces.addNewLineAtStart"))) { writer.writeText("\n", null); @@ -77,6 +75,7 @@ protected void getEndTextToRender(FacesContext context, UIComponent component, S writer.endElement("textarea"); + RenderKitUtils.renderOnchangeEventListener(context, component, false); } } diff --git a/impl/src/main/java/com/sun/faces/renderkit/html_basic/WebsocketRenderer.java b/impl/src/main/java/com/sun/faces/renderkit/html_basic/WebsocketRenderer.java index 0d1df9dd31..4e7fc52dbb 100644 --- a/impl/src/main/java/com/sun/faces/renderkit/html_basic/WebsocketRenderer.java +++ b/impl/src/main/java/com/sun/faces/renderkit/html_basic/WebsocketRenderer.java @@ -26,21 +26,20 @@ import java.util.Map; import java.util.Map.Entry; -import com.sun.faces.push.WebsocketChannelManager; -import com.sun.faces.push.WebsocketFacesListener; -import com.sun.faces.renderkit.RenderKitUtils; - import jakarta.faces.component.UIComponent; import jakarta.faces.component.UIWebsocket; import jakarta.faces.component.behavior.ClientBehavior; import jakarta.faces.context.FacesContext; -import jakarta.faces.context.ResponseWriter; import jakarta.faces.event.AbortProcessingException; import jakarta.faces.event.ComponentSystemEvent; import jakarta.faces.event.ComponentSystemEventListener; import jakarta.faces.event.ListenerFor; import jakarta.faces.event.PostAddToViewEvent; +import com.sun.faces.push.WebsocketChannelManager; +import com.sun.faces.push.WebsocketFacesListener; +import com.sun.faces.renderkit.RenderKitUtils; + /** * WebsocketRenderer is a class that renders the faces.push.init() script and decodes any client * behaviors triggered by {@link UIWebsocket}. @@ -95,11 +94,7 @@ public void encodeEnd(FacesContext context, UIComponent component) throws IOExce RenderKitUtils.renderFacesJsIfNecessary(context); - ResponseWriter writer = context.getResponseWriter(); - writer.startElement("script", component); - writer.writeAttribute("id", clientId, "id"); - writer.write(String.format(SCRIPT_INIT, clientId, url, channel, functions, behaviors, connected)); - writer.endElement("script"); + RenderKitUtils.renderScript(context, component, clientId, String.format(SCRIPT_INIT, clientId, url, channel, functions, behaviors, connected)); } } diff --git a/impl/src/main/java/jakarta/faces/application/ResourceHandler.java b/impl/src/main/java/jakarta/faces/application/ResourceHandler.java index 9acd56e630..9c91718b20 100644 --- a/impl/src/main/java/jakarta/faces/application/ResourceHandler.java +++ b/impl/src/main/java/jakarta/faces/application/ResourceHandler.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.stream.Stream; +import jakarta.el.ELContext; import jakarta.faces.context.FacesContext; /** @@ -40,7 +41,7 @@ *

* *

- * ResourceHandler defines a path based packaging convention for resources. The default implementation of + * ResourceHandler defines a path-based packaging convention for resources. The default implementation of * ResourceHandler must support packaging resources in the classpath or in the web application root. See * section 2.6.1 "Packaging Resources" of the Jakarta Faces Specification Document * for the normative specification of packaging resources. @@ -259,6 +260,63 @@ public abstract class ResourceHandler { */ public static final String RESOURCE_EXCLUDES_DEFAULT_VALUE = ".class .jsp .jspx .properties .xhtml .groovy"; + /** + *

+ * The boolean context parameter name to explicitly enable Content Security Policy (CSP) nonce generation. + * When this parameter is set to {@code true}, the runtime generates a Content Security Policy (CSP) nonce value for the current view. + * The generated nonce can be obtained via {@link #getCurrentNonce(jakarta.faces.context.FacesContext)}. + * If this context parameter is not enabled, no nonce is generated and {@link #getCurrentNonce(jakarta.faces.context.FacesContext)} returns {@code null}. + *

+ *

+ * When this parameter is set to {@code true}, the runtime will also consult {@link #CSP_POLICY_PARAM_NAME} if present, + * or fall back to {@link #DEFAULT_CSP_POLICY} to determine the exact policy sent in the Content-Security-Policy response header. + *

+ * + * @since 4.0 + */ + public static final String ENABLE_CSP_NONCE_PARAM_NAME = "jakarta.faces.ENABLE_CSP_NONCE"; + + /** + *

+ * The string context parameter name to determine the Content Security Policy (CSP) policy to be sent in the Content-Security-Policy response header. + * This parameter is only consulted and applied when {@link #ENABLE_CSP_NONCE_PARAM_NAME} is explicitly set to {@code true}. + * If nonce generation is not enabled, this parameter is ignored and no Content-Security-Policy header is added. + *

+ *

+ * The value must be a valid CSP policy string and must include the expression #{nonce} (e.g., 'nonce-#{nonce}'), + * which the runtime will substitute with the nonce value of the current view as returned by {@link #getCurrentNonce(jakarta.faces.context.FacesContext)}. + * The nonce remains the same for the duration of the view, including postbacks and Ajax requests. A new nonce is only generated when a new view is created (e.g., a fresh GET navigation to a different page). + * Other EL expressions besides the special #{nonce} placeholder will be evaluated on a per-request basis using the current {@link ELContext}. + * If this parameter is not specified, the value defined by {@link #DEFAULT_CSP_POLICY} is used. + *

+ *

+ * This has been backported from Jakarta Faces 5.0 to provide CSP nonce support for older applications. + *

+ *

+ * Example values: + *

+ *
    + *
  • Default: script-src 'self' 'nonce-#{nonce}' 'strict-dynamic'
  • + *
  • Extended: script-src 'self' 'nonce-#{nonce}' 'strict-dynamic' https://analytics.google.com
  • + *
  • With expressions: script-src 'self' 'nonce-#{nonce}' 'strict-dynamic' #{someBean.spaceSeparatedStringOfAllowedScriptSources}; + * style-src 'self' #{someBean.spaceSeparatedStringOfAllowedStyleSources}; frame-ancestors 'none'
  • + *
+ * + * @since 4.0 + */ + public static final String CSP_POLICY_PARAM_NAME = "jakarta.faces.CSP_POLICY"; + + /** + *

+ * The default value for the Content-Security-Policy response header if the {@link #ENABLE_CSP_NONCE_PARAM_NAME} is set to {@code true} and the {@link #CSP_POLICY_PARAM_NAME} is not provided. + * The value is {@value}. + * The runtime will substitute #{nonce} with the actual nonce value of the current view. + *

+ * + * @since 4.0 + */ + public static final String DEFAULT_CSP_POLICY = "script-src 'self' 'nonce-#{nonce}' 'strict-dynamic'"; + // ---------------------------------------------------------- Public Methods /** @@ -311,7 +369,7 @@ public abstract class ResourceHandler { * *
  • *

    - * Considering resource library contracts (at the locations specified in the + * Considering resource library contracts (at the locations specified in the * Jakarta Faces Specification Document section 2.7 "Resource Library Contracts"). *

    *
  • @@ -324,7 +382,7 @@ public abstract class ResourceHandler { * *
  • *

    - * Considering faces flows (at the locations specified in the + * Considering faces flows (at the locations specified in the * Jakarta Faces Specification Document section 11.3.3 "Faces Flows"). *

    *
  • @@ -421,7 +479,7 @@ public Stream getViewResources(FacesContext facesContext, String path, R *
    * *

    - * The resource must be identified according to the specification in + * The resource must be identified according to the specification in * section 2.6.1.3 "Resource Identifiers" of the Jakarta Faces Specification Document. New requirements were * introduced in version 2.2 of the specification. *

    @@ -747,4 +805,37 @@ public boolean isResourceRendered(FacesContext context, String resourceName, Str return resourceIdentifiers != null && resourceIdentifiers.contains(resourceIdentifier); } + /** + *

    + * Returns the Content Security Policy (CSP) nonce for the current request, or {@code null} if CSP nonce support is not enabled. When enabled via + * {@link #ENABLE_CSP_NONCE_PARAM_NAME}, then the runtime ensures that each view has a consistent nonce value that can be used to allow inline scripts to execute safely. + *

    + * + *

    + * The returned nonce is intended to be used: + *

    + * + *
      + *
    • As the value of the {@code nonce} attribute on rendered {@code per command, roughly doubling per-link bytes), the response is committed before flushToWriter runs, getSession(true) then fails with `IllegalStateException: Cannot create a session after the response has been committed`, aborting the render mid-form, so and the jakarta.faces.ViewState hidden input never reach the client. FaceletViewHandlingStrategy already had a pre-render getSession() guard for exactly this reason, but it was strict-equality on STATE_SAVING_METHOD_SERVER, which disagreed with the helper-selection rule in ResponseStateManagerImpl (anything not STATE_SAVING_METHOD_CLIENT → ServerSideStateHelper). Configurations where STATE_SAVING_METHOD is unset or contains an unresolved placeholder (e.g. ${webapp.stateSavingMethod}) silently used the server helper but skipped the pre-create. Fix isServerStateSaving() to mirror the helper-selection rule (!STATE_SAVING_METHOD_CLIENT.equalsIgnoreCase(...)), and tighten the pre-create to only fire when actually needed: non-transient view, no existing session, server-side state saving, and the view contains at least one UIForm (verified via a short-circuit visitTree). This avoids gratuitous session creation for plain pages that have no form and would not write state anyway, which previously caused JSESSIONID URL rewriting side-effects. Fixes Issue1817IT regression introduced by the CSP backport. --- .../view/FaceletViewHandlingStrategy.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/impl/src/main/java/com/sun/faces/application/view/FaceletViewHandlingStrategy.java b/impl/src/main/java/com/sun/faces/application/view/FaceletViewHandlingStrategy.java index 11326fe53d..fff08c0cea 100644 --- a/impl/src/main/java/com/sun/faces/application/view/FaceletViewHandlingStrategy.java +++ b/impl/src/main/java/com/sun/faces/application/view/FaceletViewHandlingStrategy.java @@ -42,7 +42,7 @@ import static jakarta.faces.application.ProjectStage.Development; import static jakarta.faces.application.Resource.COMPONENT_RESOURCE_KEY; import static jakarta.faces.application.StateManager.IS_BUILDING_INITIAL_STATE; -import static jakarta.faces.application.StateManager.STATE_SAVING_METHOD_SERVER; +import static jakarta.faces.application.StateManager.STATE_SAVING_METHOD_CLIENT; import static jakarta.faces.application.ViewHandler.CHARACTER_ENCODING_KEY; import static jakarta.faces.application.ViewHandler.DEFAULT_FACELETS_SUFFIX; import static jakarta.faces.application.ViewVisitOption.RETURN_AS_MINIMAL_IMPLICIT_OUTCOME; @@ -91,6 +91,7 @@ import jakarta.faces.component.Doctype; import jakarta.faces.component.EditableValueHolder; import jakarta.faces.component.UIComponent; +import jakarta.faces.component.UIForm; import jakarta.faces.component.UIPanel; import jakarta.faces.component.UIViewRoot; import jakarta.faces.component.html.HtmlDoctype; @@ -410,7 +411,7 @@ public void renderView(FacesContext ctx, UIViewRoot viewToRender) throws IOExcep * * Note if you flag a view as transient then we won't acquire the session as you are stating it does not need one. */ - if (isServerStateSaving() && !viewToRender.isTransient()) { + if (!viewToRender.isTransient() && extContext.getSession(false) == null && isServerStateSaving() && hasForm(ctx, viewToRender)) { getSession(ctx); } @@ -1834,11 +1835,7 @@ private void reapplyDynamicRemove(FacesContext context, ComponentStruct struct) * @return true if we are, false otherwise. */ private boolean isServerStateSaving() { - if (STATE_SAVING_METHOD_SERVER.equals(webConfig.getOptionValue(StateSavingMethod))) { - return true; - } - - return false; + return !STATE_SAVING_METHOD_CLIENT.equalsIgnoreCase(webConfig.getOptionValue(StateSavingMethod)); } /** @@ -1856,6 +1853,25 @@ private HttpSession getSession(FacesContext context) { return null; } + /** + * @return true if the view contains at least one {@link UIForm}, in which case state will be written and a session + * is needed under server-side state saving. + */ + private static boolean hasForm(FacesContext context, UIViewRoot viewRoot) { + if (viewRoot == null || viewRoot.getChildCount() == 0) { + return false; + } + boolean[] found = { false }; + viewRoot.visitTree(VisitContext.createVisitContext(context), (visitContext, target) -> { + if (target instanceof UIForm) { + found[0] = true; + return VisitResult.COMPLETE; + } + return VisitResult.ACCEPT; + }); + return found[0]; + } + /** * Gets and if needed initializes the faceletFactory * From 04d82a867901fe727d6c08db373a2b906f284eab Mon Sep 17 00:00:00 2001 From: Bauke Scholtz Date: Tue, 28 Apr 2026 11:27:50 -0400 Subject: [PATCH 15/16] Fix a couple of failing tests caused by change in doEval after CSP backport; Closure Compiler needs to be explicitly instructed to emit ES5-compatible faces.js --- impl/pom.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/impl/pom.xml b/impl/pom.xml index d66e4721e3..b2b2b5100c 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -275,7 +275,9 @@ #{path}/faces.js true - NO_TRANSPILE + + ECMASCRIPT5 false From 16b1d647e14e0d67f73005728e4993e562940d41 Mon Sep 17 00:00:00 2001 From: Bauke Scholtz Date: Tue, 28 Apr 2026 11:49:46 -0400 Subject: [PATCH 16/16] Backport faces-uncompressed.js improvements done during 4.1 --- .../jakarta.faces/faces-uncompressed.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/impl/src/main/resources/META-INF/resources/jakarta.faces/faces-uncompressed.js b/impl/src/main/resources/META-INF/resources/jakarta.faces/faces-uncompressed.js index 8a53d6e25c..a07d7fce86 100644 --- a/impl/src/main/resources/META-INF/resources/jakarta.faces/faces-uncompressed.js +++ b/impl/src/main/resources/META-INF/resources/jakarta.faces/faces-uncompressed.js @@ -117,10 +117,6 @@ if ( !( (window.faces && window.faces.specversion && window.faces.specversion >= return inputElementName in form ? form[inputElementName] : getElementByName(form,inputElementName); } - /** - * append a new pair of parameter=value to a query string - * @ignore - */ /** * return true if one of the dom elements contains @@ -355,12 +351,13 @@ if ( !( (window.faces && window.faces.specversion && window.faces.specversion >= }; /** - * Remove all the portion of code matching the script pattern from the passed string + * Remove all the portion of code matching the script pattern from the passed string, + * preserving scripts whose type is set to something other than text/javascript. * @param html a String containing a portion of html * @ignore */ const removeScripts = function removeScripts(html) { - return html.replace(SCRIPT_TAG_REGEX, function(match, content) { + return html.replace(SCRIPT_TAG_REGEX, (match) => { const type = match.match(TAG_ATTRIBUTE_TYPE_REGEX); if (!!type && type[1] !== "text/javascript") { return match; // keep non-text/javascript scripts @@ -3228,17 +3225,9 @@ mojarra.l = function l(l) { if (document.readyState === "complete") { setTimeout(l); } - else if (window.addEventListener) { - window.addEventListener("load", l, false); - } - else if (typeof window.onload === "function") { - const oldListener = window.onload; - window.onload = function() { oldListener(); l(); }; - } else { - window.onload = l; + window.addEventListener("load", l); } - }; /**