Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (c) 2002-2024 Gargoyle Software Inc.
* Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
*
* 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 java.nio.charset.StandardCharsets.ISO_8859_1;
import static org.htmlunit.BrowserVersionFeatures.EVENT_FOCUS_FOCUS_IN_BLUR_OUT;
import static org.htmlunit.BrowserVersionFeatures.EVENT_FOCUS_IN_FOCUS_OUT_BLUR;
import static org.htmlunit.BrowserVersionFeatures.EVENT_FOCUS_ON_LOAD;
import static org.htmlunit.BrowserVersionFeatures.FOCUS_BODY_ELEMENT_AT_START;
import static org.htmlunit.BrowserVersionFeatures.HTTP_HEADER_SEC_FETCH;
import static org.htmlunit.BrowserVersionFeatures.JS_EVENT_LOAD_SUPPRESSED_BY_CONTENT_SECURIRY_POLICY;
import static org.htmlunit.BrowserVersionFeatures.JS_IGNORES_UTF8_BOM_SOMETIMES;
import static org.htmlunit.BrowserVersionFeatures.PAGE_SELECTION_RANGE_FROM_SELECTABLE_TEXT_INPUT;
import static org.htmlunit.BrowserVersionFeatures.URL_MISSING_SLASHES;
import static org.htmlunit.html.DisabledElement.ATTRIBUTE_DISABLED;
import static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
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.Cache;
import org.htmlunit.ElementNotFoundException;
import org.htmlunit.FailingHttpStatusCodeException;
import org.htmlunit.History;
import org.htmlunit.HttpHeader;
import org.htmlunit.OnbeforeunloadHandler;
import org.htmlunit.Page;
import org.htmlunit.ScriptResult;
import org.htmlunit.SgmlPage;
import org.htmlunit.TopLevelWindow;
import org.htmlunit.WebAssert;
import org.htmlunit.WebClient;
import org.htmlunit.WebClientOptions;
import org.htmlunit.WebRequest;
import org.htmlunit.WebResponse;
import org.htmlunit.WebWindow;
import org.htmlunit.corejs.javascript.Function;
import org.htmlunit.corejs.javascript.Script;
import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.corejs.javascript.Undefined;
import org.htmlunit.css.ComputedCssStyleDeclaration;
import org.htmlunit.css.CssStyleSheet;
import org.htmlunit.html.FrameWindow.PageDenied;
import org.htmlunit.html.impl.SelectableTextInput;
import org.htmlunit.html.impl.SimpleRange;
import org.htmlunit.html.parser.HTMLParserDOMBuilder;
import org.htmlunit.httpclient.HttpClientConverter;
import org.htmlunit.javascript.AbstractJavaScriptEngine;
import org.htmlunit.javascript.HtmlUnitContextFactory;
import org.htmlunit.javascript.HtmlUnitScriptable;
import org.htmlunit.javascript.JavaScriptEngine;
import org.htmlunit.javascript.PostponedAction;
import org.htmlunit.javascript.host.Window;
import org.htmlunit.javascript.host.event.BeforeUnloadEvent;
import org.htmlunit.javascript.host.event.Event;
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.util.SerializableLock;
import org.htmlunit.util.UrlUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.DOMException;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.ProcessingInstruction;
/**
* A representation of an HTML page returned from a server.
*
* This class provides different methods to access the page's content like
* {@link #getForms()}, {@link #getAnchors()}, {@link #getElementById(String)}, ... as well as the
* very powerful inherited methods {@link #getByXPath(String)} and {@link #getFirstByXPath(String)}
* for fine grained user specific access to child nodes.
*
*
* Child elements allowing user interaction provide methods for this purpose like {@link HtmlAnchor#click()},
* {@link HtmlInput#type(String)}, {@link HtmlOption#setSelected(boolean)}, ...
*
*
* HtmlPage instances should not be instantiated directly. They will be returned by {@link WebClient#getPage(String)}
* when the content type of the server's response is text/html (or one of its variations).
*
* Example:
*
*
* final HtmlPage page = webClient.{@link WebClient#getPage(String) getPage}("http://mywebsite/some/page.html");
*
*
*
* @author Mike Bowler
* @author Alex Nikiforoff
* @author Noboru Sinohara
* @author David K. Taylor
* @author Andreas Hangler
* @author Christian Sell
* @author Chris Erskine
* @author Marc Guillemot
* @author Ahmed Ashour
* @author Daniel Gredler
* @author Dmitri Zoubkov
* @author Sudhan Moghe
* @author Ethan Glasser-Camp
* @author Tom Anderson
* @author Ronald Brill
* @author Frank Danek
* @author Joerg Werner
* @author Atsushi Nakagawa
* @author Rural Hunter
* @author Ronny Shapiro
*/
public class HtmlPage extends SgmlPage {
private static final Log LOG = LogFactory.getLog(HtmlPage.class);
private static final Comparator documentPositionComparator = new DocumentPositionComparator();
private HTMLParserDOMBuilder domBuilder_;
private transient Charset originalCharset_;
private final Object lock_ = new SerializableLock(); // used for synchronization
private Map> idMap_ = new ConcurrentHashMap<>();
private Map> nameMap_ = new ConcurrentHashMap<>();
private SortedSet frameElements_ = new TreeSet<>(documentPositionComparator);
private int parserCount_;
private int snippetParserCount_;
private int inlineSnippetParserCount_;
private Collection attributeListeners_;
private List afterLoadActions_ = Collections.synchronizedList(new ArrayList<>());
private boolean cleaning_;
private HtmlBase base_;
private URL baseUrl_;
private List autoCloseableList_;
private ElementFromPointHandler elementFromPointHandler_;
private DomElement elementWithFocus_;
private List selectionRanges_ = new ArrayList<>(3);
private transient ComputedStylesCache computedStylesCache_;
private static final HashSet TABBABLE_TAGS =
new HashSet<>(Arrays.asList(HtmlAnchor.TAG_NAME, HtmlArea.TAG_NAME,
HtmlButton.TAG_NAME, HtmlInput.TAG_NAME, HtmlObject.TAG_NAME,
HtmlSelect.TAG_NAME, HtmlTextArea.TAG_NAME));
private static final HashSet ACCEPTABLE_TAG_NAMES =
new HashSet<>(Arrays.asList(HtmlAnchor.TAG_NAME, HtmlArea.TAG_NAME,
HtmlButton.TAG_NAME, HtmlInput.TAG_NAME, HtmlLabel.TAG_NAME,
HtmlLegend.TAG_NAME, HtmlTextArea.TAG_NAME));
/** Definition of special cases for the smart DomHtmlAttributeChangeListenerImpl */
private static final Set ATTRIBUTES_AFFECTING_PARENT = new HashSet<>(Arrays.asList(
"style",
"class",
"height",
"width"));
static class DocumentPositionComparator implements Comparator, Serializable {
@Override
public int compare(final DomElement elt1, final DomElement elt2) {
final short relation = elt1.compareDocumentPosition(elt2);
if (relation == 0) {
return 0; // same node
}
if ((relation & DOCUMENT_POSITION_CONTAINS) != 0 || (relation & DOCUMENT_POSITION_PRECEDING) != 0) {
return 1;
}
return -1;
}
}
/**
* Creates an instance of HtmlPage.
* An HtmlPage instance is normally retrieved with {@link WebClient#getPage(String)}.
*
* @param webResponse the web response that was used to create this page
* @param webWindow the window that this page is being loaded into
*/
public HtmlPage(final WebResponse webResponse, final WebWindow webWindow) {
super(webResponse, webWindow);
}
/**
* {@inheritDoc}
*/
@Override
public HtmlPage getPage() {
return this;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasCaseSensitiveTagNames() {
return false;
}
/**
* Initialize this page.
* @throws IOException if an IO problem occurs
* @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
* {@link org.htmlunit.WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set
* to true.
*/
@Override
public void initialize() throws IOException, FailingHttpStatusCodeException {
final WebWindow enclosingWindow = getEnclosingWindow();
final boolean isAboutBlank = getUrl() == UrlUtils.URL_ABOUT_BLANK;
if (isAboutBlank) {
// a frame contains first a faked "about:blank" before its real content specified by src gets loaded
if (enclosingWindow instanceof FrameWindow
&& !((FrameWindow) enclosingWindow).getFrameElement().isContentLoaded()) {
return;
}
// save the URL that should be used to resolve relative URLs in this page
if (enclosingWindow instanceof TopLevelWindow) {
final TopLevelWindow topWindow = (TopLevelWindow) enclosingWindow;
final WebWindow openerWindow = topWindow.getOpener();
if (openerWindow != null && openerWindow.getEnclosedPage() != null) {
baseUrl_ = openerWindow.getEnclosedPage().getWebResponse().getWebRequest().getUrl();
}
}
}
if (!isAboutBlank) {
setReadyState(READY_STATE_INTERACTIVE);
getDocumentElement().setReadyState(READY_STATE_INTERACTIVE);
executeEventHandlersIfNeeded(Event.TYPE_READY_STATE_CHANGE);
}
executeDeferredScriptsIfNeeded();
executeEventHandlersIfNeeded(Event.TYPE_DOM_DOCUMENT_LOADED);
loadFrames();
// don't set the ready state if we really load the blank page into the window
// see Node.initInlineFrameIfNeeded()
if (!isAboutBlank) {
if (hasFeature(FOCUS_BODY_ELEMENT_AT_START)) {
setElementWithFocus(getBody());
}
setReadyState(READY_STATE_COMPLETE);
getDocumentElement().setReadyState(READY_STATE_COMPLETE);
executeEventHandlersIfNeeded(Event.TYPE_READY_STATE_CHANGE);
}
// frame initialization has a different order
boolean isFrameWindow = enclosingWindow instanceof FrameWindow;
boolean isFirstPageInFrameWindow = false;
if (isFrameWindow) {
isFrameWindow = ((FrameWindow) enclosingWindow).getFrameElement() instanceof HtmlFrame;
final History hist = enclosingWindow.getHistory();
if (hist.getLength() > 0 && UrlUtils.URL_ABOUT_BLANK == hist.getUrl(0)) {
isFirstPageInFrameWindow = hist.getLength() <= 2;
}
else {
isFirstPageInFrameWindow = enclosingWindow.getHistory().getLength() < 2;
}
}
if (isFrameWindow && !isFirstPageInFrameWindow) {
executeEventHandlersIfNeeded(Event.TYPE_LOAD);
}
for (final FrameWindow frameWindow : getFrames()) {
if (frameWindow.getFrameElement() instanceof HtmlFrame) {
final Page page = frameWindow.getEnclosedPage();
if (page != null && page.isHtmlPage()) {
((HtmlPage) page).executeEventHandlersIfNeeded(Event.TYPE_LOAD);
}
}
}
if (!isFrameWindow) {
executeEventHandlersIfNeeded(Event.TYPE_LOAD);
if (!isAboutBlank && enclosingWindow.getWebClient().isJavaScriptEnabled()
&& hasFeature(EVENT_FOCUS_ON_LOAD)) {
final HtmlElement body = getBody();
if (body != null) {
final Event event = new Event((Window) enclosingWindow.getScriptableObject(), Event.TYPE_FOCUS);
body.fireEvent(event);
}
}
}
try {
while (!afterLoadActions_.isEmpty()) {
final PostponedAction action = afterLoadActions_.remove(0);
action.execute();
}
}
catch (final IOException e) {
throw e;
}
catch (final Exception e) {
throw new RuntimeException(e);
}
executeRefreshIfNeeded();
}
/**
* Adds an action that should be executed once the page has been loaded.
* @param action the action
*/
void addAfterLoadAction(final PostponedAction action) {
afterLoadActions_.add(action);
}
/**
* Clean up this page.
*/
@Override
public void cleanUp() {
//To avoid endless recursion caused by window.close() in onUnload
if (cleaning_) {
return;
}
cleaning_ = true;
try {
super.cleanUp();
executeEventHandlersIfNeeded(Event.TYPE_UNLOAD);
deregisterFramesIfNeeded();
}
finally {
cleaning_ = false;
if (autoCloseableList_ != null) {
for (final AutoCloseable closeable : new ArrayList<>(autoCloseableList_)) {
try {
closeable.close();
}
catch (final Exception e) {
LOG.error("Closing the autoclosable " + closeable + " failed", e);
}
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public HtmlElement getDocumentElement() {
return (HtmlElement) super.getDocumentElement();
}
/**
* Returns the body element (or frameset element),
* or {@code null} if it does not yet exist.
* @return the body element (or frameset element),
* or {@code null} if it does not yet exist
*/
public HtmlElement getBody() {
final DomElement doc = getDocumentElement();
if (doc != null) {
for (final DomNode node : doc.getChildren()) {
if (node instanceof HtmlBody || node instanceof HtmlFrameSet) {
return (HtmlElement) node;
}
}
}
return null;
}
/**
* Returns the head element.
* @return the head element
*/
public HtmlElement getHead() {
final DomElement doc = getDocumentElement();
if (doc != null) {
for (final DomNode node : doc.getChildren()) {
if (node instanceof HtmlHead) {
return (HtmlElement) node;
}
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public Document getOwnerDocument() {
return null;
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public org.w3c.dom.Node importNode(final org.w3c.dom.Node importedNode, final boolean deep) {
throw new UnsupportedOperationException("HtmlPage.importNode is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public String getInputEncoding() {
throw new UnsupportedOperationException("HtmlPage.getInputEncoding is not yet implemented.");
}
/**
* {@inheritDoc}
*/
@Override
public String getXmlEncoding() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean getXmlStandalone() {
return false;
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public void setXmlStandalone(final boolean xmlStandalone) throws DOMException {
throw new UnsupportedOperationException("HtmlPage.setXmlStandalone is not yet implemented.");
}
/**
* {@inheritDoc}
*/
@Override
public String getXmlVersion() {
return null;
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public void setXmlVersion(final String xmlVersion) throws DOMException {
throw new UnsupportedOperationException("HtmlPage.setXmlVersion is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public boolean getStrictErrorChecking() {
throw new UnsupportedOperationException("HtmlPage.getStrictErrorChecking is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public void setStrictErrorChecking(final boolean strictErrorChecking) {
throw new UnsupportedOperationException("HtmlPage.setStrictErrorChecking is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public String getDocumentURI() {
throw new UnsupportedOperationException("HtmlPage.getDocumentURI is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public void setDocumentURI(final String documentURI) {
throw new UnsupportedOperationException("HtmlPage.setDocumentURI is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public org.w3c.dom.Node adoptNode(final org.w3c.dom.Node source) throws DOMException {
throw new UnsupportedOperationException("HtmlPage.adoptNode is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public DOMConfiguration getDomConfig() {
throw new UnsupportedOperationException("HtmlPage.getDomConfig is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public org.w3c.dom.Node renameNode(final org.w3c.dom.Node newNode, final String namespaceURI,
final String qualifiedName) throws DOMException {
throw new UnsupportedOperationException("HtmlPage.renameNode is not yet implemented.");
}
/**
* {@inheritDoc}
*/
@Override
public Charset getCharset() {
if (originalCharset_ == null) {
originalCharset_ = getWebResponse().getContentCharset();
}
return originalCharset_;
}
/**
* {@inheritDoc}
*/
@Override
public String getContentType() {
return getWebResponse().getContentType();
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public DOMImplementation getImplementation() {
throw new UnsupportedOperationException("HtmlPage.getImplementation is not yet implemented.");
}
/**
* {@inheritDoc}
* @param tagName the tag name, preferably in lowercase
*/
@Override
public DomElement createElement(String tagName) {
if (tagName.indexOf(':') == -1) {
tagName = org.htmlunit.util.StringUtils.toRootLowerCase(tagName);
}
return getWebClient().getPageCreator().getHtmlParser().getFactory(tagName)
.createElementNS(this, null, tagName, null, true);
}
/**
* {@inheritDoc}
*/
@Override
public DomElement createElementNS(final String namespaceURI, final String qualifiedName) {
return getWebClient().getPageCreator().getHtmlParser()
.getElementFactory(this, namespaceURI, qualifiedName, false, true)
.createElementNS(this, namespaceURI, qualifiedName, null, true);
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public Attr createAttributeNS(final String namespaceURI, final String qualifiedName) {
throw new UnsupportedOperationException("HtmlPage.createAttributeNS is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public EntityReference createEntityReference(final String id) {
throw new UnsupportedOperationException("HtmlPage.createEntityReference is not yet implemented.");
}
/**
* {@inheritDoc}
* Not yet implemented.
*/
@Override
public ProcessingInstruction createProcessingInstruction(final String namespaceURI, final String qualifiedName) {
throw new UnsupportedOperationException("HtmlPage.createProcessingInstruction is not yet implemented.");
}
/**
* {@inheritDoc}
*/
@Override
public DomElement getElementById(final String elementId) {
if (elementId != null) {
final SortedSet elements = idMap_.get(elementId);
if (elements != null) {
// XC start
if (elements.size() > 1)
{
notifyIncorrectness(this + ": More than one element was found for ID '" + elementId + "' " + Arrays.toString(elements.toArray()));
}
// XC end
return elements.first();
}
}
return null;
}
/**
* Returns the {@link HtmlAnchor} with the specified name.
*
* @param name the name to search by
* @return the {@link HtmlAnchor} with the specified name
* @throws ElementNotFoundException if the anchor could not be found
*/
public HtmlAnchor getAnchorByName(final String name) throws ElementNotFoundException {
return getDocumentElement().getOneHtmlElementByAttribute("a", DomElement.NAME_ATTRIBUTE, name);
}
/**
* Returns the {@link HtmlAnchor} with the specified href.
*
* @param href the string to search by
* @return the HtmlAnchor
* @throws ElementNotFoundException if the anchor could not be found
*/
public HtmlAnchor getAnchorByHref(final String href) throws ElementNotFoundException {
return getDocumentElement().getOneHtmlElementByAttribute("a", "href", href);
}
/**
* Returns a list of all anchors contained in this page.
* @return the list of {@link HtmlAnchor} in this page
*/
public List getAnchors() {
return getDocumentElement().getElementsByTagNameImpl("a");
}
/**
* Returns the first anchor with the specified text.
* @param text the text to search for
* @return the first anchor that was found
* @throws ElementNotFoundException if no anchors are found with the specified text
*/
public HtmlAnchor getAnchorByText(final String text) throws ElementNotFoundException {
WebAssert.notNull("text", text);
for (final HtmlAnchor anchor : getAnchors()) {
if (text.equals(anchor.asNormalizedText())) {
return anchor;
}
}
throw new ElementNotFoundException("a", "", text);
}
/**
* Returns the first form that matches the specified name.
* @param name the name to search for
* @return the first form
* @exception ElementNotFoundException If no forms match the specified result.
*/
public HtmlForm getFormByName(final String name) throws ElementNotFoundException {
final List forms = getDocumentElement()
.getElementsByAttribute("form", DomElement.NAME_ATTRIBUTE, name);
if (forms.isEmpty()) {
throw new ElementNotFoundException("form", DomElement.NAME_ATTRIBUTE, name);
}
return forms.get(0);
}
/**
* Returns a list of all the forms in this page.
* @return all the forms in this page
*/
public List getForms() {
return getDocumentElement().getElementsByTagNameImpl("form");
}
/**
* Given a relative URL (ie /foo), returns a fully-qualified URL based on
* the URL that was used to load this page.
*
* @param relativeUrl the relative URL
* @return the fully-qualified URL for the specified relative URL
* @exception MalformedURLException if an error occurred when creating a URL object
*/
public URL getFullyQualifiedUrl(String relativeUrl) throws MalformedURLException {
// to handle http: and http:/ in FF (Bug #474)
if (hasFeature(URL_MISSING_SLASHES)) {
boolean incorrectnessNotified = false;
while (relativeUrl.startsWith("http:") && !relativeUrl.startsWith("http://")) {
if (!incorrectnessNotified) {
notifyIncorrectness("Incorrect URL \"" + relativeUrl + "\" has been corrected");
incorrectnessNotified = true;
}
relativeUrl = "http:/" + relativeUrl.substring(5);
}
}
return WebClient.expandUrl(getBaseURL(), relativeUrl);
}
/**
* Given a target attribute value, resolve the target using a base target for the page.
*
* @param elementTarget the target specified as an attribute of the element
* @return the resolved target to use for the element
*/
public String getResolvedTarget(final String elementTarget) {
final String resolvedTarget;
if (base_ == null) {
resolvedTarget = elementTarget;
}
else if (elementTarget != null && !elementTarget.isEmpty()) {
resolvedTarget = elementTarget;
}
else {
resolvedTarget = base_.getTargetAttribute();
}
return resolvedTarget;
}
/**
* Returns a list of ids (strings) that correspond to the tabbable elements
* in this page. Return them in the same order specified in {@link #getTabbableElements}
*
* @return the list of id's
*/
public List getTabbableElementIds() {
final List list = new ArrayList<>();
for (final HtmlElement element : getTabbableElements()) {
list.add(element.getId());
}
return Collections.unmodifiableList(list);
}
/**
* Returns a list of all elements that are tabbable in the order that will
* be used for tabbing.
*
* The rules for determining tab order are as follows:
*
*
Those elements that support the tabindex attribute and assign a
* positive value to it are navigated first. Navigation proceeds from the
* element with the lowest tabindex value to the element with the highest
* value. Values need not be sequential nor must they begin with any
* particular value. Elements that have identical tabindex values should
* be navigated in the order they appear in the character stream.
*
Those elements that do not support the tabindex attribute or
* support it and assign it a value of "0" are navigated next. These
* elements are navigated in the order they appear in the character
* stream.
*
Elements that are disabled do not participate in the tabbing
* order.
*
* Additionally, the value of tabindex must be within 0 and 32767. Any
* values outside this range will be ignored.
*
* The following elements support the tabindex attribute: A, AREA, BUTTON,
* INPUT, OBJECT, SELECT, and TEXTAREA.
*
* @return all the tabbable elements in proper tab order
*/
public List getTabbableElements() {
final List tabbableElements = new ArrayList<>();
for (final HtmlElement element : getHtmlElementDescendants()) {
final String tagName = element.getTagName();
if (TABBABLE_TAGS.contains(tagName)) {
final boolean disabled = element.hasAttribute(ATTRIBUTE_DISABLED);
if (!disabled && !HtmlElement.TAB_INDEX_OUT_OF_BOUNDS.equals(element.getTabIndex())) {
tabbableElements.add(element);
}
}
}
tabbableElements.sort(createTabOrderComparator());
return Collections.unmodifiableList(tabbableElements);
}
private static Comparator createTabOrderComparator() {
return (element1, element2) -> {
final Short i1 = element1.getTabIndex();
final Short i2 = element2.getTabIndex();
final short index1;
if (i1 == null) {
index1 = -1;
}
else {
index1 = i1.shortValue();
}
final short index2;
if (i2 == null) {
index2 = -1;
}
else {
index2 = i2.shortValue();
}
final int result;
if (index1 > 0 && index2 > 0) {
result = index1 - index2;
}
else if (index1 > 0) {
result = -1;
}
else if (index2 > 0) {
result = 1;
}
else if (index1 == index2) {
result = 0;
}
else {
result = index2 - index1;
}
return result;
};
}
/**
* Returns the HTML element that is assigned to the specified access key. An
* access key (aka mnemonic key) is used for keyboard navigation of the
* page.
*
* Only the following HTML elements may have accesskeys defined: A, AREA,
* BUTTON, INPUT, LABEL, LEGEND, and TEXTAREA.
*
* @param accessKey the key to look for
* @return the HTML element that is assigned to the specified key or null
* if no elements can be found that match the specified key.
*/
public HtmlElement getHtmlElementByAccessKey(final char accessKey) {
final List elements = getHtmlElementsByAccessKey(accessKey);
if (elements.isEmpty()) {
return null;
}
return elements.get(0);
}
/**
* Returns all the HTML elements that are assigned to the specified access key. An
* access key (aka mnemonic key) is used for keyboard navigation of the
* page.
*
* The HTML specification seems to indicate that one accesskey cannot be used
* for multiple elements however Internet Explorer does seem to support this.
* It's worth noting that Firefox does not support multiple elements with one
* access key so you are making your HTML browser specific if you rely on this
* feature.
*
* Only the following HTML elements may have accesskeys defined: A, AREA,
* BUTTON, INPUT, LABEL, LEGEND, and TEXTAREA.
*
* @param accessKey the key to look for
* @return the elements that are assigned to the specified accesskey
*/
public List getHtmlElementsByAccessKey(final char accessKey) {
final List elements = new ArrayList<>();
final String searchString = Character.toString(accessKey).toLowerCase(Locale.ROOT);
for (final HtmlElement element : getHtmlElementDescendants()) {
if (ACCEPTABLE_TAG_NAMES.contains(element.getTagName())) {
final String accessKeyAttribute = element.getAttributeDirect("accesskey");
if (searchString.equalsIgnoreCase(accessKeyAttribute)) {
elements.add(element);
}
}
}
return elements;
}
/**
*
Executes the specified JavaScript code within the page. The usage would be similar to what can
* be achieved to execute JavaScript in the current page by entering "javascript:...some JS code..."
* in the URL field of a native browser.
*
Note: the provided code won't be executed if JavaScript has been disabled on the WebClient
* (see {@link org.htmlunit.WebClient#isJavaScriptEnabled()}.
* @param sourceCode the JavaScript code to execute
* @return a ScriptResult which will contain both the current page (which may be different than
* the previous page) and a JavaScript result object
*/
public ScriptResult executeJavaScript(final String sourceCode) {
return executeJavaScript(sourceCode, "injected script", 1);
}
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* Execute the specified JavaScript if a JavaScript engine was successfully
* instantiated. If this JavaScript causes the current page to be reloaded
* (through location="" or form.submit()) then return the new page. Otherwise
* return the current page.
*
*
Please note: Although this method is public, it is not intended for
* general execution of JavaScript. Users of HtmlUnit should interact with the pages
* as a user would by clicking on buttons or links and having the JavaScript event
* handlers execute as needed..
*
*
* @param sourceCode the JavaScript code to execute
* @param sourceName the name for this chunk of code (will be displayed in error messages)
* @param startLine the line at which the script source starts
* @return a ScriptResult which will contain both the current page (which may be different than
* the previous page and a JavaScript result object.
*/
public ScriptResult executeJavaScript(String sourceCode, final String sourceName, final int startLine) {
if (!getWebClient().isJavaScriptEnabled()) {
return new ScriptResult(Undefined.instance);
}
if (StringUtils.startsWithIgnoreCase(sourceCode, JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
sourceCode = sourceCode.substring(JavaScriptURLConnection.JAVASCRIPT_PREFIX.length()).trim();
if (sourceCode.startsWith("return ")) {
sourceCode = sourceCode.substring("return ".length());
}
}
final Object result = getWebClient().getJavaScriptEngine().execute(this, sourceCode, sourceName, startLine);
return new ScriptResult(result);
}
/** Various possible external JavaScript file loading results. */
enum JavaScriptLoadResult {
/** The load was aborted and nothing was done. */
NOOP,
/** The load was aborted and nothing was done. */
NO_CONTENT,
/** The external JavaScript file was downloaded and compiled successfully. */
SUCCESS,
/** The external JavaScript file was not downloaded successfully. */
DOWNLOAD_ERROR,
/** The external JavaScript file was downloaded but was not compiled successfully. */
COMPILATION_ERROR
}
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* @param srcAttribute the source attribute from the script tag
* @param scriptCharset the charset from the script tag
* @return the result of loading the specified external JavaScript file
* @throws FailingHttpStatusCodeException if the request's status code indicates a request
* failure and the {@link WebClient} was configured to throw exceptions on failing
* HTTP status codes
*/
JavaScriptLoadResult loadExternalJavaScriptFile(final String srcAttribute, final Charset scriptCharset)
throws FailingHttpStatusCodeException {
final WebClient client = getWebClient();
if (StringUtils.isBlank(srcAttribute) || !client.isJavaScriptEnabled()) {
return JavaScriptLoadResult.NOOP;
}
final URL scriptURL;
try {
scriptURL = getFullyQualifiedUrl(srcAttribute);
final String protocol = scriptURL.getProtocol();
if ("javascript".equals(protocol)) {
if (LOG.isInfoEnabled()) {
LOG.info("Ignoring script src [" + srcAttribute + "]");
}
return JavaScriptLoadResult.NOOP;
}
if (!"http".equals(protocol) && !"https".equals(protocol)
&& !"data".equals(protocol) && !"file".equals(protocol)) {
client.getJavaScriptErrorListener().malformedScriptURL(this, srcAttribute,
new MalformedURLException("unknown protocol: '" + protocol + "'"));
return JavaScriptLoadResult.NOOP;
}
}
catch (final MalformedURLException e) {
client.getJavaScriptErrorListener().malformedScriptURL(this, srcAttribute, e);
return JavaScriptLoadResult.NOOP;
}
final Object script;
try {
script = loadJavaScriptFromUrl(scriptURL, scriptCharset);
}
catch (final IOException e) {
client.getJavaScriptErrorListener().loadScriptError(this, scriptURL, e);
return JavaScriptLoadResult.DOWNLOAD_ERROR;
}
catch (final FailingHttpStatusCodeException e) {
if (e.getStatusCode() == HttpClientConverter.NO_CONTENT) {
return JavaScriptLoadResult.NO_CONTENT;
}
client.getJavaScriptErrorListener().loadScriptError(this, scriptURL, e);
throw e;
}
if (script == null) {
return JavaScriptLoadResult.COMPILATION_ERROR;
}
@SuppressWarnings("unchecked")
final AbstractJavaScriptEngine