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 final WebRequest request = new WebRequest(url, pageCharset, pageUrl);
195
196 if (isAlreadyLoadedByAncestor(url, request.getCharset())) {
197 notifyIncorrectness("Recursive src attribute of " + getTagName() + ": url=[" + source + "]. Ignored.");
198 return;
199 }
200
201 // Use parent document's charset as container charset if same origin
202 // https://html.spec.whatwg.org/multipage/parsing.html#determining-the-character-encoding
203 if (Objects.equals(pageUrl.getProtocol(), url.getProtocol())
204 && Objects.equals(pageUrl.getAuthority(), url.getAuthority())) {
205 request.setDefaultResponseContentCharset(pageCharset);
206 }
207
208 try {
209 webClient.getPage(enclosedWindow_, request);
210 }
211 catch (final IOException e) {
212 if (LOG.isErrorEnabled()) {
213 LOG.error("IOException when getting content for " + getTagName() + ": url=[" + url + "]", e);
214 }
215 }
216 }
217 }
218
219 /**
220 * Test if the provided URL is the one of the parents which would cause an infinite loop.
221 * @param url the URL to test
222 * @param charset the request charset
223 * @return {@code false} if no parent has already this URL
224 */
225 private boolean isAlreadyLoadedByAncestor(final URL url, final Charset charset) {
226 WebWindow window = getPage().getEnclosingWindow();
227 int nesting = 0;
228 while (window instanceof FrameWindow) {
229 nesting++;
230 if (nesting > 9) {
231 return true;
232 }
233
234 final URL encUrl = UrlUtils.encodeUrl(url, charset);
235 if (UrlUtils.sameFile(encUrl, window.getEnclosedPage().getUrl())) {
236 return true;
237 }
238
239 if (window == window.getParentWindow()) {
240 // TODO: should getParentWindow() return null on top windows?
241 window = null;
242 }
243 else {
244 window = window.getParentWindow();
245 }
246 }
247 return false;
248 }
249
250 /**
251 * Returns the value of the attribute {@code longdesc}. Refer to the
252 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
253 * documentation for details on the use of this attribute.
254 *
255 * @return the value of the attribute {@code longdesc} or an empty string if that attribute isn't defined
256 */
257 public final String getLongDescAttribute() {
258 return getAttributeDirect("longdesc");
259 }
260
261 /**
262 * Returns the value of the attribute {@code name}. Refer to the
263 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
264 * documentation for details on the use of this attribute.
265 *
266 * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
267 */
268 public final String getNameAttribute() {
269 return getAttributeDirect(NAME_ATTRIBUTE);
270 }
271
272 /**
273 * Sets the value of the {@code name} attribute.
274 *
275 * @param name the new window name
276 */
277 public final void setNameAttribute(final String name) {
278 setAttribute(NAME_ATTRIBUTE, name);
279 }
280
281 /**
282 * Returns the value of the attribute {@code src}. Refer to the
283 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
284 * documentation for details on the use of this attribute.
285 *
286 * @return the value of the attribute {@code src} or an empty string if that attribute isn't defined
287 */
288 public final String getSrcAttribute() {
289 return getSrcAttributeNormalized();
290 }
291
292 /**
293 * Returns the value of the attribute {@code frameborder}. Refer to the
294 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
295 * documentation for details on the use of this attribute.
296 *
297 * @return the value of the attribute {@code frameborder} or an empty string if that attribute isn't defined
298 */
299 public final String getFrameBorderAttribute() {
300 return getAttributeDirect("frameborder");
301 }
302
303 /**
304 * Returns the value of the attribute {@code marginwidth}. Refer to the
305 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
306 * documentation for details on the use of this attribute.
307 *
308 * @return the value of the attribute {@code marginwidth} or an empty string if that attribute isn't defined
309 */
310 public final String getMarginWidthAttribute() {
311 return getAttributeDirect("marginwidth");
312 }
313
314 /**
315 * Returns the value of the attribute {@code marginheight}. Refer to the
316 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
317 * documentation for details on the use of this attribute.
318 *
319 * @return the value of the attribute {@code marginheight} or an empty string if that attribute isn't defined
320 */
321 public final String getMarginHeightAttribute() {
322 return getAttributeDirect("marginheight");
323 }
324
325 /**
326 * Returns the value of the attribute {@code noresize}. Refer to the
327 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
328 * documentation for details on the use of this attribute.
329 *
330 * @return the value of the attribute {@code noresize} or an empty string if that attribute isn't defined
331 */
332 public final String getNoResizeAttribute() {
333 return getAttributeDirect("noresize");
334 }
335
336 /**
337 * Returns the value of the attribute {@code scrolling}. Refer to the
338 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
339 * documentation for details on the use of this attribute.
340 *
341 * @return the value of the attribute {@code scrolling} or an empty string if that attribute isn't defined
342 */
343 public final String getScrollingAttribute() {
344 return getAttributeDirect("scrolling");
345 }
346
347 /**
348 * Returns the value of the attribute {@code onload}. This attribute is not
349 * actually supported by the HTML specification however it is supported
350 * by the popular browsers.
351 *
352 * @return the value of the attribute {@code onload} or an empty string if that attribute isn't defined
353 */
354 public final String getOnLoadAttribute() {
355 return getAttributeDirect("onload");
356 }
357
358 /**
359 * Returns the currently loaded page in the enclosed window.
360 * This is a facility method for <code>getEnclosedWindow().getEnclosedPage()</code>.
361 * @see WebWindow#getEnclosedPage()
362 * @return the currently loaded page in the enclosed window, or {@code null} if no page has been loaded
363 */
364 public Page getEnclosedPage() {
365 return getEnclosedWindow().getEnclosedPage();
366 }
367
368 /**
369 * Gets the window enclosed in this frame.
370 * @return the window enclosed in this frame
371 */
372 public FrameWindow getEnclosedWindow() {
373 return enclosedWindow_;
374 }
375
376 /**
377 * Sets the value of the {@code src} attribute. Also loads the frame with the specified URL, if possible.
378 * @param attribute the new value of the {@code src} attribute
379 */
380 public final void setSrcAttribute(final String attribute) {
381 setAttribute(SRC_ATTRIBUTE, attribute);
382 }
383
384 /**
385 * {@inheritDoc}
386 */
387 @Override
388 protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue,
389 final boolean notifyAttributeChangeListeners, final boolean notifyMutationObserver) {
390 final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName);
391
392 if (null != attributeValue && SRC_ATTRIBUTE.equals(qualifiedNameLC)) {
393 final String attributeValueTrimmed = attributeValue.trim();
394
395 super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValueTrimmed, notifyAttributeChangeListeners,
396 notifyMutationObserver);
397
398 // do not use equals() here
399 // see HTMLIFrameElement2Test.documentCreateElement_onLoad_srcAboutBlank()
400 if (UrlUtils.ABOUT_BLANK != attributeValueTrimmed) {
401 if (isAttachedToPage()) {
402 loadSrc();
403 }
404 else {
405 loadSrcWhenAddedToPage_ = true;
406 }
407 }
408
409 return;
410 }
411
412 super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
413 notifyMutationObserver);
414 }
415
416 /**
417 * {@inheritDoc}
418 */
419 @Override
420 public Attr setAttributeNode(final Attr attribute) {
421 final String qualifiedName = attribute.getName();
422 String attributeValue = null;
423 if (SRC_ATTRIBUTE.equals(qualifiedName)) {
424 attributeValue = attribute.getValue().trim();
425 }
426
427 final Attr result = super.setAttributeNode(attribute);
428
429 if (SRC_ATTRIBUTE.equals(qualifiedName) && !UrlUtils.ABOUT_BLANK.equals(attributeValue)) {
430 if (isAttachedToPage()) {
431 loadSrc();
432 }
433 else {
434 loadSrcWhenAddedToPage_ = true;
435 }
436 }
437
438 return result;
439 }
440
441 private void loadSrc() {
442 loadSrcWhenAddedToPage_ = false;
443 final String src = getSrcAttribute();
444
445 // recreate a window if the old one was closed
446 if (enclosedWindow_.isClosed()) {
447 init();
448 }
449
450 final AbstractJavaScriptEngine<?> jsEngine = getPage().getWebClient().getJavaScriptEngine();
451 // When src is set from a script, loading is postponed until script finishes
452 // in fact this implementation is probably wrong: JavaScript URL should be
453 // first evaluated and only loading, when any, should be postponed.
454 if (jsEngine == null || !jsEngine.isScriptRunning()
455 || src.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
456 loadInnerPageIfPossible(src);
457 }
458 else {
459 final Page pageInFrame = getEnclosedPage();
460 final PostponedAction action = new PostponedAction(getPage(), "BaseFrame.loadSrc") {
461 @Override
462 public void execute() throws Exception {
463 if (!src.isEmpty() && getSrcAttribute().equals(src)) {
464 loadInnerPage();
465 }
466 }
467
468 @Override
469 public boolean isStillAlive() {
470 // skip if page in frame has already been changed
471 return super.isStillAlive() && pageInFrame == getEnclosedPage();
472 }
473 };
474 jsEngine.addPostponedAction(action);
475 }
476 }
477
478 /**
479 * Creates a new {@link WebWindow} for the new clone.
480 * {@inheritDoc}
481 */
482 @Override
483 public DomNode cloneNode(final boolean deep) {
484 final BaseFrameElement clone = (BaseFrameElement) super.cloneNode(deep);
485 clone.init();
486 return clone;
487 }
488
489 @Override
490 protected void onAddedToPage() {
491 super.onAddedToPage();
492
493 if (loadSrcWhenAddedToPage_) {
494 loadSrc();
495 }
496 }
497
498 @Override
499 public void remove() {
500 super.remove();
501 loadSrcWhenAddedToPage_ = true;
502 getEnclosedWindow().close();
503 }
504
505 @Override
506 public final void removeAttribute(final String attributeName) {
507 super.removeAttribute(attributeName);
508
509 // TODO find a better implementation without all the code duplication
510 if (isAttachedToPage()) {
511 loadSrcWhenAddedToPage_ = false;
512 final String src = getSrcAttribute();
513
514 final AbstractJavaScriptEngine<?> jsEngine = getPage().getWebClient().getJavaScriptEngine();
515 // When src is set from a script, loading is postponed until script finishes
516 // in fact this implementation is probably wrong: JavaScript URL should be
517 // first evaluated and only loading, when any, should be postponed.
518 if (jsEngine == null || !jsEngine.isScriptRunning()) {
519 loadInnerPageIfPossible(src);
520 }
521 else {
522 final Page pageInFrame = getEnclosedPage();
523 final PostponedAction action = new PostponedAction(getPage(), "BaseFrame.removeAttribute") {
524 @Override
525 public void execute() throws Exception {
526 loadInnerPage();
527 }
528
529 @Override
530 public boolean isStillAlive() {
531 // skip if page in frame has already been changed
532 return super.isStillAlive() && pageInFrame == getEnclosedPage();
533 }
534 };
535 jsEngine.addPostponedAction(action);
536 }
537 }
538 else {
539 loadSrcWhenAddedToPage_ = true;
540 }
541 }
542 }