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

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


The following commit(s) were added to refs/heads/master by this push:
     new ff2569d  SLING-12478 Add potential origins of violations to the 
RequestDispatcher include API (#53)
ff2569d is described below

commit ff2569d45e7c683535258e08a602c9d9212066c9
Author: Remo Liechti <[email protected]>
AuthorDate: Tue Nov 19 11:15:04 2024 +0100

    SLING-12478 Add potential origins of violations to the RequestDispatcher 
include API (#53)
---
 .../engine/impl/SlingHttpServletResponseImpl.java  | 101 +++++++++++++++++--
 .../impl/SlingHttpServletResponseImplTest.java     | 107 ++++++++++++++++-----
 2 files changed, 179 insertions(+), 29 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/engine/impl/SlingHttpServletResponseImpl.java 
b/src/main/java/org/apache/sling/engine/impl/SlingHttpServletResponseImpl.java
index f4f6d94..cb639c5 100644
--- 
a/src/main/java/org/apache/sling/engine/impl/SlingHttpServletResponseImpl.java
+++ 
b/src/main/java/org/apache/sling/engine/impl/SlingHttpServletResponseImpl.java
@@ -26,9 +26,19 @@ import javax.servlet.http.HttpServletResponseWrapper;
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.Spliterators;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import org.apache.sling.api.SlingException;
 import org.apache.sling.api.SlingHttpServletResponse;
@@ -41,6 +51,17 @@ public class SlingHttpServletResponseImpl extends 
HttpServletResponseWrapper imp
 
     private static final Logger LOG = 
LoggerFactory.getLogger(SlingHttpServletResponseImpl.class);
 
+    // this regex matches TIMER_START{ followed by any characters except }, 
and then
+    // a closing }. The part inside the braces is captured for later use.
+    private static final String REGEX_TIMER_START = "TIMER_START\\{([^}]+)\\}";
+
+    // this regex matches TIMER_END{ followed by one or more digits, a comma, 
any
+    // characters except }, and then a closing }. The part after the comma and
+    // before the closing brace is captured for later use.
+    private static final String REGEX_TIMER_END = 
"TIMER_END\\{\\d+,([^}]+)\\}";
+
+    private static final String TIMER_SEPARATOR = " -> ";
+
     public static class WriterAlreadyClosedException extends 
IllegalStateException {
         // just a marker class.
     }
@@ -316,7 +337,9 @@ public class SlingHttpServletResponseImpl extends 
HttpServletResponseWrapper imp
     }
 
     /**
-     * Checks if the 'Content-Type' header is being overridden and provides a 
message to log if it is.
+     * Checks if the 'Content-Type' header is being overridden and provides a
+     * message to log if it is.
+     *
      * @param contentType the 'Content-Type' value that is being set
      * @return an optional message to log
      */
@@ -339,28 +362,94 @@ public class SlingHttpServletResponseImpl extends 
HttpServletResponseWrapper imp
     }
 
     /**
-     * Retrieves the message to log when the 'Content-Type' header is changed 
via an include.
+     * Finds unmatched TIMER_START messages in a log of messages.
+     *
+     * @return a string containing the unmatched TIMER_START messages
+     */
+    private String findUnmatchedTimerStarts() {
+        Iterator<String> messages = 
requestData.getRequestProgressTracker().getMessages();
+        List<String> unmatchedStarts = new ArrayList<>();
+        Deque<String> timerDeque = new ArrayDeque<>();
+
+        Pattern startPattern = Pattern.compile(REGEX_TIMER_START);
+        Pattern endPattern = Pattern.compile(REGEX_TIMER_END);
+
+        while (messages.hasNext()) {
+            String message = messages.next();
+            Matcher startMatcher = startPattern.matcher(message);
+            Matcher endMatcher = endPattern.matcher(message);
+
+            // use a Deque to keep track of the timers that have been started. 
When
+            // an end timer is found, it is compared to the top of the deque. 
If they match,
+            // the timer is removed from the deque. If they don't match, the 
timer is added
+            // to the list of unmatched starts. As the deque is a LIFO data 
structure, the
+            // last timer that was started will be the first one to be ended. 
There is no
+            // Start1, Start2, End1 scenario, without an End2 in between.
+            if (startMatcher.find()) {
+                timerDeque.push(startMatcher.group(1));
+            } else if (endMatcher.find()) {
+                String endTimer = endMatcher.group(1);
+                if (!timerDeque.isEmpty() && 
timerDeque.peek().equals(endTimer)) {
+                    timerDeque.pop();
+                } else {
+                    unmatchedStarts.add(endTimer);
+                }
+            }
+        }
+
+        // ignore the first element, as it will never have a matching end, as 
it is the
+        // first timer started and is not finished processing
+        while (timerDeque.size() > 1) {
+            unmatchedStarts.add(timerDeque.pop());
+        }
+        StringBuilder sb = new StringBuilder();
+        for (String script : unmatchedStarts) {
+            sb.append(script).append(TIMER_SEPARATOR);
+        }
+        String ret = sb.toString();
+        if (ret.endsWith(TIMER_SEPARATOR)) {
+            ret = ret.substring(0, ret.length() - TIMER_SEPARATOR.length());
+        }
+        return ret;
+    }
+
+    /**
+     * Retrieves the message to log when the 'Content-Type' header is changed 
via an
+     * include.
+     *
      * @param currentContentType the current 'Content-Type' header
      * @param setContentType the 'Content-Type' header that is being set
      */
     private String getMessage(@Nullable String currentContentType, @Nullable 
String setContentType) {
+        String unmatchedStartTimers = findUnmatchedTimerStarts();
+        String allMessages = StreamSupport.stream(
+                        Spliterators.spliteratorUnknownSize(
+                                
requestData.getRequestProgressTracker().getMessages(), 0),
+                        false)
+                .collect(Collectors.joining(System.lineSeparator()));
         if (!isCheckContentTypeOnInclude()) {
             return String.format(
                     "Servlet %s tried to override the 'Content-Type' header 
from '%s' to '%s'. This is a violation of "
                             + "the RequestDispatcher.include() contract - "
-                            + 
"https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.";,
-                    requestData.getActiveServletName(), currentContentType, 
setContentType);
+                            + 
"https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.
 , Include stack: %s. All RequestProgressTracker messages: %s",
+                    requestData.getActiveServletName(),
+                    currentContentType,
+                    setContentType,
+                    unmatchedStartTimers,
+                    allMessages);
         }
         return String.format(
                 "Servlet %s tried to override the 'Content-Type' header from 
'%s' to '%s', however the"
                         + " %s forbids this via the %s configuration property. 
This is a violation of the "
                         + "RequestDispatcher.include() contract - "
-                        + 
"https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.";,
+                        + 
"https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.
 , Include stack: %s. All RequestProgressTracker messages: %s",
                 requestData.getActiveServletName(),
                 currentContentType,
                 setContentType,
                 Config.PID,
-                "sling.includes.checkcontenttype");
+                "sling.includes.checkcontenttype",
+                unmatchedStartTimers,
+                allMessages);
     }
 
     private static class ContentTypeChangeException extends SlingException {
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 7407d9a..b358cc1 100644
--- 
a/src/test/java/org/apache/sling/engine/impl/SlingHttpServletResponseImplTest.java
+++ 
b/src/test/java/org/apache/sling/engine/impl/SlingHttpServletResponseImplTest.java
@@ -23,16 +23,21 @@ import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletResponse;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.stream.Collectors;
 
 import org.apache.sling.api.SlingHttpServletResponse;
 import org.apache.sling.api.request.RequestProgressTracker;
 import org.apache.sling.engine.impl.request.DispatchingInfo;
 import org.apache.sling.engine.impl.request.RequestData;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -42,6 +47,43 @@ import static org.mockito.Mockito.when;
 public class SlingHttpServletResponseImplTest {
 
     private static final String ACTIVE_SERVLET_NAME = "activeServlet";
+    String[] logMessages = {
+        "0 TIMER_START{Request Processing}",
+        "6 COMMENT timer_end format is {<elapsed microseconds>,<timer name>} 
<optional message>",
+        "17 LOG Method=GET, PathInfo=null",
+        "20 TIMER_START{handleSecurity}",
+        "2104 TIMER_END{2081,handleSecurity} authenticator 
org.apache.sling.auth.core.impl.SlingAuthenticator@6367091e returns true",
+        "2478 TIMER_START{ResourceResolution}",
+        "2668 TIMER_END{189,ResourceResolution} URI=/content/slingshot.html 
resolves to Resource=JcrNodeResource, type=slingshot/Home, superType=null, 
path=/content/slingshot",
+        "2678 LOG Resource Path Info: SlingRequestPathInfo: 
path='/content/slingshot', selectorString='null', extension='html', 
suffix='null'",
+        "2678 TIMER_START{ServletResolution}",
+        "2683 TIMER_START{resolveServlet(/content/slingshot)}",
+        "3724 TIMER_END{1040,resolveServlet(/content/slingshot)} Using servlet 
/libs/slingshot/Home/html.jsp",
+        "3727 TIMER_END{1047,ServletResolution} URI=/content/slingshot.html 
handled by Servlet=/libs/slingshot/Home/html.jsp",
+        "3736 LOG Applying REQUESTfilters",
+        "3751 LOG Calling filter: 
com.composum.sling.nodes.mount.remote.RemoteRequestFilter",
+        "4722 TIMER_START{/libs/slingshot/Component/head.html.jsp#1}",
+        "3757 LOG Calling filter: org.apache.sling.i18n.impl.I18NFilter",
+        "4859 TIMER_END{135,/libs/slingshot/Component/head.html.jsp#1}",
+        "3765 LOG Calling filter: 
org.apache.sling.engine.impl.debug.RequestProgressTrackerLogFilter",
+        "2678 TIMER_START{ServletResolution}",
+        "2683 TIMER_START{resolveServlet(/content/slingshot)}",
+        "2678 TIMER_START{ServletResolution}",
+        "2683 TIMER_START{resolveServlet(/content/slingshot)}",
+        "3724 TIMER_END{1040,resolveServlet(/content/slingshot)} Using servlet 
/libs/slingshot/Home/html.jsp",
+        "3727 TIMER_END{1047,ServletResolution} URI=/content/slingshot.html 
handled by Servlet=/libs/slingshot/Home/html.jsp",
+        "3724 TIMER_END{1040,resolveServlet(/content/slingshot)} Using servlet 
/libs/slingshot/Home/html.jsp",
+        "3727 TIMER_END{1047,ServletResolution} URI=/content/slingshot.html 
handled by Servlet=/libs/slingshot/Home/html.jsp",
+        "3774 LOG Applying Componentfilters",
+        "3797 TIMER_START{/libs/slingshot/Home/html.jsp#0}",
+        "3946 LOG Adding bindings took 18 microseconds",
+        "4405 LOG Including resource JcrNodeResource, type=slingshot/Home, 
superType=null, path=/content/slingshot (SlingRequestPathInfo: 
path='/content/slingshot', selectorString='head', extension='html', 
suffix='null')",
+        "4414 TIMER_START{resolveServlet(/content/slingshot)}",
+        "4670 TIMER_END{253,resolveServlet(/content/slingshot)} Using servlet 
/libs/slingshot/Component/head.html.jsp",
+        "4673 LOG Applying Includefilters",
+        "4722 TIMER_START{/libs/slingshot/Component/head.html.jsp#1}",
+        "4749 LOG Adding bindings took 4 microseconds"
+    };
 
     @Test
     public void testReset() {
@@ -74,6 +116,9 @@ public class SlingHttpServletResponseImplTest {
         when(requestData.getDispatchingInfo()).thenReturn(info);
         
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.setProtectHeadersOnInclude(true);
 
         final HttpServletResponse include = new 
SlingHttpServletResponseImpl(requestData, orig);
@@ -90,12 +135,15 @@ public class SlingHttpServletResponseImplTest {
         Mockito.verify(orig, never()).setLocale(null);
         Mockito.verify(orig, never()).setBufferSize(4500);
 
-        verify(requestProgressTracker, times(1))
-                .log(String.format(
-                        "ERROR: Servlet %s tried to override the 
'Content-Type' header from 'null'"
-                                + " to 'text/plain'. This is a violation of 
the RequestDispatcher.include() contract -"
-                                + " 
https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.";,
-                        ACTIVE_SERVLET_NAME));
+        ArgumentCaptor<String> logCaptor = 
ArgumentCaptor.forClass(String.class);
+        verify(requestProgressTracker, times(1)).log(logCaptor.capture());
+        String logMessage = logCaptor.getValue();
+        assertEquals(
+                String.format(
+                        "ERROR: Servlet %s tried to override the 
'Content-Type' header from 'null' to 'text/plain'. This is a violation of the 
RequestDispatcher.include() contract - 
https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.
 , Include stack: /libs/slingshot/Component/head.html.jsp#1 -> 
/libs/slingshot/Home/html.jsp#0. All RequestProgressTracker messages: %s",
+                        ACTIVE_SERVLET_NAME,
+                        
Arrays.asList(logMessages).stream().collect(Collectors.joining(System.lineSeparator()))),
+                logMessage);
     }
 
     @Test
@@ -134,6 +182,8 @@ public class SlingHttpServletResponseImplTest {
         when(requestData.getDispatchingInfo()).thenReturn(info);
         when(orig.getContentType()).thenReturn("text/html");
         
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        ArrayList<String> logMessagesList = new 
ArrayList<>(Arrays.asList(logMessages));
+        when(requestProgressTracker.getMessages()).thenAnswer(invocation -> 
logMessagesList.iterator());
         info.setCheckContentTypeOnInclude(true);
 
         final HttpServletResponse include = new 
SlingHttpServletResponseImpl(requestData, orig);
@@ -146,24 +196,28 @@ public class SlingHttpServletResponseImplTest {
             throwable = e;
         }
         Mockito.verify(orig, never()).setContentType("application/json");
-        verify(requestProgressTracker, times(1))
-                .log(String.format(
-                        "ERROR: Servlet %s tried to override the 
'Content-Type' header from 'text/html'"
-                                + " to 'application/json', however the 
org.apache.sling.engine.impl.SlingMainServlet"
-                                + " forbids this via the 
sling.includes.checkcontenttype configuration property."
-                                + " This is a violation of the 
RequestDispatcher.include() contract -"
-                                + " 
https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.";,
-                        ACTIVE_SERVLET_NAME));
+        ArgumentCaptor<String> logCaptor = 
ArgumentCaptor.forClass(String.class);
+        verify(requestProgressTracker, times(1)).log(logCaptor.capture());
+        String logMessage = logCaptor.getValue();
+        assertTrue(logMessage.startsWith(String.format(
+                "ERROR: Servlet %s tried to override the 'Content-Type' header 
from 'text/html'"
+                        + " to 'application/json', however the 
org.apache.sling.engine.impl.SlingMainServlet"
+                        + " forbids this via the 
sling.includes.checkcontenttype configuration property."
+                        + " This is a violation of the 
RequestDispatcher.include() contract -"
+                        + " 
https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.
 , Include stack: /libs/slingshot/Component/head.html.jsp#1 -> 
/libs/slingshot/Home/html.jsp#0. All RequestProgressTracker messages: %s",
+                ACTIVE_SERVLET_NAME,
+                
Arrays.asList(logMessages).stream().collect(Collectors.joining(System.lineSeparator())))));
         assertNotNull("Expected a RuntimeException.", throwable);
-        assertEquals(
-                String.format(
+        assertTrue(throwable
+                .getMessage()
+                .startsWith(String.format(
                         "Servlet %s tried to override the 'Content-Type' 
header from 'text/html' to"
                                 + " 'application/json', however the 
org.apache.sling.engine.impl.SlingMainServlet forbids this"
                                 + " via the sling.includes.checkcontenttype 
configuration property."
                                 + " This is a violation of the 
RequestDispatcher.include() contract -"
-                                + " 
https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.";,
-                        ACTIVE_SERVLET_NAME),
-                throwable.getMessage());
+                                + " 
https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.
 , Include stack: /libs/slingshot/Component/head.html.jsp#1 -> 
/libs/slingshot/Home/html.jsp#0. All RequestProgressTracker messages: %s",
+                        ACTIVE_SERVLET_NAME,
+                        
Arrays.asList(logMessages).stream().collect(Collectors.joining(System.lineSeparator())))));
     }
 
     @Test
@@ -175,19 +229,26 @@ public class SlingHttpServletResponseImplTest {
         when(requestData.getDispatchingInfo()).thenReturn(info);
         when(orig.getContentType()).thenReturn("text/html");
         
when(requestData.getRequestProgressTracker()).thenReturn(requestProgressTracker);
+        ArrayList<String> logMessagesList = new 
ArrayList<>(Arrays.asList(logMessages));
+        when(requestProgressTracker.getMessages()).thenAnswer(invocation -> 
logMessagesList.iterator());
 
         final HttpServletResponse include = new 
SlingHttpServletResponseImpl(requestData, orig);
         
when(requestData.getActiveServletName()).thenReturn(ACTIVE_SERVLET_NAME);
         include.setContentType("application/json");
         Mockito.verify(orig, times(1)).setContentType("application/json");
 
-        verify(requestProgressTracker, times(1))
-                .log(String.format(
+        ArgumentCaptor<String> logCaptor = 
ArgumentCaptor.forClass(String.class);
+        verify(requestProgressTracker, times(1)).log(logCaptor.capture());
+        String logMessage = logCaptor.getValue();
+        assertEquals(
+                String.format(
                         "WARN: Servlet %s tried to override the 'Content-Type' 
header from 'text/html'"
                                 + " to 'application/json'. This is a violation 
of the RequestDispatcher.include()"
                                 + " contract -"
-                                + " 
https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.";,
-                        ACTIVE_SERVLET_NAME));
+                                + " 
https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/requestdispatcher#include-javax.servlet.ServletRequest-javax.servlet.ServletResponse-.
 , Include stack: /libs/slingshot/Component/head.html.jsp#1 -> 
/libs/slingshot/Home/html.jsp#0. All RequestProgressTracker messages: %s",
+                        ACTIVE_SERVLET_NAME,
+                        
Arrays.asList(logMessages).stream().collect(Collectors.joining(System.lineSeparator()))),
+                logMessage);
     }
 
     @Test

Reply via email to