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
1271
1272 public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
1273 CssStyleSheet sheet = imports_.get(importRule);
1274 if (sheet == null) {
1275 final String href = importRule.getHref();
1276 final String url = UrlUtils.resolveUrl(getUri(), href);
1277 sheet = loadStylesheet(owner_, null, url);
1278 imports_.put(importRule, sheet);
1279 }
1280 return sheet;
1281 }
1282
1283
1284
1285
1286
1287 public boolean isActive() {
1288 final String media;
1289 if (owner_ instanceof HtmlStyle style) {
1290 media = style.getMediaAttribute();
1291 }
1292 else if (owner_ instanceof HtmlLink link) {
1293 media = link.getMediaAttribute();
1294 }
1295 else {
1296 return true;
1297 }
1298
1299 if (StringUtils.isBlank(media)) {
1300 return true;
1301 }
1302
1303 final WebWindow webWindow = owner_.getPage().getEnclosingWindow();
1304 final MediaListImpl mediaList = parseMedia(media, webWindow.getWebClient());
1305 return isActive(mediaList, webWindow);
1306 }
1307
1308
1309
1310
1311
1312
1313
1314 public static boolean isActive(final MediaListImpl mediaList, final WebWindow webWindow) {
1315 if (mediaList.getLength() == 0) {
1316 return true;
1317 }
1318
1319 final int length = mediaList.getLength();
1320 for (int i = 0; i < length; i++) {
1321 final MediaQuery mediaQuery = mediaList.mediaQuery(i);
1322 boolean isActive = isActive(mediaQuery, webWindow);
1323 if (mediaQuery.isNot()) {
1324 isActive = !isActive;
1325 }
1326 if (isActive) {
1327 return true;
1328 }
1329 }
1330 return false;
1331 }
1332
1333 private static boolean isActive(final MediaQuery mediaQuery, final WebWindow webWindow) {
1334 final String mediaType = mediaQuery.getMedia();
1335 if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) {
1336 for (final Property property : mediaQuery.getProperties()) {
1337 final double val;
1338 switch (property.getName()) {
1339 case "max-width":
1340 val = pixelValue(property.getValue(), webWindow);
1341 if (val == -1 || val < webWindow.getInnerWidth()) {
1342 return false;
1343 }
1344 break;
1345
1346 case "min-width":
1347 val = pixelValue(property.getValue(), webWindow);
1348 if (val == -1 || val > webWindow.getInnerWidth()) {
1349 return false;
1350 }
1351 break;
1352
1353 case "max-device-width":
1354 val = pixelValue(property.getValue(), webWindow);
1355 if (val == -1 || val < webWindow.getScreen().getWidth()) {
1356 return false;
1357 }
1358 break;
1359
1360 case "min-device-width":
1361 val = pixelValue(property.getValue(), webWindow);
1362 if (val == -1 || val > webWindow.getScreen().getWidth()) {
1363 return false;
1364 }
1365 break;
1366
1367 case "max-height":
1368 val = pixelValue(property.getValue(), webWindow);
1369 if (val == -1 || val < webWindow.getInnerWidth()) {
1370 return false;
1371 }
1372 break;
1373
1374 case "min-height":
1375 val = pixelValue(property.getValue(), webWindow);
1376 if (val == -1 || val > webWindow.getInnerWidth()) {
1377 return false;
1378 }
1379 break;
1380
1381 case "max-device-height":
1382 val = pixelValue(property.getValue(), webWindow);
1383 if (val == -1 || val < webWindow.getScreen().getWidth()) {
1384 return false;
1385 }
1386 break;
1387
1388 case "min-device-height":
1389 val = pixelValue(property.getValue(), webWindow);
1390 if (val == -1 || val > webWindow.getScreen().getWidth()) {
1391 return false;
1392 }
1393 break;
1394
1395 case "resolution":
1396 final CSSValueImpl propValue = property.getValue();
1397 val = resolutionValue(propValue);
1398 if (propValue == null) {
1399 return true;
1400 }
1401 if (val == -1 || Math.round(val) != webWindow.getScreen().getDeviceXDPI()) {
1402 return false;
1403 }
1404 break;
1405
1406 case "max-resolution":
1407 val = resolutionValue(property.getValue());
1408 if (val == -1 || val < webWindow.getScreen().getDeviceXDPI()) {
1409 return false;
1410 }
1411 break;
1412
1413 case "min-resolution":
1414 val = resolutionValue(property.getValue());
1415 if (val == -1 || val > webWindow.getScreen().getDeviceXDPI()) {
1416 return false;
1417 }
1418 break;
1419
1420 case "orientation":
1421 final CSSValueImpl cssValue = property.getValue();
1422 if (cssValue == null) {
1423 LOG.warn("CSSValue is null not supported for feature 'orientation'");
1424 return true;
1425 }
1426
1427 final String orient = cssValue.getCssText();
1428 if ("portrait".equals(orient)) {
1429 if (webWindow.getInnerWidth() > webWindow.getInnerHeight()) {
1430 return false;
1431 }
1432 }
1433 else if ("landscape".equals(orient)) {
1434 if (webWindow.getInnerWidth() < webWindow.getInnerHeight()) {
1435 return false;
1436 }
1437 }
1438 else {
1439 if (LOG.isWarnEnabled()) {
1440 LOG.warn("CSSValue '" + property.getValue().getCssText()
1441 + "' not supported for feature 'orientation'.");
1442 }
1443 return false;
1444 }
1445 break;
1446
1447 default:
1448 }
1449 }
1450 return true;
1451 }
1452 else if ("print".equalsIgnoreCase(mediaType)) {
1453 final Page page = webWindow.getEnclosedPage();
1454 if (page instanceof SgmlPage sgmlPage) {
1455 return sgmlPage.isPrinting();
1456 }
1457 }
1458 return false;
1459 }
1460
1461 @SuppressWarnings("PMD.UselessParentheses")
1462 private static double pixelValue(final CSSValueImpl cssValue, final WebWindow webWindow) {
1463 if (cssValue == null) {
1464 LOG.warn("CSSValue is null but has to be a 'px', 'em', '%', 'ex', 'ch', "
1465 + "'vw', 'vh', 'vmin', 'vmax', 'dvw', 'dvh', 'dvmin', 'dvmax', "
1466 + "'lvw', 'lvh', 'lvmin', 'lvmax', 'svw', 'svh', 'svmin', 'svmax', "
1467 + "'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1468 return -1;
1469 }
1470
1471 final LexicalUnit.LexicalUnitType luType = cssValue.getLexicalUnitType();
1472 if (luType != null) {
1473 final int dpi;
1474
1475 switch (luType) {
1476 case PIXEL:
1477 return cssValue.getDoubleValue();
1478 case EM:
1479
1480 return 16f * cssValue.getDoubleValue();
1481 case PERCENTAGE:
1482
1483 return 0.16f * cssValue.getDoubleValue();
1484 case EX:
1485
1486 return 0.16f * cssValue.getDoubleValue();
1487 case CH:
1488
1489 return 0.16f * cssValue.getDoubleValue();
1490 case VW:
1491
1492 return 0.16f * cssValue.getDoubleValue();
1493 case VH:
1494
1495 return 0.16f * cssValue.getDoubleValue();
1496 case VMIN:
1497
1498 return 0.16f * cssValue.getDoubleValue();
1499 case VMAX:
1500
1501 return 0.16f * cssValue.getDoubleValue();
1502 case DVW:
1503
1504 return 0.16f * cssValue.getDoubleValue();
1505 case DVH:
1506
1507 return 0.16f * cssValue.getDoubleValue();
1508 case DVMIN:
1509
1510 return 0.16f * cssValue.getDoubleValue();
1511 case DVMAX:
1512
1513 return 0.16f * cssValue.getDoubleValue();
1514 case LVW:
1515
1516 return 0.16f * cssValue.getDoubleValue();
1517 case LVH:
1518
1519 return 0.16f * cssValue.getDoubleValue();
1520 case LVMIN:
1521
1522 return 0.16f * cssValue.getDoubleValue();
1523 case LVMAX:
1524
1525 return 0.16f * cssValue.getDoubleValue();
1526 case SVW:
1527
1528 return 0.16f * cssValue.getDoubleValue();
1529 case SVH:
1530
1531 return 0.16f * cssValue.getDoubleValue();
1532 case SVMIN:
1533
1534 return 0.16f * cssValue.getDoubleValue();
1535 case SVMAX:
1536
1537 return 0.16f * cssValue.getDoubleValue();
1538 case REM:
1539
1540 return 0.16f * cssValue.getDoubleValue();
1541 case MILLIMETER:
1542 dpi = webWindow.getScreen().getDeviceXDPI();
1543 return (dpi / 25.4f) * cssValue.getDoubleValue();
1544 case QUATER:
1545
1546 dpi = webWindow.getScreen().getDeviceXDPI();
1547 return ((dpi / 25.4f) * cssValue.getDoubleValue()) / 4d;
1548 case CENTIMETER:
1549 dpi = webWindow.getScreen().getDeviceXDPI();
1550 return (dpi / 254f) * cssValue.getDoubleValue();
1551 case POINT:
1552 dpi = webWindow.getScreen().getDeviceXDPI();
1553 return (dpi / 72f) * cssValue.getDoubleValue();
1554 default:
1555 break;
1556 }
1557 }
1558 if (LOG.isWarnEnabled()) {
1559 LOG.warn("CSSValue '" + cssValue.getCssText()
1560 + "' has to be a 'px', 'em', '%', 'ex', 'ch', "
1561 + "'vw', 'vh', 'vmin', 'vmax', 'dvw', 'dvh', 'dvmin', 'dvmax', "
1562 + "'lvw', 'lvh', 'lvmin', 'lvmax', 'svw', 'svh', 'svmin', 'svmax', "
1563 + "'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1564 }
1565 return -1;
1566 }
1567
1568 private static double resolutionValue(final CSSValueImpl cssValue) {
1569 if (cssValue == null) {
1570 LOG.warn("CSSValue is null but has to be a 'dpi', 'dpcm', or 'dppx' value.");
1571 return -1;
1572 }
1573
1574 if (cssValue.getPrimitiveType() == CSSPrimitiveValueType.CSS_DIMENSION) {
1575 final String text = cssValue.getCssText();
1576 if (text.endsWith("dpi")) {
1577 return cssValue.getDoubleValue();
1578 }
1579 if (text.endsWith("dpcm")) {
1580 return 2.54f * cssValue.getDoubleValue();
1581 }
1582 if (text.endsWith("dppx")) {
1583 return 96 * cssValue.getDoubleValue();
1584 }
1585 }
1586
1587 if (LOG.isWarnEnabled()) {
1588 LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'dpi', 'dpcm', or 'dppx' value.");
1589 }
1590 return -1;
1591 }
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602 public void modifyIfNecessary(final ComputedCssStyleDeclaration style, final DomElement element,
1603 final String pseudoElement) {
1604
1605 final BrowserVersion browser = element.getPage().getWebClient().getBrowserVersion();
1606 final List<CSSStyleSheetImpl.SelectorEntry> matchingRules =
1607 selects(getRuleIndex(), browser, element, pseudoElement, false);
1608 for (final CSSStyleSheetImpl.SelectorEntry entry : matchingRules) {
1609 final CSSStyleDeclarationImpl dec = entry.getRule().getStyle();
1610 style.applyStyleFromSelector(dec, entry.getSelector());
1611 }
1612 }
1613
1614 private CSSStyleSheetImpl.CSSStyleSheetRuleIndex getRuleIndex() {
1615 final CSSStyleSheetImpl styleSheet = getWrappedSheet();
1616 CSSStyleSheetImpl.CSSStyleSheetRuleIndex index = styleSheet.getRuleIndex();
1617
1618 if (index == null) {
1619 index = new CSSStyleSheetImpl.CSSStyleSheetRuleIndex();
1620 final CSSRuleListImpl ruleList = styleSheet.getCssRules();
1621 index(index, ruleList, new HashSet<>());
1622
1623 styleSheet.setRuleIndex(index);
1624 }
1625 return index;
1626 }
1627
1628 private void index(final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index, final CSSRuleListImpl ruleList,
1629 final Set<String> alreadyProcessing) {
1630
1631 for (final AbstractCSSRuleImpl rule : ruleList.getRules()) {
1632 if (rule instanceof CSSStyleRuleImpl styleRule) {
1633 final SelectorList selectors = styleRule.getSelectors();
1634 for (final Selector selector : selectors) {
1635 final SimpleSelector simpleSel = selector.getSimpleSelector();
1636 if (SelectorType.ELEMENT_NODE_SELECTOR == simpleSel.getSelectorType()) {
1637 final ElementSelector es = (ElementSelector) simpleSel;
1638 boolean wasClass = false;
1639 final List<Condition> conds = es.getConditions();
1640 if (conds != null && conds.size() == 1) {
1641 final Condition c = conds.get(0);
1642 if (ConditionType.CLASS_CONDITION == c.getConditionType()) {
1643 index.addClassSelector(es, c.getValue(), selector, styleRule);
1644 wasClass = true;
1645 }
1646 }
1647 if (!wasClass) {
1648 index.addElementSelector(es, selector, styleRule);
1649 }
1650 }
1651 else {
1652 index.addOtherSelector(selector, styleRule);
1653 }
1654 }
1655 }
1656 else if (rule instanceof CSSImportRuleImpl importRule) {
1657
1658 final CssStyleSheet sheet = getImportedStyleSheet(importRule);
1659
1660 if (!alreadyProcessing.contains(sheet.getUri())) {
1661 final CSSRuleListImpl sheetRuleList = sheet.getWrappedSheet().getCssRules();
1662 alreadyProcessing.add(sheet.getUri());
1663
1664 final MediaListImpl mediaList = importRule.getMedia();
1665 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1666 index(index, sheetRuleList, alreadyProcessing);
1667 }
1668 else {
1669 index(index.addMedia(mediaList), sheetRuleList, alreadyProcessing);
1670 }
1671 }
1672 }
1673 else if (rule instanceof CSSMediaRuleImpl mediaRule) {
1674 final MediaListImpl mediaList = mediaRule.getMediaList();
1675 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1676 index(index, mediaRule.getCssRules(), alreadyProcessing);
1677 }
1678 else {
1679 index(index.addMedia(mediaList), mediaRule.getCssRules(), alreadyProcessing);
1680 }
1681 }
1682 }
1683 }
1684
1685 private List<CSSStyleSheetImpl.SelectorEntry> selects(
1686 final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index,
1687 final BrowserVersion browserVersion, final DomElement element,
1688 final String pseudoElement, final boolean fromQuerySelectorAll) {
1689
1690 final List<CSSStyleSheetImpl.SelectorEntry> matchingRules = new ArrayList<>();
1691
1692 if (isActive(index.getMediaList(), element.getPage().getEnclosingWindow())) {
1693 final String elementName = element.getLowercaseName();
1694 final String[] classes = StringUtils.splitAtJavaWhitespace(
1695 element.getAttributeDirect("class"));
1696 final Iterator<CSSStyleSheetImpl.SelectorEntry> iter =
1697 index.getSelectorEntriesIteratorFor(elementName, classes);
1698
1699 CSSStyleSheetImpl.SelectorEntry entry = iter.next();
1700 while (null != entry) {
1701 if (selects(browserVersion, entry.getSelector(),
1702 element, pseudoElement, fromQuerySelectorAll, false)) {
1703 matchingRules.add(entry);
1704 }
1705 entry = iter.next();
1706 }
1707
1708 for (final CSSStyleSheetImpl.CSSStyleSheetRuleIndex child : index.getChildren()) {
1709 matchingRules.addAll(selects(child, browserVersion,
1710 element, pseudoElement, fromQuerySelectorAll));
1711 }
1712 }
1713
1714 return matchingRules;
1715 }
1716 }