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