Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:allow explicitly reveiving certain dom events for inert elements #19121

Merged
merged 14 commits into from
Apr 10, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -391,4 +391,14 @@ default public DomListenerRegistration preventDefault() {
return this;
}

/**
* Configures the event listener to bypass the server side security checks
* for modality. Handle with care! Can be ok when transferring data from
* "non-ui" component events through the Element API, like e.g. geolocation
* events.
*
* @return the DomListenerRegistration for further configuration
*/
public DomListenerRegistration allowInert();

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import java.util.function.Function;
import java.util.stream.Stream;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.dom.DebouncePhase;
import com.vaadin.flow.dom.DisabledUpdateMode;
import com.vaadin.flow.dom.DomEvent;
Expand All @@ -39,7 +42,6 @@
import com.vaadin.flow.internal.JsonUtils;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.Json;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
Expand Down Expand Up @@ -114,6 +116,7 @@ private static class DomEventListenerWrapper
private int debounceTimeout = 0;
private EnumSet<DebouncePhase> debouncePhases = NO_TIMEOUT_PHASES;
private List<SerializableRunnable> unregisterHandlers;
private boolean allowInert;

private DomEventListenerWrapper(ElementListenerMap listenerMap,
String type, DomEventListener origin) {
Expand Down Expand Up @@ -265,6 +268,12 @@ private boolean isPropertySynchronized(String propertyName) {
.contains(JsonConstants.SYNCHRONIZE_PROPERTY_TOKEN
+ propertyName);
}

@Override
public DomListenerRegistration allowInert() {
allowInert = true;
return this;
}
}

/**
Expand Down Expand Up @@ -427,7 +436,15 @@ public void fireEvent(DomEvent event) {
if (listeners == null) {
return;
}
boolean isElementEnabled = event.getSource().isEnabled();

final boolean isElementEnabled = event.getSource().isEnabled();

final boolean isNavigationRequest = UI.BrowserNavigateEvent.EVENT_NAME
.equals(event.getType())
|| UI.BrowserLeaveNavigationEvent.EVENT_NAME
.equals(event.getType());
final boolean inert = event.getSource().getNode().isInert();

List<DomEventListenerWrapper> typeListeners = listeners
.get(event.getType());
if (typeListeners == null) {
Expand All @@ -436,6 +453,15 @@ public void fireEvent(DomEvent event) {

List<DomEventListener> listeners = new ArrayList<>();
for (DomEventListenerWrapper wrapper : typeListeners) {
if (!isNavigationRequest && inert && !wrapper.allowInert) {
// drop as inert
LoggerFactory.getLogger(ElementListenerMap.class.getName())
.info("Ignored listener invocation for {} event from "
+ "the client side for an inert {} element",
event.getType(), event.getSource().getTag());
continue;
}

if ((isElementEnabled
|| DisabledUpdateMode.ALWAYS.equals(wrapper.mode))
&& wrapper.matchesFilter(event.getEventData())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,15 @@
import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.PollEvent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.UI.BrowserLeaveNavigationEvent;
import com.vaadin.flow.component.UI.BrowserNavigateEvent;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.JsonObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Abstract invocation handler implementation with common methods.
* <p>
Expand Down Expand Up @@ -62,12 +59,12 @@ public Optional<Runnable> handle(UI ui, JsonObject invocationJson) {
// ignore RPC requests from the client side for the nodes that are
// invisible, disabled or inert
if (node.isInactive()) {
getLogger().trace("Ignored RPC for invocation handler '{}' from "
getLogger().info("Ignored RPC for invocation handler '{}' from "
tepi marked this conversation as resolved.
Show resolved Hide resolved
+ "the client side for an inactive (disabled or invisible) node id='{}'",
getClass().getName(), node.getId());
return Optional.empty();
} else if (!allowInert(ui, invocationJson) && node.isInert()) {
getLogger().trace(
getLogger().info(
tepi marked this conversation as resolved.
Show resolved Hide resolved
"Ignored RPC for invocation handler '{}' from "
+ "the client side for an inert node id='{}'",
getClass().getName(), node.getId());
Expand Down Expand Up @@ -116,22 +113,6 @@ private boolean isPollEventInvocation(JsonObject invocationJson) {
invocationJson.getString(JsonConstants.RPC_EVENT_TYPE));
}

private boolean isNavigationInvocation(JsonObject invocationJson) {
if (!invocationJson.hasKey(JsonConstants.RPC_EVENT_TYPE)) {
return false;
}
if (BrowserNavigateEvent.EVENT_NAME.equals(
invocationJson.getString(JsonConstants.RPC_EVENT_TYPE))) {
return true;
}
if (BrowserLeaveNavigationEvent.EVENT_NAME.equals(
invocationJson.getString(JsonConstants.RPC_EVENT_TYPE))) {
return true;
}
return false;

}

private boolean isPollingEnabledForUI(UI ui) {
return ui.getPollInterval() > 0;
}
Expand Down Expand Up @@ -201,8 +182,7 @@ private boolean isLegitimatePollEventInvocation(UI ui,
* the current invocation or not.
*/
protected boolean allowInert(UI ui, JsonObject invocationJson) {
return isValidPollInvocation(ui, invocationJson)
|| isNavigationInvocation(invocationJson);
return isValidPollInvocation(ui, invocationJson);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@

import java.util.Optional;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.dom.DomEvent;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.internal.nodefeature.ElementListenerMap;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.Json;
import elemental.json.JsonObject;

Expand Down Expand Up @@ -64,4 +64,9 @@ public Optional<Runnable> handleNode(StateNode node,
return Optional.empty();
}

@Override
protected boolean allowInert(UI ui, JsonObject invocationJson) {
// handled separately in ElementListenerMap
return true;
}
}
29 changes: 28 additions & 1 deletion flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import com.vaadin.flow.internal.nodefeature.ElementListenersTest;
import com.vaadin.flow.internal.nodefeature.ElementPropertyMap;
import com.vaadin.flow.internal.nodefeature.ElementStylePropertyMap;
import com.vaadin.flow.internal.nodefeature.InertData;
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
import com.vaadin.flow.server.MockVaadinServletService;
import com.vaadin.flow.server.StreamResource;
Expand All @@ -59,7 +60,6 @@
import com.vaadin.tests.util.AlwaysLockedVaadinSession;
import com.vaadin.tests.util.MockUI;
import com.vaadin.tests.util.TestUtil;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
Expand Down Expand Up @@ -382,6 +382,33 @@ public void listenerReceivesEvents() {
Assert.assertEquals(1, listenerCalls.get());
}

@Test
public void listenerReceivesEventsWithAllowInert() {
Element e = ElementFactory.createDiv();
// Inert the node, verify events no more passed through
InertData inertData = e.getNode().getFeature(InertData.class);
inertData.setInertSelf(true);
inertData.generateChangesFromEmpty();

AtomicInteger listenerCalls = new AtomicInteger(0);
DomEventListener myListener = event -> listenerCalls.incrementAndGet();

DomListenerRegistration domListenerRegistration = e
.addEventListener("click", myListener);
Assert.assertEquals(0, listenerCalls.get());
e.getNode().getFeature(ElementListenerMap.class)
.fireEvent(new DomEvent(e, "click", Json.createObject()));
// Event should not go through
Assert.assertEquals(0, listenerCalls.get());

// Now should pass inert check and get notified
domListenerRegistration.allowInert();
e.getNode().getFeature(ElementListenerMap.class)
.fireEvent(new DomEvent(e, "click", Json.createObject()));
Assert.assertEquals(1, listenerCalls.get());

}

@Test
public void getPropertyDefaults() {
Element element = ElementFactory.createDiv();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@

import com.vaadin.flow.component.ComponentTest.TestComponent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.dom.DomListenerRegistration;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.internal.nodefeature.InertData;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.Json;
import elemental.json.JsonObject;

Expand Down Expand Up @@ -53,12 +54,27 @@ public void testElementEventData() throws Exception {
ui.add(c);
AtomicInteger invocationData = new AtomicInteger(0);

element.addEventListener("test-event", e -> invocationData
.addAndGet((int) e.getEventData().getNumber("nr")));
DomListenerRegistration domListenerRegistration = element
.addEventListener("test-event", e -> invocationData
.addAndGet((int) e.getEventData().getNumber("nr")));
JsonObject eventData = Json.createObject();
eventData.put("nr", 123);
sendElementEvent(element, ui, "test-event", eventData);
Assert.assertEquals(123, invocationData.get());

// Also verify inert stops the event and allowInert allows to bypass
invocationData.set(0);
eventData.put("nr", 124);
InertData inertData = element.getNode().getFeature(InertData.class);
inertData.setInertSelf(true);
inertData.generateChangesFromEmpty();
sendElementEvent(element, ui, "test-event", eventData);
Assert.assertEquals(0, invocationData.get());
// explicitly allow this event listener even when element is inert
domListenerRegistration.allowInert();
sendElementEvent(element, ui, "test-event", eventData);
Assert.assertEquals(124, invocationData.get());

}

private static JsonObject createElementEventInvocation(Element element,
Expand Down