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