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

joerghoh pushed a commit to branch SLING-13357
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-engine.git

commit 5dc158de299d95bc9eff4acd967aa4cd307aa269
Author: Joerg Hoh <[email protected]>
AuthorDate: Mon Sep 21 18:45:51 2026 +0200

    SLING-13357 add header-modification validation for SetCharacterEncoding
---
 .../impl/SlingJakartaHttpServletResponseImpl.java  | 132 +++++++-
 .../impl/SlingHttpServletResponseImplTest.java     | 332 +++++++++++++++++++++
 2 files changed, 454 insertions(+), 10 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/engine/impl/SlingJakartaHttpServletResponseImpl.java
 
b/src/main/java/org/apache/sling/engine/impl/SlingJakartaHttpServletResponseImpl.java
index be9d38e..7c66780 100644
--- 
a/src/main/java/org/apache/sling/engine/impl/SlingJakartaHttpServletResponseImpl.java
+++ 
b/src/main/java/org/apache/sling/engine/impl/SlingJakartaHttpServletResponseImpl.java
@@ -20,9 +20,9 @@ package org.apache.sling.engine.impl;
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.nio.charset.Charset;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Deque;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -375,9 +375,81 @@ public class SlingJakartaHttpServletResponseImpl extends 
HttpServletResponseWrap
         }
     }
 
+    @Override
+    public void setCharacterEncoding(final String charset) {
+        boolean isCommitedDueToSendErrorOrRedirect = this.isCommitted()
+                && (CommitReason.SEND_ERROR == this.committedReason
+                        || CommitReason.SEND_REDIRECT == this.committedReason);
+        if (isCommitedDueToSendErrorOrRedirect || !isInclude()) {
+            super.setCharacterEncoding(charset);
+            return;
+        }
+        final Optional<String> message = 
checkCharacterEncodingOverride(charset);
+        if (message.isPresent()) {
+            if (isCheckContentTypeOnInclude()) {
+                requestData.getRequestProgressTracker().log("ERROR: " + 
message.get());
+                LOG.error(CALL_STACK_MESSAGE + getCurrentStackTrace());
+                throw new ContentTypeChangeException(message.get());
+            }
+            if (isProtectHeadersOnInclude()) {
+                LOG.error(message.get());
+                LOG.error(CALL_STACK_MESSAGE + getCurrentStackTrace());
+                requestData.getRequestProgressTracker().log("ERROR: " + 
message.get());
+                return;
+            }
+            LOG.warn(message.get());
+            LOG.warn(CALL_STACK_MESSAGE + getCurrentStackTrace());
+            requestData.getRequestProgressTracker().log("WARN: " + 
message.get());
+            super.setCharacterEncoding(charset);
+        } else {
+            super.setCharacterEncoding(charset);
+        }
+    }
+
+    @Override
+    public void setCharacterEncoding(final Charset charset) {
+        // funnel the Charset variant through the checked String variant so
+        // that the include protections cannot be bypassed via this method
+        this.setCharacterEncoding(charset == null ? null : charset.name());
+    }
+
+    /**
+     * Checks if the response character encoding is being changed by an include
+     * and provides a message to log if it is. Changing the character encoding
+     * changes the charset parameter of the 'Content-Type' header and is
+     * therefore subject to the same include protections as
+     * {@link #setContentType(String)}.
+     *
+     * @param charset the character encoding that is being set
+     * @return an optional message to log
+     */
+    protected Optional<String> checkCharacterEncodingOverride(@Nullable String 
charset) {
+        // A previously detected violation must not disable the check itself -
+        // otherwise the second and any later override attempt within the same
+        // request would pass unchecked even though the first one was blocked.
+        final boolean isFirstViolation =
+                
requestData.getSlingRequestProcessor().getContentTypeHeaderState() != 
ContentTypeHeaderState.VIOLATED;
+        final String currentCharset = getCharacterEncoding();
+        if (charset != null && charset.equalsIgnoreCase(currentCharset)) {
+            // not an effective change
+            return Optional.empty();
+        }
+        
requestData.getSlingRequestProcessor().setContentTypeHeaderState(ContentTypeHeaderState.VIOLATED);
+        final String currentContentType = getContentType();
+        final String base = currentContentType == null ? "" : 
getMimeTypePart(currentContentType);
+        final String newContentType = (charset == null) ? base : base + 
";charset=" + charset;
+        return Optional.of(
+                isFirstViolation
+                        ? getMessage(currentContentType, newContentType)
+                        : getShortMessage(currentContentType, newContentType));
+    }
+
     /**
      * Checks if the 'Content-Type' header is being overridden and provides a
-     * message to log if it is.
+     * message to log if it is. Both the media type and the charset parameter
+     * are compared: changing only the charset (e.g. from
+     * 'text/html;charset=UTF-8' to 'text/html;charset=UTF-7') changes the
+     * effective response header just as much as changing the media type.
      *
      * @param contentType the 'Content-Type' value that is being set
      * @return an optional message to log
@@ -397,14 +469,23 @@ public class SlingJakartaHttpServletResponseImpl extends 
HttpServletResponseWrap
                     isFirstViolation
                             ? getMessage(currentContentType, null)
                             : getShortMessage(currentContentType, null));
-        } else {
-            Optional<String> currentMime = currentContentType == null
-                    ? Optional.of("null")
-                    : Arrays.stream(currentContentType.split(";")).findFirst();
-            Optional<String> setMime = 
Arrays.stream(contentType.split(";")).findFirst();
-            if (currentMime.isPresent()
-                    && setMime.isPresent()
-                    && !currentMime.get().equals(setMime.get())) {
+        }
+        final String currentMime = currentContentType == null ? "null" : 
getMimeTypePart(currentContentType);
+        final String setMime = getMimeTypePart(contentType);
+        if (!currentMime.equalsIgnoreCase(setMime)) {
+            
requestData.getSlingRequestProcessor().setContentTypeHeaderState(ContentTypeHeaderState.VIOLATED);
+            return Optional.of(
+                    isFirstViolation
+                            ? getMessage(currentContentType, contentType)
+                            : getShortMessage(currentContentType, 
contentType));
+        }
+        final String setCharset = getCharsetPart(contentType);
+        if (setCharset != null) {
+            String currentCharset = currentContentType == null ? null : 
getCharsetPart(currentContentType);
+            if (currentCharset == null) {
+                currentCharset = getCharacterEncoding();
+            }
+            if (!setCharset.equalsIgnoreCase(currentCharset)) {
                 
requestData.getSlingRequestProcessor().setContentTypeHeaderState(ContentTypeHeaderState.VIOLATED);
                 return Optional.of(
                         isFirstViolation
@@ -415,6 +496,37 @@ public class SlingJakartaHttpServletResponseImpl extends 
HttpServletResponseWrap
         return Optional.empty();
     }
 
+    /**
+     * Returns the media type of a 'Content-Type' header value, i.e. the part
+     * before the first parameter separator, trimmed.
+     */
+    private static String getMimeTypePart(final String contentType) {
+        final int semi = contentType.indexOf(';');
+        final String mime = semi >= 0 ? contentType.substring(0, semi) : 
contentType;
+        return mime.trim();
+    }
+
+    /**
+     * Returns the value of the charset parameter of a 'Content-Type' header
+     * value, or {@code null} if no charset parameter is present.
+     */
+    @Nullable
+    private static String getCharsetPart(final String contentType) {
+        final String[] parts = contentType.split(";");
+        for (int i = 1; i < parts.length; i++) {
+            final String param = parts[i].trim();
+            final int eq = param.indexOf('=');
+            if (eq > 0 && "charset".equalsIgnoreCase(param.substring(0, 
eq).trim())) {
+                String value = param.substring(eq + 1).trim();
+                if (value.length() > 1 && value.charAt(0) == '"' && 
value.charAt(value.length() - 1) == '"') {
+                    value = value.substring(1, value.length() - 1).trim();
+                }
+                return value.isEmpty() ? null : value;
+            }
+        }
+        return null;
+    }
+
     /**
      * Short variant of {@link #getMessage(String, String)} used for repeated
      * violations within the same request: it omits the include stack and the
diff --git 
a/src/test/java/org/apache/sling/engine/impl/SlingHttpServletResponseImplTest.java
 
b/src/test/java/org/apache/sling/engine/impl/SlingHttpServletResponseImplTest.java
index 58ac359..9584364 100644
--- 
a/src/test/java/org/apache/sling/engine/impl/SlingHttpServletResponseImplTest.java
+++ 
b/src/test/java/org/apache/sling/engine/impl/SlingHttpServletResponseImplTest.java
@@ -19,6 +19,8 @@
 package org.apache.sling.engine.impl;
 
 import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.stream.Collectors;
@@ -542,6 +544,336 @@ public class SlingHttpServletResponseImplTest {
         Mockito.verifyNoInteractions(requestProgressTracker);
     }
 
+    @Test
+    public void testCharacterEncodingProtectedOnInclude() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        
when(requestData.getActiveServletName()).thenReturn(ACTIVE_SERVLET_NAME);
+        when(orig.getContentType()).thenReturn("text/html;charset=UTF-8");
+        when(orig.getCharacterEncoding()).thenReturn("UTF-8");
+        ArrayList<String> logMessagesList = new 
ArrayList<>(Arrays.asList(logMessages));
+        when(requestProgressTracker.getMessages()).thenAnswer(invocation -> 
logMessagesList.iterator());
+        info.setProtectHeadersOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        include.setCharacterEncoding("ISO-2022-JP");
+
+        Mockito.verify(orig, 
never()).setCharacterEncoding(Mockito.anyString());
+        ArgumentCaptor<String> logCaptor = 
ArgumentCaptor.forClass(String.class);
+        verify(requestProgressTracker, times(1)).log(logCaptor.capture());
+        assertTrue(logCaptor.getValue().startsWith("ERROR: "));
+    }
+
+    @Test
+    public void testCharacterEncodingCharsetVariantProtectedOnInclude() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        
when(requestData.getActiveServletName()).thenReturn(ACTIVE_SERVLET_NAME);
+        when(orig.getCharacterEncoding()).thenReturn("UTF-8");
+        ArrayList<String> logMessagesList = new 
ArrayList<>(Arrays.asList(logMessages));
+        when(requestProgressTracker.getMessages()).thenAnswer(invocation -> 
logMessagesList.iterator());
+        info.setProtectHeadersOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        include.setCharacterEncoding(StandardCharsets.UTF_16);
+
+        Mockito.verify(orig, 
never()).setCharacterEncoding(Mockito.anyString());
+        Mockito.verify(orig, 
never()).setCharacterEncoding(Mockito.any(Charset.class));
+    }
+
+    @Test
+    public void testCharacterEncodingUnchangedNotFlaggedOnInclude() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        when(orig.getCharacterEncoding()).thenReturn("UTF-8");
+        info.setProtectHeadersOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        include.setCharacterEncoding("utf-8");
+
+        Mockito.verify(orig, times(1)).setCharacterEncoding("utf-8");
+        Mockito.verifyNoInteractions(requestProgressTracker);
+    }
+
+    @Test
+    public void testCharacterEncodingDelegatedOutsideInclude() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.FORWARD);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+
+        final HttpServletResponse response = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        response.setCharacterEncoding("ISO-2022-JP");
+
+        Mockito.verify(orig, times(1)).setCharacterEncoding("ISO-2022-JP");
+    }
+
+    @Test
+    public void testContentTypeCharsetChangeDetectedOnInclude() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        when(orig.getContentType()).thenReturn("text/html;charset=UTF-8");
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        
when(requestData.getActiveServletName()).thenReturn(ACTIVE_SERVLET_NAME);
+        ArrayList<String> logMessagesList = new 
ArrayList<>(Arrays.asList(logMessages));
+        when(requestProgressTracker.getMessages()).thenAnswer(invocation -> 
logMessagesList.iterator());
+        info.setCheckContentTypeOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        Throwable throwable = null;
+        try {
+            include.setContentType("text/html;charset=UTF-7");
+        } catch (RuntimeException e) {
+            throwable = e;
+        }
+        assertNotNull("Expected a RuntimeException for the charset change.", 
throwable);
+        Mockito.verify(orig, never()).setContentType(Mockito.anyString());
+    }
+
+    @Test
+    public void testContentTypeSameMimeAndCharsetCaseInsensitiveNotFlagged() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        when(orig.getContentType()).thenReturn("text/html; charset=UTF-8");
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        info.setProtectHeadersOnInclude(true);
+        info.setCheckContentTypeOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        include.setContentType("TEXT/HTML;charset=utf-8");
+
+        Mockito.verify(orig, 
times(1)).setContentType("TEXT/HTML;charset=utf-8");
+        Mockito.verifyNoInteractions(requestProgressTracker);
+    }
+
+    @Test
+    public void testCharacterEncodingCheckContentTypeOnIncludeThrows() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        
when(requestData.getActiveServletName()).thenReturn(ACTIVE_SERVLET_NAME);
+        when(orig.getContentType()).thenReturn("text/html;charset=UTF-8");
+        when(orig.getCharacterEncoding()).thenReturn("UTF-8");
+        ArrayList<String> logMessagesList = new 
ArrayList<>(Arrays.asList(logMessages));
+        when(requestProgressTracker.getMessages()).thenAnswer(invocation -> 
logMessagesList.iterator());
+        info.setCheckContentTypeOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        Throwable throwable = null;
+        try {
+            include.setCharacterEncoding("ISO-2022-JP");
+        } catch (RuntimeException e) {
+            throwable = e;
+        }
+        assertNotNull("Expected a RuntimeException for the character encoding 
change.", throwable);
+        Mockito.verify(orig, 
never()).setCharacterEncoding(Mockito.anyString());
+        Mockito.verify(requestProgressTracker, 
times(1)).log(Mockito.startsWith("ERROR: "));
+    }
+
+    @Test
+    public void testCharacterEncodingStillEnforcedAfterPreviousViolation() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        when(orig.getContentType()).thenReturn("text/html;charset=UTF-8");
+        when(orig.getCharacterEncoding()).thenReturn("UTF-8");
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        
when(requestData.getActiveServletName()).thenReturn(ACTIVE_SERVLET_NAME);
+        info.setCheckContentTypeOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        // a violation has already been detected earlier within this request
+        
when(requestProcessor.getContentTypeHeaderState()).thenReturn(ContentTypeHeaderState.VIOLATED);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        Throwable throwable = null;
+        try {
+            include.setCharacterEncoding("ISO-2022-JP");
+        } catch (RuntimeException e) {
+            throwable = e;
+        }
+        assertNotNull("Expected the repeated override attempt to still be 
blocked.", throwable);
+        Mockito.verify(orig, 
never()).setCharacterEncoding(Mockito.anyString());
+        // the short message must not require the RequestProgressTracker 
messages again
+        Mockito.verify(requestProgressTracker, never()).getMessages();
+    }
+
+    @Test
+    public void 
testCharacterEncodingNoViolationChecksOnCommittedResponseWhenSendRedirect() 
throws IOException {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        Mockito.when(orig.isCommitted()).thenReturn(true);
+
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        
when(requestData.getRequestProgressTracker()).thenReturn(mock(RequestProgressTracker.class));
+
+        final SlingJakartaHttpServletResponseImpl include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+        SlingJakartaHttpServletResponseImpl spyInclude = Mockito.spy(include);
+
+        spyInclude.sendRedirect("somewhere");
+
+        spyInclude.setCharacterEncoding("ISO-2022-JP");
+        Mockito.verify(orig, times(1)).setCharacterEncoding("ISO-2022-JP");
+        Mockito.verify(spyInclude, 
never()).checkCharacterEncodingOverride(Mockito.any());
+    }
+
+    @Test
+    public void 
testCharacterEncodingNoViolationChecksOnCommittedResponseWhenSendError() throws 
IOException {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(mock(SlingRequestProcessorImpl.class));
+        
when(requestData.getRequestProgressTracker()).thenReturn(mock(RequestProgressTracker.class));
+
+        final SlingJakartaHttpServletResponseImpl include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+        SlingJakartaHttpServletResponseImpl spyInclude = Mockito.spy(include);
+
+        spyInclude.sendError(501);
+        // send error will eventually commit the response, let's mock this
+        Mockito.when(orig.isCommitted()).thenReturn(true);
+
+        spyInclude.setCharacterEncoding("ISO-2022-JP");
+        Mockito.verify(orig, times(1)).setCharacterEncoding("ISO-2022-JP");
+        Mockito.verify(spyInclude, 
never()).checkCharacterEncodingOverride(Mockito.any());
+    }
+
+    @Test
+    public void testCharacterEncodingNullCharsetFallsBackToDelegate() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        
when(requestData.getActiveServletName()).thenReturn(ACTIVE_SERVLET_NAME);
+        when(orig.getContentType()).thenReturn("text/html;charset=UTF-8");
+        when(orig.getCharacterEncoding()).thenReturn("UTF-8");
+        ArrayList<String> logMessagesList = new 
ArrayList<>(Arrays.asList(logMessages));
+        when(requestProgressTracker.getMessages()).thenAnswer(invocation -> 
logMessagesList.iterator());
+        info.setProtectHeadersOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        include.setCharacterEncoding((Charset) null);
+
+        Mockito.verify(orig, 
never()).setCharacterEncoding(Mockito.anyString());
+        Mockito.verify(requestProgressTracker, 
times(1)).log(Mockito.startsWith("ERROR: "));
+    }
+
+    @Test
+    public void 
testContentTypeCharsetFallsBackToCurrentCharacterEncodingWhenNotInContentType() 
{
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        // current 'Content-Type' has no charset parameter, but the response
+        // already has a character encoding assigned
+        when(orig.getContentType()).thenReturn("text/html");
+        when(orig.getCharacterEncoding()).thenReturn("UTF-8");
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        
when(requestData.getActiveServletName()).thenReturn(ACTIVE_SERVLET_NAME);
+        ArrayList<String> logMessagesList = new 
ArrayList<>(Arrays.asList(logMessages));
+        when(requestProgressTracker.getMessages()).thenAnswer(invocation -> 
logMessagesList.iterator());
+        info.setCheckContentTypeOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        Throwable throwable = null;
+        try {
+            include.setContentType("text/html;charset=UTF-7");
+        } catch (RuntimeException e) {
+            throwable = e;
+        }
+        assertNotNull(
+                "Expected a RuntimeException since the charset differs from 
the current character encoding.",
+                throwable);
+        Mockito.verify(orig, never()).setContentType(Mockito.anyString());
+    }
+
+    @Test
+    public void testContentTypeQuotedCharsetParsedCorrectly() {
+        final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);
+        final RequestData requestData = mock(RequestData.class);
+        final DispatchingInfo info = new 
DispatchingInfo(DispatcherType.INCLUDE);
+        final RequestProgressTracker requestProgressTracker = 
mock(RequestProgressTracker.class);
+        when(requestData.getDispatchingInfo()).thenReturn(info);
+        when(orig.getContentType()).thenReturn("text/html; charset=\"UTF-8\"");
+        
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        info.setProtectHeadersOnInclude(true);
+        info.setCheckContentTypeOnInclude(true);
+
+        final SlingRequestProcessorImpl requestProcessor = 
mock(SlingRequestProcessorImpl.class);
+        
when(requestData.getSlingRequestProcessor()).thenReturn(requestProcessor);
+
+        final HttpServletResponse include = new 
SlingJakartaHttpServletResponseImpl(requestData, orig);
+
+        // same mime type and same (quoted vs. unquoted) charset must not be 
flagged
+        include.setContentType("text/html; charset=UTF-8");
+
+        Mockito.verify(orig, times(1)).setContentType("text/html; 
charset=UTF-8");
+        Mockito.verifyNoInteractions(requestProgressTracker);
+    }
+
     @Test
     public void testCookies() {
         final SlingJakartaHttpServletResponse orig = 
Mockito.mock(SlingJakartaHttpServletResponse.class);

Reply via email to