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