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;
+ }
+
+}