View Javadoc
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 }