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