com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-client-compiler-deps Show documentation
Show all versions of vaadin-client-compiler-deps Show documentation
Vaadin is a web application framework for Rich Internet Applications (RIA).
Vaadin enables easy development and maintenance of fast and
secure rich web
applications with a stunning look and feel and a wide browser support.
It features a server-side architecture with the majority of the logic
running
on the server. Ajax technology is used at the browser-side to ensure a
rich
and interactive user experience.
/*
* Copyright (c) 2002-2011 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
* http://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.javascript.host.html;
import static com.gargoylesoftware.htmlunit.util.StringUtils.containsCaseInsensitive;
import static com.gargoylesoftware.htmlunit.util.StringUtils.parseHttpDate;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.htmlunit.corejs.javascript.Callable;
import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.FunctionObject;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
import net.sourceforge.htmlunit.corejs.javascript.UniqueTag;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.DOMException;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.BrowserVersionFeatures;
import com.gargoylesoftware.htmlunit.CookieManager;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.StringWebResponse;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.html.BaseFrame;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNode;
import com.gargoylesoftware.htmlunit.html.FrameWindow;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlApplet;
import com.gargoylesoftware.htmlunit.html.HtmlArea;
import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeEvent;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlImage;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlScript;
import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
import com.gargoylesoftware.htmlunit.javascript.ScriptableWithFallbackGetter;
import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
import com.gargoylesoftware.htmlunit.javascript.host.Document;
import com.gargoylesoftware.htmlunit.javascript.host.Event;
import com.gargoylesoftware.htmlunit.javascript.host.KeyboardEvent;
import com.gargoylesoftware.htmlunit.javascript.host.MouseEvent;
import com.gargoylesoftware.htmlunit.javascript.host.MutationEvent;
import com.gargoylesoftware.htmlunit.javascript.host.NamespaceCollection;
import com.gargoylesoftware.htmlunit.javascript.host.Node;
import com.gargoylesoftware.htmlunit.javascript.host.NodeFilter;
import com.gargoylesoftware.htmlunit.javascript.host.Range;
import com.gargoylesoftware.htmlunit.javascript.host.Selection;
import com.gargoylesoftware.htmlunit.javascript.host.StaticNodeList;
import com.gargoylesoftware.htmlunit.javascript.host.TreeWalker;
import com.gargoylesoftware.htmlunit.javascript.host.UIEvent;
import com.gargoylesoftware.htmlunit.javascript.host.Window;
import com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet;
import com.gargoylesoftware.htmlunit.javascript.host.css.StyleSheetList;
import com.gargoylesoftware.htmlunit.util.Cookie;
import com.gargoylesoftware.htmlunit.util.UrlUtils;
/**
* A JavaScript object for a Document.
*
* @version $Revision: 6489 $
* @author Mike Bowler
* @author David K. Taylor
* @author Chen Jun
* @author Christian Sell
* @author Chris Erskine
* @author Marc Guillemot
* @author Daniel Gredler
* @author Michael Ottati
* @author George Murnock
* @author Ahmed Ashour
* @author Rob Di Marco
* @author Sudhan Moghe
* @author Mike Dirolf
* @author Ronald Brill
* @see MSDN documentation
* @see
* W3C DOM Level 1
*/
public class HTMLDocument extends Document implements ScriptableWithFallbackGetter {
private static final Log LOG = LogFactory.getLog(HTMLDocument.class);
/** The cookie name used for cookies with no name (HttpClient doesn't like empty names). */
public static final String EMPTY_COOKIE_NAME = "HTMLUNIT_EMPTY_COOKIE";
/** The format to use for the lastModified attribute. */
private static final String LAST_MODIFIED_DATE_FORMAT = "MM/dd/yyyy HH:mm:ss";
private static final Pattern FIRST_TAG_PATTERN = Pattern.compile("<(\\w+)(\\s+[^>]*)?>");
private static final Pattern ATTRIBUTES_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*['\"]([^'\"]*)['\"]");
/**
* Map which maps strings a caller may use when calling into
* {@link #jsxFunction_createEvent(String)} to the associated event class. To support a new
* event creation type, the event type and associated class need to be added into this map in
* the static initializer. The map is unmodifiable. Any class that is a value in this map MUST
* have a no-arg constructor.
*/
private static final Map> SUPPORTED_EVENT_TYPE_MAP;
private static final List EXECUTE_CMDS_IE = Arrays.asList(new String[] {
"2D-Position", "AbsolutePosition", "BackColor", "BackgroundImageCache" /* Undocumented */,
"BlockDirLTR", "BlockDirRTL", "Bold", "BrowseMode", "ClearAuthenticationCache", "Copy", "CreateBookmark",
"CreateLink", "Cut", "Delete", "DirLTR", "DirRTL",
"EditMode", "FontName", "FontSize", "ForeColor", "FormatBlock",
"Indent", "InlineDirLTR", "InlineDirRTL", "InsertButton", "InsertFieldset",
"InsertHorizontalRule", "InsertIFrame", "InsertImage", "InsertInputButton", "InsertInputCheckbox",
"InsertInputFileUpload", "InsertInputHidden", "InsertInputImage", "InsertInputPassword", "InsertInputRadio",
"InsertInputReset", "InsertInputSubmit", "InsertInputText", "InsertMarquee", "InsertOrderedList",
"InsertParagraph", "InsertSelectDropdown", "InsertSelectListbox", "InsertTextArea", "InsertUnorderedList",
"Italic", "JustifyCenter", "JustifyFull", "JustifyLeft", "JustifyNone",
"JustifyRight", "LiveResize", "MultipleSelection", "Open", "Outdent",
"OverWrite", "Paste", "PlayImage", "Print", "Redo",
"Refresh", "RemoveFormat", "RemoveParaFormat", "SaveAs", "SelectAll",
"SizeToControl", "SizeToControlHeight", "SizeToControlWidth", "Stop", "StopImage",
"StrikeThrough", "Subscript", "Superscript", "UnBookmark", "Underline",
"Undo", "Unlink", "Unselect"
});
/** https://developer.mozilla.org/en/Rich-Text_Editing_in_Mozilla#Executing_Commands */
private static final List EXECUTE_CMDS_FF = Arrays.asList(new String[] {
"backColor", "bold", "contentReadOnly", "copy", "createLink", "cut", "decreaseFontSize", "delete",
"fontName", "fontSize", "foreColor", "formatBlock", "heading", "hiliteColor", "increaseFontSize",
"indent", "insertHorizontalRule", "insertHTML", "insertImage", "insertOrderedList", "insertUnorderedList",
"insertParagraph", "italic", "justifyCenter", "justifyLeft", "justifyRight", "outdent", "paste", "redo",
"removeFormat", "selectAll", "strikeThrough", "subscript", "superscript", "underline", "undo", "unlink",
"useCSS", "styleWithCSS"
});
/**
* Static counter for {@link #uniqueID_}.
*/
private static int UniqueID_Counter_ = 1;
private HTMLCollection all_; // has to be a member to have equality (==) working
private HTMLCollection forms_; // has to be a member to have equality (==) working
private HTMLCollection links_; // has to be a member to have equality (==) working
private HTMLCollection images_; // has to be a member to have equality (==) working
private HTMLCollection scripts_; // has to be a member to have equality (==) working
private HTMLCollection anchors_; // has to be a member to have equality (==) working
private HTMLCollection applets_; // has to be a member to have equality (==) working
private StyleSheetList styleSheets_; // has to be a member to have equality (==) working
private NamespaceCollection namespaces_; // has to be a member to have equality (==) working
private HTMLElement activeElement_;
/** The buffer that will be used for calls to document.write(). */
private final StringBuilder writeBuffer_ = new StringBuilder();
private boolean writeInCurrentDocument_ = true;
private String domain_;
private String uniqueID_;
private String lastModified_;
private boolean closePostponedAction_;
/** Initializes the supported event type map. */
static {
final Map> eventMap = new HashMap>();
eventMap.put("Event", Event.class);
eventMap.put("Events", Event.class);
eventMap.put("KeyboardEvent", KeyboardEvent.class);
eventMap.put("KeyEvents", KeyboardEvent.class);
eventMap.put("HTMLEvents", Event.class);
eventMap.put("MouseEvent", MouseEvent.class);
eventMap.put("MouseEvents", MouseEvent.class);
eventMap.put("MutationEvent", MutationEvent.class);
eventMap.put("MutationEvents", MutationEvent.class);
eventMap.put("UIEvent", UIEvent.class);
eventMap.put("UIEvents", UIEvent.class);
SUPPORTED_EVENT_TYPE_MAP = Collections.unmodifiableMap(eventMap);
}
/**
* Creates a new instance. JavaScript objects must have a default constructor.
*/
public HTMLDocument() {
// Empty.
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public N getDomNodeOrDie() throws IllegalStateException {
try {
return (N) super.getDomNodeOrDie();
}
catch (final IllegalStateException e) {
final DomNode node = getDomNodeOrNullFromRealDocument();
if (node != null) {
return (N) node;
}
throw Context.reportRuntimeError("No node attached to this object");
}
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public DomNode getDomNodeOrNull() {
DomNode node = super.getDomNodeOrNull();
if (node == null) {
node = getDomNodeOrNullFromRealDocument();
}
return node;
}
/**
* Document functions invoked on the window end up executing on the document prototype -- and
* this is supposed to work when we're emulating IE! So when {@link #getDomNodeOrDie()} or
* {@link #getDomNodeOrNull()} are invoked on the document prototype (which would usually fail),
* we need to actually return the real document's DOM node so that other functions which rely
* on these two functions work. See {@link HTMLDocumentTest#documentMethodsWithoutDocument()}
* for sample JavaScript code.
*
* @return the real document's DOM node, or null if we're not emulating IE
*/
private DomNode getDomNodeOrNullFromRealDocument() {
DomNode node = null;
final boolean ie = getWindow().getWebWindow().getWebClient()
.getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_51);
if (ie) {
final Scriptable scope = getParentScope();
if (scope instanceof Window) {
final Window w = (Window) scope;
final Document realDocument = w.getDocument();
if (realDocument != this) {
node = realDocument.getDomNodeOrDie();
}
}
}
return node;
}
/**
* Returns the HTML page that this document is modeling.
* @return the HTML page that this document is modeling
*/
public HtmlPage getHtmlPage() {
return (HtmlPage) getDomNodeOrDie();
}
/**
* Returns the HTML page that this document is modeling, or null if the page is empty.
* @return the HTML page that this document is modeling, or null if the page is empty
*/
public HtmlPage getHtmlPageOrNull() {
return (HtmlPage) getDomNodeOrNull();
}
/**
* Returns the value of the JavaScript attribute "forms".
* @return the value of the JavaScript attribute "forms"
*/
public Object jsxGet_forms() {
if (forms_ == null) {
final HtmlPage page = getHtmlPage();
forms_ = new HTMLCollection(page, false, "HTMLDocument.forms") {
protected boolean isMatching(final DomNode node) {
return node instanceof HtmlForm;
}
};
}
return forms_;
}
/**
* Returns the value of the JavaScript attribute "links". Refer also to the
* MSDN documentation.
* @return the value of this attribute
*/
public Object jsxGet_links() {
if (links_ == null) {
links_ = new HTMLCollection(getDomNodeOrDie(), true, "HTMLDocument.links") {
@Override
protected boolean isMatching(final DomNode node) {
return (node instanceof HtmlAnchor || node instanceof HtmlArea)
&& ((HtmlElement) node).hasAttribute("href");
}
@Override
protected EffectOnCache getEffectOnCache(final HtmlAttributeChangeEvent event) {
final HtmlElement node = event.getHtmlElement();
if ((node instanceof HtmlAnchor || node instanceof HtmlArea) && "href".equals(event.getName())) {
return EffectOnCache.RESET;
}
return EffectOnCache.NONE;
}
};
}
return links_;
}
/**
* Returns the last modification date of the document.
* @see Mozilla documentation
* @return the date as string
*/
public String jsxGet_lastModified() {
if (lastModified_ == null) {
final WebResponse webResponse = getPage().getWebResponse();
String stringDate = webResponse.getResponseHeaderValue("Last-Modified");
if (stringDate == null) {
stringDate = webResponse.getResponseHeaderValue("Date");
}
final Date lastModified = parseDateOrNow(stringDate);
lastModified_ = new SimpleDateFormat(LAST_MODIFIED_DATE_FORMAT).format(lastModified);
}
return lastModified_;
}
private static Date parseDateOrNow(final String stringDate) {
final Date date = parseHttpDate(stringDate);
if (date == null) {
return new Date();
}
return date;
}
/**
* Returns the value of the JavaScript attribute "namespaces".
* @return the value of the JavaScript attribute "namespaces"
*/
public Object jsxGet_namespaces() {
if (namespaces_ == null) {
namespaces_ = new NamespaceCollection(this);
}
return namespaces_;
}
/**
* Returns the value of the JavaScript attribute "anchors".
* @see MSDN documentation
* @see
* Gecko DOM reference
* @return the value of this attribute
*/
public Object jsxGet_anchors() {
if (anchors_ == null) {
final boolean checkId =
getBrowserVersion().hasFeature(BrowserVersionFeatures.JS_ANCHORS_REQUIRES_NAME_OR_ID);
anchors_ = new HTMLCollection(getDomNodeOrDie(), true, "HTMLDocument.anchors") {
@Override
protected boolean isMatching(final DomNode node) {
if (!(node instanceof HtmlAnchor)) {
return false;
}
final HtmlAnchor anchor = (HtmlAnchor) node;
if (checkId) {
return anchor.hasAttribute("name") || anchor.hasAttribute("id");
}
return anchor.hasAttribute("name");
}
@Override
protected EffectOnCache getEffectOnCache(final HtmlAttributeChangeEvent event) {
final HtmlElement node = event.getHtmlElement();
if (!(node instanceof HtmlAnchor)) {
return EffectOnCache.NONE;
}
if ("name".equals(event.getName()) || "id".equals(event.getName())) {
return EffectOnCache.RESET;
}
return EffectOnCache.NONE;
}
};
}
return anchors_;
}
/**
* Returns the value of the JavaScript attribute "applets".
* @see
* MSDN documentation
* @see
* Gecko DOM reference
* @return the value of this attribute
*/
public Object jsxGet_applets() {
if (applets_ == null) {
applets_ = new HTMLCollection(getDomNodeOrDie(), false, "HTMLDocument.applets") {
@Override
protected boolean isMatching(final DomNode node) {
return node instanceof HtmlApplet;
}
};
}
return applets_;
}
/**
* JavaScript function "write" may accept a variable number of arguments.
* It's not documented by W3C, Mozilla or MSDN but works with Mozilla and IE.
* @param context the JavaScript context
* @param thisObj the scriptable
* @param args the arguments passed into the method
* @param function the function
* @see MSDN documentation
*/
public static void jsxFunction_write(final Context context, final Scriptable thisObj, final Object[] args,
final Function function) {
final HTMLDocument thisAsDocument = getDocument(thisObj);
thisAsDocument.write(concatArgsAsString(args));
}
/**
* Converts the arguments to strings and concatenate them.
* @param args the JavaScript arguments
* @return the string concatenation
*/
private static String concatArgsAsString(final Object[] args) {
final StringBuilder buffer = new StringBuilder();
for (final Object arg : args) {
buffer.append(Context.toString(arg));
}
return buffer.toString();
}
/**
* JavaScript function "writeln" may accept a variable number of arguments.
* It's not documented by W3C, Mozilla or MSDN but works with Mozilla and IE.
* @param context the JavaScript context
* @param thisObj the scriptable
* @param args the arguments passed into the method
* @param function the function
* @see MSDN documentation
*/
public static void jsxFunction_writeln(
final Context context, final Scriptable thisObj, final Object[] args, final Function function) {
final HTMLDocument thisAsDocument = getDocument(thisObj);
thisAsDocument.write(concatArgsAsString(args) + "\n");
}
/**
* Returns the current document instance, using thisObj as a hint.
* @param thisObj a hint as to the current document (may be the prototype when function is used without "this")
* @return the current document instance
*/
private static HTMLDocument getDocument(final Scriptable thisObj) {
// if function is used "detached", then thisObj is the top scope (ie Window), not the real object
// cf unit test DocumentTest#testDocumentWrite_AssignedToVar
// may be the prototype too
// cf DocumentTest#testDocumentWrite_AssignedToVar2
if (thisObj instanceof HTMLDocument && thisObj.getPrototype() instanceof HTMLDocument) {
return (HTMLDocument) thisObj;
}
else if (thisObj instanceof DocumentProxy && thisObj.getPrototype() instanceof HTMLDocument) {
return (HTMLDocument) ((DocumentProxy) thisObj).getDelegee();
}
final Window window = getWindow(thisObj);
final BrowserVersion browser = window.getWebWindow().getWebClient().getBrowserVersion();
if (browser.hasFeature(BrowserVersionFeatures.GENERATED_53)) {
return (HTMLDocument) window.getDocument();
}
throw Context.reportRuntimeError("Function can't be used detached from document");
}
/**
* JavaScript function "write".
*
* See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
* a good description of the semantics of open(), write(), writeln() and close().
*
* @param content the content to write
*/
protected void write(final String content) {
if (LOG.isDebugEnabled()) {
LOG.debug("write: " + content);
}
final HtmlPage page = getDomNodeOrDie();
if (!page.isBeingParsed()) {
writeInCurrentDocument_ = false;
}
// Add content to the content buffer.
writeBuffer_.append(content);
// If open() was called; don't write to doc yet -- wait for call to close().
if (!writeInCurrentDocument_) {
if (LOG.isDebugEnabled()) {
LOG.debug("wrote content to buffer");
}
scheduleImplicitClose();
return;
}
final String bufferedContent = writeBuffer_.toString();
if (!canAlreadyBeParsed(bufferedContent)) {
if (LOG.isDebugEnabled()) {
LOG.debug("write: not enough content to parse it now");
}
return;
}
writeBuffer_.setLength(0);
page.writeInParsedStream(bufferedContent);
}
private void scheduleImplicitClose() {
if (!closePostponedAction_) {
closePostponedAction_ = true;
final HtmlPage page = getDomNodeOrDie();
page.getWebClient().getJavaScriptEngine().addPostponedAction(new PostponedAction(page) {
@Override
public void execute() throws Exception {
if (writeBuffer_.length() > 0) {
jsxFunction_close();
}
closePostponedAction_ = false;
}
});
}
}
private enum PARSING_STATUS { OUTSIDE, START, IN_NAME, INSIDE, IN_STRING }
/**
* Indicates if the content is a well formed HTML snippet that can already be parsed to be added to the DOM.
*
* @param content the HTML snippet
* @return false
if it not well formed
*/
static boolean canAlreadyBeParsed(final String content) {
// all because the parser doesn't close automatically this tag
// All tags must be complete, that is from '<' to '>'.
PARSING_STATUS tagState = PARSING_STATUS.OUTSIDE;
int tagNameBeginIndex = 0;
int scriptTagCount = 0;
boolean tagIsOpen = true;
char stringBoundary = 0;
boolean stringSkipNextChar = false;
int index = 0;
char openingQuote = 0;
for (final char currentChar : content.toCharArray()) {
switch (tagState) {
case OUTSIDE:
if (currentChar == '<') {
tagState = PARSING_STATUS.START;
tagIsOpen = true;
}
else if (scriptTagCount > 0 && (currentChar == '\'' || currentChar == '"')) {
tagState = PARSING_STATUS.IN_STRING;
stringBoundary = currentChar;
stringSkipNextChar = false;
}
break;
case START:
if (currentChar == '/') {
tagIsOpen = false;
tagNameBeginIndex = index + 1;
}
else {
tagNameBeginIndex = index;
}
tagState = PARSING_STATUS.IN_NAME;
break;
case IN_NAME:
if (Character.isWhitespace(currentChar) || currentChar == '>') {
final String tagName = content.substring(tagNameBeginIndex, index);
if ("script".equalsIgnoreCase(tagName)) {
if (tagIsOpen) {
scriptTagCount++;
}
else if (scriptTagCount > 0) {
// Ignore extra close tags for now. Let the parser deal with them.
scriptTagCount--;
}
}
if (currentChar == '>') {
tagState = PARSING_STATUS.OUTSIDE;
}
else {
tagState = PARSING_STATUS.INSIDE;
}
}
else if (!Character.isLetter(currentChar)) {
tagState = PARSING_STATUS.OUTSIDE;
}
break;
case INSIDE:
if (currentChar == openingQuote) {
openingQuote = 0;
}
else if (openingQuote == 0) {
if (currentChar == '\'' || currentChar == '"') {
openingQuote = currentChar;
}
else if (currentChar == '>' && openingQuote == 0) {
tagState = PARSING_STATUS.OUTSIDE;
}
}
break;
case IN_STRING:
if (stringSkipNextChar) {
stringSkipNextChar = false;
}
else {
if (currentChar == stringBoundary) {
tagState = PARSING_STATUS.OUTSIDE;
}
else if (currentChar == '\\') {
stringSkipNextChar = true;
}
}
break;
default:
// nothing
}
index++;
}
if (scriptTagCount > 0 || tagState != PARSING_STATUS.OUTSIDE) {
return false;
}
return true;
}
/**
* Gets the node that is the last one when exploring following nodes, depth-first.
* @param node the node to search
* @return the searched node
*/
HtmlElement getLastHtmlElement(final HtmlElement node) {
final DomNode lastChild = node.getLastChild();
if (lastChild == null
|| !(lastChild instanceof HtmlElement)
|| lastChild instanceof HtmlScript) {
return node;
}
return getLastHtmlElement((HtmlElement) lastChild);
}
/**
* Returns the cookie attribute.
* @return the cookie attribute
*/
public String jsxGet_cookie() {
final HtmlPage page = getHtmlPage();
URL url = page.getWebResponse().getWebRequest().getUrl();
url = replaceForCookieIfNecessary(url);
final StringBuilder buffer = new StringBuilder();
final Set cookies = page.getWebClient().getCookieManager().getCookies(url);
for (final Cookie cookie : cookies) {
if (buffer.length() != 0) {
buffer.append("; ");
}
if (!EMPTY_COOKIE_NAME.equals(cookie.getName())) {
buffer.append(cookie.getName());
buffer.append("=");
}
buffer.append(cookie.getValue());
}
return buffer.toString();
}
/**
* Returns the "compatMode" attribute.
* Note that it is deprecated in Internet Explorer 8 in favor of the documentMode.
* @return the "compatMode" attribute
*/
public String jsxGet_compatMode() {
return getHtmlPage().isQuirksMode() ? "BackCompat" : "CSS1Compat";
}
/**
* Adds a cookie, as long as cookies are enabled.
* @see MSDN documentation
* @param newCookie in the format "name=value[;expires=date][;domain=domainname][;path=path][;secure]
*/
public void jsxSet_cookie(final String newCookie) {
final CookieManager cookieManager = getHtmlPage().getWebClient().getCookieManager();
if (cookieManager.isCookiesEnabled()) {
URL url = getHtmlPage().getWebResponse().getWebRequest().getUrl();
url = replaceForCookieIfNecessary(url);
final Cookie cookie = buildCookie(newCookie, url);
cookieManager.addCookie(cookie);
if (LOG.isDebugEnabled()) {
LOG.debug("Added cookie: " + cookie);
}
}
else if (LOG.isDebugEnabled()) {
LOG.debug("Skipped adding cookie: " + newCookie);
}
}
/**
* {@link org.apache.commons.httpclient.cookie.CookieSpec#match(String, int, String, boolean, Cookie[])} doesn't
* like empty hosts and negative ports, but these things happen if we're dealing with a local file. This method
* allows us to work around this limitation in HttpClient by feeding it a bogus host and port.
*
* @param url the URL to replace if necessary
* @return the replacement URL, or the original URL if no replacement was necessary
*/
private static URL replaceForCookieIfNecessary(URL url) {
final String protocol = url.getProtocol();
final boolean file = "file".equals(protocol);
if (file) {
try {
url = UrlUtils.getUrlWithNewPort(UrlUtils.getUrlWithNewHost(url, "LOCAL_FILESYSTEM"), 0);
}
catch (final MalformedURLException e) {
throw new RuntimeException(e);
}
}
return url;
}
/**
* Builds a cookie object from the string representation allowed in JS.
* @param newCookie in the format "name=value[;expires=date][;domain=domainname][;path=path][;secure]
* @param currentURL the URL of the current page
* @return the cookie
*/
public static Cookie buildCookie(final String newCookie, final URL currentURL) {
// Pull out the cookie name and value.
String name, value;
final StringTokenizer st = new StringTokenizer(newCookie, ";");
if (newCookie.contains("=")) {
final String nameAndValue = st.nextToken();
name = StringUtils.substringBefore(nameAndValue, "=").trim();
value = StringUtils.substringAfter(nameAndValue, "=").trim();
}
else {
name = EMPTY_COOKIE_NAME;
value = newCookie;
}
// Default attribute values (note: HttpClient doesn't like null paths).
final Map atts = new HashMap();
atts.put("domain", currentURL.getHost());
atts.put("path", "");
// Custom attribute values.
while (st.hasMoreTokens()) {
final String token = st.nextToken();
final int indexEqual = token.indexOf('=');
if (indexEqual > -1) {
atts.put(token.substring(0, indexEqual).toLowerCase().trim(), token.substring(indexEqual + 1).trim());
}
else {
atts.put(token.toLowerCase().trim(), Boolean.TRUE);
}
}
// Try to parse the value as a date if specified.
final String date = (String) atts.get("expires");
final Date expires = parseHttpDate(date);
// Build the cookie.
final String domain = (String) atts.get("domain");
final String path = (String) atts.get("path");
final boolean secure = (atts.get("secure") != null);
final Cookie cookie = new Cookie(domain, name, value, path, expires, secure);
return cookie;
}
/**
* Returns the value of the "images" property.
* @return the value of the "images" property
*/
public Object jsxGet_images() {
if (images_ == null) {
images_ = new HTMLCollection(getDomNodeOrDie(), false, "HTMLDocument.images") {
@Override
protected boolean isMatching(final DomNode node) {
return node instanceof HtmlImage;
}
};
}
return images_;
}
/**
* Returns the value of the "URL" property.
* @return the value of the "URL" property
*/
public String jsxGet_URL() {
return getHtmlPage().getWebResponse().getWebRequest().getUrl().toExternalForm();
}
/**
* Retrieves an auto-generated, unique identifier for the object.
* Note The unique ID generated is not guaranteed to be the same every time the page is loaded.
* @return an auto-generated, unique identifier for the object
*/
public String jsxGet_uniqueID() {
if (uniqueID_ == null) {
uniqueID_ = "ms__id" + UniqueID_Counter_++;
}
return uniqueID_;
}
/**
* Returns the value of the "all" property.
* @return the value of the "all" property
*/
public HTMLCollection jsxGet_all() {
if (all_ == null) {
all_ = new HTMLCollectionTags(getDomNodeOrDie(), "HTMLDocument.all") {
@Override
protected boolean isMatching(final DomNode node) {
return true;
}
};
all_.setAvoidObjectDetection(!getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_54));
}
return all_;
}
/**
* JavaScript function "open".
*
* See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
* a good description of the semantics of open(), write(), writeln() and close().
*
* @param url when a new document is opened, url is a String that specifies a MIME type for the document.
* When a new window is opened, url is a String that specifies the URL to render in the new window
* @param name the name
* @param features the features
* @param replace whether to replace in the history list or no
* @return a reference to the new document object or the window object.
* @see MSDN documentation
*/
public Object jsxFunction_open(final String url, final Object name, final Object features,
final Object replace) {
// Any open() invocations are ignored during the parsing stage, because write() and
// writeln() invocations will directly append content to the current insertion point.
final HtmlPage page = getHtmlPage();
if (page.isBeingParsed()) {
LOG.warn("Ignoring call to open() during the parsing stage.");
return null;
}
// We're not in the parsing stage; OK to continue.
if (!writeInCurrentDocument_) {
LOG.warn("Function open() called when document is already open.");
}
writeInCurrentDocument_ = false;
return null;
}
/**
* JavaScript function "close".
*
* See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
* a good description of the semantics of open(), write(), writeln() and close().
*
* @throws IOException if an IO problem occurs
*/
public void jsxFunction_close() throws IOException {
if (writeInCurrentDocument_) {
LOG.warn("close() called when document is not open.");
}
else {
final HtmlPage page = getHtmlPage();
final URL url = page.getWebResponse().getWebRequest().getUrl();
final StringWebResponse webResponse = new StringWebResponse(writeBuffer_.toString(), url);
webResponse.setFromJavascript(true);
final WebClient webClient = page.getWebClient();
final WebWindow window = page.getEnclosingWindow();
webClient.loadWebResponseInto(webResponse, window);
writeInCurrentDocument_ = true;
writeBuffer_.setLength(0);
}
}
/**
* Closes the document implicitly, i.e. flushes the document.write buffer (IE only).
*/
private void implicitCloseIfNecessary() {
final boolean ie = getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_55);
if (!writeInCurrentDocument_ && ie) {
try {
jsxFunction_close();
}
catch (final IOException e) {
throw Context.throwAsScriptRuntimeEx(e);
}
}
}
/**
* Gets the window in which this document is contained.
* @return the window
*/
public Object jsxGet_parentWindow() {
return getWindow();
}
/**
* {@inheritDoc}
*/
@Override
public Object jsxFunction_appendChild(final Object childObject) {
if (limitAppendChildToIE() && !getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_56)) {
// Firefox does not allow insertion at the document level.
throw Context.reportRuntimeError("Node cannot be inserted at the specified point in the hierarchy.");
}
// We're emulating IE; we can allow insertion.
return super.jsxFunction_appendChild(childObject);
}
/**
* Returns true if this document only allows appendChild to be called on
* it when emulating IE.
*
* @return true if this document only allows appendChild to be called on
* it when emulating IE
*
* @see HTMLDocument#limitAppendChildToIE()
* @see com.gargoylesoftware.htmlunit.javascript.host.xml.XMLDocument#limitAppendChildToIE()
*/
protected boolean limitAppendChildToIE() {
return true;
}
/**
* Create a new HTML element with the given tag name.
*
* @param tagName the tag name
* @return the new HTML element, or NOT_FOUND if the tag is not supported
*/
@Override
public Object jsxFunction_createElement(String tagName) {
Object result = NOT_FOUND;
// IE can handle HTML, but it takes only the first tag found
if (tagName.startsWith("<") && getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_57)) {
final Matcher m = FIRST_TAG_PATTERN.matcher(tagName);
if (m.find()) {
tagName = m.group(1);
result = super.jsxFunction_createElement(tagName);
if (result == NOT_FOUND || m.group(2) == null) {
return result;
}
final HTMLElement elt = (HTMLElement) result;
// handle attributes
final String attributes = m.group(2);
final Matcher mAttribute = ATTRIBUTES_PATTERN.matcher(attributes);
while (mAttribute.find()) {
final String attrName = mAttribute.group(1);
final String attrValue = mAttribute.group(2);
elt.jsxFunction_setAttribute(attrName, attrValue);
}
}
}
else {
return super.jsxFunction_createElement(tagName);
}
return result;
}
/**
* Creates a new Stylesheet.
* Current implementation just creates an empty {@link CSSStyleSheet} object.
* @param url the stylesheet URL
* @param index where to insert the sheet in the collection
* @return the newly created stylesheet
*/
public CSSStyleSheet jsxFunction_createStyleSheet(final String url, final int index) {
// minimal implementation
final CSSStyleSheet stylesheet = new CSSStyleSheet();
stylesheet.setPrototype(getPrototype(CSSStyleSheet.class));
stylesheet.setParentScope(getWindow());
return stylesheet;
}
/**
* Returns the element with the specified ID, or null if that element could not be found.
* @param id the ID to search for
* @return the element, or null if it could not be found
*/
public Object jsxFunction_getElementById(final String id) {
implicitCloseIfNecessary();
Object result = null;
try {
final boolean caseSensitive =
getBrowserVersion().hasFeature(BrowserVersionFeatures.JS_GET_ELEMENT_BY_ID_CASE_SENSITIVE);
final HtmlElement htmlElement = this.getDomNodeOrDie().getHtmlElementById(id, caseSensitive);
final Object jsElement = getScriptableFor(htmlElement);
if (jsElement == NOT_FOUND) {
if (LOG.isDebugEnabled()) {
LOG.debug("getElementById(" + id
+ ") cannot return a result as there isn't a JavaScript object for the HTML element "
+ htmlElement.getClass().getName());
}
}
else {
result = jsElement;
}
}
catch (final ElementNotFoundException e) {
// Just fall through - result is already set to null
final BrowserVersion browser = getBrowserVersion();
if (browser.hasFeature(BrowserVersionFeatures.JS_GET_ELEMENT_BY_ID_ALSO_BY_NAME)) {
final HTMLCollection elements = jsxFunction_getElementsByName(id);
result = elements.get(0, elements);
if (result instanceof UniqueTag) {
return null;
}
LOG.warn("getElementById(" + id + ") did a getElementByName for Internet Explorer");
return result;
}
if (LOG.isDebugEnabled()) {
LOG.debug("getElementById(" + id + "): no DOM node found with this id");
}
}
return result;
}
/**
* Returns all the descendant elements with the specified class name.
* @param className the name to search for
* @return all the descendant elements with the specified class name
* @see Mozilla doc
*/
public HTMLCollection jsxFunction_getElementsByClassName(final String className) {
return ((HTMLElement) jsxGet_documentElement()).jsxFunction_getElementsByClassName(className);
}
/**
* Returns all HTML elements that have a "name" attribute with the specified value.
*
* Refer to
* The DOM spec for details.
*
* @param elementName - value of the "name" attribute to look for
* @return all HTML elements that have a "name" attribute with the specified value
*/
public HTMLCollection jsxFunction_getElementsByName(final String elementName) {
implicitCloseIfNecessary();
if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_59)
&& (StringUtils.isEmpty(elementName) || "null".equals(elementName))) {
return HTMLCollection.emptyCollection(getWindow());
}
// Null must me changed to '' for proper collection initialization.
final String expElementName = "null".equals(elementName) ? "" : elementName;
final HtmlPage page = (HtmlPage) getPage();
final String description = "HTMLDocument.getElementsByName('" + elementName + "')";
final HTMLCollection collection = new HTMLCollection(page, true, description) {
@Override
protected List