View Javadoc
1   /*
2    * Copyright (c) 2002-2017 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit.javascript.host.css;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLLINK_CHECK_TYPE_FOR_STYLESHEET;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.QUERYSELECTORALL_NOT_IN_QUIRKS;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.QUERYSELECTORALL_NO_TARGET;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.QUERYSELECTOR_CSS3_PSEUDO_REQUIRE_ATTACHED_NODE;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.STYLESHEET_HREF_EMPTY_IS_NULL;
22  import static com.gargoylesoftware.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
23  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
24  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
25  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
26  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
27  import static java.nio.charset.StandardCharsets.ISO_8859_1;
28  
29  import java.io.ByteArrayInputStream;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.Reader;
33  import java.io.StringReader;
34  import java.net.MalformedURLException;
35  import java.net.URL;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.HashMap;
39  import java.util.HashSet;
40  import java.util.Iterator;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Set;
44  import java.util.concurrent.atomic.AtomicBoolean;
45  import java.util.regex.Pattern;
46  
47  import org.apache.commons.io.IOUtils;
48  import org.apache.commons.lang3.StringUtils;
49  import org.apache.commons.lang3.math.NumberUtils;
50  import org.apache.commons.logging.Log;
51  import org.apache.commons.logging.LogFactory;
52  import org.w3c.css.sac.AttributeCondition;
53  import org.w3c.css.sac.CSSException;
54  import org.w3c.css.sac.CSSParseException;
55  import org.w3c.css.sac.CombinatorCondition;
56  import org.w3c.css.sac.Condition;
57  import org.w3c.css.sac.ConditionalSelector;
58  import org.w3c.css.sac.ContentCondition;
59  import org.w3c.css.sac.DescendantSelector;
60  import org.w3c.css.sac.ElementSelector;
61  import org.w3c.css.sac.ErrorHandler;
62  import org.w3c.css.sac.InputSource;
63  import org.w3c.css.sac.LangCondition;
64  import org.w3c.css.sac.NegativeCondition;
65  import org.w3c.css.sac.NegativeSelector;
66  import org.w3c.css.sac.SACMediaList;
67  import org.w3c.css.sac.Selector;
68  import org.w3c.css.sac.SelectorList;
69  import org.w3c.css.sac.SiblingSelector;
70  import org.w3c.css.sac.SimpleSelector;
71  import org.w3c.dom.DOMException;
72  import org.w3c.dom.css.CSSImportRule;
73  import org.w3c.dom.css.CSSRule;
74  import org.w3c.dom.css.CSSRuleList;
75  import org.w3c.dom.stylesheets.MediaList;
76  
77  import com.gargoylesoftware.htmlunit.BrowserVersion;
78  import com.gargoylesoftware.htmlunit.Cache;
79  import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
80  import com.gargoylesoftware.htmlunit.HttpHeader;
81  import com.gargoylesoftware.htmlunit.WebClient;
82  import com.gargoylesoftware.htmlunit.WebRequest;
83  import com.gargoylesoftware.htmlunit.WebResponse;
84  import com.gargoylesoftware.htmlunit.WebWindow;
85  import com.gargoylesoftware.htmlunit.html.DisabledElement;
86  import com.gargoylesoftware.htmlunit.html.DomElement;
87  import com.gargoylesoftware.htmlunit.html.DomNode;
88  import com.gargoylesoftware.htmlunit.html.DomText;
89  import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput;
90  import com.gargoylesoftware.htmlunit.html.HtmlElement;
91  import com.gargoylesoftware.htmlunit.html.HtmlHtml;
92  import com.gargoylesoftware.htmlunit.html.HtmlInput;
93  import com.gargoylesoftware.htmlunit.html.HtmlLink;
94  import com.gargoylesoftware.htmlunit.html.HtmlOption;
95  import com.gargoylesoftware.htmlunit.html.HtmlPage;
96  import com.gargoylesoftware.htmlunit.html.HtmlRadioButtonInput;
97  import com.gargoylesoftware.htmlunit.html.HtmlSelect;
98  import com.gargoylesoftware.htmlunit.html.HtmlStyle;
99  import com.gargoylesoftware.htmlunit.html.HtmlTextArea;
100 import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
101 import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
102 import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
103 import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
104 import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
105 import com.gargoylesoftware.htmlunit.javascript.host.Element;
106 import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
107 import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
108 import com.gargoylesoftware.htmlunit.util.UrlUtils;
109 import com.steadystate.css.dom.CSSImportRuleImpl;
110 import com.steadystate.css.dom.CSSMediaRuleImpl;
111 import com.steadystate.css.dom.CSSRuleListImpl;
112 import com.steadystate.css.dom.CSSStyleRuleImpl;
113 import com.steadystate.css.dom.CSSStyleSheetImpl;
114 import com.steadystate.css.dom.CSSValueImpl;
115 import com.steadystate.css.dom.MediaListImpl;
116 import com.steadystate.css.dom.Property;
117 import com.steadystate.css.parser.CSSOMParser;
118 import com.steadystate.css.parser.SACMediaListImpl;
119 import com.steadystate.css.parser.SACParserCSS3;
120 import com.steadystate.css.parser.SelectorListImpl;
121 import com.steadystate.css.parser.media.MediaQuery;
122 import com.steadystate.css.parser.selectors.GeneralAdjacentSelectorImpl;
123 import com.steadystate.css.parser.selectors.PrefixAttributeConditionImpl;
124 import com.steadystate.css.parser.selectors.PseudoClassConditionImpl;
125 import com.steadystate.css.parser.selectors.SubstringAttributeConditionImpl;
126 import com.steadystate.css.parser.selectors.SuffixAttributeConditionImpl;
127 
128 import net.sourceforge.htmlunit.corejs.javascript.Context;
129 
130 /**
131  * A JavaScript object for {@code CSSStyleSheet}.
132  *
133  * @see <a href="http://msdn2.microsoft.com/en-us/library/ms535871.aspx">MSDN doc</a>
134  * @author Marc Guillemot
135  * @author Daniel Gredler
136  * @author Ahmed Ashour
137  * @author Ronald Brill
138  * @author Guy Burton
139  * @author Frank Danek
140  * @author Carsten Steul
141  */
142 @JsxClass
143 public class CSSStyleSheet extends StyleSheet {
144 
145     private static final Log LOG = LogFactory.getLog(CSSStyleSheet.class);
146     private static final Pattern NTH_NUMERIC = Pattern.compile("\\d+");
147     private static final Pattern NTH_COMPLEX = Pattern.compile("[+-]?\\d*n\\w*([+-]\\w\\d*)?");
148     private static final Pattern UNESCAPE_SELECTOR = Pattern.compile("\\\\([\\[\\]\\.:])");
149 
150     /** The parsed stylesheet which this host object wraps. */
151     private final org.w3c.dom.css.CSSStyleSheet wrapped_;
152 
153     /** The HTML element which owns this stylesheet. */
154     private final HTMLElement ownerNode_;
155 
156     /** The collection of rules defined in this style sheet. */
157     private com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList cssRules_;
158     private List<Integer> cssRulesIndexFix_;
159 
160     /** The CSS import rules and their corresponding stylesheets. */
161     private final Map<CSSImportRule, CSSStyleSheet> imports_ = new HashMap<>();
162 
163     /** This stylesheet's URI (used to resolved contained @import rules). */
164     private String uri_;
165 
166     private boolean enabled_ = true;
167 
168     private static final Set<String> CSS2_PSEUDO_CLASSES = new HashSet<>(Arrays.asList(
169             "link", "visited", "hover", "active",
170             "focus", "lang", "first-child"));
171 
172     private static final Set<String> CSS3_PSEUDO_CLASSES = new HashSet<>(Arrays.asList(
173             "checked", "disabled", "enabled", "indeterminated", "root", "target", "not()",
174             "nth-child()", "nth-last-child()", "nth-of-type()", "nth-last-of-type()",
175             "last-child", "first-of-type", "last-of-type", "only-child", "only-of-type", "empty",
176             "optional", "required"));
177 
178     static {
179         CSS3_PSEUDO_CLASSES.addAll(CSS2_PSEUDO_CLASSES);
180     }
181 
182     /**
183      * Creates a new empty stylesheet.
184      */
185     @JsxConstructor({CHROME, FF, EDGE})
186     public CSSStyleSheet() {
187         wrapped_ = new CSSStyleSheetImpl();
188         ownerNode_ = null;
189     }
190 
191     /**
192      * Creates a new stylesheet representing the CSS stylesheet for the specified input source.
193      * @param element the owning node
194      * @param source the input source which contains the CSS stylesheet which this stylesheet host object represents
195      * @param uri this stylesheet's URI (used to resolved contained @import rules)
196      */
197     public CSSStyleSheet(final HTMLElement element, final InputSource source, final String uri) {
198         setParentScope(element.getWindow());
199         setPrototype(getPrototype(CSSStyleSheet.class));
200         if (source != null) {
201             source.setURI(uri);
202         }
203         wrapped_ = parseCSS(source);
204         uri_ = uri;
205         ownerNode_ = element;
206     }
207 
208     /**
209      * Creates a new stylesheet representing the specified CSS stylesheet.
210      * @param element the owning node
211      * @param wrapped the CSS stylesheet which this stylesheet host object represents
212      * @param uri this stylesheet's URI (used to resolved contained @import rules)
213      */
214     public CSSStyleSheet(final HTMLElement element, final org.w3c.dom.css.CSSStyleSheet wrapped, final String uri) {
215         setParentScope(element.getWindow());
216         setPrototype(getPrototype(CSSStyleSheet.class));
217         wrapped_ = wrapped;
218         uri_ = uri;
219         ownerNode_ = element;
220     }
221 
222     /**
223      * Returns the wrapped stylesheet.
224      * @return the wrapped stylesheet
225      */
226     public org.w3c.dom.css.CSSStyleSheet getWrappedSheet() {
227         return wrapped_;
228     }
229 
230     /**
231      * Modifies the specified style object by adding any style rules which apply to the specified
232      * element.
233      *
234      * @param style the style to modify
235      * @param element the element to which style rules must apply in order for them to be added to
236      *        the specified style
237      * @param pseudoElement a string specifying the pseudo-element to match (may be {@code null})
238      */
239     public void modifyIfNecessary(final ComputedCSSStyleDeclaration style, final Element element,
240             final String pseudoElement) {
241         final CSSRuleList rules = getWrappedSheet().getCssRules();
242         modifyIfNecessary(style, element, pseudoElement, rules, new HashSet<String>());
243     }
244 
245     private void modifyIfNecessary(final ComputedCSSStyleDeclaration style, final Element element,
246             final String pseudoElement, final CSSRuleList rules, final Set<String> alreadyProcessing) {
247         if (rules == null) {
248             return;
249         }
250 
251         final BrowserVersion browser = getBrowserVersion();
252         final DomElement e = element.getDomNodeOrDie();
253         final int rulesLength = rules.getLength();
254         for (int i = 0; i < rulesLength; i++) {
255             final CSSRule rule = rules.item(i);
256 
257             final short ruleType = rule.getType();
258             if (CSSRule.STYLE_RULE == ruleType) {
259                 final CSSStyleRuleImpl styleRule = (CSSStyleRuleImpl) rule;
260                 final SelectorList selectors = styleRule.getSelectors();
261                 for (int j = 0; j < selectors.getLength(); j++) {
262                     final Selector selector = selectors.item(j);
263                     final boolean selected = selects(browser, selector, e, pseudoElement, false);
264                     if (selected) {
265                         final org.w3c.dom.css.CSSStyleDeclaration dec = styleRule.getStyle();
266                         style.applyStyleFromSelector(dec, selector);
267                     }
268                 }
269             }
270             else if (CSSRule.IMPORT_RULE == ruleType) {
271                 final CSSImportRuleImpl importRule = (CSSImportRuleImpl) rule;
272                 final MediaList mediaList = importRule.getMedia();
273                 if (isActive(this, mediaList)) {
274                     CSSStyleSheet sheet = imports_.get(importRule);
275                     if (sheet == null) {
276                         // TODO: surely wrong: in which case is it null and why?
277                         final String uri = (uri_ != null) ? uri_ : e.getPage().getUrl().toExternalForm();
278                         final String href = importRule.getHref();
279                         final String url = UrlUtils.resolveUrl(uri, href);
280                         sheet = loadStylesheet(ownerNode_, null, url);
281                         imports_.put(importRule, sheet);
282                     }
283 
284                     if (!alreadyProcessing.contains(sheet.getUri())) {
285                         final CSSRuleList sheetRules = sheet.getWrappedSheet().getCssRules();
286                         alreadyProcessing.add(getUri());
287                         sheet.modifyIfNecessary(style, element, pseudoElement, sheetRules, alreadyProcessing);
288                     }
289                 }
290             }
291             else if (CSSRule.MEDIA_RULE == ruleType) {
292                 final CSSMediaRuleImpl mediaRule = (CSSMediaRuleImpl) rule;
293                 final MediaList mediaList = mediaRule.getMedia();
294                 if (isActive(this, mediaList)) {
295                     final CSSRuleList internalRules = mediaRule.getCssRules();
296                     modifyIfNecessary(style, element, pseudoElement, internalRules, alreadyProcessing);
297                 }
298             }
299         }
300     }
301 
302     /**
303      * Loads the stylesheet at the specified link or href.
304      * @param element the parent DOM element
305      * @param link the stylesheet's link (may be {@code null} if a <tt>url</tt> is specified)
306      * @param url the stylesheet's url (may be {@code null} if a <tt>link</tt> is specified)
307      * @return the loaded stylesheet
308      */
309     public static CSSStyleSheet loadStylesheet(final HTMLElement element, final HtmlLink link, final String url) {
310         CSSStyleSheet sheet;
311         final HtmlPage page = (HtmlPage) element.getDomNodeOrDie().getPage();
312         String uri = page.getUrl().toExternalForm(); // fallback uri for exceptions
313         try {
314             // Retrieve the associated content and respect client settings regarding failing HTTP status codes.
315             final WebRequest request;
316             final WebResponse response;
317             final WebClient client = page.getWebClient();
318             if (link != null) {
319                 // Use link.
320                 request = link.getWebRequest();
321 
322                 if (element.getBrowserVersion().hasFeature(HTMLLINK_CHECK_TYPE_FOR_STYLESHEET)) {
323                     final String type = link.getTypeAttribute();
324                     if (StringUtils.isNotBlank(type) && !"text/css".equals(type)) {
325                         final InputSource source = new InputSource(new StringReader(""));
326                         return new CSSStyleSheet(element, source, uri);
327                     }
328                 }
329 
330                 // our cache is a bit strange;
331                 // loadWebResponse check the cache for the web response
332                 // AND also fixes the request url for the following cache lookups
333                 response = link.getWebResponse(true, request);
334             }
335             else {
336                 // Use href.
337                 final String accept = client.getBrowserVersion().getCssAcceptHeader();
338                 request = new WebRequest(new URL(url), accept);
339                 final String referer = page.getUrl().toExternalForm();
340                 request.setAdditionalHeader(HttpHeader.REFERER, referer);
341 
342                 // our cache is a bit strange;
343                 // loadWebResponse check the cache for the web response
344                 // AND also fixes the request url for the following cache lookups
345                 response = client.loadWebResponse(request);
346             }
347 
348             // now we can look into the cache with the fixed request for
349             // a cached script
350             final Cache cache = client.getCache();
351             final Object fromCache = cache.getCachedObject(request);
352             if (fromCache instanceof org.w3c.dom.css.CSSStyleSheet) {
353                 uri = request.getUrl().toExternalForm();
354                 sheet = new CSSStyleSheet(element, (org.w3c.dom.css.CSSStyleSheet) fromCache, uri);
355             }
356             else {
357                 uri = response.getWebRequest().getUrl().toExternalForm();
358                 client.printContentIfNecessary(response);
359                 client.throwFailingHttpStatusCodeExceptionIfNecessary(response);
360                 // CSS content must have downloaded OK; go ahead and build the corresponding stylesheet.
361 
362                 final InputSource source = new InputSource();
363                 final String contentType = response.getContentType();
364                 if (StringUtils.isEmpty(contentType) || "text/css".equals(contentType)) {
365                     source.setByteStream(response.getContentAsStream());
366                     source.setEncoding(response.getContentCharset().name());
367                 }
368                 else {
369                     source.setCharacterStream(new StringReader(""));
370                 }
371                 sheet = new CSSStyleSheet(element, source, uri);
372 
373                 // cache the style sheet
374                 if (!cache.cacheIfPossible(request, response, sheet.getWrappedSheet())) {
375                     response.cleanUp();
376                 }
377             }
378         }
379         catch (final FailingHttpStatusCodeException e) {
380             // Got a 404 response or something like that; behave nicely.
381             LOG.error("Exception loading " + uri, e);
382             final InputSource source = new InputSource(new StringReader(""));
383             sheet = new CSSStyleSheet(element, source, uri);
384         }
385         catch (final IOException e) {
386             // Got a basic IO error; behave nicely.
387             LOG.error("IOException loading " + uri, e);
388             final InputSource source = new InputSource(new StringReader(""));
389             sheet = new CSSStyleSheet(element, source, uri);
390         }
391         catch (final RuntimeException e) {
392             // Got something unexpected; we can throw an exception in this case.
393             LOG.error("RuntimeException loading " + uri, e);
394             throw Context.reportRuntimeError("Exception: " + e);
395         }
396         catch (final Exception e) {
397             // Got something unexpected; we can throw an exception in this case.
398             LOG.error("Exception loading " + uri, e);
399             throw Context.reportRuntimeError("Exception: " + e);
400         }
401         return sheet;
402     }
403 
404     /**
405      * Returns {@code true} if the specified selector selects the specified element.
406      *
407      * @param browserVersion the browser version
408      * @param selector the selector to test
409      * @param element the element to test
410      * @param pseudoElement the pseudo element to match, (can be {@code null})
411      * @param fromQuerySelectorAll whether this is called from {@link DomNode#querySelectorAll(String)}
412      * @return {@code true} if it does apply, {@code false} if it doesn't apply
413      */
414     public static boolean selects(final BrowserVersion browserVersion, final Selector selector,
415             final DomElement element, final String pseudoElement, final boolean fromQuerySelectorAll) {
416         switch (selector.getSelectorType()) {
417             case Selector.SAC_ANY_NODE_SELECTOR:
418                 return true;
419             case Selector.SAC_CHILD_SELECTOR:
420                 final DomNode parentNode = element.getParentNode();
421                 if (parentNode == element.getPage()) {
422                     return false;
423                 }
424                 if (!(parentNode instanceof HtmlElement)) {
425                     return false; // for instance parent is a DocumentFragment
426                 }
427                 final DescendantSelector cs = (DescendantSelector) selector;
428                 return selects(browserVersion, cs.getSimpleSelector(), element, pseudoElement, fromQuerySelectorAll)
429                     && selects(browserVersion, cs.getAncestorSelector(), (HtmlElement) parentNode,
430                             pseudoElement, fromQuerySelectorAll);
431             case Selector.SAC_DESCENDANT_SELECTOR:
432                 final DescendantSelector ds = (DescendantSelector) selector;
433                 final SimpleSelector simpleSelector = ds.getSimpleSelector();
434                 if (selects(browserVersion, simpleSelector, element, pseudoElement, fromQuerySelectorAll)) {
435                     DomNode ancestor = element;
436                     if (simpleSelector.getSelectorType() != Selector.SAC_PSEUDO_ELEMENT_SELECTOR) {
437                         ancestor = ancestor.getParentNode();
438                     }
439                     final Selector dsAncestorSelector = ds.getAncestorSelector();
440                     while (ancestor instanceof HtmlElement) {
441                         if (selects(browserVersion, dsAncestorSelector, (HtmlElement) ancestor, pseudoElement,
442                                 fromQuerySelectorAll)) {
443                             return true;
444                         }
445                         ancestor = ancestor.getParentNode();
446                     }
447                 }
448                 return false;
449             case Selector.SAC_CONDITIONAL_SELECTOR:
450                 final ConditionalSelector conditional = (ConditionalSelector) selector;
451                 final SimpleSelector simpleSel = conditional.getSimpleSelector();
452                 return (simpleSel == null || selects(browserVersion, simpleSel, element,
453                         pseudoElement, fromQuerySelectorAll))
454                     && selects(browserVersion, conditional.getCondition(), element, fromQuerySelectorAll);
455             case Selector.SAC_ELEMENT_NODE_SELECTOR:
456                 final ElementSelector es = (ElementSelector) selector;
457                 final String name = es.getLocalName();
458                 return name == null || name.equalsIgnoreCase(element.getLocalName());
459             case Selector.SAC_ROOT_NODE_SELECTOR:
460                 return HtmlHtml.TAG_NAME.equalsIgnoreCase(element.getTagName());
461             case Selector.SAC_DIRECT_ADJACENT_SELECTOR:
462                 final SiblingSelector ss = (SiblingSelector) selector;
463 
464                 if (selector instanceof GeneralAdjacentSelectorImpl) {
465                     final SimpleSelector ssSiblingSelector = ss.getSiblingSelector();
466                     for (DomNode prev = element.getPreviousSibling(); prev != null; prev = prev.getPreviousSibling()) {
467                         if (prev instanceof HtmlElement
468                             && selects(browserVersion, ss.getSelector(), (HtmlElement) prev,
469                                     pseudoElement, fromQuerySelectorAll)
470                             && selects(browserVersion, ssSiblingSelector, element,
471                                     pseudoElement, fromQuerySelectorAll)) {
472                             return true;
473                         }
474                     }
475                     return false;
476                 }
477 
478                 DomNode prev = element.getPreviousSibling();
479                 while (prev != null && !(prev instanceof HtmlElement)) {
480                     prev = prev.getPreviousSibling();
481                 }
482                 return prev != null
483                     && selects(browserVersion, ss.getSelector(), (HtmlElement) prev,
484                             pseudoElement, fromQuerySelectorAll)
485                     && selects(browserVersion, ss.getSiblingSelector(), element, pseudoElement, fromQuerySelectorAll);
486             case Selector.SAC_NEGATIVE_SELECTOR:
487                 final NegativeSelector ns = (NegativeSelector) selector;
488                 return !selects(browserVersion, ns.getSimpleSelector(), element, pseudoElement, fromQuerySelectorAll);
489             case Selector.SAC_PSEUDO_ELEMENT_SELECTOR:
490                 if (pseudoElement != null && !pseudoElement.isEmpty() && pseudoElement.charAt(0) == ':') {
491                     final String pseudoName = ((ElementSelector) selector).getLocalName();
492                     return pseudoName.equals(pseudoElement.substring(1));
493                 }
494                 return false;
495             case Selector.SAC_COMMENT_NODE_SELECTOR:
496             case Selector.SAC_CDATA_SECTION_NODE_SELECTOR:
497             case Selector.SAC_PROCESSING_INSTRUCTION_NODE_SELECTOR:
498             case Selector.SAC_TEXT_NODE_SELECTOR:
499                 return false;
500             default:
501                 LOG.error("Unknown CSS selector type '" + selector.getSelectorType() + "'.");
502                 return false;
503         }
504     }
505 
506     /**
507      * Returns {@code true} if the specified condition selects the specified element.
508      *
509      * @param browserVersion the browser version
510      * @param condition the condition to test
511      * @param element the element to test
512      * @param fromQuerySelectorAll whether this is called from {@link DomNode#querySelectorAll(String)
513      * @return {@code true} if it does apply, {@code false} if it doesn't apply
514      */
515     static boolean selects(final BrowserVersion browserVersion, final Condition condition, final DomElement element,
516             final boolean fromQuerySelectorAll) {
517         if (condition instanceof PrefixAttributeConditionImpl) {
518             final AttributeCondition ac = (AttributeCondition) condition;
519             final String value = ac.getValue();
520             return !"".equals(value) && element.getAttribute(ac.getLocalName()).startsWith(value);
521         }
522         if (condition instanceof SuffixAttributeConditionImpl) {
523             final AttributeCondition ac = (AttributeCondition) condition;
524             final String value = ac.getValue();
525             return !"".equals(value) && element.getAttribute(ac.getLocalName()).endsWith(value);
526         }
527         if (condition instanceof SubstringAttributeConditionImpl) {
528             final AttributeCondition ac = (AttributeCondition) condition;
529             final String value = ac.getValue();
530             return !"".equals(value) && element.getAttribute(ac.getLocalName()).contains(value);
531         }
532         switch (condition.getConditionType()) {
533             case Condition.SAC_ID_CONDITION:
534                 final AttributeCondition ac4 = (AttributeCondition) condition;
535                 return ac4.getValue().equals(element.getId());
536             case Condition.SAC_CLASS_CONDITION:
537                 final AttributeCondition ac3 = (AttributeCondition) condition;
538                 String v3 = ac3.getValue();
539                 if (v3.indexOf('\\') > -1) {
540                     v3 = UNESCAPE_SELECTOR.matcher(v3).replaceAll("$1");
541                 }
542                 final String a3 = element.getAttribute("class");
543                 return selectsWhitespaceSeparated(v3, a3);
544             case Condition.SAC_AND_CONDITION:
545                 final CombinatorCondition cc1 = (CombinatorCondition) condition;
546                 return selects(browserVersion, cc1.getFirstCondition(), element, fromQuerySelectorAll)
547                     && selects(browserVersion, cc1.getSecondCondition(), element, fromQuerySelectorAll);
548             case Condition.SAC_ATTRIBUTE_CONDITION:
549                 final AttributeCondition ac1 = (AttributeCondition) condition;
550                 if (ac1.getSpecified()) {
551                     String value = ac1.getValue();
552                     if (value.indexOf('\\') > -1) {
553                         value = UNESCAPE_SELECTOR.matcher(value).replaceAll("$1");
554                     }
555                     final String attrValue = element.getAttribute(ac1.getLocalName());
556                     return ATTRIBUTE_NOT_DEFINED != attrValue && attrValue.equals(value);
557                 }
558                 return element.hasAttribute(ac1.getLocalName());
559             case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
560                 final AttributeCondition ac2 = (AttributeCondition) condition;
561                 final String v = ac2.getValue();
562                 final String a = element.getAttribute(ac2.getLocalName());
563                 return selects(v, a, '-');
564             case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION:
565                 final AttributeCondition ac5 = (AttributeCondition) condition;
566                 final String v2 = ac5.getValue();
567                 final String a2 = element.getAttribute(ac5.getLocalName());
568                 return selects(v2, a2, ' ');
569             case Condition.SAC_OR_CONDITION:
570                 final CombinatorCondition cc2 = (CombinatorCondition) condition;
571                 return selects(browserVersion, cc2.getFirstCondition(), element, fromQuerySelectorAll)
572                     || selects(browserVersion, cc2.getSecondCondition(), element, fromQuerySelectorAll);
573             case Condition.SAC_NEGATIVE_CONDITION:
574                 final NegativeCondition nc = (NegativeCondition) condition;
575                 return !selects(browserVersion, nc.getCondition(), element, fromQuerySelectorAll);
576             case Condition.SAC_ONLY_CHILD_CONDITION:
577                 return element.getParentNode().getChildNodes().getLength() == 1;
578             case Condition.SAC_CONTENT_CONDITION:
579                 final ContentCondition cc = (ContentCondition) condition;
580                 return element.asText().contains(cc.getData());
581             case Condition.SAC_LANG_CONDITION:
582                 final String lcLang = ((LangCondition) condition).getLang();
583                 final int lcLangLength = lcLang.length();
584                 for (DomNode node = element; node instanceof HtmlElement; node = node.getParentNode()) {
585                     final String nodeLang = ((HtmlElement) node).getAttribute("lang");
586                     if (ATTRIBUTE_NOT_DEFINED != nodeLang) {
587                         // "en", "en-GB" should be matched by "en" but not "english"
588                         return nodeLang.startsWith(lcLang)
589                             && (nodeLang.length() == lcLangLength || '-' == nodeLang.charAt(lcLangLength));
590                     }
591                 }
592                 return false;
593             case Condition.SAC_ONLY_TYPE_CONDITION:
594                 final String tagName = element.getTagName();
595                 return ((HtmlPage) element.getPage()).getElementsByTagName(tagName).getLength() == 1;
596             case Condition.SAC_PSEUDO_CLASS_CONDITION:
597                 return selectsPseudoClass(browserVersion,
598                         (AttributeCondition) condition, element, fromQuerySelectorAll);
599             case Condition.SAC_POSITIONAL_CONDITION:
600                 return false;
601             default:
602                 LOG.error("Unknown CSS condition type '" + condition.getConditionType() + "'.");
603                 return false;
604         }
605     }
606 
607     private static boolean selects(final String condition, final String attribute, final char separator) {
608         // attribute.equals(condition)
609         // || attribute.startsWith(condition + " ") || attriubte.endsWith(" " + condition)
610         // || attribute.contains(" " + condition + " ");
611 
612         final int conditionLength = condition.length();
613         if (conditionLength < 1) {
614             return false;
615         }
616 
617         final int attribLength = attribute.length();
618         if (attribLength < conditionLength) {
619             return false;
620         }
621         if (attribLength > conditionLength) {
622             if (separator == attribute.charAt(conditionLength)
623                     && attribute.startsWith(condition)) {
624                 return true;
625             }
626             if (separator == attribute.charAt(attribLength - conditionLength - 1)
627                     && attribute.endsWith(condition)) {
628                 return true;
629             }
630             if (attribLength + 1 > conditionLength) {
631                 final StringBuilder tmp = new StringBuilder(conditionLength + 2);
632                 tmp.append(separator).append(condition).append(separator);
633                 return attribute.contains(tmp);
634             }
635             return false;
636         }
637         return attribute.equals(condition);
638     }
639 
640     private static boolean selectsWhitespaceSeparated(final String condition, final String attribute) {
641         final int conditionLength = condition.length();
642         if (conditionLength < 1) {
643             return false;
644         }
645 
646         final int attribLength = attribute.length();
647         if (attribLength < conditionLength) {
648             return false;
649         }
650 
651         int pos = attribute.indexOf(condition);
652         while (pos != -1) {
653             if (pos > 0 && !Character.isWhitespace(attribute.charAt(pos - 1))) {
654                 pos = attribute.indexOf(condition, pos + 1);
655             }
656             else {
657                 final int lastPos = pos + condition.length();
658                 if (lastPos >= attribLength || Character.isWhitespace(attribute.charAt(lastPos))) {
659                     return true;
660                 }
661                 pos = attribute.indexOf(condition, pos + 1);
662             }
663         }
664 
665         return false;
666     }
667 
668     private static boolean selectsPseudoClass(final BrowserVersion browserVersion,
669             final AttributeCondition condition, final DomElement element, final boolean fromQuerySelectorAll) {
670         if (browserVersion.hasFeature(QUERYSELECTORALL_NOT_IN_QUIRKS)) {
671             final Object sobj = element.getPage().getScriptableObject();
672             if (sobj instanceof HTMLDocument && ((HTMLDocument) sobj).getDocumentMode() < 8) {
673                 return false;
674             }
675         }
676 
677         final String value = condition.getValue();
678         switch (value) {
679             case "root":
680                 return element == element.getPage().getDocumentElement();
681 
682             case "enabled":
683                 return element instanceof DisabledElement && !((DisabledElement) element).isDisabled();
684 
685             case "disabled":
686                 return element instanceof DisabledElement && ((DisabledElement) element).isDisabled();
687 
688             case "focus":
689                 final HtmlPage htmlPage = element.getHtmlPageOrNull();
690                 if (htmlPage != null) {
691                     final DomElement focus = htmlPage.getFocusedElement();
692                     return element == focus;
693                 }
694                 return false;
695 
696             case "checked":
697                 return (element instanceof HtmlCheckBoxInput && ((HtmlCheckBoxInput) element).isChecked())
698                         || (element instanceof HtmlRadioButtonInput && ((HtmlRadioButtonInput) element).isChecked()
699                                 || (element instanceof HtmlOption && ((HtmlOption) element).isSelected()));
700 
701             case "required":
702                 return (element instanceof HtmlInput
703                             || element instanceof HtmlSelect
704                             || element instanceof HtmlTextArea)
705                         && element.hasAttribute("required");
706 
707             case "optional":
708                 return (element instanceof HtmlInput
709                             || element instanceof HtmlSelect
710                             || element instanceof HtmlTextArea)
711                         && !element.hasAttribute("required");
712 
713             case "first-child":
714                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
715                     if (n instanceof DomElement) {
716                         return false;
717                     }
718                 }
719                 return true;
720 
721             case "last-child":
722                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
723                     if (n instanceof DomElement) {
724                         return false;
725                     }
726                 }
727                 return true;
728 
729             case "first-of-type":
730                 final String firstType = element.getNodeName();
731                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
732                     if (n instanceof DomElement && n.getNodeName().equals(firstType)) {
733                         return false;
734                     }
735                 }
736                 return true;
737 
738             case "last-of-type":
739                 final String lastType = element.getNodeName();
740                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
741                     if (n instanceof DomElement && n.getNodeName().equals(lastType)) {
742                         return false;
743                     }
744                 }
745                 return true;
746 
747             case "only-child":
748                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
749                     if (n instanceof DomElement) {
750                         return false;
751                     }
752                 }
753                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
754                     if (n instanceof DomElement) {
755                         return false;
756                     }
757                 }
758                 return true;
759 
760             case "only-of-type":
761                 final String type = element.getNodeName();
762                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
763                     if (n instanceof DomElement && n.getNodeName().equals(type)) {
764                         return false;
765                     }
766                 }
767                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
768                     if (n instanceof DomElement && n.getNodeName().equals(type)) {
769                         return false;
770                     }
771                 }
772                 return true;
773 
774             case "empty":
775                 return isEmpty(element);
776 
777             case "target":
778                 if (fromQuerySelectorAll && browserVersion.hasFeature(QUERYSELECTORALL_NO_TARGET)) {
779                     return false;
780                 }
781                 final String ref = element.getPage().getUrl().getRef();
782                 return StringUtils.isNotBlank(ref) && ref.equals(element.getId());
783 
784             case "hover":
785                 return element.isMouseOver();
786 
787             default:
788                 if (value.startsWith("nth-child(")) {
789                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
790                     int index = 0;
791                     for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
792                         if (n instanceof DomElement) {
793                             index++;
794                         }
795                     }
796                     return getNth(nth, index);
797                 }
798                 else if (value.startsWith("nth-last-child(")) {
799                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
800                     int index = 0;
801                     for (DomNode n = element; n != null; n = n.getNextSibling()) {
802                         if (n instanceof DomElement) {
803                             index++;
804                         }
805                     }
806                     return getNth(nth, index);
807                 }
808                 else if (value.startsWith("nth-of-type(")) {
809                     final String nthType = element.getNodeName();
810                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
811                     int index = 0;
812                     for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
813                         if (n instanceof DomElement && n.getNodeName().equals(nthType)) {
814                             index++;
815                         }
816                     }
817                     return getNth(nth, index);
818                 }
819                 else if (value.startsWith("nth-last-of-type(")) {
820                     final String nthLastType = element.getNodeName();
821                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
822                     int index = 0;
823                     for (DomNode n = element; n != null; n = n.getNextSibling()) {
824                         if (n instanceof DomElement && n.getNodeName().equals(nthLastType)) {
825                             index++;
826                         }
827                     }
828                     return getNth(nth, index);
829                 }
830                 else if (value.startsWith("not(")) {
831                     final String selectors = value.substring(value.indexOf('(') + 1, value.length() - 1);
832                     final AtomicBoolean errorOccured = new AtomicBoolean(false);
833                     final ErrorHandler errorHandler = new ErrorHandler() {
834                         @Override
835                         public void warning(final CSSParseException exception) throws CSSException {
836                             // ignore
837                         }
838 
839                         @Override
840                         public void fatalError(final CSSParseException exception) throws CSSException {
841                             errorOccured.set(true);
842                         }
843 
844                         @Override
845                         public void error(final CSSParseException exception) throws CSSException {
846                             errorOccured.set(true);
847                         }
848                     };
849                     final CSSOMParser parser = new CSSOMParser(new SACParserCSS3());
850                     parser.setErrorHandler(errorHandler);
851                     try {
852                         final SelectorList selectorList
853                             = parser.parseSelectors(new InputSource(new StringReader(selectors)));
854                         if (errorOccured.get() || selectorList == null || selectorList.getLength() != 1) {
855                             throw new CSSException("Invalid selectors: " + selectors);
856                         }
857 
858                         validateSelectors(selectorList, 9, element);
859 
860                         return !selects(browserVersion, selectorList.item(0), element,
861                                 null, fromQuerySelectorAll);
862                     }
863                     catch (final IOException e) {
864                         throw new CSSException("Error parsing CSS selectors from '" + selectors + "': "
865                                 + e.getMessage());
866                     }
867                 }
868                 return false;
869         }
870     }
871 
872     private static boolean isEmpty(final DomElement element) {
873         for (DomNode n = element.getFirstChild(); n != null; n = n.getNextSibling()) {
874             if (n instanceof DomElement || n instanceof DomText) {
875                 return false;
876             }
877         }
878         return true;
879     }
880 
881     private static boolean getNth(final String nth, final int index) {
882         if ("odd".equalsIgnoreCase(nth)) {
883             return index % 2 != 0;
884         }
885 
886         if ("even".equalsIgnoreCase(nth)) {
887             return index % 2 == 0;
888         }
889 
890         // an+b
891         final int nIndex = nth.indexOf('n');
892         int a = 0;
893         if (nIndex != -1) {
894             String value = nth.substring(0, nIndex).trim();
895             if ("-".equals(value)) {
896                 a = -1;
897             }
898             else {
899                 if (value.startsWith("+")) {
900                     value = value.substring(1);
901                 }
902                 a = NumberUtils.toInt(value, 1);
903             }
904         }
905 
906         String value = nth.substring(nIndex + 1).trim();
907         if (value.startsWith("+")) {
908             value = value.substring(1);
909         }
910         final int b = NumberUtils.toInt(value, 0);
911         if (a == 0) {
912             return index == b && b > 0;
913         }
914 
915         final double n = (index - b) / (double) a;
916         return n >= 0 && n % 1 == 0;
917     }
918 
919     /**
920      * Parses the CSS at the specified input source. If anything at all goes wrong, this method
921      * returns an empty stylesheet.
922      *
923      * @param source the source from which to retrieve the CSS to be parsed
924      * @return the stylesheet parsed from the specified input source
925      */
926     private org.w3c.dom.css.CSSStyleSheet parseCSS(final InputSource source) {
927         org.w3c.dom.css.CSSStyleSheet ss;
928         try {
929             final ErrorHandler errorHandler = getWindow().getWebWindow().getWebClient().getCssErrorHandler();
930             final CSSOMParser parser = new CSSOMParser(new SACParserCSS3());
931             parser.setErrorHandler(errorHandler);
932             ss = parser.parseStyleSheet(source, null, null);
933         }
934         catch (final Throwable t) {
935             LOG.error("Error parsing CSS from '" + toString(source) + "': " + t.getMessage(), t);
936             ss = new CSSStyleSheetImpl();
937         }
938         return ss;
939     }
940 
941     /**
942      * Parses the selectors at the specified input source. If anything at all goes wrong, this
943      * method returns an empty selector list.
944      *
945      * @param source the source from which to retrieve the selectors to be parsed
946      * @return the selectors parsed from the specified input source
947      */
948     public SelectorList parseSelectors(final InputSource source) {
949         SelectorList selectors;
950         try {
951             final ErrorHandler errorHandler = getWindow().getWebWindow().getWebClient().getCssErrorHandler();
952             final CSSOMParser parser = new CSSOMParser(new SACParserCSS3());
953             parser.setErrorHandler(errorHandler);
954             selectors = parser.parseSelectors(source);
955             // in case of error parseSelectors returns null
956             if (null == selectors) {
957                 selectors = new SelectorListImpl();
958             }
959         }
960         catch (final Throwable t) {
961             LOG.error("Error parsing CSS selectors from '" + toString(source) + "': " + t.getMessage(), t);
962             selectors = new SelectorListImpl();
963         }
964         return selectors;
965     }
966 
967     /**
968      * Parses the given media string. If anything at all goes wrong, this
969      * method returns an empty SACMediaList list.
970      *
971      * @param source the source from which to retrieve the media to be parsed
972      * @return the media parsed from the specified input source
973      */
974     static SACMediaList parseMedia(final ErrorHandler errorHandler, final String mediaString) {
975         try {
976             final CSSOMParser parser = new CSSOMParser(new SACParserCSS3());
977             parser.setErrorHandler(errorHandler);
978 
979             final InputSource source = new InputSource(new StringReader(mediaString));
980             final SACMediaList media = parser.parseMedia(source);
981             if (media != null) {
982                 return media;
983             }
984         }
985         catch (final Exception e) {
986             LOG.error("Error parsing CSS media from '" + mediaString + "': " + e.getMessage(), e);
987         }
988         return new SACMediaListImpl();
989     }
990 
991     /**
992      * Returns the contents of the specified input source, ignoring any {@link IOException}s.
993      * @param source the input source from which to read
994      * @return the contents of the specified input source, or an empty string if an {@link IOException} occurs
995      */
996     private static String toString(final InputSource source) {
997         try {
998             final Reader reader = source.getCharacterStream();
999             if (null != reader) {
1000                 // try to reset to produce some output
1001                 if (reader instanceof StringReader) {
1002                     final StringReader sr = (StringReader) reader;
1003                     sr.reset();
1004                 }
1005                 return IOUtils.toString(reader);
1006             }
1007             final InputStream is = source.getByteStream();
1008             if (null != is) {
1009                 // try to reset to produce some output
1010                 if (is instanceof ByteArrayInputStream) {
1011                     final ByteArrayInputStream bis = (ByteArrayInputStream) is;
1012                     bis.reset();
1013                 }
1014                 return IOUtils.toString(is, ISO_8859_1);
1015             }
1016             return "";
1017         }
1018         catch (final IOException e) {
1019             return "";
1020         }
1021     }
1022 
1023     /**
1024      * Returns the owner node.
1025      * @return the owner node
1026      */
1027     @JsxGetter
1028     public HTMLElement getOwnerNode() {
1029         return ownerNode_;
1030     }
1031 
1032     /**
1033      * Returns the owner element, same as {@link #getOwnerNode()}.
1034      * @return the owner element
1035      */
1036     @JsxGetter(IE)
1037     public HTMLElement getOwningElement() {
1038         return ownerNode_;
1039     }
1040 
1041     /**
1042      * Retrieves the collection of rules defined in this style sheet.
1043      * @return the collection of rules defined in this style sheet
1044      */
1045     @JsxGetter({IE, CHROME})
1046     public com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList getRules() {
1047         return getCssRules();
1048     }
1049 
1050     /**
1051      * Returns the collection of rules defined in this style sheet.
1052      * @return the collection of rules defined in this style sheet
1053      */
1054     @JsxGetter
1055     public com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList getCssRules() {
1056         initCssRules();
1057         return cssRules_;
1058     }
1059 
1060     /**
1061      * Returns the URL of the stylesheet.
1062      * @return the URL of the stylesheet
1063      */
1064     @JsxGetter
1065     public String getHref() {
1066         final BrowserVersion version = getBrowserVersion();
1067 
1068         if (ownerNode_ != null) {
1069             final DomNode node = ownerNode_.getDomNodeOrDie();
1070             if (node instanceof HtmlLink) {
1071                 // <link rel="stylesheet" type="text/css" href="..." />
1072                 final HtmlLink link = (HtmlLink) node;
1073                 final HtmlPage page = (HtmlPage) link.getPage();
1074                 final String href = link.getHrefAttribute();
1075                 if ("".equals(href) && version.hasFeature(STYLESHEET_HREF_EMPTY_IS_NULL)) {
1076                     return null;
1077                 }
1078                 // Expand relative URLs.
1079                 try {
1080                     final URL url = page.getFullyQualifiedUrl(href);
1081                     return url.toExternalForm();
1082                 }
1083                 catch (final MalformedURLException e) {
1084                     // Log the error and fall through to the return values below.
1085                     LOG.warn(e.getMessage(), e);
1086                 }
1087             }
1088         }
1089 
1090         return null;
1091     }
1092 
1093     /**
1094      * Inserts a new rule.
1095      * @param rule the CSS rule
1096      * @param position the position at which to insert the rule
1097      * @see <a href="http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet">DOM level 2</a>
1098      * @return the position of the inserted rule
1099      */
1100     @JsxFunction
1101     public int insertRule(final String rule, final int position) {
1102         try {
1103             initCssRules();
1104             final int result = wrapped_.insertRule(rule, fixIndex(position));
1105             refreshCssRules();
1106             return result;
1107         }
1108         catch (final DOMException e) {
1109             throw Context.throwAsScriptRuntimeEx(e);
1110         }
1111     }
1112 
1113     private void refreshCssRules() {
1114         if (cssRules_ == null) {
1115             return;
1116         }
1117 
1118         cssRules_.clearRules();
1119         cssRulesIndexFix_.clear();
1120 
1121         final CSSRuleList ruleList = getWrappedSheet().getCssRules();
1122         final List<org.w3c.dom.css.CSSRule> rules = ((CSSRuleListImpl) ruleList).getRules();
1123         int pos = 0;
1124         for (Iterator<CSSRule> it = rules.iterator(); it.hasNext();) {
1125             final org.w3c.dom.css.CSSRule rule = it.next();
1126             if (rule instanceof org.w3c.dom.css.CSSCharsetRule) {
1127                 cssRulesIndexFix_.add(pos);
1128                 continue;
1129             }
1130 
1131             final com.gargoylesoftware.htmlunit.javascript.host.css.CSSRule cssRule
1132                         = com.gargoylesoftware.htmlunit.javascript.host.css.CSSRule.create(this, rule);
1133             if (null == cssRule) {
1134                 cssRulesIndexFix_.add(pos);
1135             }
1136             else {
1137                 cssRules_.addRule(cssRule);
1138             }
1139             pos++;
1140         }
1141     }
1142 
1143     private int fixIndex(int index) {
1144         for (final int fix : cssRulesIndexFix_) {
1145             if (fix > index) {
1146                 return index;
1147             }
1148             index++;
1149         }
1150         return index;
1151     }
1152 
1153     /**
1154      * Deletes an existing rule.
1155      * @param position the position of the rule to be deleted
1156      * @see <a href="http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet">DOM level 2</a>
1157      */
1158     @JsxFunction
1159     public void deleteRule(final int position) {
1160         try {
1161             initCssRules();
1162             wrapped_.deleteRule(fixIndex(position));
1163             refreshCssRules();
1164         }
1165         catch (final DOMException e) {
1166             throw Context.throwAsScriptRuntimeEx(e);
1167         }
1168     }
1169 
1170     /**
1171      * Adds a new rule.
1172      * @see <a href="http://msdn.microsoft.com/en-us/library/aa358796.aspx">MSDN</a>
1173      * @param selector the selector name
1174      * @param rule the rule
1175      * @return always return -1 as of MSDN documentation
1176      */
1177     @JsxFunction({IE, CHROME})
1178     public int addRule(final String selector, final String rule) {
1179         final String completeRule = selector + " {" + rule + "}";
1180         try {
1181             initCssRules();
1182             wrapped_.insertRule(completeRule, wrapped_.getCssRules().getLength());
1183             refreshCssRules();
1184         }
1185         catch (final DOMException e) {
1186             throw Context.throwAsScriptRuntimeEx(e);
1187         }
1188         return -1;
1189     }
1190 
1191     /**
1192      * Deletes an existing rule.
1193      * @param position the position of the rule to be deleted
1194      * @see <a href="http://msdn.microsoft.com/en-us/library/ms531195(v=VS.85).aspx">MSDN</a>
1195      */
1196     @JsxFunction({IE, CHROME})
1197     public void removeRule(final int position) {
1198         try {
1199             initCssRules();
1200             wrapped_.deleteRule(fixIndex(position));
1201             refreshCssRules();
1202         }
1203         catch (final DOMException e) {
1204             throw Context.throwAsScriptRuntimeEx(e);
1205         }
1206     }
1207 
1208     /**
1209      * Returns this stylesheet's URI (used to resolved contained @import rules).
1210      * @return this stylesheet's URI (used to resolved contained @import rules)
1211      */
1212     public String getUri() {
1213         return uri_;
1214     }
1215 
1216     /**
1217      * Returns {@code true} if this stylesheet is active, based on the media types it is associated with (if any).
1218      * @return {@code true} if this stylesheet is active, based on the media types it is associated with (if any)
1219      */
1220     public boolean isActive() {
1221         final String media;
1222         final HtmlElement e = ownerNode_.getDomNodeOrNull();
1223         if (e instanceof HtmlStyle) {
1224             final HtmlStyle style = (HtmlStyle) e;
1225             media = style.getMediaAttribute();
1226         }
1227         else if (e instanceof HtmlLink) {
1228             final HtmlLink link = (HtmlLink) e;
1229             media = link.getMediaAttribute();
1230         }
1231         else {
1232             return true;
1233         }
1234 
1235         if (StringUtils.isBlank(media)) {
1236             return true;
1237         }
1238 
1239         final WebClient webClient = getWindow().getWebWindow().getWebClient();
1240         final SACMediaList mediaList = parseMedia(webClient.getCssErrorHandler(), media);
1241         return isActive(this, new MediaListImpl(mediaList));
1242     }
1243 
1244     /**
1245      * Returns {@code true} if this stylesheet is enabled.
1246      * @return {@code true} if this stylesheet is enabled
1247      */
1248     public boolean isEnabled() {
1249         return enabled_;
1250     }
1251 
1252     /**
1253      * Sets whether this sheet is enabled or not.
1254      * @param enabled enabled or not
1255      */
1256     public void setEnabled(final boolean enabled) {
1257         enabled_ = enabled;
1258     }
1259 
1260     /**
1261      * Returns whether the specified {@link MediaList} is active or not.
1262      * @param scriptable the scriptable
1263      * @param mediaList the media list
1264      * @return whether the specified {@link MediaList} is active or not
1265      */
1266     static boolean isActive(final SimpleScriptable scriptable, final MediaList mediaList) {
1267         if (mediaList.getLength() == 0) {
1268             return true;
1269         }
1270 
1271         for (int i = 0; i < mediaList.getLength(); i++) {
1272             final MediaQuery mediaQuery = ((MediaListImpl) mediaList).mediaQuery(i);
1273             boolean isActive = isActive(scriptable, mediaQuery);
1274             if (mediaQuery.isNot()) {
1275                 isActive = !isActive;
1276             }
1277             if (isActive) {
1278                 return true;
1279             }
1280         }
1281         return false;
1282     }
1283 
1284     private static boolean isActive(final SimpleScriptable scriptable, final MediaQuery mediaQuery) {
1285         final String mediaType = mediaQuery.getMedia();
1286         if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) {
1287             for (final Property property : mediaQuery.getProperties()) {
1288                 final float val;
1289                 switch (property.getName()) {
1290                     case "max-width":
1291                         val = pixelValue((CSSValueImpl) property.getValue(), scriptable);
1292                         if (val < scriptable.getWindow().getWebWindow().getInnerWidth()) {
1293                             return false;
1294                         }
1295                         break;
1296 
1297                     case "min-width":
1298                         val = pixelValue((CSSValueImpl) property.getValue(), scriptable);
1299                         if (val > scriptable.getWindow().getWebWindow().getInnerWidth()) {
1300                             return false;
1301                         }
1302                         break;
1303 
1304                     case "max-device-width":
1305                         val = pixelValue((CSSValueImpl) property.getValue(), scriptable);
1306                         if (val < scriptable.getWindow().getScreen().getWidth()) {
1307                             return false;
1308                         }
1309                         break;
1310 
1311                     case "min-device-width":
1312                         val = pixelValue((CSSValueImpl) property.getValue(), scriptable);
1313                         if (val > scriptable.getWindow().getScreen().getWidth()) {
1314                             return false;
1315                         }
1316                         break;
1317 
1318                     case "max-height":
1319                         val = pixelValue((CSSValueImpl) property.getValue(), scriptable);
1320                         if (val < scriptable.getWindow().getWebWindow().getInnerWidth()) {
1321                             return false;
1322                         }
1323                         break;
1324 
1325                     case "min-height":
1326                         val = pixelValue((CSSValueImpl) property.getValue(), scriptable);
1327                         if (val > scriptable.getWindow().getWebWindow().getInnerWidth()) {
1328                             return false;
1329                         }
1330                         break;
1331 
1332                     case "max-device-height":
1333                         val = pixelValue((CSSValueImpl) property.getValue(), scriptable);
1334                         if (val < scriptable.getWindow().getScreen().getWidth()) {
1335                             return false;
1336                         }
1337                         break;
1338 
1339                     case "min-device-height":
1340                         val = pixelValue((CSSValueImpl) property.getValue(), scriptable);
1341                         if (val > scriptable.getWindow().getScreen().getWidth()) {
1342                             return false;
1343                         }
1344                         break;
1345 
1346                     case "resolution":
1347                         val = resolutionValue((CSSValueImpl) property.getValue());
1348                         if (Math.round(val) != scriptable.getWindow().getScreen().getDeviceXDPI()) {
1349                             return false;
1350                         }
1351                         break;
1352 
1353                     case "max-resolution":
1354                         val = resolutionValue((CSSValueImpl) property.getValue());
1355                         if (val < scriptable.getWindow().getScreen().getDeviceXDPI()) {
1356                             return false;
1357                         }
1358                         break;
1359 
1360                     case "min-resolution":
1361                         val = resolutionValue((CSSValueImpl) property.getValue());
1362                         if (val > scriptable.getWindow().getScreen().getDeviceXDPI()) {
1363                             return false;
1364                         }
1365                         break;
1366 
1367                     case "orientation":
1368                         final String orient = property.getValue().getCssText();
1369                         final WebWindow window = scriptable.getWindow().getWebWindow();
1370                         if ("portrait".equals(orient)) {
1371                             if (window.getInnerWidth() > window.getInnerHeight()) {
1372                                 return false;
1373                             }
1374                         }
1375                         else if ("landscape".equals(orient)) {
1376                             if (window.getInnerWidth() < window.getInnerHeight()) {
1377                                 return false;
1378                             }
1379                         }
1380                         else {
1381                             LOG.warn("CSSValue '" + property.getValue().getCssText()
1382                                         + "' not supported for feature 'orientation'.");
1383                             return false;
1384                         }
1385                         break;
1386 
1387                     default:
1388                 }
1389             }
1390             return true;
1391         }
1392         return false;
1393     }
1394 
1395     private static float pixelValue(final CSSValueImpl cssValue, final SimpleScriptable scriptable) {
1396         final int dpi;
1397         switch (cssValue.getPrimitiveType()) {
1398             case CSSPrimitiveValue.CSS_PX:
1399                 return cssValue.getFloatValue(CSSPrimitiveValue.CSS_PX);
1400             case CSSPrimitiveValue.CSS_EMS:
1401                 // hard coded default for the moment 16px = 1 em
1402                 return 16f * cssValue.getFloatValue(CSSPrimitiveValue.CSS_EMS);
1403             case CSSPrimitiveValue.CSS_PERCENTAGE:
1404                 // hard coded default for the moment 16px = 100%
1405                 return 0.16f * cssValue.getFloatValue(CSSPrimitiveValue.CSS_PERCENTAGE);
1406             case CSSPrimitiveValue.CSS_EXS:
1407                 // hard coded default for the moment 16px = 100%
1408                 return 0.16f * cssValue.getFloatValue(CSSPrimitiveValue.CSS_EXS);
1409             case CSSPrimitiveValue.CSS_MM:
1410                 dpi = scriptable.getWindow().getScreen().getDeviceXDPI();
1411                 return (dpi / 25.4f) * cssValue.getFloatValue(CSSPrimitiveValue.CSS_MM);
1412             case CSSPrimitiveValue.CSS_CM:
1413                 dpi = scriptable.getWindow().getScreen().getDeviceXDPI();
1414                 return (dpi / 254f) * cssValue.getFloatValue(CSSPrimitiveValue.CSS_CM);
1415             case CSSPrimitiveValue.CSS_PT:
1416                 dpi = scriptable.getWindow().getScreen().getDeviceXDPI();
1417                 return (dpi / 72f) * cssValue.getFloatValue(CSSPrimitiveValue.CSS_PT);
1418             default:
1419                 break;
1420         }
1421         LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'px', 'em', '%', 'mm', 'ex', or 'pt' value.");
1422         return -1;
1423     }
1424 
1425     private static float resolutionValue(final CSSValueImpl cssValue) {
1426         if (cssValue.getPrimitiveType() == CSSPrimitiveValue.CSS_DIMENSION) {
1427             final String text = cssValue.getCssText();
1428             if (text.endsWith("dpi")) {
1429                 return cssValue.getFloatValue(CSSPrimitiveValue.CSS_DIMENSION);
1430             }
1431             if (text.endsWith("dpcm")) {
1432                 return 2.54f * cssValue.getFloatValue(CSSPrimitiveValue.CSS_DIMENSION);
1433             }
1434             if (text.endsWith("dppx")) {
1435                 return 96 * cssValue.getFloatValue(CSSPrimitiveValue.CSS_DIMENSION);
1436             }
1437         }
1438 
1439         LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'dpi', 'dpcm', or 'dppx' value.");
1440         return -1;
1441     }
1442 
1443     /**
1444      * Validates the list of selectors.
1445      * @param selectorList the selectors
1446      * @param documentMode see {@link HTMLDocument#getDocumentMode()}
1447      * @param domNode the dom node the query should work on
1448      * @throws CSSException if a selector is invalid
1449      */
1450     public static void validateSelectors(final SelectorList selectorList, final int documentMode,
1451                 final DomNode domNode) throws CSSException {
1452         for (int i = 0; i < selectorList.getLength(); i++) {
1453             final Selector item = selectorList.item(i);
1454             if (!isValidSelector(item, documentMode, domNode)) {
1455                 throw new CSSException("Invalid selector: " + item);
1456             }
1457         }
1458     }
1459 
1460     /**
1461      * @param documentMode see {@link HTMLDocument#getDocumentMode()}
1462      */
1463     private static boolean isValidSelector(final Selector selector, final int documentMode, final DomNode domNode) {
1464         switch (selector.getSelectorType()) {
1465             case Selector.SAC_ELEMENT_NODE_SELECTOR:
1466                 return true;
1467             case Selector.SAC_CONDITIONAL_SELECTOR:
1468                 final ConditionalSelector conditional = (ConditionalSelector) selector;
1469                 final SimpleSelector simpleSel = conditional.getSimpleSelector();
1470                 return (simpleSel == null || isValidSelector(simpleSel, documentMode, domNode))
1471                         && isValidCondition(conditional.getCondition(), documentMode, domNode);
1472             case Selector.SAC_DESCENDANT_SELECTOR:
1473             case Selector.SAC_CHILD_SELECTOR:
1474                 final DescendantSelector ds = (DescendantSelector) selector;
1475                 return isValidSelector(ds.getAncestorSelector(), documentMode, domNode)
1476                         && isValidSelector(ds.getSimpleSelector(), documentMode, domNode);
1477             case Selector.SAC_DIRECT_ADJACENT_SELECTOR:
1478                 final SiblingSelector ss = (SiblingSelector) selector;
1479                 return isValidSelector(ss.getSelector(), documentMode, domNode)
1480                         && isValidSelector(ss.getSiblingSelector(), documentMode, domNode);
1481             case Selector.SAC_ANY_NODE_SELECTOR:
1482             //$FALL-THROUGH$
1483             default:
1484                 LOG.warn("Unhandled CSS selector type '" + selector.getSelectorType() + "'. Accepting it silently.");
1485                 return true; // at least in a first time to break less stuff
1486         }
1487     }
1488 
1489     /**
1490      * @param documentMode see {@link HTMLDocument#getDocumentMode()}
1491      */
1492     private static boolean isValidCondition(final Condition condition, final int documentMode, final DomNode domNode) {
1493         switch (condition.getConditionType()) {
1494             case Condition.SAC_AND_CONDITION:
1495                 final CombinatorCondition cc1 = (CombinatorCondition) condition;
1496                 return isValidCondition(cc1.getFirstCondition(), documentMode, domNode)
1497                         && isValidCondition(cc1.getSecondCondition(), documentMode, domNode);
1498             case Condition.SAC_ATTRIBUTE_CONDITION:
1499             case Condition.SAC_ID_CONDITION:
1500             case Condition.SAC_LANG_CONDITION:
1501             case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION:
1502             case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
1503             case Condition.SAC_ONLY_CHILD_CONDITION:
1504             case Condition.SAC_ONLY_TYPE_CONDITION:
1505             case Condition.SAC_CONTENT_CONDITION:
1506             case Condition.SAC_CLASS_CONDITION:
1507                 return true;
1508             case Condition.SAC_PSEUDO_CLASS_CONDITION:
1509                 final PseudoClassConditionImpl pcc = (PseudoClassConditionImpl) condition;
1510                 String value = pcc.getValue();
1511                 if (value.endsWith(")")) {
1512                     if (value.endsWith("()")) {
1513                         return false;
1514                     }
1515                     value = value.substring(0, value.indexOf('(') + 1) + ')';
1516                 }
1517                 if (documentMode < 9) {
1518                     return CSS2_PSEUDO_CLASSES.contains(value);
1519                 }
1520 
1521                 if (!CSS2_PSEUDO_CLASSES.contains(value)
1522                         && domNode.hasFeature(QUERYSELECTOR_CSS3_PSEUDO_REQUIRE_ATTACHED_NODE)
1523                         && !domNode.isAttachedToPage()
1524                         && !domNode.hasChildNodes()) {
1525                     throw new CSSException("Syntax Error");
1526                 }
1527 
1528                 if ("nth-child()".equals(value)) {
1529                     final String arg = StringUtils.substringBetween(pcc.getValue(), "(", ")").trim();
1530                     return "even".equalsIgnoreCase(arg) || "odd".equalsIgnoreCase(arg)
1531                             || NTH_NUMERIC.matcher(arg).matches()
1532                             || NTH_COMPLEX.matcher(arg).matches();
1533                 }
1534                 return CSS3_PSEUDO_CLASSES.contains(value);
1535             default:
1536                 LOG.warn("Unhandled CSS condition type '" + condition.getConditionType() + "'. Accepting it silently.");
1537                 return true;
1538         }
1539     }
1540 
1541     private void initCssRules() {
1542         if (cssRules_ == null) {
1543             cssRules_ = new com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList(this);
1544             cssRulesIndexFix_ = new ArrayList<>();
1545             refreshCssRules();
1546         }
1547     }
1548 }