View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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    * https://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 org.htmlunit.css;
16  
17  import static java.nio.charset.StandardCharsets.UTF_8;
18  import static org.htmlunit.BrowserVersionFeatures.HTMLLINK_CHECK_TYPE_FOR_STYLESHEET;
19  import static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.Reader;
25  import java.io.Serializable;
26  import java.io.StringReader;
27  import java.net.URL;
28  import java.nio.charset.Charset;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.regex.Pattern;
39  
40  import org.apache.commons.io.IOUtils;
41  import org.apache.commons.lang3.math.NumberUtils;
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  import org.htmlunit.BrowserVersion;
45  import org.htmlunit.Cache;
46  import org.htmlunit.FailingHttpStatusCodeException;
47  import org.htmlunit.Page;
48  import org.htmlunit.SgmlPage;
49  import org.htmlunit.WebClient;
50  import org.htmlunit.WebClient.PooledCSS3Parser;
51  import org.htmlunit.WebRequest;
52  import org.htmlunit.WebResponse;
53  import org.htmlunit.WebWindow;
54  import org.htmlunit.cssparser.dom.AbstractCSSRuleImpl;
55  import org.htmlunit.cssparser.dom.CSSImportRuleImpl;
56  import org.htmlunit.cssparser.dom.CSSMediaRuleImpl;
57  import org.htmlunit.cssparser.dom.CSSRuleListImpl;
58  import org.htmlunit.cssparser.dom.CSSStyleDeclarationImpl;
59  import org.htmlunit.cssparser.dom.CSSStyleRuleImpl;
60  import org.htmlunit.cssparser.dom.CSSStyleSheetImpl;
61  import org.htmlunit.cssparser.dom.CSSValueImpl;
62  import org.htmlunit.cssparser.dom.CSSValueImpl.CSSPrimitiveValueType;
63  import org.htmlunit.cssparser.dom.MediaListImpl;
64  import org.htmlunit.cssparser.dom.Property;
65  import org.htmlunit.cssparser.parser.CSSErrorHandler;
66  import org.htmlunit.cssparser.parser.CSSException;
67  import org.htmlunit.cssparser.parser.CSSOMParser;
68  import org.htmlunit.cssparser.parser.InputSource;
69  import org.htmlunit.cssparser.parser.LexicalUnit;
70  import org.htmlunit.cssparser.parser.condition.AttributeCondition;
71  import org.htmlunit.cssparser.parser.condition.Condition;
72  import org.htmlunit.cssparser.parser.condition.Condition.ConditionType;
73  import org.htmlunit.cssparser.parser.condition.NotPseudoClassCondition;
74  import org.htmlunit.cssparser.parser.media.MediaQuery;
75  import org.htmlunit.cssparser.parser.selector.ChildSelector;
76  import org.htmlunit.cssparser.parser.selector.DescendantSelector;
77  import org.htmlunit.cssparser.parser.selector.DirectAdjacentSelector;
78  import org.htmlunit.cssparser.parser.selector.ElementSelector;
79  import org.htmlunit.cssparser.parser.selector.GeneralAdjacentSelector;
80  import org.htmlunit.cssparser.parser.selector.PseudoElementSelector;
81  import org.htmlunit.cssparser.parser.selector.Selector;
82  import org.htmlunit.cssparser.parser.selector.Selector.SelectorType;
83  import org.htmlunit.cssparser.parser.selector.SelectorList;
84  import org.htmlunit.cssparser.parser.selector.SimpleSelector;
85  import org.htmlunit.html.DisabledElement;
86  import org.htmlunit.html.DomElement;
87  import org.htmlunit.html.DomNode;
88  import org.htmlunit.html.DomText;
89  import org.htmlunit.html.HtmlCheckBoxInput;
90  import org.htmlunit.html.HtmlElement;
91  import org.htmlunit.html.HtmlForm;
92  import org.htmlunit.html.HtmlInput;
93  import org.htmlunit.html.HtmlLink;
94  import org.htmlunit.html.HtmlOption;
95  import org.htmlunit.html.HtmlPage;
96  import org.htmlunit.html.HtmlRadioButtonInput;
97  import org.htmlunit.html.HtmlStyle;
98  import org.htmlunit.html.HtmlTextArea;
99  import org.htmlunit.html.ValidatableElement;
100 import org.htmlunit.javascript.host.css.MediaList;
101 import org.htmlunit.javascript.host.dom.Document;
102 import org.htmlunit.util.MimeType;
103 import org.htmlunit.util.StringUtils;
104 import org.htmlunit.util.UrlUtils;
105 
106 /**
107  * A css StyleSheet.
108  *
109  * @author Marc Guillemot
110  * @author Daniel Gredler
111  * @author Ahmed Ashour
112  * @author Ronald Brill
113  * @author Guy Burton
114  * @author Frank Danek
115  * @author Carsten Steul
116  * @author Sven Strickroth
117  */
118 public class CssStyleSheet implements Serializable {
119 
120     /** "none". */
121     public static final String NONE = "none";
122     /** "auto". */
123     public static final String AUTO = "auto";
124     /** "static". */
125     public static final String STATIC = "static";
126     /** "inherit". */
127     public static final String INHERIT = "inherit";
128     /** "initial". */
129     public static final String INITIAL = "initial";
130     /** "relative". */
131     public static final String RELATIVE = "relative";
132     /** "fixed". */
133     public static final String FIXED = "fixed";
134     /** "absolute". */
135     public static final String ABSOLUTE = "absolute";
136     /** "repeat". */
137     public static final String REPEAT = "repeat";
138     /** "block". */
139     public static final String BLOCK = "block";
140     /** "inline". */
141     public static final String INLINE = "inline";
142     /** "scroll". */
143     public static final String SCROLL = "scroll";
144 
145     private static final Log LOG = LogFactory.getLog(CssStyleSheet.class);
146 
147     private static final Pattern NTH_NUMERIC = Pattern.compile("\\d+");
148     private static final Pattern NTH_COMPLEX = Pattern.compile("[+-]?\\d*n\\w*([+-]\\w\\d*)?");
149     private static final Pattern UNESCAPE_SELECTOR = Pattern.compile("\\\\([\\[\\].:])");
150 
151     /** The parsed stylesheet which this host object wraps. */
152     private final CSSStyleSheetImpl wrapped_;
153 
154     /** The HTML element which owns this stylesheet. */
155     private final HtmlElement owner_;
156 
157     /** The CSS import rules and their corresponding stylesheets. */
158     private final Map<CSSImportRuleImpl, CssStyleSheet> imports_ = new HashMap<>();
159 
160     /** cache parsed media strings */
161     private static final Map<String, MediaListImpl> MEDIA = new HashMap<>();
162 
163     /** This stylesheet's URI (used to resolved contained @import rules). */
164     private final String uri_;
165 
166     private boolean enabled_ = true;
167 
168     /**
169      * Set of CSS2 pseudo class names.
170      */
171     public static final Set<String> CSS2_PSEUDO_CLASSES;
172 
173     private static final Set<String> CSS3_PSEUDO_CLASSES;
174 
175     /**
176      * Set of CSS4 pseudo class names.
177      */
178     public static final Set<String> CSS4_PSEUDO_CLASSES;
179 
180     static {
181         CSS2_PSEUDO_CLASSES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
182                 "link", "visited", "hover", "active", "focus", "lang", "first-child")));
183 
184         final Set<String> css3 = new HashSet<>(Arrays.asList(
185                 "checked", "disabled", "enabled", "indeterminated", "root", "target", "not()",
186                 "nth-child()", "nth-last-child()", "nth-of-type()", "nth-last-of-type()",
187                 "last-child", "first-of-type", "last-of-type", "only-child", "only-of-type", "empty",
188                 "optional", "required", "valid", "invalid"));
189         css3.addAll(CSS2_PSEUDO_CLASSES);
190         CSS3_PSEUDO_CLASSES = Collections.unmodifiableSet(css3);
191 
192         final Set<String> css4 = new HashSet<>(Arrays.asList(
193                 // only what is supported at the moment
194                 "focus-within", "focus-visible"));
195         css4.addAll(CSS3_PSEUDO_CLASSES);
196         CSS4_PSEUDO_CLASSES = Collections.unmodifiableSet(css4);
197     }
198 
199     /**
200      * Creates a new stylesheet representing the CSS stylesheet for the specified input source.
201      * @param element the owning node
202      * @param source the input source which contains the CSS stylesheet which this stylesheet host object represents
203      * @param uri this stylesheet's URI (used to resolved contained @import rules)
204      */
205     public CssStyleSheet(final HtmlElement element, final InputSource source, final String uri) {
206         if (source == null) {
207             wrapped_ = new CSSStyleSheetImpl();
208         }
209         else {
210             source.setURI(uri);
211             wrapped_ = parseCSS(source, element.getPage().getWebClient());
212         }
213         uri_ = uri;
214         owner_ = element;
215     }
216 
217     /**
218      * Creates a new stylesheet representing the CSS stylesheet for the specified input source.
219      * @param element the owning node
220      * @param styleSheet the source which contains the CSS stylesheet which this stylesheet host object represents
221      * @param uri this stylesheet's URI (used to resolved contained @import rules)
222      */
223     public CssStyleSheet(final HtmlElement element, final String styleSheet, final String uri) {
224         CSSStyleSheetImpl css = null;
225         try (InputSource source = new InputSource(new StringReader(styleSheet))) {
226             source.setURI(uri);
227             css = parseCSS(source, element.getPage().getWebClient());
228         }
229         catch (final IOException e) {
230             LOG.error(e.getMessage(), e);
231         }
232 
233         wrapped_ = css;
234         uri_ = uri;
235         owner_ = element;
236     }
237 
238     /**
239      * Creates a new stylesheet representing the specified CSS stylesheet.
240      * @param element the owning node
241      * @param wrapped the CSS stylesheet which this stylesheet host object represents
242      * @param uri this stylesheet's URI (used to resolved contained @import rules)
243      */
244     public CssStyleSheet(final HtmlElement element, final CSSStyleSheetImpl wrapped, final String uri) {
245         wrapped_ = wrapped;
246         uri_ = uri;
247         owner_ = element;
248     }
249 
250     /**
251      * Returns the wrapped stylesheet.
252      * @return the wrapped stylesheet
253      */
254     public CSSStyleSheetImpl getWrappedSheet() {
255         return wrapped_;
256     }
257 
258     /**
259      * Returns this stylesheet's URI (used to resolved contained @import rules).
260      * For inline styles this is the page uri.
261      * @return this stylesheet's URI (used to resolved contained @import rules)
262      */
263     public String getUri() {
264         return uri_;
265     }
266 
267     /**
268      * Returns {@code true} if this stylesheet is enabled.
269      * @return {@code true} if this stylesheet is enabled
270      */
271     public boolean isEnabled() {
272         return enabled_;
273     }
274 
275     /**
276      * Sets whether this sheet is enabled or not.
277      * @param enabled enabled or not
278      */
279     public void setEnabled(final boolean enabled) {
280         enabled_ = enabled;
281     }
282 
283     /**
284      * Loads the stylesheet at the specified link or href.
285      * @param element the parent DOM element
286      * @param link the stylesheet's link (may be {@code null} if a <code>url</code> is specified)
287      * @param url the stylesheet's url (may be {@code null} if a <code>link</code> is specified)
288      * @return the loaded stylesheet
289      */
290     public static CssStyleSheet loadStylesheet(final HtmlElement element, final HtmlLink link, final String url) {
291         final HtmlPage page = (HtmlPage) element.getPage();
292         String uri = page.getUrl().toExternalForm();
293         try {
294             // Retrieve the associated content and respect client settings regarding failing HTTP status codes.
295             final WebRequest request;
296             final WebResponse response;
297             final WebClient client = page.getWebClient();
298             if (link == null) {
299                 // Use href.
300                 final BrowserVersion browser = client.getBrowserVersion();
301                 request = new WebRequest(new URL(url), browser.getCssAcceptHeader(), browser.getAcceptEncodingHeader());
302                 request.setRefererHeader(page.getUrl());
303                 // https://www.w3.org/TR/css-syntax-3/#input-byte-stream
304                 request.setDefaultResponseContentCharset(UTF_8);
305 
306                 // our cache is a bit strange;
307                 // loadWebResponse check the cache for the web response
308                 // AND also fixes the request url for the following cache lookups
309                 response = client.loadWebResponse(request);
310             }
311             else {
312                 // Use link.
313                 request = link.getWebRequest();
314 
315                 final String type = link.getTypeAttribute();
316                 if (client.getBrowserVersion().hasFeature(HTMLLINK_CHECK_TYPE_FOR_STYLESHEET)) {
317                     if (StringUtils.isNotBlank(type) && !MimeType.TEXT_CSS.equals(type)) {
318                         return new CssStyleSheet(element, "", uri);
319                     }
320                 }
321 
322                 if (request.getCharset() != null) {
323                     request.setDefaultResponseContentCharset(request.getCharset());
324                 }
325                 else {
326                     // https://www.w3.org/TR/css-syntax-3/#input-byte-stream
327                     request.setDefaultResponseContentCharset(UTF_8);
328                 }
329 
330                 // our cache is a bit strange;
331                 // loadWebResponse check the cache for the web response
332                 // AND also fixes the request url for the following cache lookups
333                 response = link.getWebResponse(true, request, true, type);
334                 if (response == null) {
335                     return new CssStyleSheet(element, "", uri);
336                 }
337             }
338 
339             // now we can look into the cache with the fixed request for
340             // a cached style sheet
341             final Cache cache = client.getCache();
342             final Object fromCache = cache.getCachedObject(request);
343             if (fromCache instanceof CSSStyleSheetImpl) {
344                 uri = request.getUrl().toExternalForm();
345                 return new CssStyleSheet(element, (CSSStyleSheetImpl) fromCache, uri);
346             }
347 
348             uri = response.getWebRequest().getUrl().toExternalForm();
349             client.printContentIfNecessary(response);
350             client.throwFailingHttpStatusCodeExceptionIfNecessary(response);
351             // CSS content must have downloaded OK; go ahead and build the corresponding stylesheet.
352 
353             final CssStyleSheet sheet;
354             final String contentType = response.getContentType();
355             if (StringUtils.isEmptyOrNull(contentType) || MimeType.TEXT_CSS.equals(contentType)) {
356                 try (InputStream in = response.getContentAsStreamWithBomIfApplicable()) {
357                     if (in == null) {
358                         if (LOG.isWarnEnabled()) {
359                             LOG.warn("Loading stylesheet for url '" + uri + "' returns empty responseData");
360                         }
361                         return new CssStyleSheet(element, "", uri);
362                     }
363 
364                     final Charset cssEncoding2 = response.getContentCharset();
365                     try (InputSource source = new InputSource(new InputStreamReader(in, cssEncoding2))) {
366                         source.setURI(uri);
367                         sheet = new CssStyleSheet(element, source, uri);
368                     }
369                 }
370             }
371             else {
372                 sheet = new CssStyleSheet(element, "", uri);
373             }
374 
375             // cache the style sheet
376             if (!cache.cacheIfPossible(request, response, sheet.getWrappedSheet())) {
377                 response.cleanUp();
378             }
379 
380             return sheet;
381         }
382         catch (final FailingHttpStatusCodeException e) {
383             // Got a 404 response or something like that; behave nicely.
384             if (LOG.isErrorEnabled()) {
385                 LOG.error("Exception loading " + uri, e);
386             }
387             return new CssStyleSheet(element, "", uri);
388         }
389         catch (final IOException e) {
390             // Got a basic IO error; behave nicely.
391             if (LOG.isErrorEnabled()) {
392                 LOG.error("IOException loading " + uri, e);
393             }
394             return new CssStyleSheet(element, "", uri);
395         }
396     }
397 
398     /**
399      * Returns {@code true} if the specified selector selects the specified element.
400      *
401      * @param browserVersion the browser version
402      * @param selector the selector to test
403      * @param element the element to test
404      * @param pseudoElement the pseudo element to match, (can be {@code null})
405      * @param fromQuerySelectorAll whether this is called from {@link DomNode#querySelectorAll(String)}
406      * @param throwOnSyntax throw exception if the selector syntax is incorrect
407      * @return {@code true} if it does apply, {@code false} if it doesn't apply
408      */
409     public static boolean selects(final BrowserVersion browserVersion, final Selector selector,
410             final DomElement element, final String pseudoElement, final boolean fromQuerySelectorAll,
411             final boolean throwOnSyntax) {
412         switch (selector.getSelectorType()) {
413             case ELEMENT_NODE_SELECTOR:
414                 final ElementSelector es = (ElementSelector) selector;
415 
416                 final String name;
417                 final String elementName;
418                 if (element.getPage().hasCaseSensitiveTagNames()) {
419                     name = es.getLocalName();
420                     elementName = element.getLocalName();
421                 }
422                 else {
423                     name = es.getLocalNameLowerCase();
424                     elementName = element.getLowercaseName();
425                 }
426 
427                 if (name == null || name.equals(elementName)) {
428                     final List<Condition> conditions = es.getConditions();
429                     if (conditions != null) {
430                         for (final Condition condition : conditions) {
431                             if (!selects(browserVersion, condition, element, fromQuerySelectorAll, throwOnSyntax)) {
432                                 return false;
433                             }
434                         }
435                     }
436                     return true;
437                 }
438 
439                 return false;
440 
441             case CHILD_SELECTOR:
442                 final DomNode parentNode = element.getParentNode();
443                 if (parentNode == element.getPage()) {
444                     return false;
445                 }
446                 if (!(parentNode instanceof DomElement)) {
447                     return false; // for instance parent is a DocumentFragment
448                 }
449                 final ChildSelector cs = (ChildSelector) selector;
450                 return selects(browserVersion, cs.getSimpleSelector(), element, pseudoElement,
451                             fromQuerySelectorAll, throwOnSyntax)
452                     && selects(browserVersion, cs.getAncestorSelector(), (DomElement) parentNode,
453                             pseudoElement, fromQuerySelectorAll, throwOnSyntax);
454 
455             case DESCENDANT_SELECTOR:
456                 final DescendantSelector ds = (DescendantSelector) selector;
457                 final SimpleSelector simpleSelector = ds.getSimpleSelector();
458                 if (selects(browserVersion, simpleSelector, element, pseudoElement,
459                             fromQuerySelectorAll, throwOnSyntax)) {
460                     DomNode ancestor = element;
461                     if (simpleSelector.getSelectorType() != SelectorType.PSEUDO_ELEMENT_SELECTOR) {
462                         ancestor = ancestor.getParentNode();
463                     }
464                     final Selector dsAncestorSelector = ds.getAncestorSelector();
465                     while (ancestor instanceof DomElement) {
466                         if (selects(browserVersion, dsAncestorSelector, (DomElement) ancestor, pseudoElement,
467                                 fromQuerySelectorAll, throwOnSyntax)) {
468                             return true;
469                         }
470                         ancestor = ancestor.getParentNode();
471                     }
472                 }
473                 return false;
474 
475             case DIRECT_ADJACENT_SELECTOR:
476                 final DirectAdjacentSelector das = (DirectAdjacentSelector) selector;
477                 if (selects(browserVersion, das.getSimpleSelector(), element, pseudoElement,
478                             fromQuerySelectorAll, throwOnSyntax)) {
479                     DomNode prev = element.getPreviousSibling();
480                     while (prev != null && !(prev instanceof DomElement)) {
481                         prev = prev.getPreviousSibling();
482                     }
483                     return prev != null
484                             && selects(browserVersion, das.getSelector(),
485                                     (DomElement) prev, pseudoElement, fromQuerySelectorAll, throwOnSyntax);
486                 }
487                 return false;
488 
489             case GENERAL_ADJACENT_SELECTOR:
490                 final GeneralAdjacentSelector gas = (GeneralAdjacentSelector) selector;
491                 if (selects(browserVersion, gas.getSimpleSelector(), element, pseudoElement,
492                             fromQuerySelectorAll, throwOnSyntax)) {
493                     for (DomNode prev1 = element.getPreviousSibling(); prev1 != null;
494                                                         prev1 = prev1.getPreviousSibling()) {
495                         if (prev1 instanceof DomElement
496                             && selects(browserVersion, gas.getSelector(), (DomElement) prev1,
497                                     pseudoElement, fromQuerySelectorAll, throwOnSyntax)) {
498                             return true;
499                         }
500                     }
501                 }
502                 return false;
503             case PSEUDO_ELEMENT_SELECTOR:
504                 if (pseudoElement != null && pseudoElement.length() != 0 && pseudoElement.charAt(0) == ':') {
505                     final String pseudoName = ((PseudoElementSelector) selector).getLocalName();
506                     return pseudoName.equals(pseudoElement.substring(1));
507                 }
508                 return false;
509 
510             default:
511                 if (LOG.isErrorEnabled()) {
512                     LOG.error("Unknown CSS selector type '" + selector.getSelectorType() + "'.");
513                 }
514                 return false;
515         }
516     }
517 
518     /**
519      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
520      * Returns {@code true} if the specified condition selects the specified element.
521      *
522      * @param browserVersion the browser version
523      * @param condition the condition to test
524      * @param element the element to test
525      * @param fromQuerySelectorAll whether this is called from {@link DomNode#querySelectorAll(String)}
526      * @param throwOnSyntax throw exception if the selector syntax is incorrect
527      * @return {@code true} if it does apply, {@code false} if it doesn't apply
528      */
529     static boolean selects(final BrowserVersion browserVersion,
530             final Condition condition, final DomElement element,
531             final boolean fromQuerySelectorAll, final boolean throwOnSyntax) {
532 
533         switch (condition.getConditionType()) {
534             case ID_CONDITION:
535                 return condition.getValue().equals(element.getId());
536 
537             case CLASS_CONDITION:
538                 String v3 = condition.getValue();
539                 if (v3.indexOf('\\') > -1) {
540                     v3 = UNESCAPE_SELECTOR.matcher(v3).replaceAll("$1");
541                 }
542                 final String a3 = element.getAttributeDirect("class");
543                 return selectsWhitespaceSeparated(v3, a3);
544 
545             case ATTRIBUTE_CONDITION:
546                 final AttributeCondition attributeCondition = (AttributeCondition) condition;
547                 String value = attributeCondition.getValue();
548                 if (value != null) {
549                     if (value.indexOf('\\') > -1) {
550                         value = UNESCAPE_SELECTOR.matcher(value).replaceAll("$1");
551                     }
552                     final String name = attributeCondition.getLocalName();
553                     final String attrValue = element.getAttribute(name);
554                     if (attributeCondition.isCaseInSensitive() || DomElement.TYPE_ATTRIBUTE.equals(name)) {
555                         return ATTRIBUTE_NOT_DEFINED != attrValue && attrValue.equalsIgnoreCase(value);
556                     }
557                     return ATTRIBUTE_NOT_DEFINED != attrValue && attrValue.equals(value);
558                 }
559                 return element.hasAttribute(condition.getLocalName());
560 
561             case PREFIX_ATTRIBUTE_CONDITION:
562                 final AttributeCondition prefixAttributeCondition = (AttributeCondition) condition;
563                 final String prefixValue = prefixAttributeCondition.getValue();
564                 if (prefixAttributeCondition.isCaseInSensitive()) {
565                     return !StringUtils.isEmptyString(prefixValue)
566                             && StringUtils.startsWithIgnoreCase(
567                                     element.getAttribute(prefixAttributeCondition.getLocalName()), prefixValue);
568                 }
569                 return !StringUtils.isEmptyString(prefixValue)
570                         && element.getAttribute(prefixAttributeCondition.getLocalName()).startsWith(prefixValue);
571 
572             case SUFFIX_ATTRIBUTE_CONDITION:
573                 final AttributeCondition suffixAttributeCondition = (AttributeCondition) condition;
574                 final String suffixValue = suffixAttributeCondition.getValue();
575                 if (suffixAttributeCondition.isCaseInSensitive()) {
576                     return !StringUtils.isEmptyString(suffixValue)
577                             && StringUtils.endsWithIgnoreCase(
578                                     element.getAttribute(suffixAttributeCondition.getLocalName()), suffixValue);
579                 }
580                 return !StringUtils.isEmptyString(suffixValue)
581                         && element.getAttribute(suffixAttributeCondition.getLocalName()).endsWith(suffixValue);
582 
583             case SUBSTRING_ATTRIBUTE_CONDITION:
584                 final AttributeCondition substringAttributeCondition = (AttributeCondition) condition;
585                 final String substringValue = substringAttributeCondition.getValue();
586                 if (substringAttributeCondition.isCaseInSensitive()) {
587                     return !StringUtils.isEmptyString(substringValue)
588                             && StringUtils.containsIgnoreCase(
589                                     element.getAttribute(substringAttributeCondition.getLocalName()), substringValue);
590                 }
591                 return !StringUtils.isEmptyString(substringValue)
592                         && element.getAttribute(substringAttributeCondition.getLocalName()).contains(substringValue);
593 
594             case BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
595                 final AttributeCondition beginHyphenAttributeCondition = (AttributeCondition) condition;
596                 final String v = beginHyphenAttributeCondition.getValue();
597                 final String a = element.getAttribute(beginHyphenAttributeCondition.getLocalName());
598                 if (beginHyphenAttributeCondition.isCaseInSensitive()) {
599                     return selectsHyphenSeparated(
600                             StringUtils.toRootLowerCase(v),
601                             StringUtils.toRootLowerCase(a));
602                 }
603                 return selectsHyphenSeparated(v, a);
604 
605             case ONE_OF_ATTRIBUTE_CONDITION:
606                 final AttributeCondition oneOfAttributeCondition = (AttributeCondition) condition;
607                 final String v2 = oneOfAttributeCondition.getValue();
608                 final String a2 = element.getAttribute(oneOfAttributeCondition.getLocalName());
609                 if (oneOfAttributeCondition.isCaseInSensitive()) {
610                     return selectsOneOf(
611                             StringUtils.toRootLowerCase(v2),
612                             StringUtils.toRootLowerCase(a2));
613                 }
614                 return selectsOneOf(v2, a2);
615 
616             case LANG_CONDITION:
617                 final String lcLang = condition.getValue();
618                 final int lcLangLength = lcLang.length();
619                 for (DomNode node = element; node instanceof HtmlElement; node = node.getParentNode()) {
620                     final String nodeLang = ((HtmlElement) node).getAttributeDirect("lang");
621                     if (ATTRIBUTE_NOT_DEFINED != nodeLang) {
622                         // "en", "en-GB" should be matched by "en" but not "english"
623                         return nodeLang.startsWith(lcLang)
624                             && (nodeLang.length() == lcLangLength || '-' == nodeLang.charAt(lcLangLength));
625                     }
626                 }
627                 return false;
628 
629             case NOT_PSEUDO_CLASS_CONDITION:
630                 final NotPseudoClassCondition notPseudoCondition = (NotPseudoClassCondition) condition;
631                 final SelectorList selectorList = notPseudoCondition.getSelectors();
632                 for (final Selector selector : selectorList) {
633                     if (selects(browserVersion, selector, element, null, fromQuerySelectorAll, throwOnSyntax)) {
634                         return false;
635                     }
636                 }
637                 return true;
638 
639             case PSEUDO_CLASS_CONDITION:
640                 return selectsPseudoClass(browserVersion, condition, element);
641 
642             default:
643                 if (LOG.isErrorEnabled()) {
644                     LOG.error("Unknown CSS condition type '" + condition.getConditionType() + "'.");
645                 }
646                 return false;
647         }
648     }
649 
650     private static boolean selectsOneOf(final String condition, final String attribute) {
651         // attribute.equals(condition)
652         // || attribute.startsWith(condition + " ") || attriubte.endsWith(" " + condition)
653         // || attribute.contains(" " + condition + " ");
654 
655         final int conditionLength = condition.length();
656         if (conditionLength < 1) {
657             return false;
658         }
659 
660         final int attribLength = attribute.length();
661         if (attribLength < conditionLength) {
662             return false;
663         }
664         if (attribLength > conditionLength) {
665             if (' ' == attribute.charAt(conditionLength)
666                     && attribute.startsWith(condition)) {
667                 return true;
668             }
669             if (' ' == attribute.charAt(attribLength - conditionLength - 1)
670                     && attribute.endsWith(condition)) {
671                 return true;
672             }
673             if (attribLength + 1 > conditionLength) {
674                 final StringBuilder tmp = new StringBuilder(conditionLength + 2);
675                 tmp.append(' ').append(condition).append(' ');
676                 return attribute.contains(tmp);
677             }
678             return false;
679         }
680         return attribute.equals(condition);
681     }
682 
683     private static boolean selectsHyphenSeparated(final String condition, final String attribute) {
684         final int conditionLength = condition.length();
685         if (conditionLength < 1) {
686             if (attribute != ATTRIBUTE_NOT_DEFINED) {
687                 final int attribLength = attribute.length();
688                 return attribLength == 0 || '-' == attribute.charAt(0);
689             }
690             return false;
691         }
692 
693         final int attribLength = attribute.length();
694         if (attribLength < conditionLength) {
695             return false;
696         }
697         if (attribLength > conditionLength) {
698             return '-' == attribute.charAt(conditionLength)
699                     && attribute.startsWith(condition);
700         }
701         return attribute.equals(condition);
702     }
703 
704     private static boolean selectsWhitespaceSeparated(final String condition, final String attribute) {
705         final int conditionLength = condition.length();
706         if (conditionLength < 1) {
707             return false;
708         }
709 
710         final int attribLength = attribute.length();
711         if (attribLength < conditionLength) {
712             return false;
713         }
714 
715         int pos = attribute.indexOf(condition);
716         while (pos != -1) {
717             if (pos > 0 && !Character.isWhitespace(attribute.charAt(pos - 1))) {
718                 pos = attribute.indexOf(condition, pos + 1);
719             }
720             else {
721                 final int lastPos = pos + condition.length();
722                 if (lastPos >= attribLength || Character.isWhitespace(attribute.charAt(lastPos))) {
723                     return true;
724                 }
725                 pos = attribute.indexOf(condition, pos + 1);
726             }
727         }
728 
729         return false;
730     }
731 
732     @SuppressWarnings("PMD.UselessParentheses")
733     private static boolean selectsPseudoClass(final BrowserVersion browserVersion,
734             final Condition condition, final DomElement element) {
735         final String value = condition.getValue();
736         switch (value) {
737             case "root":
738                 return element == element.getPage().getDocumentElement();
739 
740             case "enabled":
741                 return element instanceof DisabledElement && !((DisabledElement) element).isDisabled();
742 
743             case "disabled":
744                 return element instanceof DisabledElement && ((DisabledElement) element).isDisabled();
745 
746             case "focus":
747                 final HtmlPage htmlPage = element.getHtmlPageOrNull();
748                 if (htmlPage != null) {
749                     final DomElement focus = htmlPage.getFocusedElement();
750                     return element == focus;
751                 }
752                 return false;
753 
754             case "focus-within":
755                 final HtmlPage htmlPage2 = element.getHtmlPageOrNull();
756                 if (htmlPage2 != null) {
757                     final DomElement focus = htmlPage2.getFocusedElement();
758                     return element == focus || element.isAncestorOf(focus);
759                 }
760                 return false;
761 
762             case "focus-visible":
763                 final HtmlPage htmlPage3 = element.getHtmlPageOrNull();
764                 if (htmlPage3 != null) {
765                     final DomElement focus = htmlPage3.getFocusedElement();
766                     return element == focus
767                             && ((element instanceof HtmlInput && !((HtmlInput) element).isReadOnly())
768                                 || (element instanceof HtmlTextArea && !((HtmlTextArea) element).isReadOnly()));
769                 }
770                 return false;
771 
772             case "checked":
773                 return (element instanceof HtmlCheckBoxInput && ((HtmlCheckBoxInput) element).isChecked())
774                         || (element instanceof HtmlRadioButtonInput && ((HtmlRadioButtonInput) element).isChecked()
775                                 || (element instanceof HtmlOption && ((HtmlOption) element).isSelected()));
776 
777             case "required":
778                 return element instanceof HtmlElement && ((HtmlElement) element).isRequired();
779 
780             case "optional":
781                 return element instanceof HtmlElement && ((HtmlElement) element).isOptional();
782 
783             case "first-child":
784                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
785                     if (n instanceof DomElement) {
786                         return false;
787                     }
788                 }
789                 return true;
790 
791             case "last-child":
792                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
793                     if (n instanceof DomElement) {
794                         return false;
795                     }
796                 }
797                 return true;
798 
799             case "first-of-type":
800                 final String firstType = element.getNodeName();
801                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
802                     if (n instanceof DomElement && n.getNodeName().equals(firstType)) {
803                         return false;
804                     }
805                 }
806                 return true;
807 
808             case "last-of-type":
809                 final String lastType = element.getNodeName();
810                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
811                     if (n instanceof DomElement && n.getNodeName().equals(lastType)) {
812                         return false;
813                     }
814                 }
815                 return true;
816 
817             case "only-child":
818                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
819                     if (n instanceof DomElement) {
820                         return false;
821                     }
822                 }
823                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
824                     if (n instanceof DomElement) {
825                         return false;
826                     }
827                 }
828                 return true;
829 
830             case "only-of-type":
831                 final String type = element.getNodeName();
832                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
833                     if (n instanceof DomElement && n.getNodeName().equals(type)) {
834                         return false;
835                     }
836                 }
837                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
838                     if (n instanceof DomElement && n.getNodeName().equals(type)) {
839                         return false;
840                     }
841                 }
842                 return true;
843 
844             case "valid":
845                 if (element instanceof HtmlForm || element instanceof ValidatableElement) {
846                     return ((HtmlElement) element).isValid();
847                 }
848                 return false;
849 
850             case "invalid":
851                 if (element instanceof HtmlForm || element instanceof ValidatableElement) {
852                     return !((HtmlElement) element).isValid();
853                 }
854                 return false;
855 
856             case "empty":
857                 return isEmpty(element);
858 
859             case "target":
860                 final String ref = element.getPage().getUrl().getRef();
861                 return StringUtils.isNotBlank(ref) && ref.equals(element.getId());
862 
863             case "hover":
864                 return element.isMouseOver();
865 
866             case "placeholder-shown":
867                 return element instanceof HtmlInput
868                         && StringUtils.isEmptyOrNull(((HtmlInput) element).getValue())
869                         && !StringUtils.isEmptyOrNull(((HtmlInput) element).getPlaceholder());
870 
871             default:
872                 if (value.startsWith("nth-child(")) {
873                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
874                     int index = 0;
875                     for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
876                         if (n instanceof DomElement) {
877                             index++;
878                         }
879                     }
880                     return getNthElement(nth, index);
881                 }
882                 else if (value.startsWith("nth-last-child(")) {
883                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
884                     int index = 0;
885                     for (DomNode n = element; n != null; n = n.getNextSibling()) {
886                         if (n instanceof DomElement) {
887                             index++;
888                         }
889                     }
890                     return getNthElement(nth, index);
891                 }
892                 else if (value.startsWith("nth-of-type(")) {
893                     final String nthType = element.getNodeName();
894                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
895                     int index = 0;
896                     for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
897                         if (n instanceof DomElement && n.getNodeName().equals(nthType)) {
898                             index++;
899                         }
900                     }
901                     return getNthElement(nth, index);
902                 }
903                 else if (value.startsWith("nth-last-of-type(")) {
904                     final String nthLastType = element.getNodeName();
905                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
906                     int index = 0;
907                     for (DomNode n = element; n != null; n = n.getNextSibling()) {
908                         if (n instanceof DomElement && n.getNodeName().equals(nthLastType)) {
909                             index++;
910                         }
911                     }
912                     return getNthElement(nth, index);
913                 }
914                 return false;
915         }
916     }
917 
918     private static boolean isEmpty(final DomElement element) {
919         for (DomNode n = element.getFirstChild(); n != null; n = n.getNextSibling()) {
920             if (n instanceof DomElement || n instanceof DomText) {
921                 return false;
922             }
923         }
924         return true;
925     }
926 
927     private static boolean getNthElement(final String nth, final int index) {
928         if ("odd".equalsIgnoreCase(nth)) {
929             return index % 2 != 0;
930         }
931 
932         if ("even".equalsIgnoreCase(nth)) {
933             return index % 2 == 0;
934         }
935 
936         // (numerator) * n + (denominator)
937         final int nIndex = nth.indexOf('n');
938         int denominator = 0;
939         if (nIndex != -1) {
940             String value = nth.substring(0, nIndex).trim();
941             if (StringUtils.equalsChar('-', value)) {
942                 denominator = -1;
943             }
944             else {
945                 if (value.length() > 0 && value.charAt(0) == '+') {
946                     value = value.substring(1);
947                 }
948                 denominator = NumberUtils.toInt(value, 1);
949             }
950         }
951 
952         String value = nth.substring(nIndex + 1).trim();
953         if (value.length() > 0 && value.charAt(0) == '+') {
954             value = value.substring(1);
955         }
956         final int numerator = NumberUtils.toInt(value, 0);
957         if (denominator == 0) {
958             return index == numerator && numerator > 0;
959         }
960 
961         final double n = (index - numerator) / (double) denominator;
962         return n >= 0 && n % 1 == 0;
963     }
964 
965     /**
966      * Parses the CSS at the specified input source. If anything at all goes wrong, this method
967      * returns an empty stylesheet.
968      *
969      * @param source the source from which to retrieve the CSS to be parsed
970      * @param client the client
971      * @return the stylesheet parsed from the specified input source
972      */
973     private static CSSStyleSheetImpl parseCSS(final InputSource source, final WebClient client) {
974         CSSStyleSheetImpl ss;
975 
976         // use a pooled parser, if any available to avoid expensive recreation
977         try (PooledCSS3Parser pooledParser = client.getCSS3Parser()) {
978             final CSSErrorHandler errorHandler = client.getCssErrorHandler();
979             final CSSOMParser parser = new CSSOMParser(pooledParser);
980             parser.setErrorHandler(errorHandler);
981             ss = parser.parseStyleSheet(source, null);
982         }
983         catch (final Throwable ex) {
984             if (LOG.isErrorEnabled()) {
985                 LOG.error("Error parsing CSS from '" + toString(source) + "': " + ex.getMessage(), ex);
986             }
987             ss = new CSSStyleSheetImpl();
988         }
989         return ss;
990     }
991 
992     /**
993      * Parses the given media string. If anything at all goes wrong, this
994      * method returns an empty MediaList list.
995      *
996      * @param mediaString the source from which to retrieve the media to be parsed
997      * @param webClient the {@link WebClient} to be used
998      * @return the media parsed from the specified input source
999      */
1000     public static MediaListImpl parseMedia(final String mediaString, final WebClient webClient) {
1001         MediaListImpl media = MEDIA.get(mediaString);
1002         if (media != null) {
1003             return media;
1004         }
1005 
1006         // get us a pooled parser for efficiency because a new parser is expensive
1007         try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) {
1008             final CSSOMParser parser = new CSSOMParser(pooledParser);
1009             parser.setErrorHandler(webClient.getCssErrorHandler());
1010 
1011             media = new MediaListImpl(parser.parseMedia(mediaString));
1012             MEDIA.put(mediaString, media);
1013             return media;
1014         }
1015         catch (final Exception e) {
1016             if (LOG.isErrorEnabled()) {
1017                 LOG.error("Error parsing CSS media from '" + mediaString + "': " + e.getMessage(), e);
1018             }
1019         }
1020 
1021         media = new MediaListImpl(null);
1022         MEDIA.put(mediaString, media);
1023         return media;
1024     }
1025 
1026     /**
1027      * Returns the contents of the specified input source, ignoring any {@link IOException}s.
1028      * @param source the input source from which to read
1029      * @return the contents of the specified input source, or an empty string if an {@link IOException} occurs
1030      */
1031     private static String toString(final InputSource source) {
1032         try {
1033             final Reader reader = source.getReader();
1034             if (null != reader) {
1035                 // try to reset to produce some output
1036                 if (reader instanceof StringReader) {
1037                     final StringReader sr = (StringReader) reader;
1038                     sr.reset();
1039                 }
1040                 return IOUtils.toString(reader);
1041             }
1042             return "";
1043         }
1044         catch (final IOException e) {
1045             LOG.error(e.getMessage(), e);
1046             return "";
1047         }
1048     }
1049 
1050     /**
1051      * Validates the list of selectors.
1052      * @param selectorList the selectors
1053      * @param domNode the dom node the query should work on
1054      * @throws CSSException if a selector is invalid
1055      */
1056     public static void validateSelectors(final SelectorList selectorList, final DomNode domNode) throws CSSException {
1057         for (final Selector selector : selectorList) {
1058             if (!isValidSelector(selector, domNode)) {
1059                 throw new CSSException("Invalid selector: " + selector, null);
1060             }
1061         }
1062     }
1063 
1064     /**
1065      * @param documentMode see {@link Document#getDocumentMode()}
1066      */
1067     private static boolean isValidSelector(final Selector selector, final DomNode domNode) {
1068         switch (selector.getSelectorType()) {
1069             case ELEMENT_NODE_SELECTOR:
1070                 final List<Condition> conditions = ((ElementSelector) selector).getConditions();
1071                 if (conditions != null) {
1072                     for (final Condition condition : conditions) {
1073                         if (!isValidCondition(condition, domNode)) {
1074                             return false;
1075                         }
1076                     }
1077                 }
1078                 return true;
1079             case DESCENDANT_SELECTOR:
1080                 final DescendantSelector ds = (DescendantSelector) selector;
1081                 return isValidSelector(ds.getAncestorSelector(), domNode)
1082                         && isValidSelector(ds.getSimpleSelector(), domNode);
1083             case CHILD_SELECTOR:
1084                 final ChildSelector cs = (ChildSelector) selector;
1085                 return isValidSelector(cs.getAncestorSelector(), domNode)
1086                         && isValidSelector(cs.getSimpleSelector(), domNode);
1087             case DIRECT_ADJACENT_SELECTOR:
1088                 final DirectAdjacentSelector das = (DirectAdjacentSelector) selector;
1089                 return isValidSelector(das.getSelector(), domNode)
1090                         && isValidSelector(das.getSimpleSelector(), domNode);
1091             case GENERAL_ADJACENT_SELECTOR:
1092                 final GeneralAdjacentSelector gas = (GeneralAdjacentSelector) selector;
1093                 return isValidSelector(gas.getSelector(), domNode)
1094                         && isValidSelector(gas.getSimpleSelector(), domNode);
1095             default:
1096                 if (LOG.isWarnEnabled()) {
1097                     LOG.warn("Unhandled CSS selector type '"
1098                                 + selector.getSelectorType() + "'. Accepting it silently.");
1099                 }
1100                 return true; // at least in a first time to break less stuff
1101         }
1102     }
1103 
1104     /**
1105      * @param documentMode see {@link Document#getDocumentMode()}
1106      */
1107     private static boolean isValidCondition(final Condition condition, final DomNode domNode) {
1108         switch (condition.getConditionType()) {
1109             case ATTRIBUTE_CONDITION:
1110             case ID_CONDITION:
1111             case LANG_CONDITION:
1112             case ONE_OF_ATTRIBUTE_CONDITION:
1113             case BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
1114             case CLASS_CONDITION:
1115             case PREFIX_ATTRIBUTE_CONDITION:
1116             case SUBSTRING_ATTRIBUTE_CONDITION:
1117             case SUFFIX_ATTRIBUTE_CONDITION:
1118                 return true;
1119             case NOT_PSEUDO_CLASS_CONDITION:
1120                 final NotPseudoClassCondition notPseudoCondition = (NotPseudoClassCondition) condition;
1121                 final SelectorList selectorList = notPseudoCondition.getSelectors();
1122                 for (final Selector selector : selectorList) {
1123                     if (!isValidSelector(selector, domNode)) {
1124                         return false;
1125                     }
1126                 }
1127                 return true;
1128             case PSEUDO_CLASS_CONDITION:
1129                 String value = condition.getValue();
1130                 if (value.endsWith(")")) {
1131                     if (value.endsWith("()")) {
1132                         return false;
1133                     }
1134                     value = value.substring(0, value.indexOf('(') + 1) + ')';
1135                 }
1136 
1137                 if ("nth-child()".equals(value)) {
1138                     final String arg = org.apache.commons.lang3.StringUtils
1139                                         .substringBetween(condition.getValue(), "(", ")").trim();
1140                     return "even".equalsIgnoreCase(arg) || "odd".equalsIgnoreCase(arg)
1141                             || NTH_NUMERIC.matcher(arg).matches()
1142                             || NTH_COMPLEX.matcher(arg).matches();
1143                 }
1144 
1145                 if ("placeholder-shown".equals(value)) {
1146                     return true;
1147                 }
1148 
1149                 return CSS4_PSEUDO_CLASSES.contains(value);
1150             default:
1151                 if (LOG.isWarnEnabled()) {
1152                     LOG.warn("Unhandled CSS condition type '"
1153                                 + condition.getConditionType() + "'. Accepting it silently.");
1154                 }
1155                 return true;
1156         }
1157     }
1158 
1159     /**
1160      * @param importRule the {@link CSSImportRuleImpl} that imports the {@link CssStyleSheet}
1161      * @return the {@link CssStyleSheet} imported by this rule
1162      */
1163     public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
1164         CssStyleSheet sheet = imports_.get(importRule);
1165         if (sheet == null) {
1166             final String href = importRule.getHref();
1167             final String url = UrlUtils.resolveUrl(getUri(), href);
1168             sheet = loadStylesheet(owner_, null, url);
1169             imports_.put(importRule, sheet);
1170         }
1171         return sheet;
1172     }
1173 
1174     /**
1175      * Returns {@code true} if this stylesheet is active, based on the media types it is associated with (if any).
1176      * @return {@code true} if this stylesheet is active, based on the media types it is associated with (if any)
1177      */
1178     public boolean isActive() {
1179         final String media;
1180         if (owner_ instanceof HtmlStyle) {
1181             final HtmlStyle style = (HtmlStyle) owner_;
1182             media = style.getMediaAttribute();
1183         }
1184         else if (owner_ instanceof HtmlLink) {
1185             final HtmlLink link = (HtmlLink) owner_;
1186             media = link.getMediaAttribute();
1187         }
1188         else {
1189             return true;
1190         }
1191 
1192         if (StringUtils.isBlank(media)) {
1193             return true;
1194         }
1195 
1196         final WebWindow webWindow = owner_.getPage().getEnclosingWindow();
1197         final MediaListImpl mediaList = parseMedia(media, webWindow.getWebClient());
1198         return isActive(mediaList, webWindow);
1199     }
1200 
1201     /**
1202      * Returns whether the specified {@link MediaList} is active or not.
1203      * @param mediaList the media list
1204      * @param webWindow the {@link WebWindow} for some basic data
1205      * @return whether the specified {@link MediaList} is active or not
1206      */
1207     public static boolean isActive(final MediaListImpl mediaList, final WebWindow webWindow) {
1208         if (mediaList.getLength() == 0) {
1209             return true;
1210         }
1211 
1212         final int length = mediaList.getLength();
1213         for (int i = 0; i < length; i++) {
1214             final MediaQuery mediaQuery = mediaList.mediaQuery(i);
1215             boolean isActive = isActive(mediaQuery, webWindow);
1216             if (mediaQuery.isNot()) {
1217                 isActive = !isActive;
1218             }
1219             if (isActive) {
1220                 return true;
1221             }
1222         }
1223         return false;
1224     }
1225 
1226     private static boolean isActive(final MediaQuery mediaQuery, final WebWindow webWindow) {
1227         final String mediaType = mediaQuery.getMedia();
1228         if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) {
1229             for (final Property property : mediaQuery.getProperties()) {
1230                 final double val;
1231                 switch (property.getName()) {
1232                     case "max-width":
1233                         val = pixelValue(property.getValue(), webWindow);
1234                         if (val == -1 || val < webWindow.getInnerWidth()) {
1235                             return false;
1236                         }
1237                         break;
1238 
1239                     case "min-width":
1240                         val = pixelValue(property.getValue(), webWindow);
1241                         if (val == -1 || val > webWindow.getInnerWidth()) {
1242                             return false;
1243                         }
1244                         break;
1245 
1246                     case "max-device-width":
1247                         val = pixelValue(property.getValue(), webWindow);
1248                         if (val == -1 || val < webWindow.getScreen().getWidth()) {
1249                             return false;
1250                         }
1251                         break;
1252 
1253                     case "min-device-width":
1254                         val = pixelValue(property.getValue(), webWindow);
1255                         if (val == -1 || val > webWindow.getScreen().getWidth()) {
1256                             return false;
1257                         }
1258                         break;
1259 
1260                     case "max-height":
1261                         val = pixelValue(property.getValue(), webWindow);
1262                         if (val == -1 || val < webWindow.getInnerWidth()) {
1263                             return false;
1264                         }
1265                         break;
1266 
1267                     case "min-height":
1268                         val = pixelValue(property.getValue(), webWindow);
1269                         if (val == -1 || val > webWindow.getInnerWidth()) {
1270                             return false;
1271                         }
1272                         break;
1273 
1274                     case "max-device-height":
1275                         val = pixelValue(property.getValue(), webWindow);
1276                         if (val == -1 || val < webWindow.getScreen().getWidth()) {
1277                             return false;
1278                         }
1279                         break;
1280 
1281                     case "min-device-height":
1282                         val = pixelValue(property.getValue(), webWindow);
1283                         if (val == -1 || val > webWindow.getScreen().getWidth()) {
1284                             return false;
1285                         }
1286                         break;
1287 
1288                     case "resolution":
1289                         final CSSValueImpl propValue = property.getValue();
1290                         val = resolutionValue(propValue);
1291                         if (propValue == null) {
1292                             return true;
1293                         }
1294                         if (val == -1 || Math.round(val) != webWindow.getScreen().getDeviceXDPI()) {
1295                             return false;
1296                         }
1297                         break;
1298 
1299                     case "max-resolution":
1300                         val = resolutionValue(property.getValue());
1301                         if (val == -1 || val < webWindow.getScreen().getDeviceXDPI()) {
1302                             return false;
1303                         }
1304                         break;
1305 
1306                     case "min-resolution":
1307                         val = resolutionValue(property.getValue());
1308                         if (val == -1 || val > webWindow.getScreen().getDeviceXDPI()) {
1309                             return false;
1310                         }
1311                         break;
1312 
1313                     case "orientation":
1314                         final CSSValueImpl cssValue = property.getValue();
1315                         if (cssValue == null) {
1316                             LOG.warn("CSSValue is null not supported for feature 'orientation'");
1317                             return true;
1318                         }
1319 
1320                         final String orient = cssValue.getCssText();
1321                         if ("portrait".equals(orient)) {
1322                             if (webWindow.getInnerWidth() > webWindow.getInnerHeight()) {
1323                                 return false;
1324                             }
1325                         }
1326                         else if ("landscape".equals(orient)) {
1327                             if (webWindow.getInnerWidth() < webWindow.getInnerHeight()) {
1328                                 return false;
1329                             }
1330                         }
1331                         else {
1332                             if (LOG.isWarnEnabled()) {
1333                                 LOG.warn("CSSValue '" + property.getValue().getCssText()
1334                                             + "' not supported for feature 'orientation'.");
1335                             }
1336                             return false;
1337                         }
1338                         break;
1339 
1340                     default:
1341                 }
1342             }
1343             return true;
1344         }
1345         else if ("print".equalsIgnoreCase(mediaType)) {
1346             final Page page = webWindow.getEnclosedPage();
1347             if (page instanceof SgmlPage) {
1348                 return ((SgmlPage) page).isPrinting();
1349             }
1350         }
1351         return false;
1352     }
1353 
1354     @SuppressWarnings("PMD.UselessParentheses")
1355     private static double pixelValue(final CSSValueImpl cssValue, final WebWindow webWindow) {
1356         if (cssValue == null) {
1357             LOG.warn("CSSValue is null but has to be a 'px', 'em', '%', 'ex', 'ch', "
1358                     + "'vw', 'vh', 'vmin', 'vmax', 'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1359             return -1;
1360         }
1361 
1362         final LexicalUnit.LexicalUnitType luType = cssValue.getLexicalUnitType();
1363         if (luType != null) {
1364             final int dpi;
1365 
1366             switch (luType) {
1367                 case PIXEL:
1368                     return cssValue.getDoubleValue();
1369                 case EM:
1370                     // hard coded default for the moment 16px = 1 em
1371                     return 16f * cssValue.getDoubleValue();
1372                 case PERCENTAGE:
1373                     // hard coded default for the moment 16px = 100%
1374                     return 0.16f * cssValue.getDoubleValue();
1375                 case EX:
1376                     // hard coded default for the moment 16px = 100%
1377                     return 0.16f * cssValue.getDoubleValue();
1378                 case CH:
1379                     // hard coded default for the moment 16px = 100%
1380                     return 0.16f * cssValue.getDoubleValue();
1381                 case VW:
1382                     // hard coded default for the moment 16px = 100%
1383                     return 0.16f * cssValue.getDoubleValue();
1384                 case VH:
1385                     // hard coded default for the moment 16px = 100%
1386                     return 0.16f * cssValue.getDoubleValue();
1387                 case VMIN:
1388                     // hard coded default for the moment 16px = 100%
1389                     return 0.16f * cssValue.getDoubleValue();
1390                 case VMAX:
1391                     // hard coded default for the moment 16px = 100%
1392                     return 0.16f * cssValue.getDoubleValue();
1393                 case REM:
1394                     // hard coded default for the moment 16px = 100%
1395                     return 0.16f * cssValue.getDoubleValue();
1396                 case MILLIMETER:
1397                     dpi = webWindow.getScreen().getDeviceXDPI();
1398                     return (dpi / 25.4f) * cssValue.getDoubleValue();
1399                 case QUATER:
1400                     // One quarter of a millimeter. 1Q = 1/40th of 1cm.
1401                     dpi = webWindow.getScreen().getDeviceXDPI();
1402                     return ((dpi / 25.4f) * cssValue.getDoubleValue()) / 4d;
1403                 case CENTIMETER:
1404                     dpi = webWindow.getScreen().getDeviceXDPI();
1405                     return (dpi / 254f) * cssValue.getDoubleValue();
1406                 case POINT:
1407                     dpi = webWindow.getScreen().getDeviceXDPI();
1408                     return (dpi / 72f) * cssValue.getDoubleValue();
1409                 default:
1410                     break;
1411             }
1412         }
1413         if (LOG.isWarnEnabled()) {
1414             LOG.warn("CSSValue '" + cssValue.getCssText()
1415                         + "' has to be a 'px', 'em', '%', 'ex', 'ch', "
1416                         + "'vw', 'vh', 'vmin', 'vmax', 'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1417         }
1418         return -1;
1419     }
1420 
1421     private static double resolutionValue(final CSSValueImpl cssValue) {
1422         if (cssValue == null) {
1423             LOG.warn("CSSValue is null but has to be a 'dpi', 'dpcm', or 'dppx' value.");
1424             return -1;
1425         }
1426 
1427         if (cssValue.getPrimitiveType() == CSSPrimitiveValueType.CSS_DIMENSION) {
1428             final String text = cssValue.getCssText();
1429             if (text.endsWith("dpi")) {
1430                 return cssValue.getDoubleValue();
1431             }
1432             if (text.endsWith("dpcm")) {
1433                 return 2.54f * cssValue.getDoubleValue();
1434             }
1435             if (text.endsWith("dppx")) {
1436                 return 96 * cssValue.getDoubleValue();
1437             }
1438         }
1439 
1440         if (LOG.isWarnEnabled()) {
1441             LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'dpi', 'dpcm', or 'dppx' value.");
1442         }
1443         return -1;
1444     }
1445 
1446     /**
1447      * Modifies the specified style object by adding any style rules which apply to the specified
1448      * element.
1449      *
1450      * @param style the style to modify
1451      * @param element the element to which style rules must apply in order for them to be added to
1452      *        the specified style
1453      * @param pseudoElement a string specifying the pseudo-element to match (may be {@code null})
1454      */
1455     public void modifyIfNecessary(final ComputedCssStyleDeclaration style, final DomElement element,
1456             final String pseudoElement) {
1457 
1458         final BrowserVersion browser = element.getPage().getWebClient().getBrowserVersion();
1459         final List<CSSStyleSheetImpl.SelectorEntry> matchingRules =
1460                 selects(getRuleIndex(), browser, element, pseudoElement, false);
1461         for (final CSSStyleSheetImpl.SelectorEntry entry : matchingRules) {
1462             final CSSStyleDeclarationImpl dec = entry.getRule().getStyle();
1463             style.applyStyleFromSelector(dec, entry.getSelector());
1464         }
1465     }
1466 
1467     private CSSStyleSheetImpl.CSSStyleSheetRuleIndex getRuleIndex() {
1468         final CSSStyleSheetImpl styleSheet = getWrappedSheet();
1469         CSSStyleSheetImpl.CSSStyleSheetRuleIndex index = styleSheet.getRuleIndex();
1470 
1471         if (index == null) {
1472             index = new CSSStyleSheetImpl.CSSStyleSheetRuleIndex();
1473             final CSSRuleListImpl ruleList = styleSheet.getCssRules();
1474             index(index, ruleList, new HashSet<>());
1475 
1476             styleSheet.setRuleIndex(index);
1477         }
1478         return index;
1479     }
1480 
1481     private void index(final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index, final CSSRuleListImpl ruleList,
1482             final Set<String> alreadyProcessing) {
1483 
1484         for (final AbstractCSSRuleImpl rule : ruleList.getRules()) {
1485             if (rule instanceof CSSStyleRuleImpl) {
1486                 final CSSStyleRuleImpl styleRule = (CSSStyleRuleImpl) rule;
1487                 final SelectorList selectors = styleRule.getSelectors();
1488                 for (final Selector selector : selectors) {
1489                     final SimpleSelector simpleSel = selector.getSimpleSelector();
1490                     if (SelectorType.ELEMENT_NODE_SELECTOR == simpleSel.getSelectorType()) {
1491                         final ElementSelector es = (ElementSelector) simpleSel;
1492                         boolean wasClass = false;
1493                         final List<Condition> conds = es.getConditions();
1494                         if (conds != null && conds.size() == 1) {
1495                             final Condition c = conds.get(0);
1496                             if (ConditionType.CLASS_CONDITION == c.getConditionType()) {
1497                                 index.addClassSelector(es, c.getValue(), selector, styleRule);
1498                                 wasClass = true;
1499                             }
1500                         }
1501                         if (!wasClass) {
1502                             index.addElementSelector(es, selector, styleRule);
1503                         }
1504                     }
1505                     else {
1506                         index.addOtherSelector(selector, styleRule);
1507                     }
1508                 }
1509             }
1510             else if (rule instanceof CSSImportRuleImpl) {
1511                 final CSSImportRuleImpl importRule = (CSSImportRuleImpl) rule;
1512 
1513                 final CssStyleSheet sheet = getImportedStyleSheet(importRule);
1514 
1515                 if (!alreadyProcessing.contains(sheet.getUri())) {
1516                     final CSSRuleListImpl sheetRuleList = sheet.getWrappedSheet().getCssRules();
1517                     alreadyProcessing.add(sheet.getUri());
1518 
1519                     final MediaListImpl mediaList = importRule.getMedia();
1520                     if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1521                         index(index, sheetRuleList, alreadyProcessing);
1522                     }
1523                     else {
1524                         index(index.addMedia(mediaList), sheetRuleList, alreadyProcessing);
1525                     }
1526                 }
1527             }
1528             else if (rule instanceof CSSMediaRuleImpl) {
1529                 final CSSMediaRuleImpl mediaRule = (CSSMediaRuleImpl) rule;
1530                 final MediaListImpl mediaList = mediaRule.getMediaList();
1531                 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1532                     index(index, mediaRule.getCssRules(), alreadyProcessing);
1533                 }
1534                 else {
1535                     index(index.addMedia(mediaList), mediaRule.getCssRules(), alreadyProcessing);
1536                 }
1537             }
1538         }
1539     }
1540 
1541     private List<CSSStyleSheetImpl.SelectorEntry> selects(
1542                             final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index,
1543                             final BrowserVersion browserVersion, final DomElement element,
1544                             final String pseudoElement, final boolean fromQuerySelectorAll) {
1545 
1546         final List<CSSStyleSheetImpl.SelectorEntry> matchingRules = new ArrayList<>();
1547 
1548         if (isActive(index.getMediaList(), element.getPage().getEnclosingWindow())) {
1549             final String elementName = element.getLowercaseName();
1550             final String[] classes = StringUtils.splitAtJavaWhitespace(
1551                                                             element.getAttributeDirect("class"));
1552             final Iterator<CSSStyleSheetImpl.SelectorEntry> iter =
1553                     index.getSelectorEntriesIteratorFor(elementName, classes);
1554 
1555             CSSStyleSheetImpl.SelectorEntry entry = iter.next();
1556             while (null != entry) {
1557                 if (selects(browserVersion, entry.getSelector(),
1558                             element, pseudoElement, fromQuerySelectorAll, false)) {
1559                     matchingRules.add(entry);
1560                 }
1561                 entry = iter.next();
1562             }
1563 
1564             for (final CSSStyleSheetImpl.CSSStyleSheetRuleIndex child : index.getChildren()) {
1565                 matchingRules.addAll(selects(child, browserVersion,
1566                                                     element, pseudoElement, fromQuerySelectorAll));
1567             }
1568         }
1569 
1570         return matchingRules;
1571     }
1572 }