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