This is an automated email from the ASF dual-hosted git repository. dashorst pushed a commit to branch wicket-10.x in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 9d23805cd062a8b4a7b31b5b3964ab9a0f82ec7d Author: Pedro Santos <[email protected]> AuthorDate: Sun Sep 13 16:27:44 2026 -0300 Escape the window name in AjaxNewWindowNotifyingBehavior The behaviour asks the browser for window.name, remembers it in boundName, and on every later render writes it back into the precondition of its Ajax call inside a single-quoted JavaScript string literal, so that the call is only made when the name has changed. The name went in as is, so a quote or a backslash in it ended the literal and left the rest of the name as script rather than text. The name now goes through JavaScriptUtils#escapeQuotes, and so does the generated name that respond writes into window.name for a nameless window, since a subclass may override newWindowName. There was no test for the class; the new one covers the escaping of both literals and that a different name still reports a new window. Co-Authored-By: Claude Fable 5.1 <[email protected]> --- .../ajax/AjaxNewWindowNotifyingBehaviorTest.java | 140 +++++++++++++++++++++ .../ajax/AjaxNewWindowNotifyingBehavior.java | 10 +- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxNewWindowNotifyingBehaviorTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxNewWindowNotifyingBehaviorTest.java new file mode 100644 index 0000000000..a9bb77a540 --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxNewWindowNotifyingBehaviorTest.java @@ -0,0 +1,140 @@ +/* + * 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.wicket.ajax; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.wicket.MockPageWithLinkAndComponent; +import org.apache.wicket.markup.html.WebComponent; +import org.apache.wicket.markup.html.link.Link; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AjaxNewWindowNotifyingBehavior} + */ +class AjaxNewWindowNotifyingBehaviorTest extends WicketTestCase +{ + private static final String PARAM_WINDOW_NAME = "windowName"; + + /** + * The name the browser reports is written back into the precondition of the behavior's Ajax + * call, inside a JavaScript string literal, so its quotes and backslashes have to be escaped. + */ + @Test + void windowNameIsEscapedInPrecondition() + { + AjaxNewWindowNotifyingBehavior behavior = new AjaxNewWindowNotifyingBehavior(); + MockPageWithLinkAndComponent page = newPage(behavior); + + tester.startPage(page); + assertNull(behavior.getWindowName()); + assertFalse(tester.getLastResponseAsString().contains("window.name !=="), + "no precondition before the behavior is bound to a window"); + + String name = "d:\\it's \"home\""; + tester.getRequest().setParameter(PARAM_WINDOW_NAME, name); + tester.executeBehavior(behavior); + assertEquals(name, behavior.getWindowName()); + + tester.startPage(page); + String response = tester.getLastResponseAsString(); + assertTrue(response.contains("return (window.name !== 'd:\\\\it\\'s \\\"home\\\"');"), + response); + assertFalse(response.contains("!== 'd:\\it's"), response); + } + + /** + * A nameless window gets a generated name, which is also placed inside a string literal. + */ + @Test + void generatedWindowNameIsEscaped() + { + AjaxNewWindowNotifyingBehavior behavior = new AjaxNewWindowNotifyingBehavior() + { + private static final long serialVersionUID = 1L; + + @Override + protected String newWindowName() + { + return "it's"; + } + }; + MockPageWithLinkAndComponent page = newPage(behavior); + + tester.startPage(page); + tester.getRequest().setParameter(PARAM_WINDOW_NAME, ""); + tester.executeBehavior(behavior); + + assertEquals("it's", behavior.getWindowName()); + assertTrue(tester.getLastResponseAsString().contains("window.name = 'it\\'s';"), + tester.getLastResponseAsString()); + } + + /** + * The behavior still notices a different window name once it is bound. + */ + @Test + void differentWindowNameIsNewWindow() + { + boolean[] newWindow = new boolean[1]; + AjaxNewWindowNotifyingBehavior behavior = new AjaxNewWindowNotifyingBehavior() + { + private static final long serialVersionUID = 1L; + + @Override + protected void onNewWindow(AjaxRequestTarget target) + { + newWindow[0] = true; + } + }; + MockPageWithLinkAndComponent page = newPage(behavior); + + tester.startPage(page); + tester.getRequest().setParameter(PARAM_WINDOW_NAME, "first"); + tester.executeBehavior(behavior); + assertFalse(newWindow[0]); + + tester.getRequest().setParameter(PARAM_WINDOW_NAME, "first"); + tester.executeBehavior(behavior); + assertFalse(newWindow[0]); + + tester.getRequest().setParameter(PARAM_WINDOW_NAME, "second"); + tester.executeBehavior(behavior); + assertTrue(newWindow[0]); + assertEquals("first", behavior.getWindowName()); + } + + private static MockPageWithLinkAndComponent newPage(AjaxNewWindowNotifyingBehavior behavior) + { + MockPageWithLinkAndComponent page = new MockPageWithLinkAndComponent(); + page.add(new WebComponent(MockPageWithLinkAndComponent.COMPONENT_ID).add(behavior)); + page.add(new Link<Void>(MockPageWithLinkAndComponent.LINK_ID) + { + private static final long serialVersionUID = 1L; + + @Override + public void onClick() + { + } + }); + return page; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxNewWindowNotifyingBehavior.java b/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxNewWindowNotifyingBehavior.java index f7e982d1ad..1bd100c75f 100644 --- a/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxNewWindowNotifyingBehavior.java +++ b/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxNewWindowNotifyingBehavior.java @@ -22,6 +22,7 @@ import org.apache.wicket.Component; import org.apache.wicket.Page; import org.apache.wicket.ajax.attributes.AjaxCallListener; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; +import org.apache.wicket.core.util.string.JavaScriptUtils; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.OnLoadHeaderItem; import org.apache.wicket.util.lang.Args; @@ -72,13 +73,15 @@ public class AjaxNewWindowNotifyingBehavior extends AbstractDefaultAjaxBehavior if (boundName != null) { - // already bound, send request only when changed + // already bound, send request only when changed; the name was reported by the browser + // and is placed inside a string literal, so it is escaped attributes.getAjaxCallListeners().add(new AjaxCallListener() { @Override public CharSequence getPrecondition(Component component) { - return String.format("return (window.name !== '%s');", boundName); + return String.format("return (window.name !== '%s');", + JavaScriptUtils.escapeQuotes(boundName)); } }); } @@ -109,7 +112,8 @@ public class AjaxNewWindowNotifyingBehavior extends AbstractDefaultAjaxBehavior { // create new name windowName = newWindowName(); - target.appendJavaScript(String.format("window.name = '%s';", windowName)); + target.appendJavaScript(String.format("window.name = '%s';", + JavaScriptUtils.escapeQuotes(windowName))); } // now bound to window
