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

dashorst pushed a commit to branch wicket-8.x
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 3ebdd55d2b978258a8c7353e183ce5066108196f
Author: Emond Papegaaij <[email protected]>
AuthorDate: Fri Sep 25 22:12:25 2026 +0200

    Pass a client message to a WebSocketBehavior only where the client may 
invoke it
    
    A page has a single WebSocket connection, whichever of its behaviors opened 
it, and
    every text or binary message the client sends over it is delivered to every
    WebSocketBehavior of the page. WebSocketBehavior#onEvent handed such a 
message to
    onMessage() unconditionally, so a behavior of a component that is not 
visible or not
    enabled - including one the IAuthorizationStrategy does not allow to render 
or to be
    enabled, and one inside such a container - received the messages of the 
client,
    together with a WebSocketRequestHandler to answer them.
    
    An Ajax behavior of that same component is not invoked: 
ListenerRequestHandler asks
    Behavior#canCallListener(Component) first. onMessage() is the WebSocket 
counterpart of
    such a listener, so ask the same question before calling it. A behavior 
that has to
    receive the messages of the client regardless overrides canCallListener(), 
exactly as
    an Ajax behavior does.
    
    Only the two onMessage() callbacks are affected. onPush(), onConnect(), 
onClose(),
    onError() and onAbort() are notifications of the server and keep reaching 
every
    behavior, so a component that is not visible can still be told to become 
visible. The
    event broadcast itself is unchanged: a component handling a 
WebSocketTextPayload in its
    own onEvent() receives it as it receives any other event, and the user 
guide now says
    that it has to make its own checks.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../wicket/protocol/ws/api/WebSocketBehavior.java  |  65 ++++-
 .../WebSocketBehaviorCanCallListenerTest.java      | 281 +++++++++++++++++++++
 .../nativewebsockets/nativewebsockets_2.adoc       |   4 +
 3 files changed, 344 insertions(+), 6 deletions(-)

diff --git 
a/wicket-native-websocket/wicket-native-websocket-core/src/main/java/org/apache/wicket/protocol/ws/api/WebSocketBehavior.java
 
b/wicket-native-websocket/wicket-native-websocket-core/src/main/java/org/apache/wicket/protocol/ws/api/WebSocketBehavior.java
index 30a65e034b..72ee2b5059 100644
--- 
a/wicket-native-websocket/wicket-native-websocket-core/src/main/java/org/apache/wicket/protocol/ws/api/WebSocketBehavior.java
+++ 
b/wicket-native-websocket/wicket-native-websocket-core/src/main/java/org/apache/wicket/protocol/ws/api/WebSocketBehavior.java
@@ -33,15 +33,34 @@ import 
org.apache.wicket.protocol.ws.api.message.ConnectedMessage;
 import org.apache.wicket.protocol.ws.api.message.ErrorMessage;
 import org.apache.wicket.protocol.ws.api.message.IWebSocketPushMessage;
 import org.apache.wicket.protocol.ws.api.message.TextMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * A behavior that provides optional callbacks for the WebSocket
  * messages (connect, message, close)
+ * <p>
+ * A page has a single WebSocket connection, whichever of its behaviors opened 
it, and a message
+ * the client sends over it is delivered to every WebSocketBehavior of the 
page. A message sent by
+ * the client is passed to {@link #onMessage(WebSocketRequestHandler, 
TextMessage)} or
+ * {@link #onMessage(WebSocketRequestHandler, BinaryMessage)} only when
+ * {@link #canCallListener(Component)} allows it for the component this 
behavior is attached to,
+ * the same check that decides whether the client may invoke an Ajax behavior 
of that component.
+ * By default, therefore, a behavior of a component that is not visible or not 
enabled - including
+ * one the {@link org.apache.wicket.authorization.IAuthorizationStrategy} does 
not allow to render
+ * or to be enabled - does not receive the messages of the client. Override
+ * {@link #canCallListener(Component)} to receive them regardless.
+ * </p>
+ * <p>
+ * The other callbacks are notifications of the server and are always called.
+ * </p>
  *
  * @since 6.0
  */
 public abstract class WebSocketBehavior extends BaseWebSocketBehavior
 {
+       private static final Logger LOG = 
LoggerFactory.getLogger(WebSocketBehavior.class);
+
        public WebSocketBehavior()
        {
        }
@@ -59,15 +78,21 @@ public abstract class WebSocketBehavior extends 
BaseWebSocketBehavior
 
                        if (payload instanceof WebSocketTextPayload)
                        {
-                               WebSocketTextPayload textPayload = 
(WebSocketTextPayload) payload;
-                               TextMessage data = textPayload.getMessage();
-                               onMessage(webSocketHandler, data);
+                               if (canReceiveClientMessage(component))
+                               {
+                                       WebSocketTextPayload textPayload = 
(WebSocketTextPayload) payload;
+                                       TextMessage data = 
textPayload.getMessage();
+                                       onMessage(webSocketHandler, data);
+                               }
                        }
                        else if (wsPayload instanceof WebSocketBinaryPayload)
                        {
-                               WebSocketBinaryPayload binaryPayload = 
(WebSocketBinaryPayload) wsPayload;
-                               BinaryMessage binaryData = 
binaryPayload.getMessage();
-                               onMessage(webSocketHandler, binaryData);
+                               if (canReceiveClientMessage(component))
+                               {
+                                       WebSocketBinaryPayload binaryPayload = 
(WebSocketBinaryPayload) wsPayload;
+                                       BinaryMessage binaryData = 
binaryPayload.getMessage();
+                                       onMessage(webSocketHandler, binaryData);
+                               }
                        }
                        else if (wsPayload instanceof WebSocketConnectedPayload)
                        {
@@ -102,6 +127,26 @@ public abstract class WebSocketBehavior extends 
BaseWebSocketBehavior
                }
        }
 
+       /**
+        * Decides whether a message sent by the client is passed on to this 
behavior, with the check
+        * that decides whether the client may invoke a listener of it.
+        *
+        * @param component
+        *          the component this behavior is attached to
+        * @return {@code true} if {@link #canCallListener(Component)} allows it
+        */
+       private boolean canReceiveClientMessage(Component component)
+       {
+               if (canCallListener(component))
+               {
+                       return true;
+               }
+
+               LOG.debug("A message of the client is not passed to {} of 
component '{}', because " +
+                               "canCallListener() does not allow it.", 
getClass().getName(), component.getPageRelativePath());
+               return false;
+       }
+
        /**
         * A callback method called when there is a message pushed/broadcasted 
by the
         * server, e.g. pushed by a backend service
@@ -160,6 +205,10 @@ public abstract class WebSocketBehavior extends 
BaseWebSocketBehavior
 
        /**
         * A callback method called when there is a text message sent by the 
client
+        * <p>
+        * It is only called when {@link #canCallListener(Component)} allows it 
for the component this
+        * behavior is attached to.
+        * </p>
         *
         * @param handler
         *          The request handler that can be used to send messages back 
to the client
@@ -172,6 +221,10 @@ public abstract class WebSocketBehavior extends 
BaseWebSocketBehavior
 
        /**
         * A callback method called when there is a binary message sent by the 
client
+        * <p>
+        * It is only called when {@link #canCallListener(Component)} allows it 
for the component this
+        * behavior is attached to.
+        * </p>
         *
         * @param handler
         *          The request handler that can be used to send messages back 
to the client
diff --git 
a/wicket-native-websocket/wicket-native-websocket-core/src/test/java/org/apache/wicket/protocol/ws/util/tester/WebSocketBehaviorCanCallListenerTest.java
 
b/wicket-native-websocket/wicket-native-websocket-core/src/test/java/org/apache/wicket/protocol/ws/util/tester/WebSocketBehaviorCanCallListenerTest.java
new file mode 100644
index 0000000000..aefdbfb5ee
--- /dev/null
+++ 
b/wicket-native-websocket/wicket-native-websocket-core/src/test/java/org/apache/wicket/protocol/ws/util/tester/WebSocketBehaviorCanCallListenerTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.protocol.ws.util.tester;
+
+import static org.junit.Assert.assertEquals;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import 
org.apache.wicket.authroles.authorization.strategies.role.RoleAuthorizationStrategy;
+import org.apache.wicket.authroles.authorization.strategies.role.Roles;
+import 
org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
+import org.apache.wicket.event.IEvent;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.protocol.ws.api.WebSocketBehavior;
+import org.apache.wicket.protocol.ws.api.WebSocketRequestHandler;
+import org.apache.wicket.protocol.ws.api.event.WebSocketTextPayload;
+import org.apache.wicket.protocol.ws.api.message.BinaryMessage;
+import org.apache.wicket.protocol.ws.api.message.ConnectedMessage;
+import org.apache.wicket.protocol.ws.api.message.IWebSocketPushMessage;
+import org.apache.wicket.protocol.ws.api.message.TextMessage;
+import org.apache.wicket.protocol.ws.api.registry.PageIdKey;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.WicketTester;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * A message sent by the client reaches {@link WebSocketBehavior#onMessage} 
only where the client
+ * may invoke a listener of the behavior, just like an Ajax behavior. What the 
server pushes, and
+ * the event broadcast itself, are not affected.
+ */
+public class WebSocketBehaviorCanCallListenerTest
+{
+       /** what reached which callback, as "callback:component id" */
+       private static final List<String> RECEIVED = new 
CopyOnWriteArrayList<>();
+
+       private WicketTester tester;
+
+       /**
+        * Records what reaches it, under the id of its component.
+        */
+       private static class RecordingBehavior extends WebSocketBehavior
+       {
+               private String componentId;
+
+               @Override
+               public void bind(Component component)
+               {
+                       super.bind(component);
+                       componentId = component.getId();
+               }
+
+               @Override
+               protected void onMessage(WebSocketRequestHandler handler, 
TextMessage message)
+               {
+                       RECEIVED.add("text:" + componentId);
+               }
+
+               @Override
+               protected void onMessage(WebSocketRequestHandler handler, 
BinaryMessage binaryMessage)
+               {
+                       RECEIVED.add("binary:" + componentId);
+               }
+
+               @Override
+               protected void onConnect(ConnectedMessage message)
+               {
+                       RECEIVED.add("connect:" + componentId);
+               }
+
+               @Override
+               protected void onPush(WebSocketRequestHandler handler, 
IWebSocketPushMessage message)
+               {
+                       RECEIVED.add("push:" + componentId);
+               }
+       }
+
+       private static class PushMessage implements IWebSocketPushMessage
+       {
+       }
+
+       /**
+        * A page with a behavior on the page and on containers in every state 
that matters.
+        */
+       public static class TestPage extends WebPage implements 
IMarkupResourceStreamProvider
+       {
+               public TestPage()
+               {
+                       add(new RecordingBehavior());
+
+                       add(container("visible"));
+
+                       WebMarkupContainer hidden = container("hidden");
+                       hidden.setVisible(false);
+                       hidden.add(container("hiddenChild"));
+                       add(hidden);
+
+                       WebMarkupContainer disabled = container("disabled");
+                       disabled.setEnabled(false);
+                       add(disabled);
+
+                       WebMarkupContainer renderDenied = 
container("renderDenied");
+                       
MetaDataRoleAuthorizationStrategy.authorize(renderDenied, RENDER, "ADMIN");
+                       add(renderDenied);
+
+                       WebMarkupContainer enableDenied = 
container("enableDenied");
+                       
MetaDataRoleAuthorizationStrategy.authorize(enableDenied, ENABLE, "ADMIN");
+                       add(enableDenied);
+
+                       WebMarkupContainer optedIn = new 
WebMarkupContainer("optedIn");
+                       optedIn.setVisible(false);
+                       optedIn.add(new RecordingBehavior()
+                       {
+                               @Override
+                               public boolean canCallListener(Component 
component)
+                               {
+                                       return true;
+                               }
+                       });
+                       add(optedIn);
+
+                       // a component handling the payload itself receives it 
as every event broadcast does
+                       add(new WebMarkupContainer("ownOnEvent")
+                       {
+                               @Override
+                               public void onEvent(IEvent<?> event)
+                               {
+                                       if (event.getPayload() instanceof 
WebSocketTextPayload)
+                                       {
+                                               RECEIVED.add("onEvent:" + 
getId());
+                                       }
+                               }
+                       }.setVisible(false));
+               }
+
+               private static WebMarkupContainer container(String id)
+               {
+                       WebMarkupContainer container = new 
WebMarkupContainer(id);
+                       container.add(new RecordingBehavior());
+                       return container;
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class<?> containerClass)
+               {
+                       return new 
StringResourceStream("<html><head></head><body>" +
+                               "<div wicket:id=\"visible\"></div>" +
+                               "<div wicket:id=\"hidden\"><div 
wicket:id=\"hiddenChild\"></div></div>" +
+                               "<div wicket:id=\"disabled\"></div>" +
+                               "<div wicket:id=\"renderDenied\"></div>" +
+                               "<div wicket:id=\"enableDenied\"></div>" +
+                               "<div wicket:id=\"optedIn\"></div>" +
+                               "<div wicket:id=\"ownOnEvent\"></div>" +
+                               "</body></html>");
+               }
+       }
+
+       @Before
+       public void before()
+       {
+               RECEIVED.clear();
+
+               tester = new WicketTester();
+               tester.getApplication()
+                       .getSecuritySettings()
+                       .setAuthorizationStrategy(
+                               new RoleAuthorizationStrategy(roles -> 
roles.hasAnyRole(new Roles("USER"))));
+       }
+
+       @After
+       public void after()
+       {
+               tester.destroy();
+       }
+
+       private static Set<String> expected(String callback, String... 
componentIds)
+       {
+               Set<String> expected = new TreeSet<>();
+               for (String componentId : componentIds)
+               {
+                       expected.add(callback + ":" + componentId);
+               }
+               return expected;
+       }
+
+       private static Set<String> received()
+       {
+               return new TreeSet<>(RECEIVED);
+       }
+
+       @Test
+       public void aTextMessageReachesOnlyTheBehaviorsTheClientMayInvoke()
+       {
+               TestPage page = tester.startPage(TestPage.class);
+
+               WebSocketTester webSocketTester = new WebSocketTester(tester, 
page);
+               try
+               {
+                       RECEIVED.clear();
+                       webSocketTester.sendMessage("a message");
+
+                       Set<String> expected = expected("text", page.getId(), 
"visible", "optedIn");
+                       expected.add("onEvent:ownOnEvent");
+                       assertEquals(expected, received());
+               }
+               finally
+               {
+                       webSocketTester.destroy();
+               }
+       }
+
+       @Test
+       public void aBinaryMessageReachesOnlyTheBehaviorsTheClientMayInvoke()
+       {
+               TestPage page = tester.startPage(TestPage.class);
+
+               WebSocketTester webSocketTester = new WebSocketTester(tester, 
page);
+               try
+               {
+                       RECEIVED.clear();
+                       byte[] message = "a 
message".getBytes(StandardCharsets.UTF_8);
+                       webSocketTester.sendMessage(message, 0, message.length);
+
+                       assertEquals(expected("binary", page.getId(), 
"visible", "optedIn"), received());
+               }
+               finally
+               {
+                       webSocketTester.destroy();
+               }
+       }
+
+       @Test
+       public void theNotificationsOfTheServerReachEveryBehavior()
+       {
+               TestPage page = tester.startPage(TestPage.class);
+
+               WebSocketTester webSocketTester = new WebSocketTester(tester, 
page);
+               try
+               {
+                       String[] everyBehavior = { page.getId(), "visible", 
"hidden", "hiddenChild", "disabled",
+                                       "renderDenied", "enableDenied", 
"optedIn" };
+
+                       assertEquals(expected("connect", everyBehavior), 
received());
+
+                       RECEIVED.clear();
+                       webSocketTester.broadcast(tester.getApplication(), 
tester.getHttpSession().getId(),
+                               new PageIdKey(page.getPageId()), new 
PushMessage());
+
+                       assertEquals(expected("push", everyBehavior), 
received());
+               }
+               finally
+               {
+                       webSocketTester.destroy();
+               }
+       }
+}
diff --git 
a/wicket-user-guide/src/main/asciidoc/nativewebsockets/nativewebsockets_2.adoc 
b/wicket-user-guide/src/main/asciidoc/nativewebsockets/nativewebsockets_2.adoc
index 85475228e7..a43e0aa901 100644
--- 
a/wicket-user-guide/src/main/asciidoc/nativewebsockets/nativewebsockets_2.adoc
+++ 
b/wicket-user-guide/src/main/asciidoc/nativewebsockets/nativewebsockets_2.adoc
@@ -129,6 +129,10 @@ public class MyPage extends WebPage {
 
 Use _message.getText()_ to read the message sent by the client and use 
_handler.push(String)_ to push a text message to the connected client. 
Additionally you can use _handler.add(Component...)_ to add Wicket components 
for re-render, _handler.prependJavaScript(CharSequence)_ and 
_handler.appendJavaScript(CharSequence)_ as you do with _AjaxRequestTarget_.
 
+A page has a single WebSocket connection, whichever of its behaviors opened 
it, and a message sent by the client over it is delivered to every 
_WebSocketBehavior_ of the page. Like an Ajax behavior, a _WebSocketBehavior_ 
is passed the messages of the client only when _canCallListener(Component)_ 
allows it for the component it is attached to, so by default a behavior of a 
component that is not visible or not enabled - including one the 
_IAuthorizationStrategy_ does not allow to render or  [...]
+
+The messages are also sent as an event to every component of the page, as a 
_WebSocketTextPayload_ or a _WebSocketBinaryPayload_, and like any other event 
broadcast this reaches components that are not visible or not enabled too. A 
component that handles these payloads in its own _onEvent()_ receives what the 
client sent and has to make any checks itself.
+
 * *WebSocketResource*
 
 Wicket allows one thread at a time to use a page instance to simplify the 
usage of the pages in multithreaded enviroment. When a WebSocket message is 
sent to a page Wicket needs to acquire the lock to that page to be able to pass 
the _IWebSocketMessage_ to the _WebSocketBehavior_. This may be problematic 
when the application needs to send many messages from the client to the server.

Reply via email to