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