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