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