This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch maintenance-branch
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/maintenance-branch by this
push:
new 3c5797f32f4 CAUSEWAY-3976: [v2] backport: allow client URL rewrite
only if origin is considered the same based on what the server-side thinks
3c5797f32f4 is described below
commit 3c5797f32f491822d09c0223515c4d19fd4a4ff0
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..9adb4d81b68
--- /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, replacedUrl);\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=replacedUrl;\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;
+ }
+
+}