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