This is an automated email from the ASF dual-hosted git repository.

amashchenko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 9fa53dddb92c6eea59396d9505c467acb1d74d97
Author: Aleksandr Mashchenko <aleksand...@users.noreply.github.com>
AuthorDate: Thu Nov 1 21:53:04 2018 +0200

    Merge pull request #257 from JCgH4164838Gh792C124B5/localS2_2_5_x_Branch
    
    Fix WW-4971 (broken includes for non-UTF8 content).
---
 .../java/org/apache/struts2/StrutsConstants.java   | 65 +++++++++++-----------
 .../org/apache/struts2/components/Include.java     | 36 +++++++++---
 .../struts2/util/FastByteArrayOutputStream.java    | 24 ++++++--
 .../apache/struts2/views/jsp/IncludeTagTest.java   | 60 +++++++++++++++++++-
 4 files changed, 138 insertions(+), 47 deletions(-)

diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java 
b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index 81a4f80..f786305 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -41,12 +41,15 @@ public final class StrutsConstants {
     /** The URL extension to use to determine if the request is meant for a 
Struts action */
     public static final String STRUTS_ACTION_EXTENSION = 
"struts.action.extension";
 
-       /** Comma separated list of patterns (java.util.regex.Pattern) to be 
excluded from Struts2-processing */
-       public static final String STRUTS_ACTION_EXCLUDE_PATTERN = 
"struts.action.excludePattern";
+    /** Comma separated list of patterns (java.util.regex.Pattern) to be 
excluded from Struts2-processing */
+    public static final String STRUTS_ACTION_EXCLUDE_PATTERN = 
"struts.action.excludePattern";
 
-    /** Whether to use the alterative syntax for the tags or not */
+    /** Whether to use the alternative syntax for the tags or not */
     public static final String STRUTS_TAG_ALTSYNTAX = "struts.tag.altSyntax";
 
+    /** Whether to use the response encoding (JSP page encoding) for s:include 
tag processing (false - use STRUTS_I18N_ENCODING - by default) */
+    public static final String STRUTS_TAG_INCLUDETAG_USERESPONSEENCODING = 
"struts.tag.includetag.useResponseEncoding";
+
     /** The HTTP port used by Struts URLs */
     public static final String STRUTS_URL_HTTP_PORT = "struts.url.http.port";
 
@@ -56,7 +59,7 @@ public final class StrutsConstants {
     /** The default includeParams method to generate Struts URLs */
     public static final String STRUTS_URL_INCLUDEPARAMS = 
"struts.url.includeParams";
 
-       public static final String STRUTS_URL_RENDERER = "struts.urlRenderer";
+    public static final String STRUTS_URL_RENDERER = "struts.urlRenderer";
 
     /** The com.opensymphony.xwork2.ObjectFactory implementation class */
     public static final String STRUTS_OBJECTFACTORY = "struts.objectFactory";
@@ -91,7 +94,7 @@ public final class StrutsConstants {
     /** The org.apache.struts2.views.freemarker.FreemarkerManager 
implementation class */
     public static final String STRUTS_FREEMARKER_MANAGER_CLASSNAME = 
"struts.freemarker.manager.classname";
 
-    /** Update freemarker templates cache in seconds*/
+    /** Update freemarker templates cache in seconds */
     public static final String STRUTS_FREEMARKER_TEMPLATES_CACHE_UPDATE_DELAY 
= "struts.freemarker.templatesCache.updateDelay";
     
     /** Cache model instances at BeanWrapper level */
@@ -220,12 +223,12 @@ public final class StrutsConstants {
     public static final String STRUTS_LOCALE_PROVIDER_FACTORY = 
"struts.localeProviderFactory";
 
     /** The name of the parameter to create when mapping an id (used by some 
action mappers) */
-       public static final String STRUTS_ID_PARAMETER_NAME = 
"struts.mapper.idParameterName";
-       
-       /** The name of the parameter to determine whether static method access 
will be allowed in OGNL expressions or not */
-       public static final String STRUTS_ALLOW_STATIC_METHOD_ACCESS = 
"struts.ognl.allowStaticMethodAccess";
+    public static final String STRUTS_ID_PARAMETER_NAME = 
"struts.mapper.idParameterName";
+
+    /** The name of the parameter to determine whether static method access 
will be allowed in OGNL expressions or not */
+    public static final String STRUTS_ALLOW_STATIC_METHOD_ACCESS = 
"struts.ognl.allowStaticMethodAccess";
 
-       /** The com.opensymphony.xwork2.validator.ActionValidatorManager 
implementation class */
+    /** The com.opensymphony.xwork2.validator.ActionValidatorManager 
implementation class */
     public static final String STRUTS_ACTIONVALIDATORMANAGER = 
"struts.actionValidatorManager";
 
     /** The {@link com.opensymphony.xwork2.util.ValueStackFactory} 
implementation class */
@@ -236,7 +239,7 @@ public final class StrutsConstants {
 
     /** The {@link 
com.opensymphony.xwork2.util.reflection.ReflectionContextFactory} 
implementation class */
     public static final String STRUTS_REFLECTIONCONTEXTFACTORY = 
"struts.reflectionContextFactory";
-    
+
     /** The {@link com.opensymphony.xwork2.util.PatternMatcher} implementation 
class */
     public static final String STRUTS_PATTERNMATCHER = "struts.patternMatcher";
 
@@ -246,32 +249,32 @@ public final class StrutsConstants {
     /** The {@link com.opensymphony.xwork2.UnknownHandlerManager} 
implementation class */
     public static final String STRUTS_UNKNOWN_HANDLER_MANAGER = 
"struts.unknownHandlerManager";
 
-    /** Throw RuntimeException when a property is not found, or the evaluation 
of the espression fails*/
+    /** Throw RuntimeException when a property is not found, or the evaluation 
of the expression fails */
     public static final String STRUTS_EL_THROW_EXCEPTION = 
"struts.el.throwExceptionOnFailure";
 
-    /** Logs properties that are not found (very verbose) **/
+    /** Logs properties that are not found (very verbose) */
     public static final String STRUTS_LOG_MISSING_PROPERTIES = 
"struts.ognl.logMissingProperties";
 
-    /** Enables caching of parsed OGNL expressions **/
+    /** Enables caching of parsed OGNL expressions */
     public static final String STRUTS_ENABLE_OGNL_EXPRESSION_CACHE = 
"struts.ognl.enableExpressionCache";
 
-    /** Enables evaluation of OGNL expressions **/
+    /** Enables evaluation of OGNL expressions */
     public static final String STRUTS_ENABLE_OGNL_EVAL_EXPRESSION = 
"struts.ognl.enableOGNLEvalExpression";
 
-    /** Disables {@link org.apache.struts2.dispatcher.StrutsRequestWrapper} 
request attribute value stack lookup (JSTL accessibility) **/
+    /** Disables {@link org.apache.struts2.dispatcher.StrutsRequestWrapper} 
request attribute value stack lookup (JSTL accessibility) */
     public static final String 
STRUTS_DISABLE_REQUEST_ATTRIBUTE_VALUE_STACK_LOOKUP = 
"struts.disableRequestAttributeValueStackLookup";
 
-    /** The{@link org.apache.struts2.views.util.UrlHelper} implementation 
class **/
+    /** The{@link org.apache.struts2.views.util.UrlHelper} implementation 
class */
     public static final String STRUTS_URL_HELPER = "struts.view.urlHelper";
 
-    /** {@link com.opensymphony.xwork2.conversion.impl.XWorkBasicConverter} **/
+    /** {@link com.opensymphony.xwork2.conversion.impl.XWorkBasicConverter} */
     public static final String STRUTS_CONVERTER_COLLECTION = 
"struts.converter.collection";
     public static final String STRUTS_CONVERTER_ARRAY = 
"struts.converter.array";
     public static final String STRUTS_CONVERTER_DATE = "struts.converter.date";
     public static final String STRUTS_CONVERTER_NUMBER = 
"struts.converter.number";
     public static final String STRUTS_CONVERTER_STRING = 
"struts.converter.string";
 
-    /** Enable handling exceptions by Dispatcher - true by default **/
+    /** Enable handling exceptions by Dispatcher - true by default */
     public static final String STRUTS_HANDLE_EXCEPTION = 
"struts.handle.exception";
 
     public static final String STRUTS_CONVERTER_PROPERTIES_PROCESSOR = 
"struts.converter.properties.processor";
@@ -282,42 +285,42 @@ public final class StrutsConstants {
 
     public static final String STRUTS_EXPRESSION_PARSER = 
"struts.expression.parser";
 
-    /** namespaces names' whitelist **/
+    /** Namespace names' whitelist */
     public static final String STRUTS_ALLOWED_NAMESPACE_NAMES = 
"struts.allowed.namespace.names";
-    /** default namespace name to use when namespace didn't match the 
whitelist **/
+    /** Default namespace name to use when namespace didn't match the 
whitelist */
     public static final String STRUTS_DEFAULT_NAMESPACE_NAME = 
"struts.default.namespace.name";
 
-    /** actions names' whitelist **/
+    /** Action names' whitelist */
     public static final String STRUTS_ALLOWED_ACTION_NAMES = 
"struts.allowed.action.names";
-    /** default action name to use when action didn't match the whitelist **/
+    /** Default action name to use when action didn't match the whitelist */
     public static final String STRUTS_DEFAULT_ACTION_NAME = 
"struts.default.action.name";
 
-    /** methods names' whitelist **/
+    /** Method names' whitelist */
     public static final String STRUTS_ALLOWED_METHOD_NAMES = 
"struts.allowed.method.names";
-    /** default method name to use when method didn't match the whitelist **/
+    /** Default method name to use when method didn't match the whitelist */
     public static final String STRUTS_DEFAULT_METHOD_NAME = 
"struts.default.method.name";
 
-    /** enables action: prefix **/
+    /** Enables action: prefix */
     public static final String STRUTS_MAPPER_ACTION_PREFIX_ENABLED = 
"struts.mapper.action.prefix.enabled";
 
-    /** enables access to actions in other namespaces than current with 
action: prefix **/
+    /** Enables access to actions in other namespaces than current with 
action: prefix */
     public static final String STRUTS_MAPPER_ACTION_PREFIX_CROSSNAMESPACES = 
"struts.mapper.action.prefix.crossNamespaces";
 
     public static final String DEFAULT_TEMPLATE_TYPE_CONFIG_KEY = 
"struts.ui.templateSuffix";
 
-    /** Allows override default DispatcherErrorHandler **/
+    /** Allows override default DispatcherErrorHandler */
     public static final String STRUTS_DISPATCHER_ERROR_HANDLER = 
"struts.dispatcher.errorHandler";
 
-    /** Comma delimited set of excluded classes and package names which cannot 
be accessed via expressions **/
+    /** Comma delimited set of excluded classes and package names which cannot 
be accessed via expressions */
     public static final String STRUTS_EXCLUDED_CLASSES = 
"struts.excludedClasses";
     public static final String STRUTS_EXCLUDED_PACKAGE_NAME_PATTERNS = 
"struts.excludedPackageNamePatterns";
     public static final String STRUTS_EXCLUDED_PACKAGE_NAMES = 
"struts.excludedPackageNames";
 
-    /** Dedicated services to check if passed string is excluded/accepted **/
+    /** Dedicated services to check if passed string is excluded/accepted */
     public static final String STRUTS_EXCLUDED_PATTERNS_CHECKER = 
"struts.excludedPatterns.checker";
     public static final String STRUTS_ACCEPTED_PATTERNS_CHECKER = 
"struts.acceptedPatterns.checker";
 
-    /** Constant is used to override framework's default excluded patterns **/
+    /** Constant is used to override framework's default excluded patterns */
     public static final String STRUTS_OVERRIDE_EXCLUDED_PATTERNS = 
"struts.override.excludedPatterns";
     public static final String STRUTS_OVERRIDE_ACCEPTED_PATTERNS = 
"struts.override.acceptedPatterns";
 
diff --git a/core/src/main/java/org/apache/struts2/components/Include.java 
b/core/src/main/java/org/apache/struts2/components/Include.java
index aa58ade..616aa18 100644
--- a/core/src/main/java/org/apache/struts2/components/Include.java
+++ b/core/src/main/java/org/apache/struts2/components/Include.java
@@ -20,6 +20,7 @@ package org.apache.struts2.components;
 
 import com.opensymphony.xwork2.inject.Inject;
 import com.opensymphony.xwork2.util.ValueStack;
+
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.RequestUtils;
@@ -35,6 +36,7 @@ import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponseWrapper;
+
 import java.io.*;
 import java.net.URLEncoder;
 import java.util.*;
@@ -90,17 +92,19 @@ public class Include extends Component {
 
     private static final Logger LOG = LogManager.getLogger(Include.class);
 
-    private static String systemEncoding = System.getProperty("file.encoding");
+    private static final String systemEncoding = 
System.getProperty("file.encoding");
 
     protected String value;
     private HttpServletRequest req;
     private HttpServletResponse res;
-    private static String defaultEncoding;
+    private String defaultEncoding;       // Made non-static (during WW-4971 
fix)
+    private boolean useResponseEncoding;  // Added with WW-4971 fix (allows 
switch between usage of response or default encoding)
 
     public Include(ValueStack stack, HttpServletRequest req, 
HttpServletResponse res) {
         super(stack);
         this.req = req;
         this.res = res;
+        useResponseEncoding = false;  // By default use defaultEncoding (vs. 
response/page encoding)
     }
 
     @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
@@ -108,9 +112,25 @@ public class Include extends Component {
         defaultEncoding = encoding;
     }
 
+    @Inject(value = StrutsConstants.STRUTS_TAG_INCLUDETAG_USERESPONSEENCODING, 
required=false)
+    public void setUseResponseEncoding(String useEncoding) {
+        useResponseEncoding = Boolean.parseBoolean(useEncoding);
+    }
+
     public boolean end(Writer writer, String body) {
         String page = findString(value, "value", "You must specify the URL to 
include. Example: /foo.jsp");
         StringBuilder urlBuf = new StringBuilder();
+        String encodingForInclude;
+
+        if (useResponseEncoding) {
+            encodingForInclude = res.getCharacterEncoding();  // Use response 
(page) encoding
+            if (encodingForInclude == null || encodingForInclude.length() == 
0) {
+                encodingForInclude = defaultEncoding;  // Revert to 
defaultEncoding when response (page) encoding is invalid
+            }
+        }
+        else {
+            encodingForInclude = defaultEncoding;  // Use default encoding 
(when useResponseEncoding is false)
+        }
 
         // Add URL
         urlBuf.append(page);
@@ -147,7 +167,7 @@ public class Include extends Component {
 
         // Include
         try {
-            include(result, writer, req, res, defaultEncoding);
+            include(result, writer, req, res, encodingForInclude);
         } catch (ServletException | IOException e) {
             LOG.warn("Exception thrown during include of {}", result, e);
         }
@@ -209,7 +229,7 @@ public class Include extends Component {
     }
 
     public void addParameter(String key, Object value) {
-        // don't use the default implementation of addParameter,
+        // Don't use the default implementation of addParameter,
         // instead, include tag requires that each parameter be a list of 
objects,
         // just like the HTTP servlet interfaces are (String[])
         if (value != null) {
@@ -256,7 +276,7 @@ public class Include extends Component {
             // Use given encoding
             pageResponse.getContent().writeTo(writer, encoding);
         } else {
-            //use the platform specific encoding
+            // Use the platform specific encoding
             pageResponse.getContent().writeTo(writer, systemEncoding);
         }
     }
@@ -349,9 +369,9 @@ public class Include extends Component {
          * @throws IOException
          */
         public FastByteArrayOutputStream getContent() throws IOException {
-            //if we are using a writer, we need to flush the
-            //data to the underlying outputstream.
-            //most containers do this - but it seems Jetty 4.0.5 doesn't
+            // If we are using a writer, we need to flush the
+            // data to the underlying outputstream.
+            // Most containers do this - but it seems Jetty 4.0.5 doesn't
             if (pagePrintWriter != null) {
                 pagePrintWriter.flush();
             }
diff --git 
a/core/src/main/java/org/apache/struts2/util/FastByteArrayOutputStream.java 
b/core/src/main/java/org/apache/struts2/util/FastByteArrayOutputStream.java
index fd75c46..6c593fd 100644
--- a/core/src/main/java/org/apache/struts2/util/FastByteArrayOutputStream.java
+++ b/core/src/main/java/org/apache/struts2/util/FastByteArrayOutputStream.java
@@ -18,6 +18,9 @@
  */
 package org.apache.struts2.util;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
 import javax.servlet.jsp.JspWriter;
 import java.io.*;
 import java.nio.ByteBuffer;
@@ -35,6 +38,9 @@ import java.util.LinkedList;
  *
  */
 public class FastByteArrayOutputStream extends OutputStream {
+
+    private static final Logger LOG = 
LogManager.getLogger(FastByteArrayOutputStream.class);
+
     private static final int DEFAULT_BLOCK_SIZE = 8192;
 
     private LinkedList<byte[]> buffers;
@@ -144,7 +150,7 @@ public class FastByteArrayOutputStream extends OutputStream 
{
         // Append bytes to current buffer
         // Previous data maybe partially decoded, this part will appended to 
previous
         in.put(bytes, 0, length);
-        // To begin of data
+        // To begin processing of data
         in.flip();
         decodeAndWriteBuffered(writer, in, out, decoder, endOfInput);
     }
@@ -158,7 +164,7 @@ public class FastByteArrayOutputStream extends OutputStream 
{
             if (in.hasRemaining()) {
                 // Move remaining to top of buffer
                 in.compact();
-                if (result.isOverflow() && !result.isError() && 
!result.isMalformed()) {
+                if (result.isOverflow() && !result.isError()) {  // isError 
covers isMalformed and isUnmappable
                     // Not all buffer chars decoded, spin it again
                     // Set to begin
                     in.flip();
@@ -167,16 +173,24 @@ public class FastByteArrayOutputStream extends 
OutputStream {
                 // Clean up buffer
                 in.clear();
             }
-        } while (in.hasRemaining() && result.isOverflow() && !result.isError() 
&& !result.isMalformed());
+        } while (in.hasRemaining() && result.isOverflow() && 
!result.isError());  // isError covers isMalformed and isUnmappable
+
+        if (result.isError()) {
+            if (LOG.isWarnEnabled()) {
+                // Provide a log warning when the decoding fails (prior to 
2.5.19 it failed silently).
+                // Note: Set FastByteArrayOutputStream's Logger level to error 
or higher to suppress this log warning.
+                LOG.warn("Buffer decoding-in-to-out [{}] failed, coderResult 
[{}]", decoder.charset().name(), result.toString());
+            }
+        }
     }
 
     private static CoderResult decodeAndWrite(Writer writer, ByteBuffer in, 
CharBuffer out, CharsetDecoder decoder, boolean endOfInput) throws IOException {
         CoderResult result = decoder.decode(in, out, endOfInput);
-        // To begin of decoded data
+        // To begin processing of decoded data
         out.flip();
         // Output
         writer.write(out.toString());
-        // clear output to avoid infinitive loops, see WW-4383
+        // Clear output to avoid infinite loops, see WW-4383
         out.clear();
         return result;
     }
diff --git 
a/core/src/test/java/org/apache/struts2/views/jsp/IncludeTagTest.java 
b/core/src/test/java/org/apache/struts2/views/jsp/IncludeTagTest.java
index 6b91792..4583085 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/IncludeTagTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/IncludeTagTest.java
@@ -51,7 +51,7 @@ public class IncludeTagTest extends AbstractTagTest {
 
     public void testIncludeNoParam() throws Exception {
         
-        // use always matcher as we can not determine the excact objects used 
in mock.include(request, response) call
+        // Use always matcher as we can not determine the exact objects used 
in mock.include(request, response) call
         mockRequestDispatcher.include(anyObject(ServletRequest.class), 
anyObject(ServletResponse.class));
         expectLastCall().times(1);
         
@@ -69,7 +69,7 @@ public class IncludeTagTest extends AbstractTagTest {
 
     public void testIncludeWithParameters() throws Exception {
        
-        // use always matcher as we can not determine the excact objects used 
in mock.include(request, response) call
+        // Use always matcher as we can not determine the exact objects used 
in mock.include(request, response) call
         mockRequestDispatcher.include(anyObject(ServletRequest.class), 
anyObject(ServletResponse.class));
         expectLastCall().times(1);
         
@@ -90,7 +90,7 @@ public class IncludeTagTest extends AbstractTagTest {
 
     public void testIncludeRelative2Dots() throws Exception {
         // TODO: we should test for .. in unit test - is this test correct?
-        // use always matcher as we can not determine the exact objects used 
in mock.include(request, response) call
+        // Use always matcher as we can not determine the exact objects used 
in mock.include(request, response) call
         mockRequestDispatcher.include(anyObject(ServletRequest.class), 
anyObject(ServletResponse.class));
         expectLastCall().times(1);
         
@@ -108,6 +108,60 @@ public class IncludeTagTest extends AbstractTagTest {
         verify(mockRequestDispatcher);        
     }
 
+    public void testIncludeSetUseResponseEncodingTrue() throws Exception {
+        // TODO: If possible in future mock-test an actual content-includes 
with various encodings
+        //   while setting the response encoding to match.  Doesn't appear to 
be possible
+        //   right now in unit-test form.
+        // Seems that the best we can do is verify the 
setUseResponseEncoding() doesn't fail...
+
+        // Use always matcher as we can not determine the exact objects used 
in mock.include(request, response) call
+        mockRequestDispatcher.include(anyObject(ServletRequest.class), 
anyObject(ServletResponse.class));
+        expectLastCall().times(1);
+
+        replay(mockRequestDispatcher);
+
+        tag.setValue("person/create.jsp");
+        response.setCharacterEncoding("UTF-8");
+        tag.doStartTag();
+        // Manipulate after doStartTag to ensure the tag component has 
undergone injection
+        Include include = (Include) tag.getComponent();
+        include.setUseResponseEncoding("true");
+        tag.doEndTag();
+
+        assertEquals("UTF-8", response.getCharacterEncoding());
+        assertEquals("/person/create.jsp", 
request.getRequestDispatherString());
+        assertEquals("", writer.toString());  // Nothing gets written for 
mock-include
+
+        verify(mockRequestDispatcher);
+    }
+
+    public void testIncludeSetUseResponseEncodingFalse() throws Exception {
+        // TODO: If possible in future mock-test an actual content-includes 
with various encodings
+        //   while setting the response encoding to match.  Doesn't appear to 
be possible
+        //   right now in unit-test form.
+        // Seems that the best we can do is verify the 
setUseResponseEncoding() doesn't fail...
+
+        // Use always matcher as we can not determine the exact objects used 
in mock.include(request, response) call
+        mockRequestDispatcher.include(anyObject(ServletRequest.class), 
anyObject(ServletResponse.class));
+        expectLastCall().times(1);
+
+        replay(mockRequestDispatcher);
+
+        tag.setValue("person/create.jsp");
+        response.setCharacterEncoding("UTF-8");
+        tag.doStartTag();
+        // Manipulate after doStartTag to ensure the tag component has 
undergone injection
+        Include include = (Include) tag.getComponent();
+        include.setUseResponseEncoding("false");
+        tag.doEndTag();
+
+        assertEquals("UTF-8", response.getCharacterEncoding());
+        assertEquals("/person/create.jsp", 
request.getRequestDispatherString());
+        assertEquals("", writer.toString());  // Nothing gets written for 
mock-include
+
+        verify(mockRequestDispatcher);
+    }
+
     protected void setUp() throws Exception {
         super.setUp();
         request.setupGetRequestDispatcher(new MockRequestDispatcher());

Reply via email to