1
2
3
4
5
6
7
8
9
10
11
12
13
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
110
111
112
113
114
115
116
117
118
119
120 public class CssStyleSheet implements Serializable {
121
122
123 public static final String NONE = "none";
124
125 public static final String AUTO = "auto";
126
127 public static final String STATIC = "static";
128
129 public static final String INHERIT = "inherit";
130
131 public static final String INITIAL = "initial";
132
133 public static final String RELATIVE = "relative";
134
135 public static final String FIXED = "fixed";
136
137 public static final String ABSOLUTE = "absolute";
138
139 public static final String REPEAT = "repeat";
140
141 public static final String BLOCK = "block";
142
143 public static final String INLINE = "inline";
144
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
154 private final CSSStyleSheetImpl wrapped_;
155
156
157 private final HtmlElement owner_;
158
159
160 private final Map<CSSImportRuleImpl, CssStyleSheet> imports_ = new HashMap<>();
161
162
163 private static final Map<String, MediaListImpl> MEDIA = new HashMap<>();
164
165
166 private final String uri_;
167
168 private boolean enabled_ = true;
169
170
171
172
173 public static final Set<String> CSS2_PSEUDO_CLASSES;
174
175 private static final Set<String> CSS3_PSEUDO_CLASSES;
176
177
178
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
195 "focus-within", "focus-visible"));
196 css4.addAll(CSS3_PSEUDO_CLASSES);
197 CSS4_PSEUDO_CLASSES = Collections.unmodifiableSet(css4);
198 }
199
200
201
202
203
204
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
220
221
222
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
241
242
243
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
253
254
255 public CSSStyleSheetImpl getWrappedSheet() {
256 return wrapped_;
257 }
258
259
260
261
262
263
264 public String getUri() {
265 return uri_;
266 }
267
268
269
270
271
272 public boolean isEnabled() {
273 return enabled_;
274 }
275
276
277
278
279
280 public void setEnabled(final boolean enabled) {
281 enabled_ = enabled;
282 }
283
284
285
286
287
288
289
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
296 final WebRequest request;
297 final WebResponse response;
298 final WebClient client = page.getWebClient();
299 if (link == null) {
300
301 final BrowserVersion browser = client.getBrowserVersion();
302 request = new WebRequest(new URL(url), browser.getCssAcceptHeader(), browser.getAcceptEncodingHeader());
303 request.setRefererHeader(page.getUrl());
304
305 request.setDefaultResponseContentCharset(UTF_8);
306
307
308
309
310 response = client.loadWebResponse(request);
311 }
312 else {
313
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
328 request.setDefaultResponseContentCharset(UTF_8);
329 }
330
331
332
333
334 response = link.getWebResponse(true, request, true, type);
335 if (response == null) {
336 return new CssStyleSheet(element, "", uri);
337 }
338 }
339
340
341
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
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
377 if (!cache.cacheIfPossible(request, response, sheet.getWrappedSheet())) {
378 response.cleanUp();
379 }
380
381 return sheet;
382 }
383 catch (final FailingHttpStatusCodeException e) {
384
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
392 if (LOG.isErrorEnabled()) {
393 LOG.error("IOException loading " + uri, e);
394 }
395 return new CssStyleSheet(element, "", uri);
396 }
397 }
398
399
400
401
402
403
404
405
406
407
408
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;
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
568
569
570
571
572
573
574
575
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
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
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
728
729
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
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
1051
1052
1053
1054
1055
1056
1057 private static CSSStyleSheetImpl parseCSS(final InputSource source, final WebClient client) {
1058 CSSStyleSheetImpl ss;
1059
1060
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
1078
1079
1080
1081
1082
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
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
1112
1113
1114
1115 private static String toString(final InputSource source) {
1116 try {
1117 final Reader reader = source.getReader();
1118 if (null != reader) {
1119
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
1135
1136
1137
1138
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
1178
1179
1180
1181
1182
1183
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;
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
1276
1277
1278
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
1293
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
1318
1319
1320
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
1488 return 16f * cssValue.getDoubleValue();
1489 case PERCENTAGE:
1490
1491 return 0.16f * cssValue.getDoubleValue();
1492 case EX:
1493
1494 return 0.16f * cssValue.getDoubleValue();
1495 case CH:
1496
1497 return 0.16f * cssValue.getDoubleValue();
1498 case VW:
1499
1500 return 0.16f * cssValue.getDoubleValue();
1501 case VH:
1502
1503 return 0.16f * cssValue.getDoubleValue();
1504 case VMIN:
1505
1506 return 0.16f * cssValue.getDoubleValue();
1507 case VMAX:
1508
1509 return 0.16f * cssValue.getDoubleValue();
1510 case DVW:
1511
1512 return 0.16f * cssValue.getDoubleValue();
1513 case DVH:
1514
1515 return 0.16f * cssValue.getDoubleValue();
1516 case DVMIN:
1517
1518 return 0.16f * cssValue.getDoubleValue();
1519 case DVMAX:
1520
1521 return 0.16f * cssValue.getDoubleValue();
1522 case LVW:
1523
1524 return 0.16f * cssValue.getDoubleValue();
1525 case LVH:
1526
1527 return 0.16f * cssValue.getDoubleValue();
1528 case LVMIN:
1529
1530 return 0.16f * cssValue.getDoubleValue();
1531 case LVMAX:
1532
1533 return 0.16f * cssValue.getDoubleValue();
1534 case SVW:
1535
1536 return 0.16f * cssValue.getDoubleValue();
1537 case SVH:
1538
1539 return 0.16f * cssValue.getDoubleValue();
1540 case SVMIN:
1541
1542 return 0.16f * cssValue.getDoubleValue();
1543 case SVMAX:
1544
1545 return 0.16f * cssValue.getDoubleValue();
1546 case REM:
1547
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
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
1603
1604
1605
1606
1607
1608
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 }