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