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

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/main by this push:
     new dea34403967 CAUSEWAY-3976: allow client URL rewrite only if origin is 
considered the same based on what the server-side thinks
dea34403967 is described below

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

    CAUSEWAY-3976: allow client URL rewrite only if origin is considered the
    same based on what the server-side thinks
---
 .../viewer/wicket/ui/exec/JavaScriptRedirect.java  | 90 ++++++++++++++++++++++
 .../causeway/viewer/wicket/ui/exec/Mediator.java   | 82 +++++---------------
 .../viewer/wicket/ui/exec/MediatorFactory.java     | 68 ++++++++--------
 .../wicket/ui/exec/UrlBasedRedirectContext.java    | 60 +++++++++++++++
 4 files changed, 200 insertions(+), 100 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..8f71f43bc5f
--- /dev/null
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/JavaScriptRedirect.java
@@ -0,0 +1,90 @@
+/*
+ *  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;
+
+/**
+ * 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
+ */
+record JavaScriptRedirect(
+        OriginRewrite originRewrite,
+        String url) {
+
+    enum OriginRewrite {
+        DISABLED,
+        ENABLED;
+        boolean isDisabled() { return this!=ENABLED; }
+    }
+
+    String javascriptFor_newWindow() {
+
+        if(originRewrite.isDisabled())
+            return String.format("""
+                    function(){
+                        const url = '%s';
+                        Wicket.Event.publish(Causeway.Topic.OPEN_IN_NEW_TAB, 
url);
+                    }""", url);
+
+        return String.format("""
+            function(){
+                const url = '%s';
+                const requiredOrigin = window.location.origin;
+                const replacedUrl = url.startsWith(requiredOrigin)
+                  ? url
+                  : (() => {
+                      const urlObj = new URL(url);
+                      return requiredOrigin + urlObj.pathname + urlObj.search 
+ urlObj.hash;
+                    })();
+                Wicket.Event.publish(Causeway.Topic.OPEN_IN_NEW_TAB, 
replacedUrl);
+            }""", url);
+    }
+
+    String javascriptFor_sameWindow() {
+
+        if(originRewrite.isDisabled())
+            return String.format("""
+                    function(){
+                        const url = '%s';
+                        window.location.href=url;
+                    }""", url);
+
+        return String.format("""
+            function(){
+                const url = '%s';
+                const requiredOrigin = window.location.origin;
+                const replacedUrl = url.startsWith(requiredOrigin)
+                  ? url
+                  : (() => {
+                      const urlObj = new URL(url);
+                      return requiredOrigin + urlObj.pathname + urlObj.search 
+ urlObj.hash;
+                    })();
+                window.location.href=replacedUrl;
+            }""", 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 f9d0b485bc4..3e3ea147de5 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
@@ -21,7 +21,6 @@
 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.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
@@ -33,6 +32,7 @@
 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.obj.DomainObjectPage;
 
 /**
@@ -61,7 +61,7 @@ record Mediator(
      * either {@link 
ExecutionResultHandlingStrategy#OPEN_URL_IN_NEW_BROWSER_WINDOW}
      * or {@link 
ExecutionResultHandlingStrategy#OPEN_URL_IN_SAME_BROWSER_WINDOW}
      */
-    String url) {
+    UrlBasedRedirectContext urlBasedRedirectContext) {
 
     enum ExecutionResultHandlingStrategy {
         REDIRECT_TO_PAGE,
@@ -101,7 +101,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() {
@@ -115,16 +115,18 @@ void handle() {
                 this.pageRedirect().apply();
             }
             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);
             }
             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);
             }
             case SCHEDULE_HANDLER -> {
-                var requestCycle = RequestCycle.get();
-                var ajaxTarget = 
requestCycle.find(AjaxRequestTarget.class).orElse(null);
+                final var requestCycle = RequestCycle.get();
+                final var ajaxTarget = 
requestCycle.find(AjaxRequestTarget.class).orElse(null);
                 final IRequestHandler requestHandler = handler();
 
                 if (ajaxTarget == null) {
@@ -142,13 +144,16 @@ 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 redirectHandler) {
-                    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(
@@ -160,55 +165,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(){
-                const url = '%s';
-                const requiredOrigin = window.location.origin;
-                const replacedUrl = url.startsWith(requiredOrigin)
-                  ? url
-                  : (() => {
-                      const urlObj = new URL(url);
-                      return requiredOrigin + urlObj.pathname + urlObj.search 
+ urlObj.hash;
-                    })();
-                Wicket.Event.publish(Causeway.Topic.OPEN_IN_NEW_TAB, 
replacedUrl);
-            }""", url);
-    }
-
-    private static String javascriptFor_sameWindow(final CharSequence url) {
-        return String.format("""
-            function(){
-                const url = '%s';
-                const requiredOrigin = window.location.origin;
-                const replacedUrl = url.startsWith(requiredOrigin)
-                  ? url
-                  : (() => {
-                      const urlObj = new URL(url);
-                      return requiredOrigin + urlObj.pathname + urlObj.search 
+ urlObj.hash;
-                    })();
-                window.location.href=replacedUrl;
-            }""", 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("setTimeout(%s, %d);".formatted(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 8de2add58a6..435d1500835 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
@@ -72,11 +72,11 @@ Mediator determineAndInterpretResult(
                 // force full page reload
                 case FORCE_STAY_ON_PAGE -> 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 -> 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
                 }
             : response;
     }
@@ -88,16 +88,12 @@ private IRequestHandler redirectHandler(
             final @NonNull OpenUrlStrategy openUrlStrategy,
             final @NonNull WebAppContextPath webAppContextPath) {
 
-        if(value instanceof java.net.URL) {
-            var url = (java.net.URL) value;
+        if(value instanceof java.net.URL url)
             return new 
RedirectRequestHandlerWithOpenUrlStrategy(url.toString());
-        }
-        if(value instanceof LocalResourcePath) {
-            var localResourcePath = (LocalResourcePath) value;
+        if(value instanceof LocalResourcePath localResourcePath)
             return new RedirectRequestHandlerWithOpenUrlStrategy(
                     
localResourcePath.getEffectivePath(webAppContextPath::prependContextPath),
                     localResourcePath.openUrlStrategy());
-        }
         return null;
     }
 
@@ -109,77 +105,75 @@ private Mediator actionResultResponse(
         final ActionResultResponseType responseType = 
actionResultModel.responseType();
         final ManagedObject resultAdapter = actionResultModel.resultAdapter();
 
-        switch(responseType) {
-        case COLLECTION: {
+        return switch (responseType) {
+        case COLLECTION -> {
             _Assert.assertTrue(resultAdapter instanceof PackedManagedObject);
             var collectionModel = CollectionModelStandalone
                     .forActionModel((PackedManagedObject)resultAdapter, 
actionModel);
             var pageRedirectRequest = PageRedirectRequest.forPage(
                     StandaloneCollectionPage.class, new 
StandaloneCollectionPage(collectionModel));
-            return Mediator.toPage(pageRedirectRequest);
+            yield Mediator.toPage(pageRedirectRequest);
         }
-        case OBJECT: {
+        case OBJECT -> {
             determineScalarAdapter(actionModel.getMetaModelContext(), 
resultAdapter); // intercepts collections
-            return Mediator.toDomainObjectPage(resultAdapter);
+            yield Mediator.toDomainObjectPage(resultAdapter);
         }
-        case SIGN_IN: {
+        case SIGN_IN -> {
             var signInPage = actionModel.getMetaModelContext()
                     .lookupServiceElseFail(PageClassRegistry.class)
                     .getPageClass(PageType.SIGN_IN);
             var pageRedirectRequest = 
PageRedirectRequest.forPageClass(signInPage);
-            return Mediator.toPage(pageRedirectRequest);
+            yield Mediator.toPage(pageRedirectRequest);
         }
-        case VALUE: {
+        case VALUE -> {
             var valueModel = new ValueModel(actionModel, resultAdapter);
             var valuePage = new ValuePage(valueModel, 
actionModel.getFriendlyName());
             var pageRedirectRequest = 
PageRedirectRequest.forPage(ValuePage.class, valuePage);
-            return Mediator.toPage(pageRedirectRequest);
+            yield Mediator.toPage(pageRedirectRequest);
         }
-        case VALUE_BLOB:
-        case VALUE_CLOB: {
+        case VALUE_BLOB, VALUE_CLOB -> {
             final Object value = resultAdapter.getPojo();
             IRequestHandler handler = 
LobRequestHandler.downloadHandler(actionModel.getAction(), value);
-            return Mediator.withHandler(handler);
+            yield Mediator.withHandler(handler);
         }
-        case VALUE_LOCALRESPATH_AJAX: {
+        case VALUE_LOCALRESPATH_AJAX -> {
             final LocalResourcePath localResPath = 
(LocalResourcePath)resultAdapter.getPojo();
             var webAppContextPath = 
actionModel.getMetaModelContext().getWebAppContextPath();
-            return Mediator
-                    .openUrlInBrowser(ajaxTarget, 
localResPath.getEffectivePath(webAppContextPath::prependContextPath), 
localResPath.openUrlStrategy());
+            yield Mediator
+                                .openUrlInBrowser(ajaxTarget, 
localResPath.getEffectivePath(webAppContextPath::prependContextPath), 
localResPath.openUrlStrategy());
         }
-        case VALUE_LOCALRESPATH_NOAJAX: {
+        case VALUE_LOCALRESPATH_NOAJAX -> {
             // open URL server-side redirect
             final LocalResourcePath localResPath = 
(LocalResourcePath)resultAdapter.getPojo();
             var webAppContextPath = 
actionModel.getMetaModelContext().getWebAppContextPath();
             IRequestHandler handler = redirectHandler(localResPath, 
localResPath.openUrlStrategy(), webAppContextPath);
-            return Mediator.withHandler(handler);
+            yield Mediator.withHandler(handler);
         }
-        case VALUE_URL_AJAX: {
+        case VALUE_URL_AJAX -> {
             final URL url = (URL)resultAdapter.getPojo();
-            return Mediator
-                    .openUrlInBrowser(ajaxTarget, url.toString(), 
OpenUrlStrategy.NEW_WINDOW); // default behavior
+            yield Mediator
+                                .openUrlInBrowser(ajaxTarget, url.toString(), 
OpenUrlStrategy.NEW_WINDOW); // default behavior
         }
-        case VALUE_URL_NOAJAX: {
+        case VALUE_URL_NOAJAX -> {
             // open URL server-side redirect
             final Object value = resultAdapter.getPojo();
             var webAppContextPath = 
actionModel.getMetaModelContext().getWebAppContextPath();
             IRequestHandler handler = redirectHandler(value, 
OpenUrlStrategy.NEW_WINDOW, webAppContextPath); // default behavior
-            return Mediator.withHandler(handler);
+            yield Mediator.withHandler(handler);
         }
-        case VOID_AS_EMPTY: {
+        case VOID_AS_EMPTY -> {
             var pageRedirectRequest = PageRedirectRequest
                 .forPage(VoidReturnPage.class, new VoidReturnPage(new 
VoidModel(), actionModel.getFriendlyName()));
-            return Mediator.toPage(pageRedirectRequest);
+            yield Mediator.toPage(pageRedirectRequest);
         }
-        case RELOAD: {
+        case RELOAD -> {
             var currentPage = 
PageRequestHandlerTracker.getLastHandler(RequestCycle.get()).getPage();
             var pageClass = currentPage.getClass();
             var pageRedirectRequest = PageRedirectRequest.forPage(pageClass, 
_Casts.uncheckedCast(currentPage));
-            return Mediator.toPage(pageRedirectRequest);
-        }
-        default:
-            throw _Exceptions.unmatchedCase(responseType);
+            yield Mediator.toPage(pageRedirectRequest);
         }
+        default -> throw _Exceptions.unmatchedCase(responseType);
+        };
     }
 
     private ManagedObject determineScalarAdapter(
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..65d8f26dcd7
--- /dev/null
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/UrlBasedRedirectContext.java
@@ -0,0 +1,60 @@
+/*
+ *  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.wicket.request.Url;
+import org.apache.wicket.request.cycle.RequestCycle;
+
+import 
org.apache.causeway.viewer.wicket.ui.exec.JavaScriptRedirect.OriginRewrite;
+
+/**
+ * Provides additional context for URL based redirects.
+ */
+record UrlBasedRedirectContext(
+        String fullUrl,
+        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