All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gargoylesoftware.htmlunit.html.ScriptElementSupport Maven / Gradle / Ivy

There is a newer version: 2.70.0
Show newest version
/*
 * Copyright (c) 2002-2022 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 com.gargoylesoftware.htmlunit.html;

import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONLOAD_INTERNAL_JAVASCRIPT;
import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLSCRIPT_TRIM_TYPE;
import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_SCRIPT_HANDLE_204_AS_ERROR;
import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_SCRIPT_SUPPORTS_FOR_AND_EVENT_WINDOW;
import static com.gargoylesoftware.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 com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.html.HtmlPage.JavaScriptLoadResult;
import com.gargoylesoftware.htmlunit.javascript.AbstractJavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
import com.gargoylesoftware.htmlunit.javascript.host.Window;
import com.gargoylesoftware.htmlunit.javascript.host.dom.Document;
import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
import com.gargoylesoftware.htmlunit.javascript.host.event.EventHandler;
import com.gargoylesoftware.htmlunit.javascript.host.event.EventTarget;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
import com.gargoylesoftware.htmlunit.protocol.javascript.JavaScriptURLConnection;
import com.gargoylesoftware.htmlunit.util.EncodingSniffer;
import com.gargoylesoftware.htmlunit.util.MimeType;
import com.gargoylesoftware.htmlunit.xml.XmlPage;

import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;

/**
 * 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 com.gargoylesoftware.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(); if (ATTRIBUTE_NOT_DEFINED != srcAttrib && script.isDeferred()) { return; } final WebWindow webWindow = element.getPage().getEnclosingWindow(); if (webWindow != null) { final StringBuilder description = new StringBuilder() .append("Execution of ") .append(srcAttrib == ATTRIBUTE_NOT_DEFINED ? "inline " : "external ") .append(element.getClass().getSimpleName()); if (srcAttrib != ATTRIBUTE_NOT_DEFINED) { 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 HtmlPage page = (HtmlPage) element.getPage(); final ScriptElement scriptElement = (ScriptElement) element; final String src = scriptElement.getSrcAttribute(); if (src.equals(SLASH_SLASH_COLON)) { executeEvent(element, Event.TYPE_ERROR); return; } 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 BaseFunction 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(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy