View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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 static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
18  
19  import java.nio.charset.Charset;
20  
21  import org.apache.commons.lang3.StringUtils;
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  import org.htmlunit.FailingHttpStatusCodeException;
25  import org.htmlunit.SgmlPage;
26  import org.htmlunit.WebClient;
27  import org.htmlunit.WebWindow;
28  import org.htmlunit.html.HtmlPage.JavaScriptLoadResult;
29  import org.htmlunit.javascript.AbstractJavaScriptEngine;
30  import org.htmlunit.javascript.PostponedAction;
31  import org.htmlunit.javascript.host.Window;
32  import org.htmlunit.javascript.host.dom.Document;
33  import org.htmlunit.javascript.host.event.Event;
34  import org.htmlunit.javascript.host.event.EventTarget;
35  import org.htmlunit.javascript.host.html.HTMLDocument;
36  import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
37  import org.htmlunit.util.EncodingSniffer;
38  import org.htmlunit.util.MimeType;
39  import org.htmlunit.xml.XmlPage;
40  
41  /**
42   * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
43   *
44   * A helper class to be used by elements which support {@link ScriptElement}.
45   *
46   * @author Ahmed Ashour
47   * @author Ronald Brill
48   * @author Ronny Shapiro
49   * @author Sven Strickroth
50   */
51  public final class ScriptElementSupport {
52  
53      private static final Log LOG = LogFactory.getLog(ScriptElementSupport.class);
54  
55      /** Invalid source attribute which should be ignored (used by JS libraries like jQuery). */
56      private static final String SLASH_SLASH_COLON = "//:";
57  
58      private ScriptElementSupport() {
59          // util class
60      }
61  
62      /**
63       * Support method that is called from the (html or svg) script and the link tag.
64       *
65       * @param script the ScriptElement to work for
66       * @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or not
67       */
68      public static void onAllChildrenAddedToPage(final ScriptElement script, final boolean postponed) {
69          final DomElement element = (DomElement) script;
70          if (element.getOwnerDocument() instanceof XmlPage) {
71              return;
72          }
73          if (LOG.isDebugEnabled()) {
74              LOG.debug("Script node added: " + element.asXml());
75          }
76  
77          final SgmlPage page = element.getPage();
78          final WebClient webClient = page.getWebClient();
79          if (!webClient.isJavaScriptEngineEnabled()) {
80              LOG.debug("Script found but not executed because javascript engine is disabled");
81              return;
82          }
83  
84          final String srcAttrib = script.getScriptSource();
85          final boolean hasNoSrcAttrib = ATTRIBUTE_NOT_DEFINED == srcAttrib;
86          if (!hasNoSrcAttrib && script.isDeferred()) {
87              return;
88          }
89  
90          final WebWindow webWindow = page.getEnclosingWindow();
91          if (webWindow != null) {
92              final StringBuilder description = new StringBuilder()
93                      .append("Execution of ")
94                      .append(hasNoSrcAttrib ? "inline " : "external ")
95                      .append(element.getClass().getSimpleName());
96              if (!hasNoSrcAttrib) {
97                  description.append(" (").append(srcAttrib).append(')');
98              }
99  
100             final PostponedAction action = new PostponedAction(page, description.toString()) {
101                 @Override
102                 public void execute() {
103                     // see HTMLDocument.setExecutingDynamicExternalPosponed(boolean)
104                     HTMLDocument jsDoc = null;
105                     final Window window = webWindow.getScriptableObject();
106                     if (window != null) {
107                         jsDoc = (HTMLDocument) window.getDocument();
108                         jsDoc.setExecutingDynamicExternalPosponed(element.getStartLineNumber() == -1
109                                 && !hasNoSrcAttrib);
110                     }
111                     try {
112                         executeScriptIfNeeded(script, false, false);
113                     }
114                     finally {
115                         if (jsDoc != null) {
116                             jsDoc.setExecutingDynamicExternalPosponed(false);
117                         }
118                     }
119                 }
120             };
121 
122             final AbstractJavaScriptEngine<?> engine = webClient.getJavaScriptEngine();
123             if (element.hasAttribute("async") && !hasNoSrcAttrib) {
124                 engine.addPostponedAction(action);
125             }
126             else if (postponed && !hasNoSrcAttrib) {
127                 engine.addPostponedAction(action);
128             }
129             else {
130                 try {
131                     action.execute();
132                     engine.processPostponedActions();
133                 }
134                 catch (final RuntimeException e) {
135                     throw e;
136                 }
137                 catch (final Exception e) {
138                     throw new RuntimeException(e);
139                 }
140             }
141         }
142     }
143 
144     /**
145      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
146      *
147      * Executes this script node if necessary and/or possible.
148      *
149      * @param script the ScriptElement to work for
150      * @param ignoreAttachedToPage don't do the isAttachedToPage check
151      * @param ignorePageIsAncestor don't do the element.getPage().isAncestorOf(element) check
152      */
153     public static void executeScriptIfNeeded(final ScriptElement script, final boolean ignoreAttachedToPage,
154             final boolean ignorePageIsAncestor) {
155         if (!isExecutionNeeded(script, ignoreAttachedToPage, ignorePageIsAncestor)) {
156             return;
157         }
158 
159         final String src = script.getScriptSource();
160         final DomElement element = (DomElement) script;
161         if (SLASH_SLASH_COLON.equals(src)) {
162             executeEvent(element, Event.TYPE_ERROR);
163             return;
164         }
165 
166         final HtmlPage page = (HtmlPage) element.getPage();
167         if (src != ATTRIBUTE_NOT_DEFINED) {
168             if (!src.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
169                 // <script src="[url]"></script>
170                 if (LOG.isDebugEnabled()) {
171                     LOG.debug("Loading external JavaScript: " + src);
172                 }
173                 try {
174                     script.setExecuted(true);
175                     Charset charset = EncodingSniffer.toCharset(script.getScriptCharset());
176                     if (charset == null) {
177                         charset = page.getCharset();
178                     }
179 
180                     final JavaScriptLoadResult result;
181                     final Window win = page.getEnclosingWindow().getScriptableObject();
182                     final Document doc = win.getDocument();
183                     try {
184                         doc.setCurrentScript(element.getScriptableObject());
185                         result = page.loadExternalJavaScriptFile(src, charset);
186                     }
187                     finally {
188                         doc.setCurrentScript(null);
189                     }
190 
191                     if (result == JavaScriptLoadResult.SUCCESS) {
192                         executeEvent(element, Event.TYPE_LOAD);
193                     }
194                     else if (result == JavaScriptLoadResult.DOWNLOAD_ERROR) {
195                         executeEvent(element, Event.TYPE_ERROR);
196                     }
197                     else if (result == JavaScriptLoadResult.NO_CONTENT) {
198                         executeEvent(element, Event.TYPE_LOAD);
199                     }
200                 }
201                 catch (final FailingHttpStatusCodeException e) {
202                     executeEvent(element, Event.TYPE_ERROR);
203                     throw e;
204                 }
205             }
206         }
207         else if (element.getFirstChild() != null) {
208             // <script>[code]</script>
209             final Window win = page.getEnclosingWindow().getScriptableObject();
210             final Document doc = win.getDocument();
211             try {
212                 doc.setCurrentScript(element.getScriptableObject());
213                 executeInlineScriptIfNeeded(script);
214             }
215             finally {
216                 doc.setCurrentScript(null);
217             }
218         }
219     }
220 
221     /**
222      * Indicates if script execution is necessary and/or possible.
223      *
224      * @param script the ScriptElement to work for
225      * @param ignoreAttachedToPage don't do the isAttachedToPage check
226      * @param ignorePageIsAncestor don't do the element.getPage().isAncestorOf(element) check
227      * @return {@code true} if the script should be executed
228      */
229     private static boolean isExecutionNeeded(final ScriptElement script, final boolean ignoreAttachedToPage,
230             final boolean ignorePageIsAncestor) {
231         if (script.isExecuted() || script.wasCreatedByDomParser()) {
232             return false;
233         }
234 
235         final DomElement element = (DomElement) script;
236         if (!ignoreAttachedToPage && !element.isAttachedToPage()) {
237             return false;
238         }
239 
240         // If JavaScript is disabled, we don't need to execute.
241         final SgmlPage page = element.getPage();
242         if (!page.getWebClient().isJavaScriptEnabled()) {
243             return false;
244         }
245 
246         // If innerHTML or outerHTML is being parsed
247         final HtmlPage htmlPage = element.getHtmlPageOrNull();
248         if (htmlPage != null && htmlPage.isParsingHtmlSnippet()) {
249             return false;
250         }
251 
252         // If the script node is nested in an iframe, a noframes, or a noscript node, we don't need to execute.
253         for (DomNode o = element; o != null; o = o.getParentNode()) {
254             if (o instanceof HtmlInlineFrame || o instanceof HtmlNoFrames) {
255                 return false;
256             }
257         }
258 
259         // If the underlying page no longer owns its window, the client has moved on (possibly
260         // because another script set window.location.href), and we don't need to execute.
261         if (page.getEnclosingWindow() != null && page.getEnclosingWindow().getEnclosedPage() != page) {
262             return false;
263         }
264 
265         // If the script language is not JavaScript, we can't execute.
266         final String t = element.getAttributeDirect("type");
267         final String l = element.getAttributeDirect("language");
268         if (!isJavaScript(t, l)) {
269             // Was at warn level before 2.46 but other types or tricky implementations with unsupported types
270             // are common out there and too many peoples out there thinking the is the root of problems.
271             // Browsers are also not warning about this.
272             if (LOG.isDebugEnabled()) {
273                 LOG.debug("Script is not JavaScript (type: '" + t + "', language: '" + l + "'). Skipping execution.");
274             }
275             return false;
276         }
277 
278         // If the script's root ancestor node is not the page, then the script is not a part of the page.
279         // If it isn't yet part of the page, don't execute the script; it's probably just being cloned.
280         return ignorePageIsAncestor || element.getPage().isAncestorOf(element);
281     }
282 
283     /**
284      * Returns true if a script with the specified type and language attributes is actually JavaScript.
285      * According to <a href="http://www.w3.org/TR/REC-html40/types.html#h-6.7">W3C recommendation</a>
286      * are content types case insensitive.<br>
287      *
288      * @param typeAttribute the type attribute specified in the script tag
289      * @param languageAttribute the language attribute specified in the script tag
290      * @return true if the script is JavaScript
291      */
292     public static boolean isJavaScript(String typeAttribute, final String languageAttribute) {
293         typeAttribute = typeAttribute.trim();
294 
295         if (StringUtils.isNotEmpty(typeAttribute)) {
296             return MimeType.isJavascriptMimeType(typeAttribute);
297         }
298 
299         if (StringUtils.isNotEmpty(languageAttribute)) {
300             return StringUtils.startsWithIgnoreCase(languageAttribute, "javascript");
301         }
302         return true;
303     }
304 
305     private static void executeEvent(final DomElement element, final String type) {
306         final EventTarget eventTarget = element.getScriptableObject();
307         final Event event = new Event(element, type);
308 
309         event.setParentScope(eventTarget);
310         event.setPrototype(eventTarget.getPrototype(event.getClass()));
311 
312         eventTarget.executeEventLocally(event);
313     }
314 
315     /**
316      * Executes this script node as inline script if necessary and/or possible.
317      */
318     private static void executeInlineScriptIfNeeded(final ScriptElement script) {
319         if (!isExecutionNeeded(script, false, false)) {
320             return;
321         }
322 
323         final String src = script.getScriptSource();
324         if (src != ATTRIBUTE_NOT_DEFINED) {
325             return;
326         }
327 
328         final DomElement element = (DomElement) script;
329         final String forr = element.getAttributeDirect("for");
330         String event = element.getAttributeDirect("event");
331         // The event name can be like "onload" or "onload()".
332         if (event.endsWith("()")) {
333             event = event.substring(0, event.length() - 2);
334         }
335 
336         final String scriptCode = getScriptCode(element);
337         if (forr == ATTRIBUTE_NOT_DEFINED || "onload".equals(event)) {
338             final String url = element.getPage().getUrl().toExternalForm();
339             final int line1 = element.getStartLineNumber();
340             final int line2 = element.getEndLineNumber();
341             final int col1 = element.getStartColumnNumber();
342             final int col2 = element.getEndColumnNumber();
343             final String desc = "script in " + url + " from (" + line1 + ", " + col1
344                 + ") to (" + line2 + ", " + col2 + ")";
345 
346             script.setExecuted(true);
347             ((HtmlPage) element.getPage()).executeJavaScript(scriptCode, desc, line1);
348         }
349     }
350 
351     /**
352      * Gets the script held within the script tag.
353      */
354     private static String getScriptCode(final DomElement element) {
355         final Iterable<DomNode> textNodes = element.getChildren();
356         final StringBuilder scriptCode = new StringBuilder();
357         for (final DomNode node : textNodes) {
358             if (node instanceof DomText) {
359                 final DomText domText = (DomText) node;
360                 scriptCode.append(domText.getData());
361             }
362         }
363         return scriptCode.toString();
364     }
365 
366 }