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 = Collections.synchronizedMap(new HashMap<>(100));
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", "indeterminate", "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 (nextSibling != null && 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 matchesWhitespaceSeparated(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 matchesHyphenSeparated(
648 StringUtils.toRootLowerCase(v),
649 StringUtils.toRootLowerCase(a));
650 }
651 return matchesHyphenSeparated(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 matchesWhitespaceSeparated(
659 StringUtils.toRootLowerCase(v2),
660 StringUtils.toRootLowerCase(a2));
661 }
662 return matchesWhitespaceSeparated(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 matchesHyphenSeparated(final String condition, final String attribute) {
727 final int conditionLength = condition.length();
728 if (conditionLength < 1) {
729 if (attribute != ATTRIBUTE_NOT_DEFINED) {
730 final int attribLength = attribute.length();
731 return attribLength == 0 || '-' == attribute.charAt(0);
732 }
733 return false;
734 }
735
736 final int attribLength = attribute.length();
737 if (attribLength < conditionLength) {
738 return false;
739 }
740 if (attribLength > conditionLength) {
741 return '-' == attribute.charAt(conditionLength)
742 && attribute.startsWith(condition);
743 }
744 return attribute.equals(condition);
745 }
746
747 private static boolean matchesWhitespaceSeparated(final String condition, final String attribute) {
748 final int conditionLength = condition.length();
749 if (conditionLength < 1) {
750 return false;
751 }
752
753 final int attribLength = attribute.length();
754 if (attribLength < conditionLength) {
755 return false;
756 }
757
758 int pos = attribute.indexOf(condition);
759 while (pos != -1) {
760 if (pos > 0 && !Character.isWhitespace(attribute.charAt(pos - 1))) {
761 pos = attribute.indexOf(condition, pos + 1);
762 }
763 else {
764 final int lastPos = pos + condition.length();
765 if (lastPos >= attribLength || Character.isWhitespace(attribute.charAt(lastPos))) {
766 return true;
767 }
768 pos = attribute.indexOf(condition, pos + 1);
769 }
770 }
771
772 return false;
773 }
774
775 @SuppressWarnings("PMD.UselessParentheses")
776 private static boolean selectsPseudoClass(final BrowserVersion browserVersion,
777 final Condition condition, final DomElement element) {
778 final String value = condition.getValue();
779 switch (value) {
780 case "root":
781 return element == element.getPage().getDocumentElement();
782
783 case "enabled":
784 return element instanceof DisabledElement de && !de.isDisabled();
785
786 case "disabled":
787 return element instanceof DisabledElement de && de.isDisabled();
788
789 case "focus":
790 final HtmlPage htmlPage = element.getHtmlPageOrNull();
791 if (htmlPage != null) {
792 final DomElement focus = htmlPage.getFocusedElement();
793 return element == focus;
794 }
795 return false;
796
797 case "focus-within":
798 final HtmlPage htmlPage2 = element.getHtmlPageOrNull();
799 if (htmlPage2 != null) {
800 final DomElement focus = htmlPage2.getFocusedElement();
801 return element == focus || element.isAncestorOf(focus);
802 }
803 return false;
804
805 case "focus-visible":
806 final HtmlPage htmlPage3 = element.getHtmlPageOrNull();
807 if (htmlPage3 != null) {
808 final DomElement focus = htmlPage3.getFocusedElement();
809 return element == focus
810 && ((element instanceof HtmlInput hi && !hi.isReadOnly())
811 || (element instanceof HtmlTextArea hta && !hta.isReadOnly()));
812 }
813 return false;
814
815 case "checked":
816 return (element instanceof HtmlCheckBoxInput hcbi && hcbi.isChecked())
817 || (element instanceof HtmlRadioButtonInput hrbi && hrbi.isChecked())
818 || (element instanceof HtmlOption ho && ho.isSelected());
819
820 case "required":
821 return element instanceof HtmlElement he && he.isRequired();
822
823 case "optional":
824 return element instanceof HtmlElement he && he.isOptional();
825
826 case "first-child":
827 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
828 if (n instanceof DomElement) {
829 return false;
830 }
831 }
832 return true;
833
834 case "last-child":
835 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
836 if (n instanceof DomElement) {
837 return false;
838 }
839 }
840 return true;
841
842 case "first-of-type":
843 final String firstType = element.getNodeName();
844 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
845 if (n instanceof DomElement && n.getNodeName().equals(firstType)) {
846 return false;
847 }
848 }
849 return true;
850
851 case "last-of-type":
852 final String lastType = element.getNodeName();
853 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
854 if (n instanceof DomElement && n.getNodeName().equals(lastType)) {
855 return false;
856 }
857 }
858 return true;
859
860 case "only-child":
861 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
862 if (n instanceof DomElement) {
863 return false;
864 }
865 }
866 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
867 if (n instanceof DomElement) {
868 return false;
869 }
870 }
871 return true;
872
873 case "only-of-type":
874 final String type = element.getNodeName();
875 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
876 if (n instanceof DomElement && n.getNodeName().equals(type)) {
877 return false;
878 }
879 }
880 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
881 if (n instanceof DomElement && n.getNodeName().equals(type)) {
882 return false;
883 }
884 }
885 return true;
886
887 case "valid":
888 if (element instanceof ValidatableHtmlElement validatable) {
889 return validatable.willValidate()
890 && ((HtmlElement) validatable).isValid();
891 }
892 else if (element instanceof HtmlForm form) {
893 return form.isValid();
894 }
895 return false;
896
897 case "invalid":
898 if (element instanceof ValidatableHtmlElement validatable) {
899 return validatable.willValidate()
900 && !((HtmlElement) validatable).isValid();
901 }
902 else if (element instanceof HtmlForm form) {
903 return !form.isValid();
904 }
905 return false;
906
907 case "empty":
908 return isEmpty(element);
909
910 case "target":
911 final String ref = element.getPage().getUrl().getRef();
912 return StringUtils.isNotBlank(ref) && ref.equals(element.getId());
913
914 case "hover":
915 return element.isMouseOver();
916
917 case "placeholder-shown":
918 return element instanceof HtmlInput hi
919 && StringUtils.isEmptyOrNull(hi.getValue())
920 && !StringUtils.isEmptyOrNull(hi.getPlaceholder());
921
922 default:
923 if (value.startsWith("nth-child(")) {
924 final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
925 int index = 0;
926 for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
927 if (n instanceof DomElement) {
928 index++;
929 }
930 }
931 return getNthElement(nth, index);
932 }
933 else if (value.startsWith("nth-last-child(")) {
934 final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
935 int index = 0;
936 for (DomNode n = element; n != null; n = n.getNextSibling()) {
937 if (n instanceof DomElement) {
938 index++;
939 }
940 }
941 return getNthElement(nth, index);
942 }
943 else if (value.startsWith("nth-of-type(")) {
944 final String nthType = element.getNodeName();
945 final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
946 int index = 0;
947 for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
948 if (n instanceof DomElement && n.getNodeName().equals(nthType)) {
949 index++;
950 }
951 }
952 return getNthElement(nth, index);
953 }
954 else if (value.startsWith("nth-last-of-type(")) {
955 final String nthLastType = element.getNodeName();
956 final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
957 int index = 0;
958 for (DomNode n = element; n != null; n = n.getNextSibling()) {
959 if (n instanceof DomElement && n.getNodeName().equals(nthLastType)) {
960 index++;
961 }
962 }
963 return getNthElement(nth, index);
964 }
965 return false;
966 }
967 }
968
969 private static boolean isEmpty(final DomElement element) {
970 for (DomNode n = element.getFirstChild(); n != null; n = n.getNextSibling()) {
971 if (n instanceof DomElement || n instanceof DomText) {
972 return false;
973 }
974 }
975 return true;
976 }
977
978 private static boolean getNthElement(final String nth, final int index) {
979 if ("odd".equalsIgnoreCase(nth)) {
980 return index % 2 != 0;
981 }
982
983 if ("even".equalsIgnoreCase(nth)) {
984 return index % 2 == 0;
985 }
986
987
988 final int nIndex = nth.indexOf('n');
989 int denominator = 0;
990 if (nIndex != -1) {
991 String value = nth.substring(0, nIndex).trim();
992 if (StringUtils.equalsChar('-', value)) {
993 denominator = -1;
994 }
995 else {
996 if (value.length() > 0 && value.charAt(0) == '+') {
997 value = value.substring(1);
998 }
999 denominator = StringUtils.toInt(value, 1);
1000 }
1001 }
1002
1003 String value = nth.substring(nIndex + 1).trim();
1004 if (value.length() > 0 && value.charAt(0) == '+') {
1005 value = value.substring(1);
1006 }
1007 final int numerator = StringUtils.toInt(value, 0);
1008 if (denominator == 0) {
1009 return index == numerator && numerator > 0;
1010 }
1011
1012 final int diff = index - numerator;
1013 if (denominator > 0) {
1014 return diff >= 0 && diff % denominator == 0;
1015 }
1016
1017 return diff <= 0 && diff % denominator == 0;
1018 }
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028 private static CSSStyleSheetImpl parseCSS(final InputSource source, final WebClient client) {
1029 CSSStyleSheetImpl ss;
1030
1031
1032 try (PooledCSS3Parser pooledParser = client.getCSS3Parser()) {
1033 final CSSErrorHandler errorHandler = client.getCssErrorHandler();
1034 final CSSOMParser parser = new CSSOMParser(pooledParser);
1035 parser.setErrorHandler(errorHandler);
1036 ss = parser.parseStyleSheet(source, null);
1037 }
1038 catch (final Throwable ex) {
1039 if (LOG.isErrorEnabled()) {
1040 LOG.error("Error parsing CSS from '" + toString(source) + "': " + ex.getMessage(), ex);
1041 }
1042 ss = new CSSStyleSheetImpl();
1043 }
1044 return ss;
1045 }
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055 public static MediaListImpl parseMedia(final String mediaString, final WebClient webClient) {
1056 return MEDIA.computeIfAbsent(mediaString, s -> {
1057
1058 try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) {
1059 final CSSOMParser parser = new CSSOMParser(pooledParser);
1060 parser.setErrorHandler(webClient.getCssErrorHandler());
1061
1062 return new MediaListImpl(parser.parseMedia(mediaString));
1063 }
1064 catch (final Exception e) {
1065 if (LOG.isErrorEnabled()) {
1066 LOG.error("Error parsing CSS media from '" + mediaString + "': " + e.getMessage(), e);
1067 }
1068 }
1069
1070 return new MediaListImpl(null);
1071 });
1072 }
1073
1074
1075
1076
1077
1078
1079 private static String toString(final InputSource source) {
1080 try {
1081 final Reader reader = source.getReader();
1082 if (null != reader) {
1083
1084 if (reader instanceof StringReader sr) {
1085 sr.reset();
1086 }
1087 return IOUtils.toString(reader);
1088 }
1089 return "";
1090 }
1091 catch (final IOException e) {
1092 LOG.error(e.getMessage(), e);
1093 return "";
1094 }
1095 }
1096
1097
1098
1099
1100
1101
1102
1103
1104 public static void validateSelectors(final SelectorList selectorList, final DomNode domNode) throws CSSException {
1105 for (final Selector selector : selectorList) {
1106 if (!isValidSelector(selector, domNode)) {
1107 throw new CSSException("Invalid selector: " + selector, null);
1108 }
1109 }
1110 }
1111
1112 private static boolean isValidSelector(final Selector selector, final DomNode domNode) {
1113 switch (selector.getSelectorType()) {
1114 case ELEMENT_NODE_SELECTOR:
1115 final List<Condition> conditions = ((ElementSelector) selector).getConditions();
1116 if (conditions != null) {
1117 for (final Condition condition : conditions) {
1118 if (!isValidCondition(condition, domNode)) {
1119 return false;
1120 }
1121 }
1122 }
1123 return true;
1124 case DESCENDANT_SELECTOR:
1125 final DescendantSelector ds = (DescendantSelector) selector;
1126 return isValidSelector(ds.getAncestorSelector(), domNode)
1127 && isValidSelector(ds.getSimpleSelector(), domNode);
1128 case CHILD_SELECTOR:
1129 final ChildSelector cs = (ChildSelector) selector;
1130 return isValidSelector(cs.getAncestorSelector(), domNode)
1131 && isValidSelector(cs.getSimpleSelector(), domNode);
1132 case DIRECT_ADJACENT_SELECTOR:
1133 final DirectAdjacentSelector das = (DirectAdjacentSelector) selector;
1134 return isValidSelector(das.getSelector(), domNode)
1135 && isValidSelector(das.getSimpleSelector(), domNode);
1136 case GENERAL_ADJACENT_SELECTOR:
1137 final GeneralAdjacentSelector gas = (GeneralAdjacentSelector) selector;
1138 return isValidSelector(gas.getSelector(), domNode)
1139 && isValidSelector(gas.getSimpleSelector(), domNode);
1140 case PSEUDO_ELEMENT_SELECTOR:
1141
1142
1143
1144
1145
1146
1147
1148
1149 return true;
1150 case RELATIVE_SELECTOR:
1151 final RelativeSelector rs = (RelativeSelector) selector;
1152 return isValidSelector(rs.getSelector(), domNode);
1153 default:
1154 if (LOG.isWarnEnabled()) {
1155 LOG.warn("Unhandled CSS selector type '"
1156 + selector.getSelectorType() + "'. Accepting it silently.");
1157 }
1158 return true;
1159 }
1160 }
1161
1162 private static boolean isValidCondition(final Condition condition, final DomNode domNode) {
1163 switch (condition.getConditionType()) {
1164 case ATTRIBUTE_CONDITION:
1165 case ID_CONDITION:
1166 case LANG_CONDITION:
1167 case ONE_OF_ATTRIBUTE_CONDITION:
1168 case BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
1169 case CLASS_CONDITION:
1170 case PREFIX_ATTRIBUTE_CONDITION:
1171 case SUBSTRING_ATTRIBUTE_CONDITION:
1172 case SUFFIX_ATTRIBUTE_CONDITION:
1173 return true;
1174 case NOT_PSEUDO_CLASS_CONDITION:
1175 final NotPseudoClassCondition notPseudoCondition = (NotPseudoClassCondition) condition;
1176 final SelectorList notSelectorList = notPseudoCondition.getSelectors();
1177 for (final Selector selector : notSelectorList) {
1178 if (!isValidSelector(selector, domNode)) {
1179 return false;
1180 }
1181 }
1182 return true;
1183 case IS_PSEUDO_CLASS_CONDITION:
1184 final IsPseudoClassCondition conditionIsPseudo = (IsPseudoClassCondition) condition;
1185 for (final Selector selector : conditionIsPseudo.getSelectors()) {
1186 if (!isValidSelector(selector, domNode)) {
1187 return false;
1188 }
1189 }
1190 return true;
1191 case WHERE_PSEUDO_CLASS_CONDITION:
1192 final WherePseudoClassCondition conditionWherePseudo = (WherePseudoClassCondition) condition;
1193 for (final Selector selector : conditionWherePseudo.getSelectors()) {
1194 if (!isValidSelector(selector, domNode)) {
1195 return false;
1196 }
1197 }
1198 return true;
1199 case HAS_PSEUDO_CLASS_CONDITION:
1200 final HasPseudoClassCondition conditionHasPseudo = (HasPseudoClassCondition) condition;
1201 for (final Selector selector : conditionHasPseudo.getSelectors()) {
1202 if (!isValidSelector(selector, domNode)) {
1203 return false;
1204 }
1205 }
1206 return true;
1207 case PSEUDO_CLASS_CONDITION:
1208 String value = condition.getValue();
1209 if (value.endsWith(")")) {
1210 if (value.endsWith("()")) {
1211 return false;
1212 }
1213 value = value.substring(0, value.indexOf('(') + 1) + ')';
1214 }
1215
1216 if ("nth-child()".equals(value)
1217 || "nth-last-child()".equals(value)
1218 || "nth-of-type()".equals(value)
1219 || "nth-last-of-type()".equals(value)) {
1220 final String arg = org.apache.commons.lang3.StringUtils
1221 .substringBetween(condition.getValue(), "(", ")").trim();
1222 return "even".equalsIgnoreCase(arg)
1223 || "odd".equalsIgnoreCase(arg)
1224 || NTH_NUMERIC.matcher(arg).matches()
1225 || NTH_COMPLEX.matcher(arg).matches();
1226 }
1227
1228 if ("placeholder-shown".equals(value)) {
1229 return true;
1230 }
1231
1232 return CSS4_PSEUDO_CLASSES.contains(value);
1233 default:
1234 if (LOG.isWarnEnabled()) {
1235 LOG.warn("Unhandled CSS condition type '"
1236 + condition.getConditionType() + "'. Accepting it silently.");
1237 }
1238 return true;
1239 }
1240 }
1241
1242
1243
1244
1245
1246
1247
1248 public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
1249 CssStyleSheet sheet = imports_.get(importRule);
1250 if (sheet == null) {
1251 final String href = importRule.getHref();
1252 final String url = UrlUtils.resolveUrl(getUri(), href);
1253 sheet = loadStylesheet(owner_, null, url);
1254 imports_.put(importRule, sheet);
1255 }
1256 return sheet;
1257 }
1258
1259
1260
1261
1262
1263 public boolean isActive() {
1264 final String media;
1265 if (owner_ instanceof HtmlStyle style) {
1266 media = style.getMediaAttribute();
1267 }
1268 else if (owner_ instanceof HtmlLink link) {
1269 media = link.getMediaAttribute();
1270 }
1271 else {
1272 return true;
1273 }
1274
1275 if (StringUtils.isBlank(media)) {
1276 return true;
1277 }
1278
1279 final WebWindow webWindow = owner_.getPage().getEnclosingWindow();
1280 final MediaListImpl mediaList = parseMedia(media, webWindow.getWebClient());
1281 return isActive(mediaList, webWindow);
1282 }
1283
1284
1285
1286
1287
1288
1289
1290 public static boolean isActive(final MediaListImpl mediaList, final WebWindow webWindow) {
1291 if (mediaList.getLength() == 0) {
1292 return true;
1293 }
1294
1295 final int length = mediaList.getLength();
1296 for (int i = 0; i < length; i++) {
1297 final MediaQuery mediaQuery = mediaList.mediaQuery(i);
1298 boolean isActive = isActive(mediaQuery, webWindow);
1299 if (mediaQuery.isNot()) {
1300 isActive = !isActive;
1301 }
1302 if (isActive) {
1303 return true;
1304 }
1305 }
1306 return false;
1307 }
1308
1309 private static boolean isActive(final MediaQuery mediaQuery, final WebWindow webWindow) {
1310 final String mediaType = mediaQuery.getMedia();
1311 if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) {
1312 for (final Property property : mediaQuery.getProperties()) {
1313 final double val;
1314 switch (property.getName()) {
1315 case "max-width":
1316 val = pixelValue(property.getValue(), webWindow);
1317 if (val == -1 || val < webWindow.getInnerWidth()) {
1318 return false;
1319 }
1320 break;
1321
1322 case "min-width":
1323 val = pixelValue(property.getValue(), webWindow);
1324 if (val == -1 || val > webWindow.getInnerWidth()) {
1325 return false;
1326 }
1327 break;
1328
1329 case "max-device-width":
1330 val = pixelValue(property.getValue(), webWindow);
1331 if (val == -1 || val < webWindow.getScreen().getWidth()) {
1332 return false;
1333 }
1334 break;
1335
1336 case "min-device-width":
1337 val = pixelValue(property.getValue(), webWindow);
1338 if (val == -1 || val > webWindow.getScreen().getWidth()) {
1339 return false;
1340 }
1341 break;
1342
1343 case "max-height":
1344 val = pixelValue(property.getValue(), webWindow);
1345 if (val == -1 || val < webWindow.getInnerHeight()) {
1346 return false;
1347 }
1348 break;
1349
1350 case "min-height":
1351 val = pixelValue(property.getValue(), webWindow);
1352 if (val == -1 || val > webWindow.getInnerHeight()) {
1353 return false;
1354 }
1355 break;
1356
1357 case "max-device-height":
1358 val = pixelValue(property.getValue(), webWindow);
1359 if (val == -1 || val < webWindow.getScreen().getHeight()) {
1360 return false;
1361 }
1362 break;
1363
1364 case "min-device-height":
1365 val = pixelValue(property.getValue(), webWindow);
1366 if (val == -1 || val > webWindow.getScreen().getHeight()) {
1367 return false;
1368 }
1369 break;
1370
1371 case "resolution":
1372 final CSSValueImpl propValue = property.getValue();
1373 val = resolutionValue(propValue);
1374 if (propValue == null) {
1375 return true;
1376 }
1377 if (val == -1 || Math.round(val) != webWindow.getScreen().getDeviceXDPI()) {
1378 return false;
1379 }
1380 break;
1381
1382 case "max-resolution":
1383 val = resolutionValue(property.getValue());
1384 if (val == -1 || val < webWindow.getScreen().getDeviceXDPI()) {
1385 return false;
1386 }
1387 break;
1388
1389 case "min-resolution":
1390 val = resolutionValue(property.getValue());
1391 if (val == -1 || val > webWindow.getScreen().getDeviceXDPI()) {
1392 return false;
1393 }
1394 break;
1395
1396 case "orientation":
1397 final CSSValueImpl cssValue = property.getValue();
1398 if (cssValue == null) {
1399 LOG.warn("CSSValue is null not supported for feature 'orientation'");
1400 return true;
1401 }
1402
1403 final String orient = cssValue.getCssText();
1404 if ("portrait".equals(orient)) {
1405 if (webWindow.getInnerWidth() > webWindow.getInnerHeight()) {
1406 return false;
1407 }
1408 }
1409 else if ("landscape".equals(orient)) {
1410 if (webWindow.getInnerWidth() < webWindow.getInnerHeight()) {
1411 return false;
1412 }
1413 }
1414 else {
1415 if (LOG.isWarnEnabled()) {
1416 LOG.warn("CSSValue '" + property.getValue().getCssText()
1417 + "' not supported for feature 'orientation'.");
1418 }
1419 return false;
1420 }
1421 break;
1422
1423 default:
1424 }
1425 }
1426 return true;
1427 }
1428 else if ("print".equalsIgnoreCase(mediaType)) {
1429 final Page page = webWindow.getEnclosedPage();
1430 if (page instanceof SgmlPage sgmlPage) {
1431 return sgmlPage.isPrinting();
1432 }
1433 }
1434 return false;
1435 }
1436
1437 @SuppressWarnings("PMD.UselessParentheses")
1438 private static double pixelValue(final CSSValueImpl cssValue, final WebWindow webWindow) {
1439 if (cssValue == null) {
1440 LOG.warn("CSSValue is null but has to be a 'px', 'em', '%', 'ex', 'ch', "
1441 + "'vw', 'vh', 'vmin', 'vmax', 'dvw', 'dvh', 'dvmin', 'dvmax', "
1442 + "'lvw', 'lvh', 'lvmin', 'lvmax', 'svw', 'svh', 'svmin', 'svmax', "
1443 + "'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1444 return -1;
1445 }
1446
1447 final LexicalUnit.LexicalUnitType luType = cssValue.getLexicalUnitType();
1448 if (luType != null) {
1449 final int dpi;
1450
1451 switch (luType) {
1452 case PIXEL:
1453 return cssValue.getDoubleValue();
1454 case EM:
1455
1456 return 16f * cssValue.getDoubleValue();
1457 case PERCENTAGE:
1458
1459 return 0.16f * cssValue.getDoubleValue();
1460 case EX:
1461
1462 return 0.16f * cssValue.getDoubleValue();
1463 case CH:
1464
1465 return 0.16f * cssValue.getDoubleValue();
1466 case VW:
1467
1468 return 0.16f * cssValue.getDoubleValue();
1469 case VH:
1470
1471 return 0.16f * cssValue.getDoubleValue();
1472 case VMIN:
1473
1474 return 0.16f * cssValue.getDoubleValue();
1475 case VMAX:
1476
1477 return 0.16f * cssValue.getDoubleValue();
1478 case DVW:
1479
1480 return 0.16f * cssValue.getDoubleValue();
1481 case DVH:
1482
1483 return 0.16f * cssValue.getDoubleValue();
1484 case DVMIN:
1485
1486 return 0.16f * cssValue.getDoubleValue();
1487 case DVMAX:
1488
1489 return 0.16f * cssValue.getDoubleValue();
1490 case LVW:
1491
1492 return 0.16f * cssValue.getDoubleValue();
1493 case LVH:
1494
1495 return 0.16f * cssValue.getDoubleValue();
1496 case LVMIN:
1497
1498 return 0.16f * cssValue.getDoubleValue();
1499 case LVMAX:
1500
1501 return 0.16f * cssValue.getDoubleValue();
1502 case SVW:
1503
1504 return 0.16f * cssValue.getDoubleValue();
1505 case SVH:
1506
1507 return 0.16f * cssValue.getDoubleValue();
1508 case SVMIN:
1509
1510 return 0.16f * cssValue.getDoubleValue();
1511 case SVMAX:
1512
1513 return 0.16f * cssValue.getDoubleValue();
1514 case REM:
1515
1516 return 0.16f * cssValue.getDoubleValue();
1517 case MILLIMETER:
1518 dpi = webWindow.getScreen().getDeviceXDPI();
1519 return (dpi / 25.4f) * cssValue.getDoubleValue();
1520 case QUARTER:
1521
1522 dpi = webWindow.getScreen().getDeviceXDPI();
1523 return ((dpi / 25.4f) * cssValue.getDoubleValue()) / 4d;
1524 case CENTIMETER:
1525 dpi = webWindow.getScreen().getDeviceXDPI();
1526 return (dpi / 2.54f) * cssValue.getDoubleValue();
1527 case POINT:
1528 dpi = webWindow.getScreen().getDeviceXDPI();
1529 return (dpi / 72f) * cssValue.getDoubleValue();
1530 default:
1531 break;
1532 }
1533 }
1534 if (LOG.isWarnEnabled()) {
1535 LOG.warn("CSSValue '" + cssValue.getCssText()
1536 + "' has to be a 'px', 'em', '%', 'ex', 'ch', "
1537 + "'vw', 'vh', 'vmin', 'vmax', 'dvw', 'dvh', 'dvmin', 'dvmax', "
1538 + "'lvw', 'lvh', 'lvmin', 'lvmax', 'svw', 'svh', 'svmin', 'svmax', "
1539 + "'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1540 }
1541 return -1;
1542 }
1543
1544 private static double resolutionValue(final CSSValueImpl cssValue) {
1545 if (cssValue == null) {
1546 LOG.warn("CSSValue is null but has to be a 'dpi', 'dpcm', or 'dppx' value.");
1547 return -1;
1548 }
1549
1550 if (cssValue.getPrimitiveType() == CSSPrimitiveValueType.CSS_DIMENSION) {
1551 final String text = cssValue.getCssText();
1552 if (text.endsWith("dpi")) {
1553 return cssValue.getDoubleValue();
1554 }
1555 if (text.endsWith("dpcm")) {
1556 return 2.54f * cssValue.getDoubleValue();
1557 }
1558 if (text.endsWith("dppx")) {
1559 return 96 * cssValue.getDoubleValue();
1560 }
1561 }
1562
1563 if (LOG.isWarnEnabled()) {
1564 LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'dpi', 'dpcm', or 'dppx' value.");
1565 }
1566 return -1;
1567 }
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578 public void modifyIfNecessary(final ComputedCssStyleDeclaration style, final DomElement element,
1579 final String pseudoElement) {
1580
1581 final BrowserVersion browser = element.getPage().getWebClient().getBrowserVersion();
1582 final List<CSSStyleSheetImpl.SelectorEntry> matchingRules =
1583 selects(getRuleIndex(), browser, element, pseudoElement, false);
1584 for (final CSSStyleSheetImpl.SelectorEntry entry : matchingRules) {
1585 final CSSStyleDeclarationImpl dec = entry.getRule().getStyle();
1586 style.applyStyleFromSelector(dec, entry.getSelector());
1587 }
1588 }
1589
1590 private CSSStyleSheetImpl.CSSStyleSheetRuleIndex getRuleIndex() {
1591 final CSSStyleSheetImpl styleSheet = getWrappedSheet();
1592 CSSStyleSheetImpl.CSSStyleSheetRuleIndex index = styleSheet.getRuleIndex();
1593
1594 if (index == null) {
1595 index = new CSSStyleSheetImpl.CSSStyleSheetRuleIndex();
1596 final CSSRuleListImpl ruleList = styleSheet.getCssRules();
1597 index(index, ruleList, new HashSet<>());
1598
1599 styleSheet.setRuleIndex(index);
1600 }
1601 return index;
1602 }
1603
1604 private void index(final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index, final CSSRuleListImpl ruleList,
1605 final Set<String> alreadyProcessing) {
1606
1607 for (final AbstractCSSRuleImpl rule : ruleList.getRules()) {
1608 if (rule instanceof CSSStyleRuleImpl styleRule) {
1609 final SelectorList selectors = styleRule.getSelectors();
1610 for (final Selector selector : selectors) {
1611 final SimpleSelector simpleSel = selector.getSimpleSelector();
1612 if (SelectorType.ELEMENT_NODE_SELECTOR == simpleSel.getSelectorType()) {
1613 final ElementSelector es = (ElementSelector) simpleSel;
1614 boolean wasClass = false;
1615 final List<Condition> conds = es.getConditions();
1616 if (conds != null && conds.size() == 1) {
1617 final Condition c = conds.get(0);
1618 if (ConditionType.CLASS_CONDITION == c.getConditionType()) {
1619 index.addClassSelector(es, c.getValue(), selector, styleRule);
1620 wasClass = true;
1621 }
1622 }
1623 if (!wasClass) {
1624 index.addElementSelector(es, selector, styleRule);
1625 }
1626 }
1627 else {
1628 index.addOtherSelector(selector, styleRule);
1629 }
1630 }
1631 }
1632 else if (rule instanceof CSSImportRuleImpl importRule) {
1633
1634 final CssStyleSheet sheet = getImportedStyleSheet(importRule);
1635
1636 if (!alreadyProcessing.contains(sheet.getUri())) {
1637 final CSSRuleListImpl sheetRuleList = sheet.getWrappedSheet().getCssRules();
1638 alreadyProcessing.add(sheet.getUri());
1639
1640 final MediaListImpl mediaList = importRule.getMedia();
1641 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1642 index(index, sheetRuleList, alreadyProcessing);
1643 }
1644 else {
1645 index(index.addMedia(mediaList), sheetRuleList, alreadyProcessing);
1646 }
1647 }
1648 }
1649 else if (rule instanceof CSSMediaRuleImpl mediaRule) {
1650 final MediaListImpl mediaList = mediaRule.getMediaList();
1651 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1652 index(index, mediaRule.getCssRules(), alreadyProcessing);
1653 }
1654 else {
1655 index(index.addMedia(mediaList), mediaRule.getCssRules(), alreadyProcessing);
1656 }
1657 }
1658 }
1659 }
1660
1661 private List<CSSStyleSheetImpl.SelectorEntry> selects(
1662 final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index,
1663 final BrowserVersion browserVersion, final DomElement element,
1664 final String pseudoElement, final boolean fromQuerySelectorAll) {
1665
1666 final List<CSSStyleSheetImpl.SelectorEntry> matchingRules = new ArrayList<>();
1667
1668 if (isActive(index.getMediaList(), element.getPage().getEnclosingWindow())) {
1669 final String elementName = element.getLowercaseName();
1670 final String[] classes = StringUtils.splitAtJavaWhitespace(
1671 element.getAttributeDirect("class"));
1672 final Iterator<CSSStyleSheetImpl.SelectorEntry> iter =
1673 index.getSelectorEntriesIteratorFor(elementName, classes);
1674
1675 CSSStyleSheetImpl.SelectorEntry entry = iter.next();
1676 while (null != entry) {
1677 if (selects(browserVersion, entry.getSelector(),
1678 element, pseudoElement, fromQuerySelectorAll, false)) {
1679 matchingRules.add(entry);
1680 }
1681 entry = iter.next();
1682 }
1683
1684 for (final CSSStyleSheetImpl.CSSStyleSheetRuleIndex child : index.getChildren()) {
1685 matchingRules.addAll(selects(child, browserVersion,
1686 element, pseudoElement, fromQuerySelectorAll));
1687 }
1688 }
1689
1690 return matchingRules;
1691 }
1692 }