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

ahuber pushed a commit to branch 3973-reverse.proxy.fix
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 87d081b198b2e58f016360ee0b116945ac727074
Author: andi-huber <[email protected]>
AuthorDate: Fri Mar 13 11:12:19 2026 +0100

    CAUSEWAY-3976: [v2] backport: allow client URL rewrite only if origin is
    considered the
    same based on what the server-side thinks
---
 .../viewer/wicket/ui/exec/JavaScriptRedirect.java  | 93 ++++++++++++++++++++++
 .../causeway/viewer/wicket/ui/exec/Mediator.java   | 76 ++++--------------
 .../viewer/wicket/ui/exec/MediatorFactory.java     |  4 +-
 .../wicket/ui/exec/UrlBasedRedirectContext.java    | 66 +++++++++++++++
 4 files changed, 178 insertions(+), 61 deletions(-)

diff --git 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/JavaScriptRedirect.java
 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/JavaScriptRedirect.java
new file mode 100644
index 00000000000..fbb6042306a
--- /dev/null
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/JavaScriptRedirect.java
@@ -0,0 +1,93 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.viewer.wicket.ui.exec;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+
+/**
+ * Generates client-side java-script to help with page redirecting.
+ *
+ * @implNote in some certain reverse proxy scenarios Wicket (at the time of 
writing)
+ *  might not be able to produce the correct
+ *  URL origin for the currently rendered page.
+ *  We workaround that, by asking the client/browser, what its 
'window.location.origin' is
+ *  and rewrite redirect URLs if required.
+ *
+ * @apiNote {@link OriginRewrite} was introduced as a workaround,
+ *  perhaps can be removed in future versions,
+ *  when reverting non-rewriting logic
+ */
+@AllArgsConstructor
+@Getter @Accessors(fluent = true)
+final class JavaScriptRedirect {
+       
+       private final OriginRewrite originRewrite;
+       private final String url;
+
+    enum OriginRewrite {
+        DISABLED,
+        ENABLED;
+        boolean isDisabled() { return this!=ENABLED; }
+    }
+
+    String javascriptFor_newWindow() {
+
+        if(originRewrite.isDisabled())
+            return String.format("function(){\n"
+                       + "    const url = '%s';\n"
+                       + "    
Wicket.Event.publish(Causeway.Topic.OPEN_IN_NEW_TAB, url);\n"
+                       + "}", url);
+
+        return String.format("function(){\n"
+                       + "    const url = '%s';\n"
+                       + "    const requiredOrigin = window.location.origin;\n"
+                       + "    const replacedUrl = 
url.startsWith(requiredOrigin)\n"
+                       + "      ? url\n"
+                       + "      : (() => {\n"
+                       + "          const urlObj = new URL(url);\n"
+                       + "          return requiredOrigin + urlObj.pathname + 
urlObj.search + urlObj.hash;\n"
+                       + "        })();\n"
+                       + "    
Wicket.Event.publish(Causeway.Topic.OPEN_IN_NEW_TAB, replacedUrl);\n"
+                       + "}", url);
+    }
+
+    String javascriptFor_sameWindow() {
+
+        if(originRewrite.isDisabled())
+               return String.format("function(){\n"
+                       + "    const url = '%s';\n"
+                       + "    window.location.href=url;\n"
+                       + "}", url);
+
+        return String.format("function(){\n"
+                       + "    const url = '%s';\n"
+                       + "    const requiredOrigin = window.location.origin;\n"
+                       + "    const replacedUrl = 
url.startsWith(requiredOrigin)\n"
+                       + "      ? url\n"
+                       + "      : (() => {\n"
+                       + "          const urlObj = new URL(url);\n"
+                       + "          return requiredOrigin + urlObj.pathname + 
urlObj.search + urlObj.hash;\n"
+                       + "        })();\n"
+                       + "    window.location.href=replacedUrl;\n"
+                       + "}", url);
+    }
+
+}
diff --git 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/Mediator.java
 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/Mediator.java
index 499ef3321e7..634eb2c1c99 100644
--- 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/Mediator.java
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/Mediator.java
@@ -26,11 +26,11 @@
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.viewer.wicket.model.models.ActionModel;
 import 
org.apache.causeway.viewer.wicket.model.models.RedirectRequestHandlerWithOpenUrlStrategy;
+import 
org.apache.causeway.viewer.wicket.ui.exec.JavaScriptRedirect.OriginRewrite;
 import org.apache.causeway.viewer.wicket.ui.pages.entity.EntityPage;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.behavior.AbstractAjaxBehavior;
 import org.apache.wicket.request.IRequestHandler;
-import org.apache.wicket.request.Url;
 import org.apache.wicket.request.cycle.RequestCycle;
 import org.springframework.lang.NonNull;
 import org.springframework.lang.Nullable;
@@ -68,7 +68,7 @@ final class Mediator {
      * either {@link 
ExecutionResultHandlingStrategy#OPEN_URL_IN_NEW_BROWSER_WINDOW}
      * or {@link 
ExecutionResultHandlingStrategy#OPEN_URL_IN_SAME_BROWSER_WINDOW}
      */
-       private final String url;
+       private final UrlBasedRedirectContext urlBasedRedirectContext;
 
     enum ExecutionResultHandlingStrategy {
         REDIRECT_TO_PAGE,
@@ -108,7 +108,7 @@ static Mediator openUrlInBrowser(
                 openUrlStrategy.isNewWindow()
                     ? 
ExecutionResultHandlingStrategy.OPEN_URL_IN_NEW_BROWSER_WINDOW
                     : 
ExecutionResultHandlingStrategy.OPEN_URL_IN_SAME_BROWSER_WINDOW,
-                null, null, ajaxTarget, url);
+                null, null, ajaxTarget, UrlBasedRedirectContext.of(url));
     }
 
     void handle() {
@@ -123,13 +123,15 @@ void handle() {
                 return;
             }
             case OPEN_URL_IN_NEW_BROWSER_WINDOW: {
-                final String fullUrl = expanded(RequestCycle.get(), url());
-                scheduleJs(ajaxTarget(), javascriptFor_newWindow(fullUrl), 
100);
+               var js = urlBasedRedirectContext.createJavaScriptRedirect()
+                        .javascriptFor_newWindow();
+                    scheduleJs(ajaxTarget, js, 100);
                 return;
             }
             case OPEN_URL_IN_SAME_BROWSER_WINDOW: {
-                final String fullUrl = expanded(RequestCycle.get(), url());
-                scheduleJs(ajaxTarget(), javascriptFor_sameWindow(fullUrl), 
100);
+               var js = urlBasedRedirectContext.createJavaScriptRedirect()
+                        .javascriptFor_sameWindow();
+                    scheduleJs(ajaxTarget, js, 100);
                 return;
             }
             case SCHEDULE_HANDLER: {
@@ -152,14 +154,17 @@ void handle() {
                     var relativeDownloadPageUri = 
TextUtils.cutter(streamingBehavior.getCallbackUrl().toString())
                         .keepAfterLast("/")
                         .getValue();
-                    scheduleJs(ajaxTarget, 
javascriptFor_sameWindow(relativeDownloadPageUri), 10);
+                    // never rewrite relative URLs
+                    var js = new JavaScriptRedirect(OriginRewrite.DISABLED, 
relativeDownloadPageUri)
+                            .javascriptFor_sameWindow();
+                    scheduleJs(ajaxTarget, js, 10);
                 } else if(requestHandler instanceof 
RedirectRequestHandlerWithOpenUrlStrategy) {
                        var redirectHandler = 
(RedirectRequestHandlerWithOpenUrlStrategy) requestHandler;
-                    var fullUrl = expanded(requestCycle, 
redirectHandler.getRedirectUrl());
+                       var jsFactory = 
UrlBasedRedirectContext.of(redirectHandler.getRedirectUrl())
+                            .createJavaScriptRedirect();
                     var js = redirectHandler.getOpenUrlStrategy().isNewWindow()
-                        ? javascriptFor_newWindow(fullUrl)
-                        : javascriptFor_sameWindow(fullUrl);
-
+                        ? jsFactory.javascriptFor_newWindow()
+                        : jsFactory.javascriptFor_sameWindow();
                     scheduleJs(ajaxTarget, js, 100);
                 } else {
                     throw _Exceptions.unrecoverable(
@@ -173,53 +178,6 @@ void handle() {
 
     // -- HELPER
 
-    /**
-     * @see #expanded(String)
-     */
-    private static String expanded(final RequestCycle requestCycle, final 
String url) {
-        String urlStr = expanded(url);
-        return requestCycle.getUrlRenderer().renderFullUrl(Url.parse(urlStr));
-    }
-
-    /**
-     * very simple template support, the idea being that 
"antiCache=${currentTimeMillis}"
-     * will be replaced automatically.
-     */
-    private static String expanded(String urlStr) {
-        if(urlStr.contains("antiCache=${currentTimeMillis}")) {
-            urlStr = urlStr.replace("antiCache=${currentTimeMillis}", 
"antiCache="+System.currentTimeMillis());
-        }
-        return urlStr;
-    }
-
-    private static String javascriptFor_newWindow(final CharSequence url) {
-        return String.format("function(){\n"
-                       + "    const url = '%s';\n"
-                       + "    const requiredOrigin = window.location.origin;\n"
-                       + "    const replacedUrl = 
url.startsWith(requiredOrigin)\n"
-                       + "      ? url\n"
-                       + "      : (() => {\n"
-                       + "          const urlObj = new URL(url);\n"
-                       + "          return requiredOrigin + urlObj.pathname + 
urlObj.search + urlObj.hash;\n"
-                       + "        })();\n"
-                       + "    
Wicket.Event.publish(Causeway.Topic.OPEN_IN_NEW_TAB, replacedUrl);\n"
-                       + "}", url);
-    }
-
-    private static String javascriptFor_sameWindow(final CharSequence url) {
-        return String.format("function(){\n"
-                       + "    const url = '%s';\n"
-                       + "    const requiredOrigin = window.location.origin;\n"
-                       + "    const replacedUrl = 
url.startsWith(requiredOrigin)\n"
-                       + "      ? url\n"
-                       + "      : (() => {\n"
-                       + "          const urlObj = new URL(url);\n"
-                       + "          return requiredOrigin + urlObj.pathname + 
urlObj.search + urlObj.hash;\n"
-                       + "        })();\n"
-                       + "    window.location.href=replacedUrl;\n"
-                       + "}", url);
-    }
-
     private static void scheduleJs(final AjaxRequestTarget target, final 
String js, final int millis) {
         // the timeout is needed to let Wicket release the channel
         target.appendJavaScript(String.format("setTimeout(%s, %d);", js, 
millis));
diff --git 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/MediatorFactory.java
 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/MediatorFactory.java
index c5c19d06da5..ac43d424f44 100644
--- 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/MediatorFactory.java
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/MediatorFactory.java
@@ -76,11 +76,11 @@ Mediator determineAndInterpretResult(
             // force full page reload
             case FORCE_STAY_ON_PAGE: return new Mediator(
                 
ExecutionResultHandlingStrategy.OPEN_URL_IN_SAME_BROWSER_WINDOW,
-                null, null, ajaxTarget, response.pageRedirect().toUrl()); // 
page redirect should point to current page
+                null, null, ajaxTarget, 
UrlBasedRedirectContext.of(response.pageRedirect().toUrl())); // page redirect 
should point to current page
             // open result page in new browser tab/win
             case FORCE_NEW_BROWSER_WINDOW: return new Mediator(
                 ExecutionResultHandlingStrategy.OPEN_URL_IN_NEW_BROWSER_WINDOW,
-                null, null, ajaxTarget, response.pageRedirect().toUrl()); // 
page redirect should point to action result
+                null, null, ajaxTarget, 
UrlBasedRedirectContext.of(response.pageRedirect().toUrl())); // page redirect 
should point to action result
         }
         
         throw _Exceptions.unmatchedCase(actionModel.columnActionModifier());
diff --git 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/UrlBasedRedirectContext.java
 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/UrlBasedRedirectContext.java
new file mode 100644
index 00000000000..c0e7abfe381
--- /dev/null
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/UrlBasedRedirectContext.java
@@ -0,0 +1,66 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.viewer.wicket.ui.exec;
+
+import 
org.apache.causeway.viewer.wicket.ui.exec.JavaScriptRedirect.OriginRewrite;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.cycle.RequestCycle;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+
+/**
+ * Provides additional context for URL based redirects.
+ */
+@AllArgsConstructor
+@Getter @Accessors(fluent = true)
+final class UrlBasedRedirectContext {
+       
+    private final String fullUrl;
+    private final boolean isSameOrigin; 
+
+    static UrlBasedRedirectContext of(
+            final String url) {
+        var urlRenderer = RequestCycle.get().getUrlRenderer(); 
//CAUSEWAY[3976] might not reliable work in reverse proxy situations
+        var origin = urlRenderer.renderFullUrl(Url.parse("./"));
+        var fullUrl = urlRenderer.renderFullUrl(Url.parse(interpolate(url)));
+        var isSameOrigin = fullUrl.startsWith(origin);
+        return new UrlBasedRedirectContext(fullUrl, isSameOrigin);
+    }
+
+    JavaScriptRedirect createJavaScriptRedirect() {
+        return new JavaScriptRedirect(
+                isSameOrigin
+                    ? OriginRewrite.ENABLED
+                    : OriginRewrite.DISABLED,
+                fullUrl());
+    }
+
+    /**
+     * very simple template support, the idea being that 
"antiCache=${currentTimeMillis}"
+     * will be replaced automatically.
+     */
+    private static String interpolate(final String urlStr) {
+        return urlStr.contains("antiCache=${currentTimeMillis}")
+            ? urlStr.replace("antiCache=${currentTimeMillis}", 
"antiCache="+System.currentTimeMillis())
+            : urlStr;
+    }
+
+}

Reply via email to