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