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