From a0c210457bda606eba58f367678bdefc69d5cd15 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 24 Oct 2014 11:46:09 +0200 Subject: [PATCH] Use response encoding when escaping HTML With SPR-9293, it is now possible to HTML escape text while taking into account the current response encoding. When using UTF-* encodings, only XML markup significant characters are escaped, since UTF-* natively support those characters. This commit adds a new servlet context parameter to enable this fix by default in a Spring MVC application: responseEncodedHtmlEscape true Issue: SPR-12350, SPR-12132 --- .../springframework/web/util/WebUtils.java | 28 +++++++++++++ .../web/servlet/support/RequestContext.java | 27 ++++++++++++- .../web/servlet/tags/EscapeBodyTag.java | 5 +-- .../web/servlet/tags/EvalTag.java | 5 +-- .../servlet/tags/HtmlEscapingAwareTag.java | 40 ++++++++++++++++++- .../web/servlet/tags/MessageTag.java | 5 +-- .../web/servlet/tags/TransformTag.java | 2 +- .../web/servlet/tags/UrlTag.java | 2 +- .../web/servlet/tags/MessageTagTests.java | 27 +++++++++++-- 9 files changed, 124 insertions(+), 17 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java index b35f0b4a79..d07fad227a 100644 --- a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java @@ -106,6 +106,12 @@ public abstract class WebUtils { */ public static final String HTML_ESCAPE_CONTEXT_PARAM = "defaultHtmlEscape"; + /** + * Use of response encoding for HTML escaping parameter at the servlet context level + * (i.e. a context-param in {@code web.xml}): "responseEncodedHtmlEscape". + */ + public static final String RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM = "responseEncodedHtmlEscape"; + /** * Web app root key parameter at the servlet context level * (i.e. a context-param in {@code web.xml}): "webAppRootKey". @@ -175,7 +181,9 @@ public abstract class WebUtils { * (if any). Falls back to {@code false} in case of no explicit default given. * @param servletContext the servlet context of the web application * @return whether default HTML escaping is enabled (default is false) + * @deprecated as of Spring 4.1, in favor of {@link #getDefaultHtmlEscape} */ + @Deprecated public static boolean isDefaultHtmlEscape(ServletContext servletContext) { if (servletContext == null) { return false; @@ -202,6 +210,26 @@ public abstract class WebUtils { return (StringUtils.hasText(param)? Boolean.valueOf(param) : null); } + /** + * Return whether response encoding should be used when HTML escaping characters, + * thus only escaping XML markup significant characters with UTF-* encodings. + * This option is enabled for the web application with a ServletContext param, + * i.e. the value of the "responseEncodedHtmlEscape" context-param in {@code web.xml} + * (if any). + *

This method differentiates between no param specified at all and + * an actual boolean value specified, allowing to have a context-specific + * default in case of no setting at the global level. + * @param servletContext the servlet context of the web application + * @return whether response encoding is used for HTML escaping (null = no explicit default) + */ + public static Boolean getResponseEncodedHtmlEscape(ServletContext servletContext) { + if (servletContext == null) { + return null; + } + String param = servletContext.getInitParameter(RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM); + return (StringUtils.hasText(param)? Boolean.valueOf(param) : null); + } + /** * Return the temporary directory for the current web application, * as provided by the servlet container. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java index 276907d8da..0d4bb2227b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -115,6 +115,8 @@ public class RequestContext { private Boolean defaultHtmlEscape; + private Boolean responseEncodedHtmlEscape; + private UrlPathHelper urlPathHelper; private RequestDataValueProcessor requestDataValueProcessor; @@ -263,6 +265,8 @@ public class RequestContext { // context-param in web.xml, if any. this.defaultHtmlEscape = WebUtils.getDefaultHtmlEscape(this.webApplicationContext.getServletContext()); + this.responseEncodedHtmlEscape = WebUtils.getResponseEncodedHtmlEscape(this.webApplicationContext.getServletContext()); + this.urlPathHelper = new UrlPathHelper(); if (this.webApplicationContext.containsBean(REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) { @@ -484,6 +488,27 @@ public class RequestContext { return this.defaultHtmlEscape; } + /** + * Is HTML escaping using the response encoding by default? + * If enabled, only XML markup significant characters will be escaped with UTF-* encodings. + *

Falls back to {@code false} in case of no explicit default given. + * @since 4.1.2 + */ + public boolean isResponseEncodedHtmlEscape() { + return (this.responseEncodedHtmlEscape != null && this.responseEncodedHtmlEscape.booleanValue()); + } + + /** + * Return the default setting about use of response encoding for HTML escape setting, + * differentiating between no default specified and an explicit value. + * @return whether default use of response encoding HTML escaping is enabled (null = no explicit default) + * @since 4.1.2 + */ + public Boolean getResponseEncodedHtmlEscape() { + return this.responseEncodedHtmlEscape; + } + + /** * Set the UrlPathHelper to use for context path and request URI decoding. * Can be used to pass a shared UrlPathHelper instance in. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EscapeBodyTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EscapeBodyTag.java index 5ef72651f8..afe4de6c9a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EscapeBodyTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EscapeBodyTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.BodyContent; import javax.servlet.jsp.tagext.BodyTag; -import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.JavaScriptUtils; /** @@ -79,7 +78,7 @@ public class EscapeBodyTag extends HtmlEscapingAwareTag implements BodyTag { try { String content = readBodyContent(); // HTML and/or JavaScript escape, if demanded - content = isHtmlEscape() ? HtmlUtils.htmlEscape(content) : content; + content = htmlEscape(content); content = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(content) : content; writeBodyContent(content); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java index 82407b5057..4290a55c50 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,6 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.util.ObjectUtils; -import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.JavaScriptUtils; import org.springframework.web.util.TagUtils; @@ -121,7 +120,7 @@ public class EvalTag extends HtmlEscapingAwareTag { try { String result = this.expression.getValue(evaluationContext, String.class); result = ObjectUtils.getDisplayString(result); - result = (isHtmlEscape() ? HtmlUtils.htmlEscape(result) : result); + result = htmlEscape(result); result = (this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(result) : result); this.pageContext.getOut().print(result); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java index 862d57c1a3..4e887d160c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ package org.springframework.web.servlet.tags; import javax.servlet.jsp.JspException; +import org.springframework.web.util.HtmlUtils; + /** * Superclass for tags that output content that might get HTML-escaped. * @@ -31,7 +33,8 @@ import javax.servlet.jsp.JspException; * @see #setHtmlEscape * @see HtmlEscapeTag * @see org.springframework.web.servlet.support.RequestContext#isDefaultHtmlEscape - * @see org.springframework.web.util.WebUtils#isDefaultHtmlEscape + * @see org.springframework.web.util.WebUtils#getDefaultHtmlEscape + * @see org.springframework.web.util.WebUtils#getResponseEncodedHtmlEscape */ @SuppressWarnings("serial") public abstract class HtmlEscapingAwareTag extends RequestContextAwareTag { @@ -72,4 +75,37 @@ public abstract class HtmlEscapingAwareTag extends RequestContextAwareTag { return getRequestContext().isDefaultHtmlEscape(); } + /** + * Return the applicable default for the use of response encoding with HTML escape for this tag. + *

The default implementation checks the RequestContext's setting, + * falling back to {@code false} in case of no explicit default given. + * @see #getRequestContext() + * @since 4.1.2 + */ + protected boolean isResponseEncodedHtmlEscape() { + return getRequestContext().isResponseEncodedHtmlEscape(); + } + + /** + * HTML encodes the given string, only if the htmlEscape setting is enabled. + * The response encoding will be taken into account if the responseEncodedHtmlEscape setting is enabled. + * @param content + * @return + * @see #isHtmlEscape() + * @see #isResponseEncodedHtmlEscape() + * @since 4.1.2 + */ + protected String htmlEscape(String content) { + String out = content; + if(isHtmlEscape()) { + if(isResponseEncodedHtmlEscape()) { + out = HtmlUtils.htmlEscape(content, this.pageContext.getResponse().getCharacterEncoding()); + } + else { + out = HtmlUtils.htmlEscape(content); + } + } + return out; + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/MessageTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/MessageTag.java index ba9f5aa51b..16dc88c838 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/MessageTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/MessageTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import org.springframework.context.MessageSourceResolvable; import org.springframework.context.NoSuchMessageException; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.JavaScriptUtils; import org.springframework.web.util.TagUtils; @@ -181,7 +180,7 @@ public class MessageTag extends HtmlEscapingAwareTag implements ArgumentAware { String msg = resolveMessage(); // HTML and/or JavaScript escape, if demanded. - msg = isHtmlEscape() ? HtmlUtils.htmlEscape(msg) : msg; + msg = htmlEscape(msg); msg = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(msg) : msg; // Expose as variable, if demanded, else write to the page. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java index 58a9041703..05d662b474 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java @@ -105,7 +105,7 @@ public class TransformTag extends HtmlEscapingAwareTag { // Else, just do a toString. result = this.value.toString(); } - result = isHtmlEscape() ? HtmlUtils.htmlEscape(result) : result; + result = htmlEscape(result); if (this.var != null) { pageContext.setAttribute(this.var, result, TagUtils.getScope(this.scope)); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java index 45fb84102a..c8713386c6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java @@ -226,7 +226,7 @@ public class UrlTag extends HtmlEscapingAwareTag implements ParamAware { } // HTML and/or JavaScript escape, if demanded. - urlStr = isHtmlEscape() ? HtmlUtils.htmlEscape(urlStr) : urlStr; + urlStr = htmlEscape(urlStr); urlStr = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(urlStr) : urlStr; return urlStr; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/MessageTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/MessageTagTests.java index 3eeeb07f3d..a9520aad28 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/MessageTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/MessageTagTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.servlet.support.RequestContext; import org.springframework.web.servlet.support.RequestContextUtils; +import org.springframework.web.util.WebUtils; /** * Tests for {@link MessageTag}. @@ -266,11 +267,31 @@ public class MessageTagTests extends AbstractTagTests { } }; tag.setPageContext(pc); - tag.setText("test & text"); + tag.setText("test & text é"); tag.setHtmlEscape(true); assertTrue("Correct doStartTag return value", tag.doStartTag() == Tag.EVAL_BODY_INCLUDE); assertEquals("Correct doEndTag return value", Tag.EVAL_PAGE, tag.doEndTag()); - assertEquals("Correct message", "test & text", message.toString()); + assertEquals("Correct message", "test & text é", message.toString()); + } + + @SuppressWarnings("serial") + public void testMessageTagWithTextEncodingEscaped() throws JspException { + PageContext pc = createPageContext(); + pc.getServletContext().setInitParameter(WebUtils.RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM, "true"); + pc.getResponse().setCharacterEncoding("UTF-8"); + final StringBuffer message = new StringBuffer(); + MessageTag tag = new MessageTag() { + @Override + protected void writeMessage(String msg) { + message.append(msg); + } + }; + tag.setPageContext(pc); + tag.setText("test <&> é"); + tag.setHtmlEscape(true); + assertTrue("Correct doStartTag return value", tag.doStartTag() == Tag.EVAL_BODY_INCLUDE); + assertEquals("Correct doEndTag return value", Tag.EVAL_PAGE, tag.doEndTag()); + assertEquals("Correct message", "test <&> é", message.toString()); } @SuppressWarnings("serial")