View Javadoc
1   /*
2    * Copyright (c) 2002-2018 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    * http://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 com.gargoylesoftware.htmlunit.html;
16  
17  import java.io.IOException;
18  import java.net.MalformedURLException;
19  import java.net.URL;
20  import java.util.Map;
21  
22  import org.apache.commons.lang3.StringUtils;
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.w3c.dom.Attr;
26  
27  import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
28  import com.gargoylesoftware.htmlunit.HttpHeader;
29  import com.gargoylesoftware.htmlunit.Page;
30  import com.gargoylesoftware.htmlunit.SgmlPage;
31  import com.gargoylesoftware.htmlunit.WebClient;
32  import com.gargoylesoftware.htmlunit.WebRequest;
33  import com.gargoylesoftware.htmlunit.WebWindow;
34  import com.gargoylesoftware.htmlunit.javascript.AbstractJavaScriptEngine;
35  import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
36  import com.gargoylesoftware.htmlunit.protocol.javascript.JavaScriptURLConnection;
37  import com.gargoylesoftware.htmlunit.util.UrlUtils;
38  
39  /**
40   * Base class for frame and iframe.
41   *
42   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
43   * @author David K. Taylor
44   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
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   */
54  public abstract class BaseFrameElement extends HtmlElement {
55  
56      private static final Log LOG = LogFactory.getLog(BaseFrameElement.class);
57      private FrameWindow enclosedWindow_;
58      private boolean contentLoaded_ = false;
59      private boolean createdByJavascript_ = false;
60      private boolean loadSrcWhenAddedToPage_ = false;
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              if (src != ATTRIBUTE_NOT_DEFINED && !WebClient.ABOUT_BLANK.equals(src)) {
80                  loadSrcWhenAddedToPage_ = true;
81              }
82          }
83      }
84  
85      private void init() {
86          FrameWindow enclosedWindow = null;
87          try {
88              final HtmlPage htmlPage = getHtmlPageOrNull();
89              if (null != htmlPage) { // if loaded as part of XHR.responseXML, don't load content
90                  enclosedWindow = new FrameWindow(this);
91                  // put about:blank in the window to allow JS to run on this frame before the
92                  // real content is loaded
93                  final WebClient webClient = htmlPage.getWebClient();
94                  final HtmlPage temporaryPage = webClient.getPage(enclosedWindow,
95                      new WebRequest(WebClient.URL_ABOUT_BLANK));
96                  temporaryPage.setReadyState(READY_STATE_LOADING);
97              }
98          }
99          catch (final FailingHttpStatusCodeException e) {
100             // should never occur
101         }
102         catch (final IOException e) {
103             // should never occur
104         }
105         enclosedWindow_ = enclosedWindow;
106     }
107 
108     /**
109      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
110      *
111      * Called after the node for the {@code frame} or {@code iframe} has been added to the containing page.
112      * The node needs to be added first to allow JavaScript in the frame to see the frame in the parent.
113      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
114      *      {@link com.gargoylesoftware.htmlunit.WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is
115      *      set to true
116      */
117 
118     public void loadInnerPage() throws FailingHttpStatusCodeException {
119         String source = getSrcAttribute();
120         if (source.isEmpty() || StringUtils.startsWithIgnoreCase(source, WebClient.ABOUT_SCHEME)) {
121             source = WebClient.ABOUT_BLANK;
122         }
123 
124         loadInnerPageIfPossible(source);
125 
126         final Page enclosedPage = getEnclosedPage();
127         if (enclosedPage != null && enclosedPage.isHtmlPage()) {
128             final HtmlPage htmlPage = (HtmlPage) enclosedPage;
129             final AbstractJavaScriptEngine<?> jsEngine = getPage().getWebClient().getJavaScriptEngine();
130             if (jsEngine.isScriptRunning()) {
131                 final PostponedAction action = new PostponedAction(getPage()) {
132                     @Override
133                     public void execute() throws Exception {
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 com.gargoylesoftware.htmlunit.html.HtmlPage.loadFrames().
159      */
160     void setContentLoaded() {
161         contentLoaded_ = true;
162     }
163 
164     /**
165      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
166      *      {@link WebClient#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
167      */
168     private void loadInnerPageIfPossible(final String src) throws FailingHttpStatusCodeException {
169         setContentLoaded();
170         if (!src.isEmpty()) {
171             final URL url;
172             try {
173                 url = ((HtmlPage) getPage()).getFullyQualifiedUrl(src);
174             }
175             catch (final MalformedURLException e) {
176                 notifyIncorrectness("Invalid src attribute of " + getTagName() + ": url=[" + src + "]. Ignored.");
177                 return;
178             }
179             if (isAlreadyLoadedByAncestor(url)) {
180                 notifyIncorrectness("Recursive src attribute of " + getTagName() + ": url=[" + src + "]. Ignored.");
181                 return;
182             }
183             try {
184                 final WebRequest request = new WebRequest(url);
185                 request.setAdditionalHeader(HttpHeader.REFERER, getPage().getUrl().toExternalForm());
186                 getPage().getEnclosingWindow().getWebClient().getPage(enclosedWindow_, request);
187             }
188             catch (final IOException e) {
189                 LOG.error("IOException when getting content for " + getTagName() + ": url=[" + url + "]", e);
190             }
191         }
192     }
193 
194     /**
195      * Test if the provided URL is the one of one of the parents which would cause an infinite loop.
196      * @param url the URL to test
197      * @return {@code false} if no parent has already this URL
198      */
199     private boolean isAlreadyLoadedByAncestor(final URL url) {
200         WebWindow window = getPage().getEnclosingWindow();
201         while (window != null) {
202             if (UrlUtils.sameFile(url, window.getEnclosedPage().getUrl())) {
203                 return true;
204             }
205             if (window == window.getParentWindow()) {
206                 // TODO: should getParentWindow() return null on top windows?
207                 window = null;
208             }
209             else {
210                 window = window.getParentWindow();
211             }
212         }
213         return false;
214     }
215 
216     /**
217      * Returns the value of the attribute {@code longdesc}. Refer to the
218      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
219      * documentation for details on the use of this attribute.
220      *
221      * @return the value of the attribute {@code longdesc} or an empty string if that attribute isn't defined
222      */
223     public final String getLongDescAttribute() {
224         return getAttributeDirect("longdesc");
225     }
226 
227     /**
228      * Returns the value of the attribute {@code name}. Refer to the
229      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
230      * documentation for details on the use of this attribute.
231      *
232      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
233      */
234     public final String getNameAttribute() {
235         return getAttributeDirect("name");
236     }
237 
238     /**
239      * Sets the value of the {@code name} attribute.
240      *
241      * @param name the new window name
242      */
243     public final void setNameAttribute(final String name) {
244         setAttribute("name", name);
245     }
246 
247     /**
248      * Returns the value of the attribute {@code src}. Refer to the
249      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
250      * documentation for details on the use of this attribute.
251      *
252      * @return the value of the attribute {@code src} or an empty string if that attribute isn't defined
253      */
254     public final String getSrcAttribute() {
255         return getSrcAttributeNormalized();
256     }
257 
258     /**
259      * Returns the value of the attribute {@code frameborder}. Refer to the
260      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
261      * documentation for details on the use of this attribute.
262      *
263      * @return the value of the attribute {@code frameborder} or an empty string if that attribute isn't defined
264      */
265     public final String getFrameBorderAttribute() {
266         return getAttributeDirect("frameborder");
267     }
268 
269     /**
270      * Returns the value of the attribute {@code marginwidth}. Refer to the
271      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
272      * documentation for details on the use of this attribute.
273      *
274      * @return the value of the attribute {@code marginwidth} or an empty string if that attribute isn't defined
275      */
276     public final String getMarginWidthAttribute() {
277         return getAttributeDirect("marginwidth");
278     }
279 
280     /**
281      * Returns the value of the attribute {@code marginheight}. Refer to the
282      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
283      * documentation for details on the use of this attribute.
284      *
285      * @return the value of the attribute {@code marginheight} or an empty string if that attribute isn't defined
286      */
287     public final String getMarginHeightAttribute() {
288         return getAttributeDirect("marginheight");
289     }
290 
291     /**
292      * Returns the value of the attribute {@code noresize}. Refer to the
293      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
294      * documentation for details on the use of this attribute.
295      *
296      * @return the value of the attribute {@code noresize} or an empty string if that attribute isn't defined
297      */
298     public final String getNoResizeAttribute() {
299         return getAttributeDirect("noresize");
300     }
301 
302     /**
303      * Returns the value of the attribute {@code scrolling}. Refer to the
304      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
305      * documentation for details on the use of this attribute.
306      *
307      * @return the value of the attribute {@code scrolling} or an empty string if that attribute isn't defined
308      */
309     public final String getScrollingAttribute() {
310         return getAttributeDirect("scrolling");
311     }
312 
313     /**
314      * Returns the value of the attribute {@code onload}. This attribute is not
315      * actually supported by the HTML specification however it is supported
316      * by the popular browsers.
317      *
318      * @return the value of the attribute {@code onload} or an empty string if that attribute isn't defined
319      */
320     public final String getOnLoadAttribute() {
321         return getAttributeDirect("onload");
322     }
323 
324     /**
325      * Returns the currently loaded page in the enclosed window.
326      * This is a facility method for <code>getEnclosedWindow().getEnclosedPage()</code>.
327      * @see WebWindow#getEnclosedPage()
328      * @return the currently loaded page in the enclosed window, or {@code null} if no page has been loaded
329      */
330     public Page getEnclosedPage() {
331         return getEnclosedWindow().getEnclosedPage();
332     }
333 
334     /**
335      * Gets the window enclosed in this frame.
336      * @return the window enclosed in this frame
337      */
338     public FrameWindow getEnclosedWindow() {
339         return enclosedWindow_;
340     }
341 
342     /**
343      * Sets the value of the {@code src} attribute. Also loads the frame with the specified URL, if possible.
344      * @param attribute the new value of the {@code src} attribute
345      */
346     public final void setSrcAttribute(final String attribute) {
347         setAttribute("src", attribute);
348     }
349 
350     /**
351      * {@inheritDoc}
352      */
353     @Override
354     protected void setAttributeNS(final String namespaceURI, final String qualifiedName, String attributeValue,
355             final boolean notifyAttributeChangeListeners, final boolean notifyMutationObserver) {
356         if (null != attributeValue && "src".equals(qualifiedName)) {
357             attributeValue = attributeValue.trim();
358         }
359 
360         super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
361                 notifyMutationObserver);
362 
363         if ("src".equals(qualifiedName) && WebClient.ABOUT_BLANK != attributeValue) {
364             if (isAttachedToPage()) {
365                 loadSrc();
366             }
367             else {
368                 loadSrcWhenAddedToPage_ = true;
369             }
370         }
371     }
372 
373     /**
374      * {@inheritDoc}
375      */
376     @Override
377     public Attr setAttributeNode(final Attr attribute) {
378         final String qualifiedName = attribute.getName();
379         String attributeValue = null;
380         if ("src".equals(qualifiedName)) {
381             attributeValue = attribute.getValue().trim();
382         }
383 
384         final Attr result = super.setAttributeNode(attribute);
385 
386         if ("src".equals(qualifiedName) && !WebClient.ABOUT_BLANK.equals(attributeValue)) {
387             if (isAttachedToPage()) {
388                 loadSrc();
389             }
390             else {
391                 loadSrcWhenAddedToPage_ = true;
392             }
393         }
394 
395         return result;
396     }
397 
398     private void loadSrc() {
399         loadSrcWhenAddedToPage_ = false;
400         final String src = getSrcAttribute();
401 
402         final AbstractJavaScriptEngine<?> jsEngine = getPage().getWebClient().getJavaScriptEngine();
403         // When src is set from a script, loading is postponed until script finishes
404         // in fact this implementation is probably wrong: JavaScript URL should be
405         // first evaluated and only loading, when any, should be postponed.
406         if (!jsEngine.isScriptRunning() || src.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
407             loadInnerPageIfPossible(src);
408         }
409         else {
410             final Page pageInFrame = getEnclosedPage();
411             final PostponedAction action = new PostponedAction(getPage()) {
412                 @Override
413                 public void execute() throws Exception {
414                     if (!src.isEmpty() && getSrcAttribute().equals(src)) {
415                         loadInnerPage();
416                     }
417                 }
418 
419                 @Override
420                 public boolean isStillAlive() {
421                     // skip if page in frame has already been changed
422                     return super.isStillAlive() && pageInFrame == getEnclosedPage();
423                 }
424             };
425             jsEngine.addPostponedAction(action);
426         }
427     }
428 
429     @Override
430     protected void onAddedToPage() {
431         super.onAddedToPage();
432 
433         if (loadSrcWhenAddedToPage_) {
434             loadSrc();
435         }
436     }
437 
438     /**
439      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
440      *
441      * Marks this frame as created by javascript. This is needed to handle
442      * some special IE behavior.
443      */
444     public void markAsCreatedByJavascript() {
445         createdByJavascript_ = true;
446     }
447 
448     /**
449      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
450      *
451      * Unmarks this frame as created by javascript. This is needed to handle
452      * some special IE behavior.
453      */
454     public void unmarkAsCreatedByJavascript() {
455         createdByJavascript_ = false;
456     }
457 
458     /**
459      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
460      *
461      * Returns true if this frame was created by javascript. This is needed to handle
462      * some special IE behavior.
463      * @return true or false
464      */
465     public boolean wasCreatedByJavascript() {
466         return createdByJavascript_;
467     }
468 
469     /**
470      * Creates a new {@link WebWindow} for the new clone.
471      * {@inheritDoc}
472      */
473     @Override
474     public DomNode cloneNode(final boolean deep) {
475         final BaseFrameElement clone = (BaseFrameElement) super.cloneNode(deep);
476         clone.init();
477         return clone;
478     }
479 
480     /**
481      * Remove our window also.
482      * {@inheritDoc}
483      */
484     @Override
485     public void remove() {
486         super.remove();
487         getEnclosedWindow().close();
488     }
489 }