org.htmlunit.html.ScriptElementSupport Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xlt Show documentation
Show all versions of xlt Show documentation
XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.
The newest version!
/*
* Copyright (c) 2002-2024 Gargoyle Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.html;
import static org.htmlunit.BrowserVersionFeatures.EVENT_ONLOAD_INTERNAL_JAVASCRIPT;
import static org.htmlunit.BrowserVersionFeatures.HTMLSCRIPT_TRIM_TYPE;
import static org.htmlunit.BrowserVersionFeatures.JS_SCRIPT_HANDLE_204_AS_ERROR;
import static org.htmlunit.BrowserVersionFeatures.JS_SCRIPT_SUPPORTS_FOR_AND_EVENT_WINDOW;
import static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
import java.nio.charset.Charset;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.htmlunit.BrowserVersion;
import org.htmlunit.FailingHttpStatusCodeException;
import org.htmlunit.SgmlPage;
import org.htmlunit.WebWindow;
import org.htmlunit.html.HtmlPage.JavaScriptLoadResult;
import org.htmlunit.javascript.AbstractJavaScriptEngine;
import org.htmlunit.javascript.PostponedAction;
import org.htmlunit.javascript.host.Window;
import org.htmlunit.javascript.host.dom.Document;
import org.htmlunit.javascript.host.event.Event;
import org.htmlunit.javascript.host.event.EventHandler;
import org.htmlunit.javascript.host.event.EventTarget;
import org.htmlunit.javascript.host.html.HTMLDocument;
import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
import org.htmlunit.util.EncodingSniffer;
import org.htmlunit.util.MimeType;
import org.htmlunit.xml.XmlPage;
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* A helper class to be used by elements which support {@link ScriptElement}.
*
* @author Ahmed Ashour
* @author Ronald Brill
* @author Ronny Shapiro
*/
public final class ScriptElementSupport {
private static final Log LOG = LogFactory.getLog(ScriptElementSupport.class);
/** Invalid source attribute which should be ignored (used by JS libraries like jQuery). */
private static final String SLASH_SLASH_COLON = "//:";
private ScriptElementSupport() {
}
/**
* Lifecycle method invoked after a node and all its children have been added to a page, during
* parsing of the HTML. Intended to be overridden by nodes which need to perform custom logic
* after they and all their child nodes have been processed by the HTML parser. This method is
* not recursive, and the default implementation is empty, so there is no need to call
* super.onAllChildrenAddedToPage()
if you implement this method.
* @param element the element
* @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or no
*/
public static void onAllChildrenAddedToPage(final DomElement element, final boolean postponed) {
if (element.getOwnerDocument() instanceof XmlPage) {
return;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Script node added: " + element.asXml());
}
if (!element.getPage().getWebClient().isJavaScriptEngineEnabled()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Script found but not executed because javascript engine is disabled");
}
return;
}
final ScriptElement script = (ScriptElement) element;
final String srcAttrib = script.getSrcAttribute();
final boolean hasSrcAttrib = ATTRIBUTE_NOT_DEFINED == srcAttrib;
if (!hasSrcAttrib && script.isDeferred()) {
return;
}
final WebWindow webWindow = element.getPage().getEnclosingWindow();
if (webWindow != null) {
final StringBuilder description = new StringBuilder()
.append("Execution of ")
.append(hasSrcAttrib ? "inline " : "external ")
.append(element.getClass().getSimpleName());
if (!hasSrcAttrib) {
description.append(" (").append(srcAttrib).append(')');
}
final PostponedAction action = new PostponedAction(element.getPage(), description.toString()) {
@Override
public void execute() {
// see HTMLDocument.setExecutingDynamicExternalPosponed(boolean)
HTMLDocument jsDoc = null;
final Window window = webWindow.getScriptableObject();
if (window != null) {
jsDoc = (HTMLDocument) window.getDocument();
jsDoc.setExecutingDynamicExternalPosponed(element.getStartLineNumber() == -1
&& ATTRIBUTE_NOT_DEFINED != srcAttrib);
}
try {
executeScriptIfNeeded(element, false, false);
}
finally {
if (jsDoc != null) {
jsDoc.setExecutingDynamicExternalPosponed(false);
}
}
}
};
final AbstractJavaScriptEngine> engine = element.getPage().getWebClient().getJavaScriptEngine();
if (engine != null
&& element.hasAttribute("async") && !engine.isScriptRunning()) {
final HtmlPage owningPage = element.getHtmlPageOrNull();
owningPage.addAfterLoadAction(action);
}
else if (engine != null
&& (element.hasAttribute("async")
|| postponed && StringUtils.isBlank(element.getTextContent()))) {
engine.addPostponedAction(action);
}
else {
try {
action.execute();
}
catch (final RuntimeException e) {
throw e;
}
catch (final Exception e) {
throw new RuntimeException(e);
}
}
}
}
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* Executes this script node if necessary and/or possible.
* @param element the element
* @param ignoreAttachedToPage don't do the isAttachedToPage check
* @param ignorePageIsAncestor don't do the element.getPage().isAncestorOf(element) check
*/
public static void executeScriptIfNeeded(final DomElement element, final boolean ignoreAttachedToPage,
final boolean ignorePageIsAncestor) {
if (!isExecutionNeeded(element, ignoreAttachedToPage, ignorePageIsAncestor)) {
return;
}
final ScriptElement scriptElement = (ScriptElement) element;
final String src = scriptElement.getSrcAttribute();
if (src.equals(SLASH_SLASH_COLON)) {
executeEvent(element, Event.TYPE_ERROR);
return;
}
final HtmlPage page = (HtmlPage) element.getPage();
if (src != ATTRIBUTE_NOT_DEFINED) {
if (!src.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
//
if (LOG.isDebugEnabled()) {
LOG.debug("Loading external JavaScript: " + src);
}
try {
scriptElement.setExecuted(true);
Charset charset = EncodingSniffer.toCharset(scriptElement.getCharsetAttribute());
if (charset == null) {
charset = page.getCharset();
}
final JavaScriptLoadResult result;
final Window win = page.getEnclosingWindow().getScriptableObject();
final Document doc = win.getDocument();
try {
doc.setCurrentScript(element.getScriptableObject());
result = page.loadExternalJavaScriptFile(src, charset);
}
finally {
doc.setCurrentScript(null);
}
if (result == JavaScriptLoadResult.SUCCESS) {
executeEvent(element, Event.TYPE_LOAD);
}
else if (result == JavaScriptLoadResult.DOWNLOAD_ERROR) {
executeEvent(element, Event.TYPE_ERROR);
}
else if (result == JavaScriptLoadResult.NO_CONTENT) {
final BrowserVersion browserVersion = page.getWebClient().getBrowserVersion();
if (browserVersion.hasFeature(JS_SCRIPT_HANDLE_204_AS_ERROR)) {
executeEvent(element, Event.TYPE_ERROR);
}
else {
executeEvent(element, Event.TYPE_LOAD);
}
}
}
catch (final FailingHttpStatusCodeException e) {
executeEvent(element, Event.TYPE_ERROR);
throw e;
}
}
}
else if (element.getFirstChild() != null) {
//
final Window win = page.getEnclosingWindow().getScriptableObject();
final Document doc = win.getDocument();
try {
doc.setCurrentScript(element.getScriptableObject());
executeInlineScriptIfNeeded(element);
}
finally {
doc.setCurrentScript(null);
}
if (element.hasFeature(EVENT_ONLOAD_INTERNAL_JAVASCRIPT)) {
executeEvent(element, Event.TYPE_LOAD);
}
}
}
/**
* Indicates if script execution is necessary and/or possible.
*
* @param element the element
* @param ignoreAttachedToPage don't do the isAttachedToPage check
* @param ignorePageIsAncestor don't do the element.getPage().isAncestorOf(element) check
* @return {@code true} if the script should be executed
*/
private static boolean isExecutionNeeded(final DomElement element, final boolean ignoreAttachedToPage,
final boolean ignorePageIsAncestor) {
final ScriptElement script = (ScriptElement) element;
if (script.isExecuted() || script.wasCreatedByDomParser()) {
return false;
}
if (!ignoreAttachedToPage && !element.isAttachedToPage()) {
return false;
}
// If JavaScript is disabled, we don't need to execute.
final SgmlPage page = element.getPage();
if (!page.getWebClient().isJavaScriptEnabled()) {
return false;
}
// If innerHTML or outerHTML is being parsed
final HtmlPage htmlPage = element.getHtmlPageOrNull();
if (htmlPage != null && htmlPage.isParsingHtmlSnippet()) {
return false;
}
// If the script node is nested in an iframe, a noframes, or a noscript node, we don't need to execute.
for (DomNode o = element; o != null; o = o.getParentNode()) {
if (o instanceof HtmlInlineFrame || o instanceof HtmlNoFrames) {
return false;
}
}
// If the underlying page no longer owns its window, the client has moved on (possibly
// because another script set window.location.href), and we don't need to execute.
if (page.getEnclosingWindow() != null && page.getEnclosingWindow().getEnclosedPage() != page) {
return false;
}
// If the script language is not JavaScript, we can't execute.
final String t = element.getAttributeDirect("type");
final String l = element.getAttributeDirect("language");
if (!isJavaScript(element, t, l)) {
// Was at warn level before 2.46 but other types or tricky implementations with unsupported types
// are common out there and too many peoples out there thinking the is the root of problems.
// Browsers are also not warning about this.
if (LOG.isDebugEnabled()) {
LOG.debug("Script is not JavaScript (type: '" + t + "', language: '" + l + "'). Skipping execution.");
}
return false;
}
// If the script's root ancestor node is not the page, then the script is not a part of the page.
// If it isn't yet part of the page, don't execute the script; it's probably just being cloned.
return ignorePageIsAncestor || element.getPage().isAncestorOf(element);
}
/**
* Returns true if a script with the specified type and language attributes is actually JavaScript.
* According to W3C recommendation
* are content types case insensitive.
* IE supports only a limited number of values for the type attribute. For testing you can
* use
* http://www.robinlionheart.com/stds/html4/scripts.
* @param element the element
* @param typeAttribute the type attribute specified in the script tag
* @param languageAttribute the language attribute specified in the script tag
* @return true if the script is JavaScript
*/
public static boolean isJavaScript(final DomElement element, String typeAttribute, final String languageAttribute) {
final BrowserVersion browserVersion = element.getPage().getWebClient().getBrowserVersion();
if (browserVersion.hasFeature(HTMLSCRIPT_TRIM_TYPE)) {
typeAttribute = typeAttribute.trim();
}
if (StringUtils.isNotEmpty(typeAttribute)) {
return MimeType.isJavascriptMimeType(typeAttribute);
}
if (StringUtils.isNotEmpty(languageAttribute)) {
return StringUtils.startsWithIgnoreCase(languageAttribute, "javascript");
}
return true;
}
private static void executeEvent(final DomElement element, final String type) {
final EventTarget eventTarget = element.getScriptableObject();
final Event event = new Event(element, type);
eventTarget.executeEventLocally(event);
}
/**
* Executes this script node as inline script if necessary and/or possible.
*/
private static void executeInlineScriptIfNeeded(final DomElement element) {
if (!isExecutionNeeded(element, false, false)) {
return;
}
final ScriptElement scriptElement = (ScriptElement) element;
final String src = scriptElement.getSrcAttribute();
if (src != ATTRIBUTE_NOT_DEFINED) {
return;
}
final String forr = element.getAttributeDirect("for");
String event = element.getAttributeDirect("event");
// The event name can be like "onload" or "onload()".
if (event.endsWith("()")) {
event = event.substring(0, event.length() - 2);
}
final String scriptCode = getScriptCode(element);
if (event != ATTRIBUTE_NOT_DEFINED
&& forr != ATTRIBUTE_NOT_DEFINED
&& element.hasFeature(JS_SCRIPT_SUPPORTS_FOR_AND_EVENT_WINDOW)
&& "window".equals(forr)) {
final Window window = element.getPage().getEnclosingWindow().getScriptableObject();
final EventHandler function = new EventHandler(element, event, scriptCode);
window.getEventListenersContainer().addEventListener(StringUtils.substring(event, 2), function, false);
return;
}
if (forr == ATTRIBUTE_NOT_DEFINED || "onload".equals(event)) {
final String url = element.getPage().getUrl().toExternalForm();
final int line1 = element.getStartLineNumber();
final int line2 = element.getEndLineNumber();
final int col1 = element.getStartColumnNumber();
final int col2 = element.getEndColumnNumber();
final String desc = "script in " + url + " from (" + line1 + ", " + col1
+ ") to (" + line2 + ", " + col2 + ")";
scriptElement.setExecuted(true);
((HtmlPage) element.getPage()).executeJavaScript(scriptCode, desc, line1);
}
}
/**
* Gets the script held within the script tag.
*/
private static String getScriptCode(final DomElement element) {
final Iterable textNodes = element.getChildren();
final StringBuilder scriptCode = new StringBuilder();
for (final DomNode node : textNodes) {
if (node instanceof DomText) {
final DomText domText = (DomText) node;
scriptCode.append(domText.getData());
}
}
return scriptCode.toString();
}
}