1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit.html;
16
17 import java.io.IOException;
18 import java.net.MalformedURLException;
19 import java.net.URL;
20 import java.nio.charset.Charset;
21 import java.util.Map;
22 import java.util.Objects;
23
24 import org.apache.commons.logging.Log;
25 import org.apache.commons.logging.LogFactory;
26 import org.htmlunit.FailingHttpStatusCodeException;
27 import org.htmlunit.FrameContentHandler;
28 import org.htmlunit.Page;
29 import org.htmlunit.SgmlPage;
30 import org.htmlunit.WebClient;
31 import org.htmlunit.WebRequest;
32 import org.htmlunit.WebWindow;
33 import org.htmlunit.javascript.AbstractJavaScriptEngine;
34 import org.htmlunit.javascript.PostponedAction;
35 import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
36 import org.htmlunit.util.UrlUtils;
37 import org.w3c.dom.Attr;
38
39 /**
40 * Base class for frame and iframe.
41 *
42 * @author Mike Bowler
43 * @author David K. Taylor
44 * @author Christian Sell
45 * @author Marc Guillemot
46 * @author David D. Kilzer
47 * @author Stefan Anzinger
48 * @author Ahmed Ashour
49 * @author Dmitri Zoubkov
50 * @author Daniel Gredler
51 * @author Ronald Brill
52 * @author Frank Danek
53 * @author Lai Quang Duong
54 */
55 public abstract class BaseFrameElement extends HtmlElement {
56
57 private static final Log LOG = LogFactory.getLog(BaseFrameElement.class);
58 private FrameWindow enclosedWindow_;
59 private boolean contentLoaded_;
60 private boolean loadSrcWhenAddedToPage_;
61
62 /**
63 * Creates an instance of BaseFrame.
64 *
65 * @param qualifiedName the qualified name of the element type to instantiate
66 * @param page the HtmlPage that contains this element
67 * @param attributes the initial attributes
68 */
69 protected BaseFrameElement(final String qualifiedName, final SgmlPage page,
70 final Map<String, DomAttr> attributes) {
71 super(qualifiedName, page, attributes);
72
73 init();
74
75 if (null != page && page.isHtmlPage() && ((HtmlPage) page).isParsingHtmlSnippet()) {
76 // if created by the HTMLParser the src attribute is not set via setAttribute() or some other method but is
77 // part of the given attributes already.
78 final String src = getSrcAttribute();
79
80 // src-less IFrame or src='about:blank'
81 // these are loaded sync
82 if (ATTRIBUTE_NOT_DEFINED != src && !UrlUtils.ABOUT_BLANK.equals(src.trim())) {
83 loadSrcWhenAddedToPage_ = true;
84 }
85 }
86 }
87
88 private void init() {
89 FrameWindow enclosedWindow = null;
90 try {
91 final HtmlPage htmlPage = getHtmlPageOrNull();
92 if (null != htmlPage) { // if loaded as part of XHR.responseXML, don't load content
93 enclosedWindow = new FrameWindow(this);
94 // put about:blank in the window to allow JS to run on this frame before the
95 // real content is loaded
96 final WebClient webClient = htmlPage.getWebClient();
97 final HtmlPage temporaryPage = webClient.getPage(enclosedWindow, WebRequest.newAboutBlankRequest());
98 temporaryPage.setReadyState(READY_STATE_LOADING);
99 }
100 }
101 catch (final FailingHttpStatusCodeException | IOException ignored) {
102 // should never occur
103 }
104 enclosedWindow_ = enclosedWindow;
105 }
106
107 /**
108 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
109 *
110 * Called after the node for the {@code frame} or {@code iframe} has been added to the containing page.
111 * The node needs to be added first to allow JavaScript in the frame to see the frame in the parent.
112 * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
113 * {@link org.htmlunit.WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is
114 * set to true
115 */
116
117 public void loadInnerPage() throws FailingHttpStatusCodeException {
118 String source = getSrcAttribute();
119 if (source.isEmpty()) {
120 source = UrlUtils.ABOUT_BLANK;
121 }
122
123 loadInnerPageIfPossible(source);
124
125 final Page enclosedPage = getEnclosedPage();
126 if (enclosedPage != null && enclosedPage.isHtmlPage()) {
127 final HtmlPage htmlPage = (HtmlPage) enclosedPage;
128
129 final AbstractJavaScriptEngine<?> jsEngine = htmlPage.getWebClient().getJavaScriptEngine();
130 if (jsEngine != null && jsEngine.isScriptRunning()) {
131 final PostponedAction action = new PostponedAction(getPage(), "BaseFrame.loadInnerPage") {
132 @Override
133 public void execute() {
134 htmlPage.setReadyState(READY_STATE_COMPLETE);
135 }
136 };
137 jsEngine.addPostponedAction(action);
138 }
139 else {
140 htmlPage.setReadyState(READY_STATE_COMPLETE);
141 }
142 }
143 }
144
145 /**
146 * Indicates if the content specified by the {@code src} attribute has been loaded or not.
147 * The initial state of a frame contains an "about:blank" that is not loaded like
148 * something specified in {@code src} attribute.
149 * @return {@code false} if the frame is still in its initial state.
150 */
151 boolean isContentLoaded() {
152 return contentLoaded_;
153 }
154
155 /**
156 * Changes the state of the {@code contentLoaded_} attribute to true.
157 * This is needed, if the content is set from javascript to avoid
158 * later overwriting from method org.htmlunit.html.HtmlPage.loadFrames().
159 */
160 void setContentLoaded() {
161 contentLoaded_ = true;
162 }
163
164 private void loadInnerPageIfPossible(final String src) throws FailingHttpStatusCodeException {
165 setContentLoaded();
166
167 String source = src;
168 final SgmlPage page = getPage();
169 final WebClient webClient = page.getWebClient();
170 final FrameContentHandler handler = webClient.getFrameContentHandler();
171 if (null != handler && !handler.loadFrameDocument(this)) {
172 source = UrlUtils.ABOUT_BLANK;
173 }
174
175 if (!source.isEmpty()) {
176 final URL url;
177 try {
178 url = ((HtmlPage) page).getFullyQualifiedUrl(source);
179 }
180 catch (final MalformedURLException e) {
181 notifyIncorrectness("Invalid src attribute of " + getTagName() + ": url=[" + source + "]. Ignored.");
182 return;
183 }
184
185 final URL pageUrl = page.getUrl();
186
187 // accessing to local resource is forbidden for security reason
188 if (!"file".equals(pageUrl.getProtocol()) && "file".equals(url.getProtocol())) {
189 notifyIncorrectness("Not allowed to load local resource: " + source);
190 return;
191 }
192
193 final Charset pageCharset = page.getCharset();
194
195 if (isAlreadyLoadedByAncestor(url, pageCharset)) {
196 notifyIncorrectness("Recursive src attribute of " + getTagName() + ": url=[" + source + "]. Ignored.");
197 return;
198 }
199
200 final WebRequest webRequest = new WebRequest(url, webClient.getBrowserVersion().getHtmlAcceptHeader(),
201 webClient.getBrowserVersion().getAcceptEncodingHeader());
202 webRequest.setCharset(pageCharset);
203 webRequest.setRefererHeader(pageUrl);
204
205 // Use parent document's charset as container charset if same origin
206 // https://html.spec.whatwg.org/multipage/parsing.html#determining-the-character-encoding
207 if (Objects.equals(pageUrl.getProtocol(), url.getProtocol())
208 && Objects.equals(pageUrl.getAuthority(), url.getAuthority())) {
209 webRequest.setDefaultResponseContentCharset(pageCharset);
210 }
211
212 // Sec-Fetch-* support (https://www.w3.org/TR/fetch-metadata/): a frame/iframe
213 // load is a navigation initiated by the containing page, but - unlike a click
214 // or form submission - never carries user activation, since there is no
215 // gesture involved in an automatic <iframe>/<frame> load.
216 final WebRequest.FetchDestination fetchDestination = this instanceof HtmlInlineFrame
217 ? WebRequest.FetchDestination.IFRAME
218 : WebRequest.FetchDestination.FRAME;
219 webRequest.markAsNavigation(fetchDestination, pageUrl, false);
220
221 try {
222 webClient.getPage(enclosedWindow_, webRequest);
223 }
224 catch (final IOException e) {
225 if (LOG.isErrorEnabled()) {
226 LOG.error("IOException when getting content for " + getTagName() + ": url=[" + url + "]", e);
227 }
228 }
229 }
230 }
231
232 /**
233 * Test if the provided URL is the one of the parents which would cause an infinite loop.
234 * @param url the URL to test
235 * @param charset the request charset
236 * @return {@code false} if no parent has already this URL
237 */
238 private boolean isAlreadyLoadedByAncestor(final URL url, final Charset charset) {
239 WebWindow window = getPage().getEnclosingWindow();
240 int nesting = 0;
241 while (window instanceof FrameWindow) {
242 nesting++;
243 if (nesting > 9) {
244 return true;
245 }
246
247 final URL encUrl = UrlUtils.encodeUrl(url, charset);
248 if (UrlUtils.sameFile(encUrl, window.getEnclosedPage().getUrl())) {
249 return true;
250 }
251
252 if (window == window.getParentWindow()) {
253 // TODO: should getParentWindow() return null on top windows?
254 window = null;
255 }
256 else {
257 window = window.getParentWindow();
258 }
259 }
260 return false;
261 }
262
263 /**
264 * Returns the value of the attribute {@code longdesc}. Refer to the
265 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
266 * documentation for details on the use of this attribute.
267 *
268 * @return the value of the attribute {@code longdesc} or an empty string if that attribute isn't defined
269 */
270 public final String getLongDescAttribute() {
271 return getAttributeDirect("longdesc");
272 }
273
274 /**
275 * Returns the value of the attribute {@code name}. Refer to the
276 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
277 * documentation for details on the use of this attribute.
278 *
279 * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
280 */
281 public final String getNameAttribute() {
282 return getAttributeDirect(NAME_ATTRIBUTE);
283 }
284
285 /**
286 * Sets the value of the {@code name} attribute.
287 *
288 * @param name the new window name
289 */
290 public final void setNameAttribute(final String name) {
291 setAttribute(NAME_ATTRIBUTE, name);
292 }
293
294 /**
295 * Returns the value of the attribute {@code src}. Refer to the
296 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
297 * documentation for details on the use of this attribute.
298 *
299 * @return the value of the attribute {@code src} or an empty string if that attribute isn't defined
300 */
301 public final String getSrcAttribute() {
302 return getSrcAttributeNormalized();
303 }
304
305 /**
306 * Returns the value of the attribute {@code frameborder}. Refer to the
307 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
308 * documentation for details on the use of this attribute.
309 *
310 * @return the value of the attribute {@code frameborder} or an empty string if that attribute isn't defined
311 */
312 public final String getFrameBorderAttribute() {
313 return getAttributeDirect("frameborder");
314 }
315
316 /**
317 * Returns the value of the attribute {@code marginwidth}. Refer to the
318 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
319 * documentation for details on the use of this attribute.
320 *
321 * @return the value of the attribute {@code marginwidth} or an empty string if that attribute isn't defined
322 */
323 public final String getMarginWidthAttribute() {
324 return getAttributeDirect("marginwidth");
325 }
326
327 /**
328 * Returns the value of the attribute {@code marginheight}. Refer to the
329 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
330 * documentation for details on the use of this attribute.
331 *
332 * @return the value of the attribute {@code marginheight} or an empty string if that attribute isn't defined
333 */
334 public final String getMarginHeightAttribute() {
335 return getAttributeDirect("marginheight");
336 }
337
338 /**
339 * Returns the value of the attribute {@code noresize}. Refer to the
340 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
341 * documentation for details on the use of this attribute.
342 *
343 * @return the value of the attribute {@code noresize} or an empty string if that attribute isn't defined
344 */
345 public final String getNoResizeAttribute() {
346 return getAttributeDirect("noresize");
347 }
348
349 /**
350 * Returns the value of the attribute {@code scrolling}. Refer to the
351 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
352 * documentation for details on the use of this attribute.
353 *
354 * @return the value of the attribute {@code scrolling} or an empty string if that attribute isn't defined
355 */
356 public final String getScrollingAttribute() {
357 return getAttributeDirect("scrolling");
358 }
359
360 /**
361 * Returns the value of the attribute {@code onload}. This attribute is not
362 * actually supported by the HTML specification however it is supported
363 * by the popular browsers.
364 *
365 * @return the value of the attribute {@code onload} or an empty string if that attribute isn't defined
366 */
367 public final String getOnLoadAttribute() {
368 return getAttributeDirect("onload");
369 }
370
371 /**
372 * Returns the currently loaded page in the enclosed window.
373 * This is a facility method for <code>getEnclosedWindow().getEnclosedPage()</code>.
374 * @see WebWindow#getEnclosedPage()
375 * @return the currently loaded page in the enclosed window, or {@code null} if no page has been loaded
376 */
377 public Page getEnclosedPage() {
378 return getEnclosedWindow().getEnclosedPage();
379 }
380
381 /**
382 * Gets the window enclosed in this frame.
383 * @return the window enclosed in this frame
384 */
385 public FrameWindow getEnclosedWindow() {
386 return enclosedWindow_;
387 }
388
389 /**
390 * Sets the value of the {@code src} attribute. Also loads the frame with the specified URL, if possible.
391 * @param attribute the new value of the {@code src} attribute
392 */
393 public final void setSrcAttribute(final String attribute) {
394 setAttribute(SRC_ATTRIBUTE, attribute);
395 }
396
397 /**
398 * {@inheritDoc}
399 */
400 @Override
401 protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue,
402 final boolean notifyAttributeChangeListeners, final boolean notifyMutationObserver) {
403 final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName);
404
405 if (null != attributeValue && SRC_ATTRIBUTE.equals(qualifiedNameLC)) {
406 final String attributeValueTrimmed = attributeValue.trim();
407
408 super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValueTrimmed, notifyAttributeChangeListeners,
409 notifyMutationObserver);
410
411 // do not use equals() here
412 // see HTMLIFrameElement2Test.documentCreateElement_onLoad_srcAboutBlank()
413 if (UrlUtils.ABOUT_BLANK != attributeValueTrimmed) {
414 if (isAttachedToPage()) {
415 loadSrc();
416 }
417 else {
418 loadSrcWhenAddedToPage_ = true;
419 }
420 }
421
422 return;
423 }
424
425 super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
426 notifyMutationObserver);
427 }
428
429 /**
430 * {@inheritDoc}
431 */
432 @Override
433 public Attr setAttributeNode(final Attr attribute) {
434 final String qualifiedName = attribute.getName();
435 String attributeValue = null;
436 if (SRC_ATTRIBUTE.equals(qualifiedName)) {
437 attributeValue = attribute.getValue().trim();
438 }
439
440 final Attr result = super.setAttributeNode(attribute);
441
442 if (SRC_ATTRIBUTE.equals(qualifiedName) && !UrlUtils.ABOUT_BLANK.equals(attributeValue)) {
443 if (isAttachedToPage()) {
444 loadSrc();
445 }
446 else {
447 loadSrcWhenAddedToPage_ = true;
448 }
449 }
450
451 return result;
452 }
453
454 private void loadSrc() {
455 loadSrcWhenAddedToPage_ = false;
456 final String src = getSrcAttribute();
457
458 // recreate a window if the old one was closed
459 if (enclosedWindow_.isClosed()) {
460 init();
461 }
462
463 final AbstractJavaScriptEngine<?> jsEngine = getPage().getWebClient().getJavaScriptEngine();
464 // When src is set from a script, loading is postponed until script finishes
465 // in fact this implementation is probably wrong: JavaScript URL should be
466 // first evaluated and only loading, when any, should be postponed.
467 if (jsEngine == null || !jsEngine.isScriptRunning()
468 || src.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
469 loadInnerPageIfPossible(src);
470 }
471 else {
472 final Page pageInFrame = getEnclosedPage();
473 final PostponedAction action = new PostponedAction(getPage(), "BaseFrame.loadSrc") {
474 @Override
475 public void execute() throws Exception {
476 if (!src.isEmpty() && getSrcAttribute().equals(src)) {
477 loadInnerPage();
478 }
479 }
480
481 @Override
482 public boolean isStillAlive() {
483 // skip if page in frame has already been changed
484 return super.isStillAlive() && pageInFrame == getEnclosedPage();
485 }
486 };
487 jsEngine.addPostponedAction(action);
488 }
489 }
490
491 /**
492 * Creates a new {@link WebWindow} for the new clone.
493 * {@inheritDoc}
494 */
495 @Override
496 public DomNode cloneNode(final boolean deep) {
497 final BaseFrameElement clone = (BaseFrameElement) super.cloneNode(deep);
498 clone.init();
499 return clone;
500 }
501
502 @Override
503 protected void onAddedToPage() {
504 super.onAddedToPage();
505
506 if (loadSrcWhenAddedToPage_) {
507 loadSrc();
508 }
509 }
510
511 @Override
512 public void remove() {
513 super.remove();
514 loadSrcWhenAddedToPage_ = true;
515 getEnclosedWindow().close();
516 }
517
518 @Override
519 public final void removeAttribute(final String attributeName) {
520 super.removeAttribute(attributeName);
521
522 // TODO find a better implementation without all the code duplication
523 if (isAttachedToPage()) {
524 loadSrcWhenAddedToPage_ = false;
525 final String src = getSrcAttribute();
526
527 final AbstractJavaScriptEngine<?> jsEngine = getPage().getWebClient().getJavaScriptEngine();
528 // When src is set from a script, loading is postponed until script finishes
529 // in fact this implementation is probably wrong: JavaScript URL should be
530 // first evaluated and only loading, when any, should be postponed.
531 if (jsEngine == null || !jsEngine.isScriptRunning()) {
532 loadInnerPageIfPossible(src);
533 }
534 else {
535 final Page pageInFrame = getEnclosedPage();
536 final PostponedAction action = new PostponedAction(getPage(), "BaseFrame.removeAttribute") {
537 @Override
538 public void execute() throws Exception {
539 loadInnerPage();
540 }
541
542 @Override
543 public boolean isStillAlive() {
544 // skip if page in frame has already been changed
545 return super.isStillAlive() && pageInFrame == getEnclosedPage();
546 }
547 };
548 jsEngine.addPostponedAction(action);
549 }
550 }
551 else {
552 loadSrcWhenAddedToPage_ = true;
553 }
554 }
555 }