Java tutorial
/* * Copyright (c) 2002-2016 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.css; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.QUERYSELECTORALL_NOT_IN_QUIRKS; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.QUERYSELECTOR_CSS3_PSEUDO_REQUIRE_ATTACHED_NODE; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.STYLESHEET_HREF_EMPTY_IS_NULL; import static com.gargoylesoftware.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED; import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.CHROME; import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.EDGE; import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.FF; import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.IE; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import net.sourceforge.htmlunit.corejs.javascript.Context; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.css.sac.AttributeCondition; import org.w3c.css.sac.CSSException; import org.w3c.css.sac.CSSParseException; import org.w3c.css.sac.CombinatorCondition; import org.w3c.css.sac.Condition; import org.w3c.css.sac.ConditionalSelector; import org.w3c.css.sac.ContentCondition; import org.w3c.css.sac.DescendantSelector; import org.w3c.css.sac.ElementSelector; import org.w3c.css.sac.ErrorHandler; import org.w3c.css.sac.InputSource; import org.w3c.css.sac.LangCondition; import org.w3c.css.sac.NegativeCondition; import org.w3c.css.sac.NegativeSelector; import org.w3c.css.sac.SACMediaList; import org.w3c.css.sac.Selector; import org.w3c.css.sac.SelectorList; import org.w3c.css.sac.SiblingSelector; import org.w3c.css.sac.SimpleSelector; import org.w3c.dom.DOMException; import org.w3c.dom.css.CSSImportRule; import org.w3c.dom.css.CSSRule; import org.w3c.dom.css.CSSRuleList; import org.w3c.dom.stylesheets.MediaList; import com.gargoylesoftware.htmlunit.BrowserVersion; import com.gargoylesoftware.htmlunit.Cache; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.WebWindow; import com.gargoylesoftware.htmlunit.html.DisabledElement; import com.gargoylesoftware.htmlunit.html.DomElement; import com.gargoylesoftware.htmlunit.html.DomNode; import com.gargoylesoftware.htmlunit.html.DomText; import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput; import com.gargoylesoftware.htmlunit.html.HtmlElement; import com.gargoylesoftware.htmlunit.html.HtmlHtml; import com.gargoylesoftware.htmlunit.html.HtmlLink; import com.gargoylesoftware.htmlunit.html.HtmlOption; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlRadioButtonInput; import com.gargoylesoftware.htmlunit.html.HtmlStyle; import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable; import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass; import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClasses; import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor; import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction; import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter; import com.gargoylesoftware.htmlunit.javascript.configuration.WebBrowser; import com.gargoylesoftware.htmlunit.javascript.host.Element; import com.gargoylesoftware.htmlunit.javascript.host.Window; import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument; import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement; import com.gargoylesoftware.htmlunit.util.UrlUtils; import com.steadystate.css.dom.CSSImportRuleImpl; import com.steadystate.css.dom.CSSMediaRuleImpl; import com.steadystate.css.dom.CSSStyleRuleImpl; import com.steadystate.css.dom.CSSStyleSheetImpl; import com.steadystate.css.dom.CSSValueImpl; import com.steadystate.css.dom.MediaListImpl; import com.steadystate.css.dom.Property; import com.steadystate.css.parser.CSSOMParser; import com.steadystate.css.parser.SACMediaListImpl; import com.steadystate.css.parser.SACParserCSS3; import com.steadystate.css.parser.SelectorListImpl; import com.steadystate.css.parser.media.MediaQuery; import com.steadystate.css.parser.selectors.GeneralAdjacentSelectorImpl; import com.steadystate.css.parser.selectors.PrefixAttributeConditionImpl; import com.steadystate.css.parser.selectors.PseudoClassConditionImpl; import com.steadystate.css.parser.selectors.SubstringAttributeConditionImpl; import com.steadystate.css.parser.selectors.SuffixAttributeConditionImpl; /** * A JavaScript object for {@code CSSStyleSheet}. * * @see <a href="http://msdn2.microsoft.com/en-us/library/ms535871.aspx">MSDN doc</a> * @author Marc Guillemot * @author Daniel Gredler * @author Ahmed Ashour * @author Ronald Brill * @author Guy Burton * @author Frank Danek */ @JsxClasses({ @JsxClass(browsers = { @WebBrowser(CHROME), @WebBrowser(FF), @WebBrowser(IE), @WebBrowser(EDGE) }) }) public class CSSStyleSheet extends StyleSheet { private static final Log LOG = LogFactory.getLog(CSSStyleSheet.class); private static final Pattern NTH_NUMERIC = Pattern.compile("\\d+"); private static final Pattern NTH_COMPLEX = Pattern.compile("[+-]?\\d*n\\w*([+-]\\w\\d*)?"); private static final Pattern UNESCAPE_SELECTOR = Pattern.compile("\\\\([\\[\\]\\.:])"); /** The parsed stylesheet which this host object wraps. */ private final org.w3c.dom.css.CSSStyleSheet wrapped_; /** The HTML element which owns this stylesheet. */ private final HTMLElement ownerNode_; /** The collection of rules defined in this style sheet. */ private com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList cssRules_; /** The CSS import rules and their corresponding stylesheets. */ private final Map<CSSImportRule, CSSStyleSheet> imports_ = new HashMap<>(); /** This stylesheet's URI (used to resolved contained @import rules). */ private String uri_; private static final Set<String> CSS2_PSEUDO_CLASSES = new HashSet<>( Arrays.asList("link", "visited", "hover", "active", "focus", "lang", "first-child")); private static final Set<String> CSS3_PSEUDO_CLASSES = new HashSet<>( Arrays.asList("checked", "disabled", "enabled", "indeterminated", "root", "target", "not()", "nth-child()", "nth-last-child()", "nth-of-type()", "nth-last-of-type()", "last-child", "first-of-type", "last-of-type", "only-child", "only-of-type", "empty")); static { CSS3_PSEUDO_CLASSES.addAll(CSS2_PSEUDO_CLASSES); } /** * Creates a new empty stylesheet. */ @JsxConstructor({ @WebBrowser(CHROME), @WebBrowser(FF), @WebBrowser(EDGE) }) public CSSStyleSheet() { wrapped_ = new CSSStyleSheetImpl(); ownerNode_ = null; } /** * Creates a new stylesheet representing the CSS stylesheet for the specified input source. * @param element the owning node * @param source the input source which contains the CSS stylesheet which this stylesheet host object represents * @param uri this stylesheet's URI (used to resolved contained @import rules) */ public CSSStyleSheet(final HTMLElement element, final InputSource source, final String uri) { setParentScope(element.getWindow()); setPrototype(getPrototype(CSSStyleSheet.class)); if (source != null) { source.setURI(uri); } wrapped_ = parseCSS(source); uri_ = uri; ownerNode_ = element; } /** * Creates a new stylesheet representing the specified CSS stylesheet. * @param element the owning node * @param wrapped the CSS stylesheet which this stylesheet host object represents * @param uri this stylesheet's URI (used to resolved contained @import rules) */ public CSSStyleSheet(final HTMLElement element, final org.w3c.dom.css.CSSStyleSheet wrapped, final String uri) { setParentScope(element.getWindow()); setPrototype(getPrototype(CSSStyleSheet.class)); wrapped_ = wrapped; uri_ = uri; ownerNode_ = element; } /** * Returns the wrapped stylesheet. * @return the wrapped stylesheet */ public org.w3c.dom.css.CSSStyleSheet getWrappedSheet() { return wrapped_; } /** * Modifies the specified style object by adding any style rules which apply to the specified * element. * * @param style the style to modify * @param element the element to which style rules must apply in order for them to be added to * the specified style */ public void modifyIfNecessary(final ComputedCSSStyleDeclaration style, final Element element) { final CSSRuleList rules = getWrappedSheet().getCssRules(); modifyIfNecessary(style, element, rules, new HashSet<String>()); } private void modifyIfNecessary(final ComputedCSSStyleDeclaration style, final Element element, final CSSRuleList rules, final Set<String> alreadyProcessing) { if (rules == null) { return; } final BrowserVersion browser = getBrowserVersion(); final DomElement e = element.getDomNodeOrDie(); final int rulesLength = rules.getLength(); for (int i = 0; i < rulesLength; i++) { final CSSRule rule = rules.item(i); final short ruleType = rule.getType(); if (CSSRule.STYLE_RULE == ruleType) { final CSSStyleRuleImpl styleRule = (CSSStyleRuleImpl) rule; final SelectorList selectors = styleRule.getSelectors(); for (int j = 0; j < selectors.getLength(); j++) { final Selector selector = selectors.item(j); final boolean selected = selects(browser, selector, e); if (selected) { final org.w3c.dom.css.CSSStyleDeclaration dec = styleRule.getStyle(); style.applyStyleFromSelector(dec, selector); } } } else if (CSSRule.IMPORT_RULE == ruleType) { final CSSImportRuleImpl importRule = (CSSImportRuleImpl) rule; final MediaList mediaList = importRule.getMedia(); if (isActive(this, mediaList)) { CSSStyleSheet sheet = imports_.get(importRule); if (sheet == null) { // TODO: surely wrong: in which case is it null and why? final String uri = (uri_ != null) ? uri_ : e.getPage().getUrl().toExternalForm(); final String href = importRule.getHref(); final String url = UrlUtils.resolveUrl(uri, href); sheet = loadStylesheet(getWindow(), ownerNode_, null, url); imports_.put(importRule, sheet); } if (!alreadyProcessing.contains(sheet.getUri())) { final CSSRuleList sheetRules = sheet.getWrappedSheet().getCssRules(); alreadyProcessing.add(getUri()); sheet.modifyIfNecessary(style, element, sheetRules, alreadyProcessing); } } } else if (CSSRule.MEDIA_RULE == ruleType) { final CSSMediaRuleImpl mediaRule = (CSSMediaRuleImpl) rule; final MediaList mediaList = mediaRule.getMedia(); if (isActive(this, mediaList)) { final CSSRuleList internalRules = mediaRule.getCssRules(); modifyIfNecessary(style, element, internalRules, alreadyProcessing); } } } } /** * Loads the stylesheet at the specified link or href. * @param window the current window * @param element the parent DOM element * @param link the stylesheet's link (may be {@code null} if an <tt>url</tt> is specified) * @param url the stylesheet's url (may be {@code null} if a <tt>link</tt> is specified) * @return the loaded stylesheet */ public static CSSStyleSheet loadStylesheet(final Window window, final HTMLElement element, final HtmlLink link, final String url) { CSSStyleSheet sheet; final HtmlPage page = (HtmlPage) element.getDomNodeOrDie().getPage(); String uri = page.getUrl().toExternalForm(); // fallback uri for exceptions try { // Retrieve the associated content and respect client settings regarding failing HTTP status codes. final WebRequest request; final WebResponse response; final WebClient client = page.getWebClient(); if (link != null) { // Use link. request = link.getWebRequest(); // our cache is a bit strange; // loadWebResponse check the cache for the web response // AND also fixes the request url for the following cache lookups response = link.getWebResponse(true, request); } else { // Use href. final String accept = client.getBrowserVersion().getCssAcceptHeader(); request = new WebRequest(new URL(url), accept); final String referer = page.getUrl().toExternalForm(); request.setAdditionalHeader("Referer", referer); // our cache is a bit strange; // loadWebResponse check the cache for the web response // AND also fixes the request url for the following cache lookups response = client.loadWebResponse(request); } // now we can look into the cache with the fixed request for // a cached script final Cache cache = client.getCache(); final Object fromCache = cache.getCachedObject(request); if (fromCache != null && fromCache instanceof org.w3c.dom.css.CSSStyleSheet) { uri = request.getUrl().toExternalForm(); sheet = new CSSStyleSheet(element, (org.w3c.dom.css.CSSStyleSheet) fromCache, uri); } else { uri = response.getWebRequest().getUrl().toExternalForm(); client.printContentIfNecessary(response); client.throwFailingHttpStatusCodeExceptionIfNecessary(response); // CSS content must have downloaded OK; go ahead and build the corresponding stylesheet. final InputSource source = new InputSource(); source.setByteStream(response.getContentAsStream()); source.setEncoding(response.getContentCharset()); sheet = new CSSStyleSheet(element, source, uri); // cache the style sheet if (!cache.cacheIfPossible(request, response, sheet.getWrappedSheet())) { response.cleanUp(); } } } catch (final FailingHttpStatusCodeException e) { // Got a 404 response or something like that; behave nicely. LOG.error("Exception loading " + uri, e); final InputSource source = new InputSource(new StringReader("")); sheet = new CSSStyleSheet(element, source, uri); } catch (final IOException e) { // Got a basic IO error; behave nicely. LOG.error("IOException loading " + uri, e); final InputSource source = new InputSource(new StringReader("")); sheet = new CSSStyleSheet(element, source, uri); } catch (final RuntimeException e) { // Got something unexpected; we can throw an exception in this case. LOG.error("RuntimeException loading " + uri, e); throw Context.reportRuntimeError("Exception: " + e); } catch (final Exception e) { // Got something unexpected; we can throw an exception in this case. LOG.error("Exception loading " + uri, e); throw Context.reportRuntimeError("Exception: " + e); } return sheet; } /** * Returns {@code true} if the specified selector selects the specified element. * * @param selector the selector to test * @param element the element to test * @return {@code true} if it does apply, {@code false} if it doesn't apply */ boolean selects(final Selector selector, final DomElement element) { return selects(getBrowserVersion(), selector, element); } /** * Returns {@code true} if the specified selector selects the specified element. * * @param browserVersion the browser version * @param selector the selector to test * @param element the element to test * @return {@code true} if it does apply, {@code false} if it doesn't apply */ public static boolean selects(final BrowserVersion browserVersion, final Selector selector, final DomElement element) { switch (selector.getSelectorType()) { case Selector.SAC_ANY_NODE_SELECTOR: if (selector instanceof GeneralAdjacentSelectorImpl) { final SiblingSelector ss = (SiblingSelector) selector; final Selector ssSelector = ss.getSelector(); final SimpleSelector ssSiblingSelector = ss.getSiblingSelector(); for (DomNode prev = element.getPreviousSibling(); prev != null; prev = prev.getPreviousSibling()) { if (prev instanceof HtmlElement && selects(browserVersion, ssSelector, (HtmlElement) prev) && selects(browserVersion, ssSiblingSelector, element)) { return true; } } return false; } return true; case Selector.SAC_CHILD_SELECTOR: final DomNode parentNode = element.getParentNode(); if (parentNode == element.getPage()) { return false; } if (!(parentNode instanceof HtmlElement)) { return false; // for instance parent is a DocumentFragment } final DescendantSelector cs = (DescendantSelector) selector; return selects(browserVersion, cs.getSimpleSelector(), element) && selects(browserVersion, cs.getAncestorSelector(), (HtmlElement) parentNode); case Selector.SAC_DESCENDANT_SELECTOR: final DescendantSelector ds = (DescendantSelector) selector; if (selects(browserVersion, ds.getSimpleSelector(), element)) { DomNode ancestor = element.getParentNode(); final Selector dsAncestorSelector = ds.getAncestorSelector(); while (ancestor instanceof HtmlElement) { if (selects(browserVersion, dsAncestorSelector, (HtmlElement) ancestor)) { return true; } ancestor = ancestor.getParentNode(); } } return false; case Selector.SAC_CONDITIONAL_SELECTOR: final ConditionalSelector conditional = (ConditionalSelector) selector; final SimpleSelector simpleSel = conditional.getSimpleSelector(); return (simpleSel == null || selects(browserVersion, simpleSel, element)) && selects(browserVersion, conditional.getCondition(), element); case Selector.SAC_ELEMENT_NODE_SELECTOR: final ElementSelector es = (ElementSelector) selector; final String name = es.getLocalName(); return name == null || name.equalsIgnoreCase(element.getLocalName()); case Selector.SAC_ROOT_NODE_SELECTOR: return HtmlHtml.TAG_NAME.equalsIgnoreCase(element.getTagName()); case Selector.SAC_DIRECT_ADJACENT_SELECTOR: final SiblingSelector ss = (SiblingSelector) selector; DomNode prev = element.getPreviousSibling(); while (prev != null && !(prev instanceof HtmlElement)) { prev = prev.getPreviousSibling(); } return prev != null && selects(browserVersion, ss.getSelector(), (HtmlElement) prev) && selects(browserVersion, ss.getSiblingSelector(), element); case Selector.SAC_NEGATIVE_SELECTOR: final NegativeSelector ns = (NegativeSelector) selector; return !selects(browserVersion, ns.getSimpleSelector(), element); case Selector.SAC_PSEUDO_ELEMENT_SELECTOR: case Selector.SAC_COMMENT_NODE_SELECTOR: case Selector.SAC_CDATA_SECTION_NODE_SELECTOR: case Selector.SAC_PROCESSING_INSTRUCTION_NODE_SELECTOR: case Selector.SAC_TEXT_NODE_SELECTOR: return false; default: LOG.error("Unknown CSS selector type '" + selector.getSelectorType() + "'."); return false; } } /** * Returns {@code true} if the specified condition selects the specified element. * * @param browserVersion the browser version * @param condition the condition to test * @param element the element to test * @return {@code true} if it does apply, {@code false} if it doesn't apply */ static boolean selects(final BrowserVersion browserVersion, final Condition condition, final DomElement element) { if (condition instanceof PrefixAttributeConditionImpl) { final AttributeCondition ac = (AttributeCondition) condition; final String value = ac.getValue(); return !"".equals(value) && element.getAttribute(ac.getLocalName()).startsWith(value); } if (condition instanceof SuffixAttributeConditionImpl) { final AttributeCondition ac = (AttributeCondition) condition; final String value = ac.getValue(); return !"".equals(value) && element.getAttribute(ac.getLocalName()).endsWith(value); } if (condition instanceof SubstringAttributeConditionImpl) { final AttributeCondition ac = (AttributeCondition) condition; final String value = ac.getValue(); return !"".equals(value) && element.getAttribute(ac.getLocalName()).contains(value); } switch (condition.getConditionType()) { case Condition.SAC_ID_CONDITION: final AttributeCondition ac4 = (AttributeCondition) condition; return ac4.getValue().equals(element.getId()); case Condition.SAC_CLASS_CONDITION: final AttributeCondition ac3 = (AttributeCondition) condition; String v3 = ac3.getValue(); if (v3.indexOf('\\') > -1) { v3 = UNESCAPE_SELECTOR.matcher(v3).replaceAll("$1"); } final String a3 = element.getAttribute("class"); return selectsWhitespaceSeparated(v3, a3); case Condition.SAC_AND_CONDITION: final CombinatorCondition cc1 = (CombinatorCondition) condition; return selects(browserVersion, cc1.getFirstCondition(), element) && selects(browserVersion, cc1.getSecondCondition(), element); case Condition.SAC_ATTRIBUTE_CONDITION: final AttributeCondition ac1 = (AttributeCondition) condition; if (ac1.getSpecified()) { String value = ac1.getValue(); if (value.indexOf('\\') > -1) { value = UNESCAPE_SELECTOR.matcher(value).replaceAll("$1"); } final String attrValue = element.getAttribute(ac1.getLocalName()); return DomElement.ATTRIBUTE_NOT_DEFINED != attrValue && attrValue.equals(value); } return element.hasAttribute(ac1.getLocalName()); case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION: final AttributeCondition ac2 = (AttributeCondition) condition; final String v = ac2.getValue(); final String a = element.getAttribute(ac2.getLocalName()); return selects(v, a, '-'); case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION: final AttributeCondition ac5 = (AttributeCondition) condition; final String v2 = ac5.getValue(); final String a2 = element.getAttribute(ac5.getLocalName()); return selects(v2, a2, ' '); case Condition.SAC_OR_CONDITION: final CombinatorCondition cc2 = (CombinatorCondition) condition; return selects(browserVersion, cc2.getFirstCondition(), element) || selects(browserVersion, cc2.getSecondCondition(), element); case Condition.SAC_NEGATIVE_CONDITION: final NegativeCondition nc = (NegativeCondition) condition; return !selects(browserVersion, nc.getCondition(), element); case Condition.SAC_ONLY_CHILD_CONDITION: return element.getParentNode().getChildNodes().getLength() == 1; case Condition.SAC_CONTENT_CONDITION: final ContentCondition cc = (ContentCondition) condition; return element.asText().contains(cc.getData()); case Condition.SAC_LANG_CONDITION: final String lcLang = ((LangCondition) condition).getLang(); final int lcLangLength = lcLang.length(); for (DomNode node = element; node instanceof HtmlElement; node = node.getParentNode()) { final String nodeLang = ((HtmlElement) node).getAttribute("lang"); if (ATTRIBUTE_NOT_DEFINED != nodeLang) { // "en", "en-GB" should be matched by "en" but not "english" return nodeLang.startsWith(lcLang) && (nodeLang.length() == lcLangLength || '-' == nodeLang.charAt(lcLangLength)); } } return false; case Condition.SAC_ONLY_TYPE_CONDITION: final String tagName = element.getTagName(); return ((HtmlPage) element.getPage()).getElementsByTagName(tagName).getLength() == 1; case Condition.SAC_PSEUDO_CLASS_CONDITION: return selectsPseudoClass(browserVersion, (AttributeCondition) condition, element); case Condition.SAC_POSITIONAL_CONDITION: return false; default: LOG.error("Unknown CSS condition type '" + condition.getConditionType() + "'."); return false; } } private static boolean selects(final String condition, final String attribute, final char separator) { // attribute.equals(condition) // || attribute.startsWith(condition + " ") || attriubte.endsWith(" " + condition) // || attribute.contains(" " + condition + " "); final int conditionLength = condition.length(); if (conditionLength < 1) { return false; } final int attribLength = attribute.length(); if (attribLength < conditionLength) { return false; } if (attribLength > conditionLength) { if (separator == attribute.charAt(conditionLength) && attribute.startsWith(condition)) { return true; } if (separator == attribute.charAt(attribLength - conditionLength - 1) && attribute.endsWith(condition)) { return true; } if (attribLength + 1 > conditionLength) { final StringBuilder tmp = new StringBuilder(conditionLength + 2); tmp.append(separator).append(condition).append(separator); return attribute.contains(tmp); } return false; } return attribute.equals(condition); } private static boolean selectsWhitespaceSeparated(final String condition, final String attribute) { final int conditionLength = condition.length(); if (conditionLength < 1) { return false; } final int attribLength = attribute.length(); if (attribLength < conditionLength) { return false; } int pos = attribute.indexOf(condition); while (pos != -1) { if (pos > 0 && !Character.isWhitespace(attribute.charAt(pos - 1))) { pos = attribute.indexOf(condition, pos + 1); continue; } final int lastPos = pos + condition.length(); if (lastPos < attribLength && !Character.isWhitespace(attribute.charAt(lastPos))) { pos = attribute.indexOf(condition, pos + 1); continue; } return true; } return false; } private static boolean selectsPseudoClass(final BrowserVersion browserVersion, final AttributeCondition condition, final DomElement element) { if (browserVersion.hasFeature(QUERYSELECTORALL_NOT_IN_QUIRKS)) { final Object sobj = element.getPage().getScriptableObject(); if (sobj instanceof HTMLDocument && ((HTMLDocument) sobj).getDocumentMode() < 8) { return false; } } final String value = condition.getValue(); if ("root".equals(value)) { return element == element.getPage().getDocumentElement(); } else if ("enabled".equals(value)) { return element instanceof DisabledElement && !((DisabledElement) element).isDisabled(); } if ("disabled".equals(value)) { return element instanceof DisabledElement && ((DisabledElement) element).isDisabled(); } if ("focus".equals(value)) { final HtmlPage htmlPage = element.getHtmlPageOrNull(); if (htmlPage != null) { final DomElement focus = htmlPage.getFocusedElement(); return element == focus; } } else if ("checked".equals(value)) { return (element instanceof HtmlCheckBoxInput && ((HtmlCheckBoxInput) element).isChecked()) || (element instanceof HtmlRadioButtonInput && ((HtmlRadioButtonInput) element).isChecked() || (element instanceof HtmlOption && ((HtmlOption) element).isSelected())); } else if ("first-child".equals(value)) { for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) { if (n instanceof DomElement) { return false; } } return true; } else if ("last-child".equals(value)) { for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) { if (n instanceof DomElement) { return false; } } return true; } else if ("first-of-type".equals(value)) { final String type = element.getNodeName(); for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) { if (n instanceof DomElement && n.getNodeName().equals(type)) { return false; } } return true; } else if ("last-of-type".equals(value)) { final String type = element.getNodeName(); for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) { if (n instanceof DomElement && n.getNodeName().equals(type)) { return false; } } return true; } else if (value.startsWith("nth-child(")) { final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1); int index = 0; for (DomNode n = element; n != null; n = n.getPreviousSibling()) { if (n instanceof DomElement) { index++; } } return getNth(nth, index); } else if (value.startsWith("nth-last-child(")) { final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1); int index = 0; for (DomNode n = element; n != null; n = n.getNextSibling()) { if (n instanceof DomElement) { index++; } } return getNth(nth, index); } else if (value.startsWith("nth-of-type(")) { final String type = element.getNodeName(); final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1); int index = 0; for (DomNode n = element; n != null; n = n.getPreviousSibling()) { if (n instanceof DomElement && n.getNodeName().equals(type)) { index++; } } return getNth(nth, index); } else if (value.startsWith("nth-last-of-type(")) { final String type = element.getNodeName(); final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1); int index = 0; for (DomNode n = element; n != null; n = n.getNextSibling()) { if (n instanceof DomElement && n.getNodeName().equals(type)) { index++; } } return getNth(nth, index); } else if ("only-child".equals(value)) { for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) { if (n instanceof DomElement) { return false; } } for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) { if (n instanceof DomElement) { return false; } } return true; } else if ("only-of-type".equals(value)) { final String type = element.getNodeName(); for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) { if (n instanceof DomElement && n.getNodeName().equals(type)) { return false; } } for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) { if (n instanceof DomElement && n.getNodeName().equals(type)) { return false; } } return true; } else if ("empty".equals(value)) { return isEmpty(element); } else if ("target".equals(value)) { final String ref = element.getPage().getUrl().getRef(); return StringUtils.isNotBlank(ref) && ref.equals(element.getId()); } else if (value.startsWith("not(")) { final String selectors = value.substring(value.indexOf('(') + 1, value.length() - 1); final AtomicBoolean errorOccured = new AtomicBoolean(false); final ErrorHandler errorHandler = new ErrorHandler() { @Override public void warning(final CSSParseException exception) throws CSSException { // ignore } @Override public void fatalError(final CSSParseException exception) throws CSSException { errorOccured.set(true); } @Override public void error(final CSSParseException exception) throws CSSException { errorOccured.set(true); } }; final CSSOMParser parser = new CSSOMParser(new SACParserCSS3()); parser.setErrorHandler(errorHandler); try { final SelectorList selectorList = parser .parseSelectors(new InputSource(new StringReader(selectors))); if (errorOccured.get() || selectorList == null || selectorList.getLength() != 1) { throw new CSSException("Invalid selectors: " + selectors); } validateSelectors(selectorList, 9, element); return !CSSStyleSheet.selects(browserVersion, selectorList.item(0), element); } catch (final IOException e) { throw new CSSException("Error parsing CSS selectors from '" + selectors + "': " + e.getMessage()); } } return false; } private static boolean isEmpty(final DomElement element) { for (DomNode n = element.getFirstChild(); n != null; n = n.getNextSibling()) { if (n instanceof DomElement || n instanceof DomText) { return false; } } return true; } private static boolean getNth(final String nth, final int index) { if ("odd".equalsIgnoreCase(nth)) { return index % 2 != 0; } if ("even".equalsIgnoreCase(nth)) { return index % 2 == 0; } // an+b final int nIndex = nth.indexOf('n'); int a = 0; if (nIndex != -1) { String value = nth.substring(0, nIndex).trim(); if ("-".equals(value)) { a = -1; } else { if (value.startsWith("+")) { value = value.substring(1); } a = NumberUtils.toInt(value, 1); } } String value = nth.substring(nIndex + 1).trim(); if (value.startsWith("+")) { value = value.substring(1); } final int b = NumberUtils.toInt(value, 0); if (a == 0) { return index == b && b > 0; } final double n = (index - b) / (double) a; return (n >= 0) && (n % 1 == 0); } /** * Parses the CSS at the specified input source. If anything at all goes wrong, this method * returns an empty stylesheet. * * @param source the source from which to retrieve the CSS to be parsed * @return the stylesheet parsed from the specified input source */ private org.w3c.dom.css.CSSStyleSheet parseCSS(final InputSource source) { org.w3c.dom.css.CSSStyleSheet ss; try { final ErrorHandler errorHandler = getWindow().getWebWindow().getWebClient().getCssErrorHandler(); final CSSOMParser parser = new CSSOMParser(new SACParserCSS3()); parser.setErrorHandler(errorHandler); ss = parser.parseStyleSheet(source, null, null); } catch (final Throwable t) { LOG.error("Error parsing CSS from '" + toString(source) + "': " + t.getMessage(), t); ss = new CSSStyleSheetImpl(); } return ss; } /** * Parses the selectors at the specified input source. If anything at all goes wrong, this * method returns an empty selector list. * * @param source the source from which to retrieve the selectors to be parsed * @return the selectors parsed from the specified input source */ public SelectorList parseSelectors(final InputSource source) { SelectorList selectors; try { final ErrorHandler errorHandler = getWindow().getWebWindow().getWebClient().getCssErrorHandler(); final CSSOMParser parser = new CSSOMParser(new SACParserCSS3()); parser.setErrorHandler(errorHandler); selectors = parser.parseSelectors(source); // in case of error parseSelectors returns null if (null == selectors) { selectors = new SelectorListImpl(); } } catch (final Throwable t) { LOG.error("Error parsing CSS selectors from '" + toString(source) + "': " + t.getMessage(), t); selectors = new SelectorListImpl(); } return selectors; } /** * Parses the given media string. If anything at all goes wrong, this * method returns an empty SACMediaList list. * * @param source the source from which to retrieve the media to be parsed * @return the media parsed from the specified input source */ static SACMediaList parseMedia(final ErrorHandler errorHandler, final String mediaString) { try { final CSSOMParser parser = new CSSOMParser(new SACParserCSS3()); parser.setErrorHandler(errorHandler); final InputSource source = new InputSource(new StringReader(mediaString)); final SACMediaList media = parser.parseMedia(source); if (media != null) { return media; } } catch (final Exception e) { LOG.error("Error parsing CSS media from '" + mediaString + "': " + e.getMessage(), e); } return new SACMediaListImpl(); } /** * Returns the contents of the specified input source, ignoring any {@link IOException}s. * @param source the input source from which to read * @return the contents of the specified input source, or an empty string if an {@link IOException} occurs */ private static String toString(final InputSource source) { try { final Reader reader = source.getCharacterStream(); if (null != reader) { // try to reset to produce some output if (reader instanceof StringReader) { final StringReader sr = (StringReader) reader; sr.reset(); } return IOUtils.toString(reader); } final InputStream is = source.getByteStream(); if (null != is) { // try to reset to produce some output if (is instanceof ByteArrayInputStream) { final ByteArrayInputStream bis = (ByteArrayInputStream) is; bis.reset(); } return IOUtils.toString(is); } return ""; } catch (final IOException e) { return ""; } } /** * For Firefox. * @return the owner */ @JsxGetter({ @WebBrowser(FF), @WebBrowser(IE), @WebBrowser(CHROME) }) public HTMLElement getOwnerNode() { return ownerNode_; } /** * For Internet Explorer. * @return the owner */ @JsxGetter(@WebBrowser(IE)) public HTMLElement getOwningElement() { return ownerNode_; } /** * Retrieves the collection of rules defined in this style sheet. * @return the collection of rules defined in this style sheet */ @JsxGetter({ @WebBrowser(IE), @WebBrowser(CHROME) }) public com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList getRules() { return getCssRules(); } /** * Returns the collection of rules defined in this style sheet. * @return the collection of rules defined in this style sheet */ @JsxGetter({ @WebBrowser(FF), @WebBrowser(CHROME), @WebBrowser(IE) }) public com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList getCssRules() { if (cssRules_ == null) { cssRules_ = new com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList(this); } return cssRules_; } /** * Returns the URL of the stylesheet. * @return the URL of the stylesheet */ @JsxGetter public String getHref() { final BrowserVersion version = getBrowserVersion(); if (ownerNode_ != null) { final DomNode node = ownerNode_.getDomNodeOrDie(); if (node instanceof HtmlLink) { // <link rel="stylesheet" type="text/css" href="..." /> final HtmlLink link = (HtmlLink) node; final HtmlPage page = (HtmlPage) link.getPage(); final String href = link.getHrefAttribute(); if ("".equals(href) && version.hasFeature(STYLESHEET_HREF_EMPTY_IS_NULL)) { return null; } // Expand relative URLs. try { final URL url = page.getFullyQualifiedUrl(href); return url.toExternalForm(); } catch (final MalformedURLException e) { // Log the error and fall through to the return values below. LOG.warn(e.getMessage(), e); } } } return null; } /** * Inserts a new rule. * @param rule the CSS rule * @param position the position at which to insert the rule * @see <a href="http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet">DOM level 2</a> * @return the position of the inserted rule */ @JsxFunction({ @WebBrowser(FF), @WebBrowser(CHROME), @WebBrowser(IE) }) public int insertRule(final String rule, final int position) { try { return wrapped_.insertRule(rule, position); } catch (final DOMException e) { throw Context.throwAsScriptRuntimeEx(e); } } /** * Deletes an existing rule. * @param position the position of the rule to be deleted * @see <a href="http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet">DOM level 2</a> */ @JsxFunction({ @WebBrowser(FF), @WebBrowser(IE), @WebBrowser(CHROME) }) public void deleteRule(final int position) { try { wrapped_.deleteRule(position); } catch (final DOMException e) { throw Context.throwAsScriptRuntimeEx(e); } } /** * Adds a new rule. * @see <a href="http://msdn.microsoft.com/en-us/library/aa358796.aspx">MSDN</a> * @param selector the selector name * @param rule the rule * @return always return -1 as of MSDN documentation */ @JsxFunction({ @WebBrowser(IE), @WebBrowser(CHROME) }) public int addRule(final String selector, final String rule) { final String completeRule = selector + " {" + rule + "}"; try { wrapped_.insertRule(completeRule, wrapped_.getCssRules().getLength()); } catch (final DOMException e) { throw Context.throwAsScriptRuntimeEx(e); } return -1; } /** * Deletes an existing rule. * @param position the position of the rule to be deleted * @see <a href="http://msdn.microsoft.com/en-us/library/ms531195(v=VS.85).aspx">MSDN</a> */ @JsxFunction({ @WebBrowser(IE), @WebBrowser(CHROME) }) public void removeRule(final int position) { try { wrapped_.deleteRule(position); } catch (final DOMException e) { throw Context.throwAsScriptRuntimeEx(e); } } /** * Returns this stylesheet's URI (used to resolved contained @import rules). * @return this stylesheet's URI (used to resolved contained @import rules) */ public String getUri() { return uri_; } /** * Returns {@code true} if this stylesheet is active, based on the media types it is associated with (if any). * @return {@code true} if this stylesheet is active, based on the media types it is associated with (if any) */ public boolean isActive() { final String media; final HtmlElement e = ownerNode_.getDomNodeOrNull(); if (e instanceof HtmlStyle) { final HtmlStyle style = (HtmlStyle) e; media = style.getMediaAttribute(); } else if (e instanceof HtmlLink) { final HtmlLink link = (HtmlLink) e; media = link.getMediaAttribute(); } else { return true; } if (StringUtils.isBlank(media)) { return true; } final WebClient webClient = getWindow().getWebWindow().getWebClient(); final SACMediaList mediaList = parseMedia(webClient.getCssErrorHandler(), media); return isActive(this, new MediaListImpl(mediaList)); } /** * Returns whether the specified {@link MediaList} is active or not. * @param scriptable the scriptable * @param mediaList the media list * @return whether the specified {@link MediaList} is active or not */ static boolean isActive(final SimpleScriptable scriptable, final MediaList mediaList) { if (mediaList.getLength() == 0) { return true; } for (int i = 0; i < mediaList.getLength(); i++) { final MediaQuery mediaQuery = ((MediaListImpl) mediaList).mediaQuery(i); boolean isActive = isActive(scriptable, mediaQuery); if (mediaQuery.isNot()) { isActive = !isActive; } if (isActive) { return true; } } return false; } private static boolean isActive(final SimpleScriptable scriptable, final MediaQuery mediaQuery) { final String mediaType = mediaQuery.getMedia(); if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) { for (final Property property : mediaQuery.getProperties()) { float val; switch (property.getName()) { case "max-width": val = pixelValue((CSSValueImpl) property.getValue()); if (val < scriptable.getWindow().getWebWindow().getInnerWidth()) { return false; } break; case "min-width": val = pixelValue((CSSValueImpl) property.getValue()); if (val > scriptable.getWindow().getWebWindow().getInnerWidth()) { return false; } break; case "max-device-width": val = pixelValue((CSSValueImpl) property.getValue()); if (val < scriptable.getWindow().getScreen().getWidth()) { return false; } break; case "min-device-width": val = pixelValue((CSSValueImpl) property.getValue()); if (val > scriptable.getWindow().getScreen().getWidth()) { return false; } break; case "max-height": val = pixelValue((CSSValueImpl) property.getValue()); if (val < scriptable.getWindow().getWebWindow().getInnerWidth()) { return false; } break; case "min-height": val = pixelValue((CSSValueImpl) property.getValue()); if (val > scriptable.getWindow().getWebWindow().getInnerWidth()) { return false; } break; case "max-device-height": val = pixelValue((CSSValueImpl) property.getValue()); if (val < scriptable.getWindow().getScreen().getWidth()) { return false; } break; case "min-device-height": val = pixelValue((CSSValueImpl) property.getValue()); if (val > scriptable.getWindow().getScreen().getWidth()) { return false; } break; case "resolution": val = resolutionValue((CSSValueImpl) property.getValue()); if (Math.round(val) != scriptable.getWindow().getScreen().getDeviceXDPI()) { return false; } break; case "max-resolution": val = resolutionValue((CSSValueImpl) property.getValue()); if (val < scriptable.getWindow().getScreen().getDeviceXDPI()) { return false; } break; case "min-resolution": val = resolutionValue((CSSValueImpl) property.getValue()); if (val > scriptable.getWindow().getScreen().getDeviceXDPI()) { return false; } break; case "orientation": final String orient = property.getValue().getCssText(); final WebWindow window = scriptable.getWindow().getWebWindow(); if ("portrait".equals(orient)) { if (window.getInnerWidth() > window.getInnerHeight()) { return false; } } else if ("landscape".equals(orient)) { if (window.getInnerWidth() < window.getInnerHeight()) { return false; } } else { LOG.warn("CSSValue '" + property.getValue().getCssText() + "' not supported for feature 'orientation'."); return false; } break; default: } } return true; } return false; } private static float pixelValue(final CSSValueImpl cssValue) { if (cssValue.getPrimitiveType() == CSSPrimitiveValue.CSS_PX) { return cssValue.getFloatValue(CSSPrimitiveValue.CSS_PX); } LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'px' value."); return -1; } private static float resolutionValue(final CSSValueImpl cssValue) { if (cssValue.getPrimitiveType() == CSSPrimitiveValue.CSS_DIMENSION) { final String text = cssValue.getCssText(); if (text.endsWith("dpi")) { return cssValue.getFloatValue(CSSPrimitiveValue.CSS_DIMENSION); } if (text.endsWith("dpcm")) { return 2.54f * cssValue.getFloatValue(CSSPrimitiveValue.CSS_DIMENSION); } if (text.endsWith("dppx")) { return 96 * cssValue.getFloatValue(CSSPrimitiveValue.CSS_DIMENSION); } } LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'px' value."); return -1; } /** * Validates the list of selectors. * @param selectorList the selectors * @param documentMode see {@link HTMLDocument#getDocumentMode()} * @param domNode the dom node the query should work on * @throws CSSException if a selector is invalid */ public static void validateSelectors(final SelectorList selectorList, final int documentMode, final DomNode domNode) throws CSSException { for (int i = 0; i < selectorList.getLength(); i++) { final Selector item = selectorList.item(i); if (!isValidSelector(item, documentMode, domNode)) { throw new CSSException("Invalid selector: " + item); } } } /** * @param documentMode see {@link HTMLDocument#getDocumentMode()} */ private static boolean isValidSelector(final Selector selector, final int documentMode, final DomNode domNode) { switch (selector.getSelectorType()) { case Selector.SAC_ELEMENT_NODE_SELECTOR: return true; case Selector.SAC_CONDITIONAL_SELECTOR: final ConditionalSelector conditional = (ConditionalSelector) selector; final SimpleSelector simpleSel = conditional.getSimpleSelector(); return (simpleSel == null || isValidSelector(simpleSel, documentMode, domNode)) && isValidCondition(conditional.getCondition(), documentMode, domNode); case Selector.SAC_DESCENDANT_SELECTOR: case Selector.SAC_CHILD_SELECTOR: final DescendantSelector ds = (DescendantSelector) selector; return isValidSelector(ds.getAncestorSelector(), documentMode, domNode) && isValidSelector(ds.getSimpleSelector(), documentMode, domNode); case Selector.SAC_DIRECT_ADJACENT_SELECTOR: final SiblingSelector ss = (SiblingSelector) selector; return isValidSelector(ss.getSelector(), documentMode, domNode) && isValidSelector(ss.getSiblingSelector(), documentMode, domNode); case Selector.SAC_ANY_NODE_SELECTOR: if (selector instanceof SiblingSelector) { final SiblingSelector sibling = (SiblingSelector) selector; return isValidSelector(sibling.getSelector(), documentMode, domNode) && isValidSelector(sibling.getSiblingSelector(), documentMode, domNode); } default: LOG.warn("Unhandled CSS selector type '" + selector.getSelectorType() + "'. Accepting it silently."); return true; // at least in a first time to break less stuff } } /** * @param documentMode see {@link HTMLDocument#getDocumentMode()} */ private static boolean isValidCondition(final Condition condition, final int documentMode, final DomNode domNode) { switch (condition.getConditionType()) { case Condition.SAC_AND_CONDITION: final CombinatorCondition cc1 = (CombinatorCondition) condition; return isValidCondition(cc1.getFirstCondition(), documentMode, domNode) && isValidCondition(cc1.getSecondCondition(), documentMode, domNode); case Condition.SAC_ATTRIBUTE_CONDITION: case Condition.SAC_ID_CONDITION: case Condition.SAC_LANG_CONDITION: case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION: case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION: case Condition.SAC_ONLY_CHILD_CONDITION: case Condition.SAC_ONLY_TYPE_CONDITION: case Condition.SAC_CONTENT_CONDITION: case Condition.SAC_CLASS_CONDITION: return true; case Condition.SAC_PSEUDO_CLASS_CONDITION: final PseudoClassConditionImpl pcc = (PseudoClassConditionImpl) condition; String value = pcc.getValue(); if (value.endsWith(")")) { if (value.endsWith("()")) { return false; } value = value.substring(0, value.indexOf('(') + 1) + ')'; } if (documentMode < 9) { return CSS2_PSEUDO_CLASSES.contains(value); } if (!CSS2_PSEUDO_CLASSES.contains(value) && domNode.hasFeature(QUERYSELECTOR_CSS3_PSEUDO_REQUIRE_ATTACHED_NODE) && !domNode.isDirectlyAttachedToPage() && !domNode.hasChildNodes()) { throw new CSSException("Syntax Error"); } if ("nth-child()".equals(value)) { final String arg = StringUtils.substringBetween(pcc.getValue(), "(", ")").trim(); return "even".equalsIgnoreCase(arg) || "odd".equalsIgnoreCase(arg) || NTH_NUMERIC.matcher(arg).matches() || NTH_COMPLEX.matcher(arg).matches(); } return CSS3_PSEUDO_CLASSES.contains(value); default: LOG.warn("Unhandled CSS condition type '" + condition.getConditionType() + "'. Accepting it silently."); return true; } } }