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 static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONLOAD_INTERNAL_JAVASCRIPT;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLSCRIPT_TRIM_TYPE;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_SCRIPT_SUPPORTS_FOR_AND_EVENT_WINDOW;
20  import static com.gargoylesoftware.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
21  
22  import java.nio.charset.Charset;
23  
24  import org.apache.commons.lang3.StringUtils;
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  
28  import com.gargoylesoftware.htmlunit.BrowserVersion;
29  import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
30  import com.gargoylesoftware.htmlunit.SgmlPage;
31  import com.gargoylesoftware.htmlunit.html.HtmlPage.JavaScriptLoadResult;
32  import com.gargoylesoftware.htmlunit.javascript.AbstractJavaScriptEngine;
33  import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
34  import com.gargoylesoftware.htmlunit.javascript.host.Window;
35  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
36  import com.gargoylesoftware.htmlunit.javascript.host.event.EventHandler;
37  import com.gargoylesoftware.htmlunit.javascript.host.event.EventTarget;
38  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
39  import com.gargoylesoftware.htmlunit.protocol.javascript.JavaScriptURLConnection;
40  import com.gargoylesoftware.htmlunit.util.EncodingSniffer;
41  import com.gargoylesoftware.htmlunit.xml.XmlPage;
42  
43  import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;
44  
45  /**
46   * A helper class to be used by elements which support {@link ScriptElement}.
47   *
48   * @author Ahmed Ashour
49   */
50  public final class ScriptElementSupport {
51  
52      private static final Log LOG = LogFactory.getLog(ScriptElementSupport.class);
53      /** Invalid source attribute which should be ignored (used by JS libraries like jQuery). */
54      private static final String SLASH_SLASH_COLON = "//:";
55  
56      private ScriptElementSupport() {
57      }
58  
59      /**
60       * Lifecycle method invoked after a node and all its children have been added to a page, during
61       * parsing of the HTML. Intended to be overridden by nodes which need to perform custom logic
62       * after they and all their child nodes have been processed by the HTML parser. This method is
63       * not recursive, and the default implementation is empty, so there is no need to call
64       * <tt>super.onAllChildrenAddedToPage()</tt> if you implement this method.
65       * @param element the element
66       * @param postponed whether to use {@link com.gargoylesoftware.htmlunit.javascript.PostponedAction} or no
67       */
68      public static void onAllChildrenAddedToPage(final DomElement element, final boolean postponed) {
69          if (element.getOwnerDocument() instanceof XmlPage) {
70              return;
71          }
72          if (LOG.isDebugEnabled()) {
73              LOG.debug("Script node added: " + element.asXml());
74          }
75  
76          final PostponedAction action = new PostponedAction(element.getPage(), "Execution of script " + element) {
77              @Override
78              public void execute() {
79                  final HTMLDocument jsDoc = (HTMLDocument)
80                          ((Window) element.getPage().getEnclosingWindow().getScriptableObject()).getDocument();
81                  jsDoc.setExecutingDynamicExternalPosponed(element.getStartLineNumber() == -1
82                          && ((ScriptElement) element).getSrcAttribute() != ATTRIBUTE_NOT_DEFINED);
83  
84                  try {
85                      executeScriptIfNeeded(element);
86                  }
87                  finally {
88                      jsDoc.setExecutingDynamicExternalPosponed(false);
89                  }
90              }
91          };
92  
93          final AbstractJavaScriptEngine<?> engine = element.getPage().getWebClient().getJavaScriptEngine();
94          if (element.hasAttribute("async") && !engine.isScriptRunning()) {
95              final HtmlPage owningPage = element.getHtmlPageOrNull();
96              owningPage.addAfterLoadAction(action);
97          }
98          else if (element.hasAttribute("async")
99                  || postponed && StringUtils.isBlank(element.getTextContent())) {
100             engine.addPostponedAction(action);
101         }
102         else {
103             try {
104                 action.execute();
105             }
106             catch (final RuntimeException e) {
107                 throw e;
108             }
109             catch (final Exception e) {
110                 throw new RuntimeException(e);
111             }
112         }
113     }
114 
115     /**
116      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
117      *
118      * Executes this script node if necessary and/or possible.
119      * @param element the element
120      */
121     public static void executeScriptIfNeeded(final DomElement element) {
122         if (!isExecutionNeeded(element)) {
123             return;
124         }
125 
126         final HtmlPage page = (HtmlPage) element.getPage();
127 
128         final String src = ((ScriptElement) element).getSrcAttribute();
129         if (src.equals(SLASH_SLASH_COLON)) {
130             executeEvent(element, Event.TYPE_ERROR);
131             return;
132         }
133 
134         if (src != ATTRIBUTE_NOT_DEFINED) {
135             if (!src.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
136                 // <script src="[url]"></script>
137                 if (LOG.isDebugEnabled()) {
138                     LOG.debug("Loading external JavaScript: " + src);
139                 }
140                 try {
141                     final ScriptElement scriptElement = (ScriptElement) element;
142                     scriptElement.setExecuted(true);
143                     Charset charset = EncodingSniffer.toCharset(scriptElement.getCharsetAttribute());
144                     if (charset == null) {
145                         charset = page.getCharset();
146                     }
147                     final JavaScriptLoadResult result = page.loadExternalJavaScriptFile(src, charset);
148                     if (result == JavaScriptLoadResult.SUCCESS) {
149                         executeEvent(element, Event.TYPE_LOAD);
150                     }
151                     else if (result == JavaScriptLoadResult.DOWNLOAD_ERROR) {
152                         executeEvent(element, Event.TYPE_ERROR);
153                     }
154                 }
155                 catch (final FailingHttpStatusCodeException e) {
156                     executeEvent(element, Event.TYPE_ERROR);
157                     throw e;
158                 }
159             }
160         }
161         else if (element.getFirstChild() != null) {
162             // <script>[code]</script>
163             executeInlineScriptIfNeeded(element);
164 
165             if (element.hasFeature(EVENT_ONLOAD_INTERNAL_JAVASCRIPT)) {
166                 executeEvent(element, Event.TYPE_LOAD);
167             }
168         }
169     }
170 
171     /**
172      * Indicates if script execution is necessary and/or possible.
173      *
174      * @param element the element
175      * @return {@code true} if the script should be executed
176      */
177     private static boolean isExecutionNeeded(final DomElement element) {
178         if (((ScriptElement) element).isExecuted()) {
179             return false;
180         }
181 
182         if (!element.isAttachedToPage()) {
183             return false;
184         }
185 
186         // If JavaScript is disabled, we don't need to execute.
187         final SgmlPage page = element.getPage();
188         if (!page.getWebClient().getOptions().isJavaScriptEnabled()) {
189             return false;
190         }
191 
192         // If innerHTML or outerHTML is being parsed
193         final HtmlPage htmlPage = element.getHtmlPageOrNull();
194         if (htmlPage != null && htmlPage.isParsingHtmlSnippet()) {
195             return false;
196         }
197 
198         // If the script node is nested in an iframe, a noframes, or a noscript node, we don't need to execute.
199         for (DomNode o = element; o != null; o = o.getParentNode()) {
200             if (o instanceof HtmlInlineFrame || o instanceof HtmlNoFrames) {
201                 return false;
202             }
203         }
204 
205         // If the underlying page no longer owns its window, the client has moved on (possibly
206         // because another script set window.location.href), and we don't need to execute.
207         if (page.getEnclosingWindow() != null && page.getEnclosingWindow().getEnclosedPage() != page) {
208             return false;
209         }
210 
211         // If the script language is not JavaScript, we can't execute.
212         final String t = element.getAttribute("type");
213         final String l = element.getAttribute("language");
214         if (!isJavaScript(element, t, l)) {
215             LOG.warn("Script is not JavaScript (type: " + t + ", language: " + l + "). Skipping execution.");
216             return false;
217         }
218 
219         // If the script's root ancestor node is not the page, then the script is not a part of the page.
220         // If it isn't yet part of the page, don't execute the script; it's probably just being cloned.
221 
222         return element.getPage().isAncestorOf(element);
223     }
224 
225     /**
226      * Returns true if a script with the specified type and language attributes is actually JavaScript.
227      * According to <a href="http://www.w3.org/TR/REC-html40/types.html#h-6.7">W3C recommendation</a>
228      * are content types case insensitive.<b>
229      * IE supports only a limited number of values for the type attribute. For testing you can
230      * use http://www.robinlionheart.com/stds/html4/scripts.
231      * @param element the element
232      * @param typeAttribute the type attribute specified in the script tag
233      * @param languageAttribute the language attribute specified in the script tag
234      * @return true if the script is JavaScript
235      */
236     static boolean isJavaScript(final DomElement element, String typeAttribute, final String languageAttribute) {
237         final BrowserVersion browserVersion = element.getPage().getWebClient().getBrowserVersion();
238 
239         if (browserVersion.hasFeature(HTMLSCRIPT_TRIM_TYPE)) {
240             typeAttribute = typeAttribute.trim();
241         }
242 
243         if (StringUtils.isNotEmpty(typeAttribute)) {
244             if ("text/javascript".equalsIgnoreCase(typeAttribute)
245                     || "text/ecmascript".equalsIgnoreCase(typeAttribute)) {
246                 return true;
247             }
248 
249             if ("application/javascript".equalsIgnoreCase(typeAttribute)
250                             || "application/ecmascript".equalsIgnoreCase(typeAttribute)
251                             || "application/x-javascript".equalsIgnoreCase(typeAttribute)) {
252                 return true;
253             }
254             return false;
255         }
256 
257         if (StringUtils.isNotEmpty(languageAttribute)) {
258             return StringUtils.startsWithIgnoreCase(languageAttribute, "javascript");
259         }
260         return true;
261     }
262 
263     private static void executeEvent(final DomElement element, final String type) {
264         final EventTarget eventTarget = (EventTarget) element.getScriptableObject();
265         final Event event = new Event(element, type);
266         eventTarget.executeEventLocally(event);
267     }
268 
269     /**
270      * Executes this script node as inline script if necessary and/or possible.
271      */
272     private static void executeInlineScriptIfNeeded(final DomElement element) {
273         if (!isExecutionNeeded(element)) {
274             return;
275         }
276 
277         final String src = ((ScriptElement) element).getSrcAttribute();
278         if (src != ATTRIBUTE_NOT_DEFINED) {
279             return;
280         }
281 
282         final String forr = element.getAttribute("for");
283         String event = element.getAttribute("event");
284         // The event name can be like "onload" or "onload()".
285         if (event.endsWith("()")) {
286             event = event.substring(0, event.length() - 2);
287         }
288 
289         final String scriptCode = getScriptCode(element);
290         if (event != ATTRIBUTE_NOT_DEFINED && forr != ATTRIBUTE_NOT_DEFINED) {
291             if (element.hasFeature(JS_SCRIPT_SUPPORTS_FOR_AND_EVENT_WINDOW) && "window".equals(forr)) {
292                 final Window window = (Window) element.getPage().getEnclosingWindow().getScriptableObject();
293                 final BaseFunction function = new EventHandler(element, event, scriptCode);
294                 window.getEventListenersContainer().addEventListener(StringUtils.substring(event, 2), function, false);
295                 return;
296             }
297         }
298         if (forr == ATTRIBUTE_NOT_DEFINED || "onload".equals(event)) {
299             final String url = element.getPage().getUrl().toExternalForm();
300             final int line1 = element.getStartLineNumber();
301             final int line2 = element.getEndLineNumber();
302             final int col1 = element.getStartColumnNumber();
303             final int col2 = element.getEndColumnNumber();
304             final String desc = "script in " + url + " from (" + line1 + ", " + col1
305                 + ") to (" + line2 + ", " + col2 + ")";
306 
307             ((ScriptElement) element).setExecuted(true);
308             ((HtmlPage) element.getPage()).executeJavaScript(scriptCode, desc, line1);
309         }
310     }
311 
312     /**
313      * Gets the script held within the script tag.
314      */
315     private static String getScriptCode(final DomElement element) {
316         final Iterable<DomNode> textNodes = element.getChildren();
317         final StringBuilder scriptCode = new StringBuilder();
318         for (final DomNode node : textNodes) {
319             if (node instanceof DomText) {
320                 final DomText domText = (DomText) node;
321                 scriptCode.append(domText.getData());
322             }
323         }
324         return scriptCode.toString();
325     }
326 
327 }