Skip to content

Commit 46ff0c5

Browse files
authored
fix: load Image/IFrame sources when disabled (CP: 24.10) (#24365) (CP: 24.9) (#24370)
When an Image or IFrame backed by a DownloadHandler lives inside a disabled component, the browser receives a 403 and the resource never loads. Image.setSrc(DownloadHandler) and IFrame.setSrc(DownloadHandler) now allow the resource to be served regardless of the owner's enabled state, since these sources are fetched passively as part of rendering rather than as a user action. Fixes #22772
1 parent 3a4baf3 commit 46ff0c5

8 files changed

Lines changed: 390 additions & 8 deletions

File tree

flow-html-components/src/main/java/com/vaadin/flow/component/html/IFrame.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,12 @@ public void setSrc(AbstractStreamResource src) {
190190
* {@link DownloadHandler}, as well as for other
191191
* {@link AbstractDownloadHandler} implementations.
192192
*
193+
* The handler is wrapped with {@link DownloadHandler#allowDisabled()} so
194+
* that the iframe content is still served when the component, or one of its
195+
* ancestors, is disabled. The browser fetches the content as part of
196+
* rendering rather than as a user action, so blocking the request on the
197+
* disabled state would leave the iframe empty.
198+
*
193199
* @see #setSrc(String)
194200
*
195201
* @param downloadHandler
@@ -201,7 +207,7 @@ public void setSrc(DownloadHandler downloadHandler) {
201207
// where it is 'attachment' by default
202208
handler.inline();
203209
}
204-
getElement().setAttribute("src", downloadHandler);
210+
getElement().setAttribute("src", downloadHandler.allowDisabled());
205211
}
206212

207213
/**

flow-html-components/src/main/java/com/vaadin/flow/component/html/Image.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ public void setSrc(AbstractStreamResource src) {
156156
* {@link DownloadHandler}, as well as for other
157157
* {@link AbstractDownloadHandler} implementations.
158158
*
159+
* The handler is wrapped with {@link DownloadHandler#allowDisabled()} so
160+
* that the image is still served when the component, or one of its
161+
* ancestors, is disabled. The browser fetches the image as part of
162+
* rendering rather than as a user action, so blocking the request on the
163+
* disabled state would leave the icon broken.
164+
*
159165
* @param downloadHandler
160166
* the download handler resource, not null
161167
*/
@@ -165,7 +171,7 @@ public void setSrc(DownloadHandler downloadHandler) {
165171
// where it is 'attachment' by default
166172
handler.inline();
167173
}
168-
getElement().setAttribute("src", downloadHandler);
174+
getElement().setAttribute("src", downloadHandler.allowDisabled());
169175
}
170176

171177
/**

flow-html-components/src/test/java/com/vaadin/flow/component/html/IFrameTest.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@
1515
*/
1616
package com.vaadin.flow.component.html;
1717

18-
import com.vaadin.flow.component.Component;
19-
import com.vaadin.flow.dom.Element;
20-
import com.vaadin.flow.server.streams.DownloadHandler;
21-
import com.vaadin.flow.server.streams.DownloadResponse;
22-
import com.vaadin.flow.server.streams.InputStreamDownloadHandler;
18+
import java.lang.reflect.Field;
2319

2420
import org.junit.Assert;
2521
import org.junit.Test;
22+
import org.mockito.ArgumentCaptor;
2623
import org.mockito.Mockito;
2724

28-
import java.lang.reflect.Field;
25+
import com.vaadin.flow.component.Component;
26+
import com.vaadin.flow.dom.DisabledUpdateMode;
27+
import com.vaadin.flow.dom.Element;
28+
import com.vaadin.flow.server.streams.DownloadHandler;
29+
import com.vaadin.flow.server.streams.DownloadResponse;
30+
import com.vaadin.flow.server.streams.InputStreamDownloadHandler;
2931

3032
public class IFrameTest extends ComponentTest {
3133

@@ -68,6 +70,29 @@ public void testHasAriaLabelIsImplemented() {
6870
super.testHasAriaLabelIsImplemented();
6971
}
7072

73+
@Test
74+
public void setSrc_downloadHandler_disabledUpdateModeIsAlways() {
75+
Element element = Mockito.mock(Element.class);
76+
class TestIFrame extends IFrame {
77+
@Override
78+
public Element getElement() {
79+
return element;
80+
}
81+
}
82+
// Plain lambda DownloadHandler, not an AbstractDownloadHandler subclass
83+
DownloadHandler lambda = event -> {
84+
};
85+
86+
new TestIFrame().setSrc(lambda);
87+
88+
ArgumentCaptor<DownloadHandler> captor = ArgumentCaptor
89+
.forClass(DownloadHandler.class);
90+
Mockito.verify(element).setAttribute(Mockito.eq("src"),
91+
captor.capture());
92+
Assert.assertEquals(DisabledUpdateMode.ALWAYS,
93+
captor.getValue().getDisabledUpdateMode());
94+
}
95+
7196
@Test
7297
public void downloadHandler_isSetToInline() {
7398
Element element = Mockito.mock(Element.class);

flow-html-components/src/test/java/com/vaadin/flow/component/html/ImageTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
import org.junit.Assert;
2222
import org.junit.Test;
23+
import org.mockito.ArgumentCaptor;
2324
import org.mockito.Mockito;
2425

26+
import com.vaadin.flow.dom.DisabledUpdateMode;
2527
import com.vaadin.flow.dom.Element;
2628
import com.vaadin.flow.server.streams.DownloadHandler;
2729
import com.vaadin.flow.server.streams.DownloadResponse;
@@ -52,6 +54,29 @@ public void emptyAltKeepsAttribute() {
5254
Assert.assertFalse(img.getElement().hasAttribute("alt"));
5355
}
5456

57+
@Test
58+
public void setSrc_downloadHandler_disabledUpdateModeIsAlways() {
59+
Element element = Mockito.mock(Element.class);
60+
class TestImage extends Image {
61+
@Override
62+
public Element getElement() {
63+
return element;
64+
}
65+
}
66+
// Plain lambda DownloadHandler, not an AbstractDownloadHandler subclass
67+
DownloadHandler lambda = event -> {
68+
};
69+
70+
new TestImage().setSrc(lambda);
71+
72+
ArgumentCaptor<DownloadHandler> captor = ArgumentCaptor
73+
.forClass(DownloadHandler.class);
74+
Mockito.verify(element).setAttribute(Mockito.eq("src"),
75+
captor.capture());
76+
Assert.assertEquals(DisabledUpdateMode.ALWAYS,
77+
captor.getValue().getDisabledUpdateMode());
78+
}
79+
5580
@Test
5681
public void downloadHandler_isSetToInline() {
5782
Element element = Mockito.mock(Element.class);

flow-server/src/main/java/com/vaadin/flow/server/streams/DownloadHandler.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.IOException;
2121
import java.util.Optional;
2222

23+
import com.vaadin.flow.dom.DisabledUpdateMode;
2324
import com.vaadin.flow.dom.Element;
2425
import com.vaadin.flow.server.VaadinRequest;
2526
import com.vaadin.flow.server.VaadinResponse;
@@ -104,6 +105,59 @@ default void handleRequest(VaadinRequest request, VaadinResponse response,
104105
handleDownloadRequest(downloadEvent);
105106
}
106107

108+
/**
109+
* Returns a view of this handler that is served even when the owning
110+
* component is disabled.
111+
* <p>
112+
* By default a {@link DownloadHandler} inherits
113+
* {@link DisabledUpdateMode#ONLY_WHEN_ENABLED} from
114+
* {@link ElementRequestHandler}, which causes the framework to respond with
115+
* HTTP 403 when the owning element is disabled. That is appropriate for
116+
* action-style downloads such as a "save file" link on an
117+
* {@code com.vaadin.flow.component.html.Anchor}, but not for resources that
118+
* the browser fetches passively as part of rendering, such as the
119+
* {@code src} of an icon or image inside a disabled container.
120+
* <p>
121+
* This method returns a wrapper that delegates
122+
* {@link #handleDownloadRequest}, {@link #getUrlPostfix()} and
123+
* {@link #isAllowInert()} to this handler and overrides
124+
* {@link #getDisabledUpdateMode()} to return
125+
* {@link DisabledUpdateMode#ALWAYS}. If this handler already reports
126+
* {@code ALWAYS}, the same instance is returned.
127+
*
128+
* @return a {@link DownloadHandler} that is served regardless of the
129+
* owner's enabled state, or {@code this} if the disabled mode is
130+
* already {@link DisabledUpdateMode#ALWAYS}
131+
*/
132+
default DownloadHandler allowDisabled() {
133+
if (getDisabledUpdateMode() == DisabledUpdateMode.ALWAYS) {
134+
return this;
135+
}
136+
DownloadHandler delegate = this;
137+
return new DownloadHandler() {
138+
@Override
139+
public void handleDownloadRequest(DownloadEvent event)
140+
throws IOException {
141+
delegate.handleDownloadRequest(event);
142+
}
143+
144+
@Override
145+
public String getUrlPostfix() {
146+
return delegate.getUrlPostfix();
147+
}
148+
149+
@Override
150+
public boolean isAllowInert() {
151+
return delegate.isAllowInert();
152+
}
153+
154+
@Override
155+
public DisabledUpdateMode getDisabledUpdateMode() {
156+
return DisabledUpdateMode.ALWAYS;
157+
}
158+
};
159+
}
160+
107161
/**
108162
* Get a download handler for serving given {@link File}.
109163
* <p>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.server.streams;
17+
18+
import java.util.concurrent.atomic.AtomicReference;
19+
20+
import org.junit.Assert;
21+
import org.junit.Test;
22+
import org.mockito.Mockito;
23+
24+
import com.vaadin.flow.dom.DisabledUpdateMode;
25+
import com.vaadin.flow.dom.Element;
26+
import com.vaadin.flow.server.VaadinRequest;
27+
import com.vaadin.flow.server.VaadinResponse;
28+
import com.vaadin.flow.server.VaadinSession;
29+
30+
public class DownloadHandlerTest {
31+
32+
@Test
33+
public void allowDisabled_lambda_disabledUpdateModeIsAlways() {
34+
DownloadHandler delegate = event -> {
35+
};
36+
37+
DownloadHandler wrapped = delegate.allowDisabled();
38+
39+
Assert.assertEquals(DisabledUpdateMode.ALWAYS,
40+
wrapped.getDisabledUpdateMode());
41+
}
42+
43+
@Test
44+
public void allowDisabled_lambda_forwardsHandleDownloadRequest()
45+
throws Exception {
46+
AtomicReference<DownloadEvent> received = new AtomicReference<>();
47+
DownloadHandler delegate = received::set;
48+
49+
DownloadEvent event = new DownloadEvent(
50+
Mockito.mock(VaadinRequest.class),
51+
Mockito.mock(VaadinResponse.class),
52+
Mockito.mock(VaadinSession.class), Mockito.mock(Element.class));
53+
54+
delegate.allowDisabled().handleDownloadRequest(event);
55+
56+
Assert.assertSame(
57+
"Wrapped handler must forward the event to the delegate", event,
58+
received.get());
59+
}
60+
61+
@Test
62+
public void allowDisabled_forwardsUrlPostfixAndAllowInert() {
63+
DownloadHandler delegate = new DownloadHandler() {
64+
@Override
65+
public void handleDownloadRequest(DownloadEvent event) {
66+
}
67+
68+
@Override
69+
public String getUrlPostfix() {
70+
return "icon.svg";
71+
}
72+
73+
@Override
74+
public boolean isAllowInert() {
75+
return true;
76+
}
77+
};
78+
79+
DownloadHandler wrapped = delegate.allowDisabled();
80+
81+
Assert.assertEquals("icon.svg", wrapped.getUrlPostfix());
82+
Assert.assertTrue(wrapped.isAllowInert());
83+
}
84+
85+
@Test
86+
public void allowDisabled_abstractDownloadHandlerWrapped_modeIsAlways_inlinePreserved() {
87+
InputStreamDownloadHandler handler = DownloadHandler
88+
.fromInputStream(event -> DownloadResponse.error(500));
89+
handler.inline();
90+
91+
DownloadHandler wrapped = handler.allowDisabled();
92+
93+
Assert.assertEquals(DisabledUpdateMode.ALWAYS,
94+
wrapped.getDisabledUpdateMode());
95+
// The wrap does not interfere with the original handler's inline state
96+
Assert.assertTrue(handler.isInline());
97+
}
98+
99+
@Test
100+
public void allowDisabled_isIdempotent_returnsSameInstance() {
101+
DownloadHandler alwaysAllowed = new DownloadHandler() {
102+
@Override
103+
public void handleDownloadRequest(DownloadEvent event) {
104+
}
105+
106+
@Override
107+
public DisabledUpdateMode getDisabledUpdateMode() {
108+
return DisabledUpdateMode.ALWAYS;
109+
}
110+
};
111+
112+
Assert.assertSame(
113+
"allowDisabled() on a handler that is already ALWAYS must be a no-op",
114+
alwaysAllowed, alwaysAllowed.allowDisabled());
115+
}
116+
117+
@Test
118+
public void allowDisabled_doubleWrap_returnsFirstWrapper() {
119+
DownloadHandler delegate = event -> {
120+
};
121+
DownloadHandler wrappedOnce = delegate.allowDisabled();
122+
DownloadHandler wrappedTwice = wrappedOnce.allowDisabled();
123+
124+
Assert.assertSame(
125+
"Wrapping a handler that is already ALWAYS must be a no-op",
126+
wrappedOnce, wrappedTwice);
127+
}
128+
}

0 commit comments

Comments
 (0)