1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.html;
16
17 import static org.htmlunit.BrowserVersionFeatures.EVENT_FOCUS_ON_LOAD;
18 import static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.ObjectInputStream;
23 import java.io.ObjectOutputStream;
24 import java.io.Serializable;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.nio.charset.Charset;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Comparator;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.LinkedHashSet;
38 import java.util.List;
39 import java.util.Locale;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.WeakHashMap;
43 import java.util.concurrent.ConcurrentHashMap;
44
45 import org.apache.commons.lang3.StringUtils;
46 import org.apache.commons.logging.Log;
47 import org.apache.commons.logging.LogFactory;
48 import org.htmlunit.Cache;
49 import org.htmlunit.ElementNotFoundException;
50 import org.htmlunit.FailingHttpStatusCodeException;
51 import org.htmlunit.History;
52 import org.htmlunit.HttpHeader;
53 import org.htmlunit.OnbeforeunloadHandler;
54 import org.htmlunit.Page;
55 import org.htmlunit.ScriptResult;
56 import org.htmlunit.SgmlPage;
57 import org.htmlunit.TopLevelWindow;
58 import org.htmlunit.WebAssert;
59 import org.htmlunit.WebClient;
60 import org.htmlunit.WebClientOptions;
61 import org.htmlunit.WebRequest;
62 import org.htmlunit.WebResponse;
63 import org.htmlunit.WebWindow;
64 import org.htmlunit.corejs.javascript.Function;
65 import org.htmlunit.corejs.javascript.Script;
66 import org.htmlunit.corejs.javascript.Scriptable;
67 import org.htmlunit.corejs.javascript.ScriptableObject;
68 import org.htmlunit.corejs.javascript.VarScope;
69 import org.htmlunit.css.ComputedCssStyleDeclaration;
70 import org.htmlunit.css.CssStyleSheet;
71 import org.htmlunit.html.impl.SimpleRange;
72 import org.htmlunit.html.parser.HTMLParserDOMBuilder;
73 import org.htmlunit.http.HttpStatus;
74 import org.htmlunit.javascript.AbstractJavaScriptEngine;
75 import org.htmlunit.javascript.HtmlUnitScriptable;
76 import org.htmlunit.javascript.JavaScriptEngine;
77 import org.htmlunit.javascript.PostponedAction;
78 import org.htmlunit.javascript.host.Window;
79 import org.htmlunit.javascript.host.event.BeforeUnloadEvent;
80 import org.htmlunit.javascript.host.event.Event;
81 import org.htmlunit.javascript.host.event.EventTarget;
82 import org.htmlunit.javascript.host.html.HTMLDocument;
83 import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
84 import org.htmlunit.util.MimeType;
85 import org.htmlunit.util.SerializableLock;
86 import org.htmlunit.util.UrlUtils;
87 import org.w3c.dom.Attr;
88 import org.w3c.dom.Comment;
89 import org.w3c.dom.DOMConfiguration;
90 import org.w3c.dom.DOMException;
91 import org.w3c.dom.DOMImplementation;
92 import org.w3c.dom.Document;
93 import org.w3c.dom.DocumentType;
94 import org.w3c.dom.Element;
95 import org.w3c.dom.EntityReference;
96 import org.w3c.dom.ProcessingInstruction;
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 @SuppressWarnings("PMD.TooManyFields")
145 public class HtmlPage extends SgmlPage {
146
147 private static final Log LOG = LogFactory.getLog(HtmlPage.class);
148
149 private static final Comparator<DomElement> DOCUMENT_POSITION_COMPERATOR = new DocumentPositionComparator();
150
151 private HTMLParserDOMBuilder domBuilder_;
152 private transient Charset originalCharset_;
153 private final Object lock_ = new SerializableLock();
154
155 private Map<String, MappedElementIndexEntry> idMap_ = new ConcurrentHashMap<>();
156 private Map<String, MappedElementIndexEntry> nameMap_ = new ConcurrentHashMap<>();
157
158
159
160 private boolean mappedElementsBuilt_;
161
162 private List<BaseFrameElement> frameElements_ = new ArrayList<>();
163 private int parserCount_;
164 private int snippetParserCount_;
165 private int inlineSnippetParserCount_;
166 private Collection<HtmlAttributeChangeListener> attributeListeners_;
167 private List<PostponedAction> afterLoadActions_ = Collections.synchronizedList(new ArrayList<>());
168 private boolean cleaning_;
169 private HtmlBase base_;
170 private URL baseUrl_;
171 private List<AutoCloseable> autoCloseableList_;
172 private ElementFromPointHandler elementFromPointHandler_;
173 private DomElement elementWithFocus_;
174 private List<SimpleRange> selectionRanges_ = new ArrayList<>(3);
175
176 private transient ComputedStylesCache computedStylesCache_;
177
178 private static final HashSet<String> TABBABLE_TAGS =
179 new HashSet<>(Arrays.asList(HtmlAnchor.TAG_NAME, HtmlArea.TAG_NAME,
180 HtmlButton.TAG_NAME, HtmlInput.TAG_NAME, HtmlObject.TAG_NAME,
181 HtmlSelect.TAG_NAME, HtmlTextArea.TAG_NAME));
182 private static final HashSet<String> ACCEPTABLE_TAG_NAMES =
183 new HashSet<>(Arrays.asList(HtmlAnchor.TAG_NAME, HtmlArea.TAG_NAME,
184 HtmlButton.TAG_NAME, HtmlInput.TAG_NAME, HtmlLabel.TAG_NAME,
185 HtmlLegend.TAG_NAME, HtmlTextArea.TAG_NAME));
186
187
188 private static final Set<String> ATTRIBUTES_AFFECTING_PARENT = new HashSet<>(Arrays.asList(
189 "style",
190 "class",
191 "height",
192 "width"));
193
194 static class DocumentPositionComparator implements Comparator<DomElement>, Serializable {
195 @Override
196 public int compare(final DomElement elt1, final DomElement elt2) {
197 final short relation = elt1.compareDocumentPosition(elt2);
198 if (relation == 0) {
199 return 0;
200 }
201 if ((relation & DOCUMENT_POSITION_CONTAINS) != 0 || (relation & DOCUMENT_POSITION_PRECEDING) != 0) {
202 return 1;
203 }
204
205 return -1;
206 }
207 }
208
209
210
211
212
213
214
215
216 public HtmlPage(final WebResponse webResponse, final WebWindow webWindow) {
217 super(webResponse, webWindow);
218 }
219
220
221
222
223 @Override
224 public HtmlPage getPage() {
225 return this;
226 }
227
228
229
230
231 @Override
232 public boolean hasCaseSensitiveTagNames() {
233 return false;
234 }
235
236
237
238
239
240
241
242
243 @Override
244 public void initialize() throws IOException, FailingHttpStatusCodeException {
245 final WebWindow enclosingWindow = getEnclosingWindow();
246 final boolean isAboutBlank = getUrl() == UrlUtils.URL_ABOUT_BLANK;
247 if (isAboutBlank) {
248
249 if (enclosingWindow instanceof FrameWindow window
250 && !window.getFrameElement().isContentLoaded()) {
251 return;
252 }
253
254
255 if (enclosingWindow instanceof TopLevelWindow topWindow) {
256 final WebWindow openerWindow = topWindow.getOpener();
257 if (openerWindow != null && openerWindow.getEnclosedPage() != null) {
258 baseUrl_ = openerWindow.getEnclosedPage().getWebResponse().getWebRequest().getUrl();
259 }
260 }
261 }
262
263 if (!isAboutBlank) {
264 setReadyState(READY_STATE_INTERACTIVE);
265 getDocumentElement().setReadyState(READY_STATE_INTERACTIVE);
266 executeEventHandlersIfNeeded(Event.TYPE_READY_STATE_CHANGE);
267 }
268
269 executeDeferredScriptsIfNeeded();
270
271 executeEventHandlersIfNeeded(Event.TYPE_DOM_DOCUMENT_LOADED);
272
273
274
275 processPostponedActionsIfNeeded();
276
277 loadFrames();
278
279
280
281 if (!isAboutBlank) {
282 setReadyState(READY_STATE_COMPLETE);
283 getDocumentElement().setReadyState(READY_STATE_COMPLETE);
284 executeEventHandlersIfNeeded(Event.TYPE_READY_STATE_CHANGE);
285 }
286
287
288 boolean isFrameWindow = enclosingWindow instanceof FrameWindow;
289 boolean isFirstPageInFrameWindow = false;
290 if (isFrameWindow) {
291 isFrameWindow = ((FrameWindow) enclosingWindow).getFrameElement() instanceof HtmlFrame;
292
293 final History hist = enclosingWindow.getHistory();
294 if (hist.getLength() > 0 && UrlUtils.URL_ABOUT_BLANK == hist.getUrl(0)) {
295 isFirstPageInFrameWindow = hist.getLength() <= 2;
296 }
297 else {
298 isFirstPageInFrameWindow = enclosingWindow.getHistory().getLength() < 2;
299 }
300 }
301
302 if (isFrameWindow && !isFirstPageInFrameWindow) {
303 executeEventHandlersIfNeeded(Event.TYPE_LOAD);
304 }
305
306 for (final BaseFrameElement frameElement : new ArrayList<>(frameElements_)) {
307 if (frameElement instanceof HtmlFrame) {
308 final Page page = frameElement.getEnclosedWindow().getEnclosedPage();
309 if (page != null && page.isHtmlPage()) {
310 ((HtmlPage) page).executeEventHandlersIfNeeded(Event.TYPE_LOAD);
311 }
312 }
313 }
314
315 if (!isFrameWindow) {
316 executeEventHandlersIfNeeded(Event.TYPE_LOAD);
317
318 if (!isAboutBlank && enclosingWindow.getWebClient().isJavaScriptEnabled()
319 && hasFeature(EVENT_FOCUS_ON_LOAD)) {
320 final HtmlElement body = getBody();
321 if (body != null) {
322 final Event event = new Event((Window) enclosingWindow.getScriptableObject(), Event.TYPE_FOCUS);
323 body.fireEvent(event);
324 }
325 }
326 }
327
328 try {
329 while (!afterLoadActions_.isEmpty()) {
330 final PostponedAction action = afterLoadActions_.remove(0);
331 action.execute();
332 }
333 }
334 catch (final IOException e) {
335 throw e;
336 }
337 catch (final Exception e) {
338 throw new RuntimeException(e);
339 }
340 executeRefreshIfNeeded();
341 }
342
343
344
345
346
347 void addAfterLoadAction(final PostponedAction action) {
348 afterLoadActions_.add(action);
349 }
350
351
352
353
354 @Override
355 public void cleanUp() {
356
357 if (cleaning_) {
358 return;
359 }
360
361 cleaning_ = true;
362 try {
363 super.cleanUp();
364 executeEventHandlersIfNeeded(Event.TYPE_UNLOAD);
365 deregisterFramesIfNeeded();
366 }
367 finally {
368 cleaning_ = false;
369
370 if (autoCloseableList_ != null) {
371 for (final AutoCloseable closeable : new ArrayList<>(autoCloseableList_)) {
372 try {
373 closeable.close();
374 }
375 catch (final Exception e) {
376 LOG.error("Closing the autoclosable " + closeable + " failed", e);
377 }
378 }
379 }
380 }
381 }
382
383
384
385
386 @Override
387 public HtmlElement getDocumentElement() {
388 return (HtmlElement) super.getDocumentElement();
389 }
390
391
392
393
394
395
396
397 public HtmlBody getBody() {
398 final DomElement doc = getDocumentElement();
399 if (doc != null) {
400 for (final DomNode node : doc.getChildren()) {
401 if (node instanceof HtmlBody body) {
402 return body;
403 }
404 }
405 }
406 return null;
407 }
408
409
410
411
412
413 public HtmlElement getHead() {
414 final DomElement doc = getDocumentElement();
415 if (doc != null) {
416 for (final DomNode node : doc.getChildren()) {
417 if (node instanceof HtmlHead) {
418 return (HtmlElement) node;
419 }
420 }
421 }
422 return null;
423 }
424
425
426
427
428 @Override
429 public Document getOwnerDocument() {
430 return null;
431 }
432
433
434
435
436
437 @Override
438 public org.w3c.dom.Node importNode(final org.w3c.dom.Node importedNode, final boolean deep) {
439 throw new UnsupportedOperationException("HtmlPage.importNode is not yet implemented.");
440 }
441
442
443
444
445
446 @Override
447 public String getInputEncoding() {
448 throw new UnsupportedOperationException("HtmlPage.getInputEncoding is not yet implemented.");
449 }
450
451
452
453
454 @Override
455 public String getXmlEncoding() {
456 return null;
457 }
458
459
460
461
462 @Override
463 public boolean getXmlStandalone() {
464 return false;
465 }
466
467
468
469
470
471 @Override
472 public void setXmlStandalone(final boolean xmlStandalone) throws DOMException {
473 throw new UnsupportedOperationException("HtmlPage.setXmlStandalone is not yet implemented.");
474 }
475
476
477
478
479 @Override
480 public String getXmlVersion() {
481 return null;
482 }
483
484
485
486
487
488 @Override
489 public void setXmlVersion(final String xmlVersion) throws DOMException {
490 throw new UnsupportedOperationException("HtmlPage.setXmlVersion is not yet implemented.");
491 }
492
493
494
495
496
497 @Override
498 public boolean getStrictErrorChecking() {
499 throw new UnsupportedOperationException("HtmlPage.getStrictErrorChecking is not yet implemented.");
500 }
501
502
503
504
505
506 @Override
507 public void setStrictErrorChecking(final boolean strictErrorChecking) {
508 throw new UnsupportedOperationException("HtmlPage.setStrictErrorChecking is not yet implemented.");
509 }
510
511
512
513
514
515 @Override
516 public String getDocumentURI() {
517 throw new UnsupportedOperationException("HtmlPage.getDocumentURI is not yet implemented.");
518 }
519
520
521
522
523
524 @Override
525 public void setDocumentURI(final String documentURI) {
526 throw new UnsupportedOperationException("HtmlPage.setDocumentURI is not yet implemented.");
527 }
528
529
530
531
532
533 @Override
534 public org.w3c.dom.Node adoptNode(final org.w3c.dom.Node source) throws DOMException {
535 throw new UnsupportedOperationException("HtmlPage.adoptNode is not yet implemented.");
536 }
537
538
539
540
541
542 @Override
543 public DOMConfiguration getDomConfig() {
544 throw new UnsupportedOperationException("HtmlPage.getDomConfig is not yet implemented.");
545 }
546
547
548
549
550
551 @Override
552 public org.w3c.dom.Node renameNode(final org.w3c.dom.Node newNode, final String namespaceURI,
553 final String qualifiedName) throws DOMException {
554 throw new UnsupportedOperationException("HtmlPage.renameNode is not yet implemented.");
555 }
556
557
558
559
560 @Override
561 public Charset getCharset() {
562 if (originalCharset_ == null) {
563 originalCharset_ = getWebResponse().getContentCharset();
564 }
565 return originalCharset_;
566 }
567
568
569
570
571 @Override
572 public String getContentType() {
573 return getWebResponse().getContentType();
574 }
575
576
577
578
579
580 @Override
581 public DOMImplementation getImplementation() {
582 throw new UnsupportedOperationException("HtmlPage.getImplementation is not yet implemented.");
583 }
584
585
586
587
588
589 @Override
590 public DomElement createElement(String tagName) {
591 if (tagName.indexOf(':') == -1) {
592 tagName = org.htmlunit.util.StringUtils.toRootLowerCase(tagName);
593 }
594 return getWebClient().getPageCreator().getHtmlParser().getFactory(tagName)
595 .createElementNS(this, null, tagName, null);
596 }
597
598
599
600
601 @Override
602 public DomElement createElementNS(final String namespaceURI, final String qualifiedName) {
603 return getWebClient().getPageCreator().getHtmlParser()
604 .getElementFactory(this, namespaceURI, qualifiedName, false, true)
605 .createElementNS(this, namespaceURI, qualifiedName, null);
606 }
607
608
609
610
611
612 @Override
613 public Attr createAttributeNS(final String namespaceURI, final String qualifiedName) {
614 throw new UnsupportedOperationException("HtmlPage.createAttributeNS is not yet implemented.");
615 }
616
617
618
619
620
621 @Override
622 public EntityReference createEntityReference(final String id) {
623 throw new UnsupportedOperationException("HtmlPage.createEntityReference is not yet implemented.");
624 }
625
626
627
628
629
630 @Override
631 public ProcessingInstruction createProcessingInstruction(final String namespaceURI, final String qualifiedName) {
632 throw new UnsupportedOperationException("HtmlPage.createProcessingInstruction is not yet implemented.");
633 }
634
635
636
637
638 @Override
639 public DomElement getElementById(final String elementId) {
640 if (elementId != null) {
641 ensureMappedElementsBuilt();
642 final MappedElementIndexEntry elements = idMap_.get(elementId);
643 if (elements != null) {
644 return elements.first();
645 }
646 }
647 return null;
648 }
649
650
651
652
653
654
655
656
657 public HtmlAnchor getAnchorByName(final String name) throws ElementNotFoundException {
658 return getDocumentElement().getOneHtmlElementByAttribute("a", DomElement.NAME_ATTRIBUTE, name);
659 }
660
661
662
663
664
665
666
667
668 public HtmlAnchor getAnchorByHref(final String href) throws ElementNotFoundException {
669 return getDocumentElement().getOneHtmlElementByAttribute("a", "href", href);
670 }
671
672
673
674
675
676 public List<HtmlAnchor> getAnchors() {
677 return getDocumentElement().getElementsByTagNameImpl("a");
678 }
679
680
681
682
683
684
685
686 public HtmlAnchor getAnchorByText(final String text) throws ElementNotFoundException {
687 WebAssert.notNull("text", text);
688
689 for (final HtmlAnchor anchor : getAnchors()) {
690 if (text.equals(anchor.asNormalizedText())) {
691 return anchor;
692 }
693 }
694 throw new ElementNotFoundException("a", "<text>", text);
695 }
696
697
698
699
700
701
702
703 public HtmlForm getFormByName(final String name) throws ElementNotFoundException {
704 final List<HtmlForm> forms = getDocumentElement()
705 .getElementsByAttribute("form", DomElement.NAME_ATTRIBUTE, name);
706 if (forms.isEmpty()) {
707 throw new ElementNotFoundException("form", DomElement.NAME_ATTRIBUTE, name);
708 }
709 return forms.get(0);
710 }
711
712
713
714
715
716 public List<HtmlForm> getForms() {
717 return getDocumentElement().getElementsByTagNameImpl("form");
718 }
719
720
721
722
723
724
725
726
727
728 public URL getFullyQualifiedUrl(String relativeUrl) throws MalformedURLException {
729
730 boolean incorrectnessNotified = false;
731 while (relativeUrl.startsWith("http:") && !relativeUrl.startsWith("http://")) {
732 if (!incorrectnessNotified) {
733 notifyIncorrectness("Incorrect URL \"" + relativeUrl + "\" has been corrected");
734 incorrectnessNotified = true;
735 }
736 relativeUrl = "http:/" + relativeUrl.substring(5);
737 }
738
739 return WebClient.expandUrl(getBaseURL(), relativeUrl);
740 }
741
742
743
744
745
746
747
748 public String getResolvedTarget(final String elementTarget) {
749 final String resolvedTarget;
750 if (base_ == null) {
751 resolvedTarget = elementTarget;
752 }
753 else if (elementTarget != null && !elementTarget.isEmpty()) {
754 resolvedTarget = elementTarget;
755 }
756 else {
757 resolvedTarget = base_.getTargetAttribute();
758 }
759 return resolvedTarget;
760 }
761
762
763
764
765
766
767
768 public List<String> getTabbableElementIds() {
769 final List<String> list = new ArrayList<>();
770
771 for (final HtmlElement element : getTabbableElements()) {
772 list.add(element.getId());
773 }
774
775 return Collections.unmodifiableList(list);
776 }
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809 public List<HtmlElement> getTabbableElements() {
810 final List<HtmlElement> tabbableElements = new ArrayList<>();
811 for (final HtmlElement element : getHtmlElementDescendants()) {
812 final String tagName = element.getTagName();
813 if (TABBABLE_TAGS.contains(tagName)) {
814 final boolean disabled = element.isDisabledElementAndDisabled();
815 if (!disabled && !HtmlElement.TAB_INDEX_OUT_OF_BOUNDS.equals(element.getTabIndex())) {
816 tabbableElements.add(element);
817 }
818 }
819 }
820 tabbableElements.sort(createTabOrderComparator());
821 return Collections.unmodifiableList(tabbableElements);
822 }
823
824 private static Comparator<HtmlElement> createTabOrderComparator() {
825 return (element1, element2) -> {
826 final Short i1 = element1.getTabIndex();
827 final Short i2 = element2.getTabIndex();
828
829 final short index1;
830 if (i1 == null) {
831 index1 = -1;
832 }
833 else {
834 index1 = i1.shortValue();
835 }
836
837 final short index2;
838 if (i2 == null) {
839 index2 = -1;
840 }
841 else {
842 index2 = i2.shortValue();
843 }
844
845 final int result;
846 if (index1 > 0 && index2 > 0) {
847 result = index1 - index2;
848 }
849 else if (index1 > 0) {
850 result = -1;
851 }
852 else if (index2 > 0) {
853 result = 1;
854 }
855 else if (index1 == index2) {
856 result = 0;
857 }
858 else {
859 result = index2 - index1;
860 }
861
862 return result;
863 };
864 }
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879 public HtmlElement getHtmlElementByAccessKey(final char accessKey) {
880 final List<HtmlElement> elements = getHtmlElementsByAccessKey(accessKey);
881 if (elements.isEmpty()) {
882 return null;
883 }
884 return elements.get(0);
885 }
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907 public List<HtmlElement> getHtmlElementsByAccessKey(final char accessKey) {
908 final List<HtmlElement> elements = new ArrayList<>();
909
910 final String searchString = Character.toString(accessKey).toLowerCase(Locale.ROOT);
911 for (final HtmlElement element : getHtmlElementDescendants()) {
912 if (ACCEPTABLE_TAG_NAMES.contains(element.getTagName())) {
913 final String accessKeyAttribute = element.getAttributeDirect("accesskey");
914 if (searchString.equalsIgnoreCase(accessKeyAttribute)) {
915 elements.add(element);
916 }
917 }
918 }
919
920 return elements;
921 }
922
923
924
925
926
927
928
929
930
931
932
933 public ScriptResult executeJavaScript(final String sourceCode) {
934 return executeJavaScript(sourceCode, "injected script", 1);
935 }
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957 public ScriptResult executeJavaScript(String sourceCode, final String sourceName, final int startLine) {
958 if (!getWebClient().isJavaScriptEnabled()) {
959 return new ScriptResult(JavaScriptEngine.UNDEFINED);
960 }
961
962 if (org.htmlunit.util.StringUtils.startsWithIgnoreCase(sourceCode,
963 JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
964 sourceCode = sourceCode.substring(JavaScriptURLConnection.JAVASCRIPT_PREFIX.length()).trim();
965 if (sourceCode.startsWith("return ")) {
966 sourceCode = sourceCode.substring("return ".length());
967 }
968 }
969
970 final Window window = getEnclosingWindow().getScriptableObject();
971 final VarScope scope = ScriptableObject.getTopLevelScope(window.getParentScope());
972
973 final Object result = getWebClient().getJavaScriptEngine()
974 .execute(this, scope, sourceCode, sourceName, startLine);
975 return new ScriptResult(result);
976 }
977
978
979 enum JavaScriptLoadResult {
980
981 NOOP,
982
983 NO_CONTENT,
984
985 SUCCESS,
986
987 DOWNLOAD_ERROR,
988
989 COMPILATION_ERROR
990 }
991
992
993
994
995
996
997
998
999
1000
1001
1002 JavaScriptLoadResult loadExternalJavaScriptFile(final String srcAttribute, final Charset scriptCharset)
1003 throws FailingHttpStatusCodeException {
1004
1005 final WebClient client = getWebClient();
1006 if (org.htmlunit.util.StringUtils.isBlank(srcAttribute) || !client.isJavaScriptEnabled()) {
1007 return JavaScriptLoadResult.NOOP;
1008 }
1009
1010 final URL scriptURL;
1011 try {
1012 scriptURL = getFullyQualifiedUrl(srcAttribute);
1013 final String protocol = scriptURL.getProtocol();
1014 if ("javascript".equals(protocol)) {
1015 if (LOG.isInfoEnabled()) {
1016 LOG.info("Ignoring script src [" + srcAttribute + "]");
1017 }
1018 return JavaScriptLoadResult.NOOP;
1019 }
1020 if (!"http".equals(protocol) && !"https".equals(protocol)
1021 && !"data".equals(protocol) && !"file".equals(protocol)) {
1022 client.getJavaScriptErrorListener().malformedScriptURL(this, srcAttribute,
1023 new MalformedURLException("unknown protocol: '" + protocol + "'"));
1024 return JavaScriptLoadResult.NOOP;
1025 }
1026 }
1027 catch (final MalformedURLException e) {
1028 client.getJavaScriptErrorListener().malformedScriptURL(this, srcAttribute, e);
1029 return JavaScriptLoadResult.NOOP;
1030 }
1031
1032 final Object script;
1033 try {
1034 script = loadJavaScriptFromUrl(scriptURL, scriptCharset);
1035 }
1036 catch (final IOException e) {
1037 client.getJavaScriptErrorListener().loadScriptError(this, scriptURL, e);
1038 return JavaScriptLoadResult.DOWNLOAD_ERROR;
1039 }
1040 catch (final FailingHttpStatusCodeException e) {
1041 if (e.getStatusCode() == HttpStatus.NO_CONTENT_204) {
1042 return JavaScriptLoadResult.NO_CONTENT;
1043 }
1044 client.getJavaScriptErrorListener().loadScriptError(this, scriptURL, e);
1045 throw e;
1046 }
1047
1048 if (script == null) {
1049 return JavaScriptLoadResult.COMPILATION_ERROR;
1050 }
1051
1052 final Window window = getEnclosingWindow().getScriptableObject();
1053 final VarScope scope = ScriptableObject.getTopLevelScope(window.getParentScope());
1054
1055 @SuppressWarnings("unchecked")
1056 final AbstractJavaScriptEngine<Object> engine = (AbstractJavaScriptEngine<Object>) client.getJavaScriptEngine();
1057 engine.execute(this, scope, script);
1058 return JavaScriptLoadResult.SUCCESS;
1059 }
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073 private Object loadJavaScriptFromUrl(final URL url, final Charset scriptCharset) throws IOException,
1074 FailingHttpStatusCodeException {
1075
1076 final WebRequest referringRequest = getWebResponse().getWebRequest();
1077
1078 final WebClient client = getWebClient();
1079 final WebRequest request = new WebRequest(url);
1080
1081 request.setAdditionalHeaders(new HashMap<>(referringRequest.getAdditionalHeaders()));
1082
1083
1084 request.setAdditionalHeader(HttpHeader.ACCEPT, client.getBrowserVersion().getScriptAcceptHeader());
1085 request.setAdditionalHeader(HttpHeader.SEC_FETCH_SITE, "same-origin");
1086 request.setAdditionalHeader(HttpHeader.SEC_FETCH_MODE, "no-cors");
1087 request.setAdditionalHeader(HttpHeader.SEC_FETCH_DEST, "script");
1088
1089 request.setRefererHeader(referringRequest.getUrl());
1090 request.setCharset(scriptCharset);
1091
1092
1093
1094 if (scriptCharset != null) {
1095 request.setDefaultResponseContentCharset(scriptCharset);
1096 }
1097 else {
1098 request.setDefaultResponseContentCharset(StandardCharsets.UTF_8);
1099 }
1100
1101
1102
1103
1104 final WebResponse response = client.loadWebResponse(request);
1105
1106
1107
1108 final Cache cache = client.getCache();
1109 final Object cachedScript = cache.getCachedObject(request);
1110 if (cachedScript instanceof Script) {
1111 return cachedScript;
1112 }
1113
1114 client.printContentIfNecessary(response);
1115 client.throwFailingHttpStatusCodeExceptionIfNecessary(response);
1116
1117 final int statusCode = response.getStatusCode();
1118 if (statusCode == HttpStatus.NO_CONTENT_204) {
1119 throw new FailingHttpStatusCodeException(response);
1120 }
1121
1122 if (!response.isSuccess()) {
1123 throw new IOException("Unable to download JavaScript from '" + url + "' (status " + statusCode + ").");
1124 }
1125
1126 final String contentType = response.getContentType();
1127 if (contentType != null) {
1128 if (MimeType.isObsoleteJavascriptMimeType(contentType)) {
1129 getWebClient().getIncorrectnessListener().notify(
1130 "Obsolete content type encountered: '" + contentType + "' "
1131 + "for remotely loaded JavaScript element at '" + url + "'.", this);
1132 }
1133 else if (!MimeType.isJavascriptMimeType(contentType)) {
1134 getWebClient().getIncorrectnessListener().notify(
1135 "Expect content type of '" + MimeType.TEXT_JAVASCRIPT + "' "
1136 + "for remotely loaded JavaScript element at '" + url + "', "
1137 + "but got '" + contentType + "'.", this);
1138 }
1139 }
1140
1141 final Charset scriptEncoding = response.getContentCharset();
1142 final String scriptCode = response.getContentAsString(scriptEncoding);
1143 if (null != scriptCode) {
1144 final AbstractJavaScriptEngine<?> javaScriptEngine = client.getJavaScriptEngine();
1145
1146 final Window window = getEnclosingWindow().getScriptableObject();
1147 final VarScope scope = ScriptableObject.getTopLevelScope(window.getParentScope());
1148
1149 final Object script = javaScriptEngine.compile(this, scope, scriptCode, url.toExternalForm(), 1);
1150 if (script != null && cache.cacheIfPossible(request, response, script)) {
1151
1152 return script;
1153 }
1154
1155 response.cleanUp();
1156 return script;
1157 }
1158
1159 response.cleanUp();
1160 return null;
1161 }
1162
1163
1164
1165
1166
1167
1168 public String getTitleText() {
1169 final HtmlTitle titleElement = getTitleElement();
1170 if (titleElement != null) {
1171 return titleElement.asNormalizedText();
1172 }
1173 return "";
1174 }
1175
1176
1177
1178
1179
1180
1181 public void setTitleText(final String message) {
1182 HtmlTitle titleElement = getTitleElement();
1183 if (titleElement == null) {
1184 LOG.debug("No title element, creating one");
1185 final HtmlHead head = (HtmlHead) getFirstChildElement(getDocumentElement(), HtmlHead.class);
1186 if (head == null) {
1187
1188 throw new IllegalStateException("Headelement was not defined for this page");
1189 }
1190 final Map<String, DomAttr> emptyMap = Collections.emptyMap();
1191 titleElement = new HtmlTitle(HtmlTitle.TAG_NAME, this, emptyMap);
1192 if (head.getFirstChild() != null) {
1193 head.getFirstChild().insertBefore(titleElement);
1194 }
1195 else {
1196 head.appendChild(titleElement);
1197 }
1198 }
1199
1200 titleElement.setNodeValue(message);
1201 }
1202
1203
1204
1205
1206
1207
1208
1209 private static DomElement getFirstChildElement(final DomElement startElement, final Class<?> clazz) {
1210 if (startElement == null) {
1211 return null;
1212 }
1213 for (final DomElement element : startElement.getChildElements()) {
1214 if (clazz.isInstance(element)) {
1215 return element;
1216 }
1217 }
1218
1219 return null;
1220 }
1221
1222
1223
1224
1225
1226
1227
1228 private DomElement getFirstChildElementRecursive(final DomElement startElement, final Class<?> clazz) {
1229 if (startElement == null) {
1230 return null;
1231 }
1232 for (final DomElement element : startElement.getChildElements()) {
1233 if (clazz.isInstance(element)) {
1234 return element;
1235 }
1236 final DomElement childFound = getFirstChildElementRecursive(element, clazz);
1237 if (childFound != null) {
1238 return childFound;
1239 }
1240 }
1241
1242 return null;
1243 }
1244
1245
1246
1247
1248
1249
1250 private HtmlTitle getTitleElement() {
1251 return (HtmlTitle) getFirstChildElementRecursive(getDocumentElement(), HtmlTitle.class);
1252 }
1253
1254
1255
1256
1257
1258
1259 private boolean executeEventHandlersIfNeeded(final String eventType) {
1260
1261 if (!getWebClient().isJavaScriptEnabled()) {
1262 return true;
1263 }
1264
1265
1266 final WebWindow window = getEnclosingWindow();
1267 if (window.getScriptableObject() instanceof Window) {
1268 final Event event;
1269 if (Event.TYPE_BEFORE_UNLOAD.equals(eventType)) {
1270 event = new BeforeUnloadEvent(this, eventType);
1271 }
1272 else {
1273 event = new Event(this, eventType);
1274 }
1275
1276
1277
1278 if (LOG.isDebugEnabled()) {
1279 LOG.debug("Firing " + event);
1280 }
1281
1282 final EventTarget jsNode;
1283 if (Event.TYPE_DOM_DOCUMENT_LOADED.equals(eventType)) {
1284 jsNode = getScriptableObject();
1285 }
1286 else if (Event.TYPE_READY_STATE_CHANGE.equals(eventType)) {
1287 jsNode = getDocumentElement().getScriptableObject();
1288 }
1289 else {
1290
1291 jsNode = window.getScriptableObject();
1292 }
1293
1294 ((JavaScriptEngine) getWebClient().getJavaScriptEngine()).callSecured(cx -> jsNode.fireEvent(event), this);
1295
1296 if (!isOnbeforeunloadAccepted(this, event)) {
1297 return false;
1298 }
1299 }
1300
1301
1302 if (window instanceof FrameWindow fw) {
1303 final BaseFrameElement frame = fw.getFrameElement();
1304
1305
1306 if (Event.TYPE_LOAD.equals(eventType) && frame.getParentNode() instanceof DomDocumentFragment) {
1307 return true;
1308 }
1309
1310 if (frame.hasEventHandlers("on" + eventType)) {
1311 if (LOG.isDebugEnabled()) {
1312 LOG.debug("Executing on" + eventType + " handler for " + frame);
1313 }
1314 if (window.getScriptableObject() instanceof Window) {
1315 final Event event;
1316 if (Event.TYPE_BEFORE_UNLOAD.equals(eventType)) {
1317 event = new BeforeUnloadEvent(frame, eventType);
1318 }
1319 else {
1320 event = new Event(frame, eventType);
1321 }
1322
1323
1324
1325
1326 frame.fireEvent(event);
1327
1328 if (!isOnbeforeunloadAccepted((HtmlPage) frame.getPage(), event)) {
1329 return false;
1330 }
1331 }
1332 }
1333 }
1334
1335 return true;
1336 }
1337
1338
1339
1340
1341
1342
1343 public boolean isOnbeforeunloadAccepted() {
1344 return executeEventHandlersIfNeeded(Event.TYPE_BEFORE_UNLOAD);
1345 }
1346
1347 private boolean isOnbeforeunloadAccepted(final HtmlPage page, final Event event) {
1348 if (event instanceof BeforeUnloadEvent beforeUnloadEvent) {
1349 if (beforeUnloadEvent.isBeforeUnloadMessageSet()) {
1350 final OnbeforeunloadHandler handler = getWebClient().getOnbeforeunloadHandler();
1351 if (handler == null) {
1352 LOG.warn("document.onbeforeunload() returned a string in event.returnValue,"
1353 + " but no onbeforeunload handler installed.");
1354 }
1355 else {
1356 final String message = JavaScriptEngine.toString(beforeUnloadEvent.getReturnValue());
1357 return handler.handleEvent(page, message);
1358 }
1359 }
1360 }
1361 return true;
1362 }
1363
1364
1365
1366
1367
1368
1369 private void executeRefreshIfNeeded() throws IOException {
1370
1371
1372
1373 final WebWindow window = getEnclosingWindow();
1374 if (window == null) {
1375 return;
1376 }
1377
1378 final String refreshString = getRefreshStringOrNull();
1379 if (refreshString == null || refreshString.isEmpty()) {
1380 return;
1381 }
1382
1383 final double time;
1384 final URL url;
1385
1386 final int index = StringUtils.indexOfAnyBut(refreshString, "0123456789.");
1387
1388 if (index == -1) {
1389
1390 try {
1391 time = Double.parseDouble(refreshString);
1392 }
1393 catch (final NumberFormatException e) {
1394 if (LOG.isErrorEnabled()) {
1395 LOG.error("Malformed refresh string (no ';' but not a number): " + refreshString, e);
1396 }
1397 return;
1398 }
1399 url = getUrl();
1400 }
1401 else {
1402
1403 try {
1404 time = Double.parseDouble(refreshString.substring(0, index));
1405 }
1406 catch (final NumberFormatException e) {
1407 if (LOG.isErrorEnabled()) {
1408 LOG.error("Malformed refresh string (no valid number before ';') " + refreshString, e);
1409 }
1410 return;
1411 }
1412
1413 String urlPart = refreshString.substring(index);
1414 final char separator = urlPart.charAt(0);
1415 if (";, \r\n\t".indexOf(separator) >= 0) {
1416 urlPart = StringUtils.stripStart(urlPart, ";, \r\n\t");
1417 if (urlPart.toLowerCase(Locale.ROOT).startsWith("url")) {
1418 urlPart = urlPart.substring(3);
1419 urlPart = urlPart.trim();
1420
1421 if (urlPart.toLowerCase().startsWith("=")) {
1422 urlPart = urlPart.substring(1);
1423 urlPart = urlPart.trim();
1424 }
1425 }
1426
1427 if (org.htmlunit.util.StringUtils.isBlank(urlPart)) {
1428
1429 url = getUrl();
1430 }
1431 else {
1432 if (urlPart.charAt(0) == '"' || urlPart.charAt(0) == 0x27) {
1433 urlPart = urlPart.substring(1);
1434 }
1435 if (urlPart.charAt(urlPart.length() - 1) == '"' || urlPart.charAt(urlPart.length() - 1) == 0x27) {
1436 urlPart = urlPart.substring(0, urlPart.length() - 1);
1437 }
1438 try {
1439 url = getFullyQualifiedUrl(urlPart);
1440 }
1441 catch (final MalformedURLException e) {
1442 if (LOG.isErrorEnabled()) {
1443 LOG.error("Malformed URL in refresh string: " + refreshString, e);
1444 }
1445 return;
1446 }
1447 }
1448 }
1449 else {
1450 if (LOG.isErrorEnabled()) {
1451 LOG.error("Malformed refresh string (separator after time missing): " + refreshString);
1452 }
1453 return;
1454 }
1455 }
1456
1457 processRefresh(url, time);
1458 }
1459
1460
1461
1462
1463 private void processRefresh(final URL url, final double time) throws IOException {
1464 final WebClient webClient = getWebClient();
1465
1466 final int refreshLimit = webClient.getOptions().getPageRefreshLimit();
1467 if (refreshLimit == 0) {
1468 final WebResponse webResponse = getWebResponse();
1469 throw new FailingHttpStatusCodeException("Too many redirects for "
1470 + webResponse.getWebRequest().getUrl(), webResponse);
1471 }
1472
1473 if (refreshLimit >= 0) {
1474 final StackTraceElement[] elements = new Exception().getStackTrace();
1475 int count = 0;
1476 final int elementCountLimit = refreshLimit > 50 ? 400 : refreshLimit > 10 ? 80 : 5;
1477 final int elementCount = elements.length;
1478
1479 if (elementCount > elementCountLimit) {
1480 for (int i = 0; i < elementCount; i++) {
1481 if ("processRefresh".equals(elements[i].getMethodName())
1482 && "org.htmlunit.html.HtmlPage".equals(elements[i].getClassName())) {
1483 count++;
1484 if (count >= refreshLimit) {
1485 final WebResponse webResponse = getWebResponse();
1486 throw new FailingHttpStatusCodeException(
1487 "Too many redirects (>= " + count + ") for "
1488 + webResponse.getWebRequest().getUrl(), webResponse);
1489 }
1490 }
1491 }
1492 }
1493 }
1494
1495 webClient.getRefreshHandler().handleRefresh(this, url, (int) time);
1496 }
1497
1498
1499
1500
1501
1502
1503 private String getRefreshStringOrNull() {
1504 final List<HtmlMeta> metaTags = getMetaTags("refresh");
1505 if (!metaTags.isEmpty()) {
1506 return metaTags.get(0).getContentAttribute().trim();
1507 }
1508 return getWebResponse().getResponseHeaderValue("Refresh");
1509 }
1510
1511 private void processPostponedActionsIfNeeded() {
1512 if (!getWebClient().isJavaScriptEnabled()) {
1513 return;
1514 }
1515 getWebClient().getJavaScriptEngine().processPostponedActions();
1516 }
1517
1518
1519
1520
1521 private void executeDeferredScriptsIfNeeded() {
1522 if (!getWebClient().isJavaScriptEnabled()) {
1523 return;
1524 }
1525 final DomElement doc = getDocumentElement();
1526 final List<HtmlScript> scripts = new ArrayList<>();
1527
1528
1529 for (final HtmlElement elem : doc.getHtmlElementDescendants()) {
1530 if ("script".equals(elem.getLocalName()) && (elem instanceof HtmlScript script)) {
1531 if (script.isDeferred() && ATTRIBUTE_NOT_DEFINED != script.getSrcAttribute()) {
1532 scripts.add(script);
1533 }
1534 }
1535 }
1536 for (final HtmlScript script : scripts) {
1537 ScriptElementSupport.executeScriptIfNeeded(script, true, true);
1538 }
1539 }
1540
1541
1542
1543
1544 public void deregisterFramesIfNeeded() {
1545 final List<BaseFrameElement> frameElementsCopy = new ArrayList<>(frameElements_);
1546 for (final BaseFrameElement frameElement : frameElementsCopy) {
1547 final WebWindow window = frameElement.getEnclosedWindow();
1548 getWebClient().deregisterWebWindow(window);
1549 final Page page = window.getEnclosedPage();
1550 if (page != null && page.isHtmlPage()) {
1551
1552
1553 ((HtmlPage) page).deregisterFramesIfNeeded();
1554 }
1555 }
1556 }
1557
1558
1559
1560
1561
1562
1563 public List<FrameWindow> getFrames() {
1564 final List<BaseFrameElement> frameElements = new ArrayList<>(frameElements_);
1565 frameElements.sort(DOCUMENT_POSITION_COMPERATOR);
1566
1567 final List<FrameWindow> list = new ArrayList<>(frameElements.size());
1568 for (final BaseFrameElement frameElement : frameElements) {
1569 list.add(frameElement.getEnclosedWindow());
1570 }
1571 return list;
1572 }
1573
1574
1575
1576
1577
1578
1579
1580 public FrameWindow getFrameByName(final String name) throws ElementNotFoundException {
1581 for (final BaseFrameElement frameElement : frameElements_) {
1582 final FrameWindow fw = frameElement.getEnclosedWindow();
1583 if (fw.getName().equals(name)) {
1584 return fw;
1585 }
1586 }
1587
1588 throw new ElementNotFoundException("frame or iframe", DomElement.NAME_ATTRIBUTE, name);
1589 }
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601 public DomElement pressAccessKey(final char accessKey) throws IOException {
1602 final HtmlElement element = getHtmlElementByAccessKey(accessKey);
1603 if (element != null) {
1604 element.focus();
1605 if (element instanceof HtmlAnchor
1606 || element instanceof HtmlArea
1607 || element instanceof HtmlButton
1608 || element instanceof HtmlInput
1609 || element instanceof HtmlLabel
1610 || element instanceof HtmlLegend
1611 || element instanceof HtmlTextArea) {
1612 final Page newPage = element.click();
1613
1614 if (newPage != this && getFocusedElement() == element) {
1615
1616 getFocusedElement().blur();
1617 }
1618 }
1619 }
1620
1621 return getFocusedElement();
1622 }
1623
1624
1625
1626
1627
1628
1629
1630 public HtmlElement tabToNextElement() {
1631 final List<HtmlElement> elements = getTabbableElements();
1632 if (elements.isEmpty()) {
1633 setFocusedElement(null);
1634 return null;
1635 }
1636
1637 final HtmlElement elementToGiveFocus;
1638 final DomElement elementWithFocus = getFocusedElement();
1639 if (elementWithFocus == null) {
1640 elementToGiveFocus = elements.get(0);
1641 }
1642 else {
1643 final int index = elements.indexOf(elementWithFocus);
1644 if (index == -1) {
1645
1646 elementToGiveFocus = elements.get(0);
1647 }
1648 else if (index == elements.size() - 1) {
1649
1650 elementToGiveFocus = elements.get(0);
1651 }
1652 else {
1653 elementToGiveFocus = elements.get(index + 1);
1654 }
1655 }
1656
1657 setFocusedElement(elementToGiveFocus);
1658 return elementToGiveFocus;
1659 }
1660
1661
1662
1663
1664
1665
1666
1667 public HtmlElement tabToPreviousElement() {
1668 final List<HtmlElement> elements = getTabbableElements();
1669 if (elements.isEmpty()) {
1670 setFocusedElement(null);
1671 return null;
1672 }
1673
1674 final HtmlElement elementToGiveFocus;
1675 final DomElement elementWithFocus = getFocusedElement();
1676 if (elementWithFocus == null) {
1677 elementToGiveFocus = elements.get(elements.size() - 1);
1678 }
1679 else {
1680 final int index = elements.indexOf(elementWithFocus);
1681 if (index == -1) {
1682
1683 elementToGiveFocus = elements.get(elements.size() - 1);
1684 }
1685 else if (index == 0) {
1686
1687 elementToGiveFocus = elements.get(elements.size() - 1);
1688 }
1689 else {
1690 elementToGiveFocus = elements.get(index - 1);
1691 }
1692 }
1693
1694 setFocusedElement(elementToGiveFocus);
1695 return elementToGiveFocus;
1696 }
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708 @SuppressWarnings("unchecked")
1709 public <E extends HtmlElement> E getHtmlElementById(final String elementId) throws ElementNotFoundException {
1710 final DomElement element = getElementById(elementId);
1711 if (element == null) {
1712 throw new ElementNotFoundException("*", DomElement.ID_ATTRIBUTE, elementId);
1713 }
1714 return (E) element;
1715 }
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725 public List<DomElement> getElementsById(final String elementId) {
1726 if (elementId != null) {
1727 ensureMappedElementsBuilt();
1728 final MappedElementIndexEntry elements = idMap_.get(elementId);
1729 if (elements != null) {
1730 return new ArrayList<>(elements.elements());
1731 }
1732 }
1733 return Collections.emptyList();
1734 }
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745 @SuppressWarnings("unchecked")
1746 public <E extends DomElement> E getElementByName(final String name) throws ElementNotFoundException {
1747 if (name != null) {
1748 ensureMappedElementsBuilt();
1749 final MappedElementIndexEntry elements = nameMap_.get(name);
1750 if (elements != null) {
1751 return (E) elements.first();
1752 }
1753 }
1754 throw new ElementNotFoundException("*", DomElement.NAME_ATTRIBUTE, name);
1755 }
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765 public List<DomElement> getElementsByName(final String name) {
1766 if (name != null) {
1767 ensureMappedElementsBuilt();
1768 final MappedElementIndexEntry elements = nameMap_.get(name);
1769 if (elements != null) {
1770 return new ArrayList<>(elements.elements());
1771 }
1772 }
1773 return Collections.emptyList();
1774 }
1775
1776
1777
1778
1779
1780
1781
1782
1783 public List<DomElement> getElementsByIdAndOrName(final String idAndOrName) {
1784 if (idAndOrName == null) {
1785 return Collections.emptyList();
1786 }
1787 ensureMappedElementsBuilt();
1788 final MappedElementIndexEntry list1 = idMap_.get(idAndOrName);
1789 final MappedElementIndexEntry list2 = nameMap_.get(idAndOrName);
1790 final List<DomElement> list = new ArrayList<>();
1791 if (list1 != null) {
1792 list.addAll(list1.elements());
1793 }
1794 if (list2 != null) {
1795 for (final DomElement elt : list2.elements()) {
1796 if (!list.contains(elt)) {
1797 list.add(elt);
1798 }
1799 }
1800 }
1801 return list;
1802 }
1803
1804
1805
1806
1807
1808
1809 void notifyNodeAdded(final DomNode node) {
1810 if (node instanceof DomElement element1) {
1811 addMappedElement(element1, true);
1812
1813 if (node instanceof BaseFrameElement element) {
1814 frameElements_.add(element);
1815 }
1816
1817 if (node.getFirstChild() != null) {
1818 for (final Iterator<HtmlElement> iterator = node.new DescendantHtmlElementsIterator();
1819 iterator.hasNext();) {
1820 final HtmlElement child = iterator.next();
1821 if (child instanceof BaseFrameElement element) {
1822 frameElements_.add(element);
1823 }
1824 }
1825 }
1826
1827 if ("base".equals(node.getNodeName())) {
1828 calculateBase();
1829 }
1830 }
1831 node.onAddedToPage();
1832 }
1833
1834
1835
1836
1837
1838
1839 void notifyNodeRemoved(final DomNode node) {
1840 if (node instanceof HtmlElement element) {
1841 removeMappedElement(element, true, true);
1842
1843 if (node instanceof BaseFrameElement) {
1844 frameElements_.remove(node);
1845 }
1846 for (final HtmlElement child : node.getHtmlElementDescendants()) {
1847 if (child instanceof BaseFrameElement) {
1848 frameElements_.remove(child);
1849 }
1850 }
1851
1852 if ("base".equals(node.getNodeName())) {
1853 calculateBase();
1854 }
1855 }
1856 }
1857
1858
1859
1860
1861
1862
1863 void addMappedElement(final DomElement element, final boolean recurse) {
1864
1865
1866 if (!mappedElementsBuilt_) {
1867 return;
1868 }
1869 if (isAncestorOf(element)) {
1870 addElement(element, recurse);
1871 }
1872 }
1873
1874 private void ensureMappedElementsBuilt() {
1875 if (mappedElementsBuilt_) {
1876 return;
1877 }
1878
1879 final DomElement root = getDocumentElement();
1880 if (root != null) {
1881 addElement(root, true);
1882 }
1883
1884
1885
1886
1887 mappedElementsBuilt_ = true;
1888 }
1889
1890 private void addElement(final DomElement element, final boolean recurse) {
1891 final String idValue = element.getAttribute(DomElement.ID_ATTRIBUTE);
1892 if (ATTRIBUTE_NOT_DEFINED != idValue) {
1893 MappedElementIndexEntry elements = idMap_.get(idValue);
1894 if (elements == null) {
1895 elements = new MappedElementIndexEntry();
1896 elements.add(element);
1897 idMap_.put(idValue, elements);
1898 }
1899 else {
1900 elements.add(element);
1901 }
1902 }
1903
1904 final String nameValue = element.getAttribute(DomElement.NAME_ATTRIBUTE);
1905 if (ATTRIBUTE_NOT_DEFINED != nameValue) {
1906 MappedElementIndexEntry elements = nameMap_.get(nameValue);
1907 if (elements == null) {
1908 elements = new MappedElementIndexEntry();
1909 elements.add(element);
1910 nameMap_.put(nameValue, elements);
1911 }
1912 else {
1913 elements.add(element);
1914 }
1915 }
1916
1917 if (recurse) {
1918
1919
1920 DomNode nextChild = element.getFirstChild();
1921 while (nextChild != null) {
1922 if (nextChild instanceof DomElement domElement) {
1923 addElement(domElement, true);
1924 }
1925 nextChild = nextChild.getNextSibling();
1926 }
1927 }
1928 }
1929
1930
1931
1932
1933
1934
1935
1936 void removeMappedElement(final DomElement element, final boolean recurse, final boolean descendant) {
1937
1938 if (!mappedElementsBuilt_) {
1939 return;
1940 }
1941 if (descendant || isAncestorOf(element)) {
1942 removeElement(element, recurse);
1943 }
1944 }
1945
1946 private void removeElement(final DomElement element, final boolean recurse) {
1947 final String idValue = element.getAttribute(DomElement.ID_ATTRIBUTE);
1948 if (ATTRIBUTE_NOT_DEFINED != idValue) {
1949 final MappedElementIndexEntry elements = idMap_.remove(idValue);
1950 if (elements != null) {
1951 elements.remove(element);
1952 if (!elements.elements_.isEmpty()) {
1953 idMap_.put(idValue, elements);
1954 }
1955 }
1956 }
1957
1958 final String nameValue = element.getAttribute(DomElement.NAME_ATTRIBUTE);
1959 if (ATTRIBUTE_NOT_DEFINED != nameValue) {
1960 final MappedElementIndexEntry elements = nameMap_.remove(nameValue);
1961 if (elements != null) {
1962 elements.remove(element);
1963 if (!elements.elements_.isEmpty()) {
1964 nameMap_.put(nameValue, elements);
1965 }
1966 }
1967 }
1968
1969 if (recurse) {
1970 for (final DomElement child : element.getChildElements()) {
1971 removeElement(child, true);
1972 }
1973 }
1974 }
1975
1976
1977
1978
1979
1980
1981
1982 static boolean isMappedElement(final Document document, final String attributeName) {
1983 return document instanceof HtmlPage
1984 && (DomElement.NAME_ATTRIBUTE.equals(attributeName) || DomElement.ID_ATTRIBUTE.equals(attributeName));
1985 }
1986
1987 private void calculateBase() {
1988 final List<HtmlElement> baseElements = getDocumentElement().getStaticElementsByTagName("base");
1989
1990 base_ = null;
1991 for (final HtmlElement baseElement : baseElements) {
1992 if (baseElement instanceof HtmlBase base) {
1993 if (base_ != null) {
1994 notifyIncorrectness("Multiple 'base' detected, only the first is used.");
1995 break;
1996 }
1997 base_ = base;
1998 }
1999 }
2000 }
2001
2002
2003
2004
2005
2006
2007
2008 void loadFrames() throws FailingHttpStatusCodeException {
2009 for (final BaseFrameElement frameElement : new ArrayList<>(frameElements_)) {
2010
2011
2012
2013 if (frameElement.getEnclosedWindow() != null
2014 && UrlUtils.URL_ABOUT_BLANK == frameElement.getEnclosedPage().getUrl()
2015 && !frameElement.isContentLoaded()) {
2016 frameElement.loadInnerPage();
2017 }
2018 }
2019 }
2020
2021
2022
2023
2024
2025 @Override
2026 public String toString() {
2027 final StringBuilder builder = new StringBuilder()
2028 .append("HtmlPage(")
2029 .append(getUrl())
2030 .append(")@")
2031 .append(hashCode());
2032 return builder.toString();
2033 }
2034
2035
2036
2037
2038
2039
2040 protected List<HtmlMeta> getMetaTags(final String httpEquiv) {
2041 if (getDocumentElement() == null) {
2042 return Collections.emptyList();
2043 }
2044 final List<HtmlMeta> tags = getDocumentElement().getStaticElementsByTagName("meta");
2045 final List<HtmlMeta> foundTags = new ArrayList<>();
2046 for (final HtmlMeta htmlMeta : tags) {
2047 if (httpEquiv.equalsIgnoreCase(htmlMeta.getHttpEquivAttribute())) {
2048 foundTags.add(htmlMeta);
2049 }
2050 }
2051 return foundTags;
2052 }
2053
2054
2055
2056
2057
2058
2059 @Override
2060 protected HtmlPage clone() {
2061 final HtmlPage result = (HtmlPage) super.clone();
2062 result.elementWithFocus_ = null;
2063
2064 result.idMap_ = new ConcurrentHashMap<>();
2065 result.nameMap_ = new ConcurrentHashMap<>();
2066 result.mappedElementsBuilt_ = false;
2067
2068 return result;
2069 }
2070
2071
2072
2073
2074 @Override
2075 public HtmlPage cloneNode(final boolean deep) {
2076
2077 final HtmlPage result = (HtmlPage) super.cloneNode(false);
2078 if (getWebClient().isJavaScriptEnabled()) {
2079 final HtmlUnitScriptable jsObjClone = getScriptableObject().clone();
2080 jsObjClone.setDomNode(result);
2081 }
2082
2083
2084 if (deep) {
2085
2086
2087
2088 result.attributeListeners_ = null;
2089
2090 result.selectionRanges_ = new ArrayList<>(3);
2091
2092 result.afterLoadActions_ = Collections.synchronizedList(new ArrayList<>());
2093 result.frameElements_ = new ArrayList<>();
2094 for (DomNode child = getFirstChild(); child != null; child = child.getNextSibling()) {
2095 result.appendChild(child.cloneNode(true));
2096 }
2097 }
2098 return result;
2099 }
2100
2101
2102
2103
2104
2105
2106
2107
2108 public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
2109 WebAssert.notNull("listener", listener);
2110 synchronized (lock_) {
2111 if (attributeListeners_ == null) {
2112 attributeListeners_ = new LinkedHashSet<>();
2113 }
2114 attributeListeners_.add(listener);
2115 }
2116 }
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126 public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
2127 WebAssert.notNull("listener", listener);
2128 synchronized (lock_) {
2129 if (attributeListeners_ != null) {
2130 attributeListeners_.remove(listener);
2131 }
2132 }
2133 }
2134
2135
2136
2137
2138
2139 void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
2140 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2141 if (listeners != null) {
2142 for (final HtmlAttributeChangeListener listener : listeners) {
2143 listener.attributeAdded(event);
2144 }
2145 }
2146 }
2147
2148
2149
2150
2151
2152 void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
2153 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2154 if (listeners != null) {
2155 for (final HtmlAttributeChangeListener listener : listeners) {
2156 listener.attributeReplaced(event);
2157 }
2158 }
2159 }
2160
2161
2162
2163
2164
2165 void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
2166 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2167 if (listeners != null) {
2168 for (final HtmlAttributeChangeListener listener : listeners) {
2169 listener.attributeRemoved(event);
2170 }
2171 }
2172 }
2173
2174 private List<HtmlAttributeChangeListener> safeGetAttributeListeners() {
2175 synchronized (lock_) {
2176 if (attributeListeners_ != null) {
2177 return new ArrayList<>(attributeListeners_);
2178 }
2179 return null;
2180 }
2181 }
2182
2183
2184
2185
2186 @Override
2187 protected void checkChildHierarchy(final org.w3c.dom.Node newChild) throws DOMException {
2188 if (newChild instanceof Element) {
2189 if (getDocumentElement() != null) {
2190 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2191 "The Document may only have a single child Element.");
2192 }
2193 }
2194 else if (newChild instanceof DocumentType) {
2195 if (getDoctype() != null) {
2196 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2197 "The Document may only have a single child DocumentType.");
2198 }
2199 }
2200 else if (!(newChild instanceof Comment || newChild instanceof ProcessingInstruction)) {
2201 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2202 "The Document may not have a child of this type: " + newChild.getNodeType());
2203 }
2204 super.checkChildHierarchy(newChild);
2205 }
2206
2207
2208
2209
2210
2211 public boolean isBeingParsed() {
2212 return parserCount_ > 0;
2213 }
2214
2215
2216
2217
2218
2219
2220 public void registerParsingStart() {
2221 parserCount_++;
2222 }
2223
2224
2225
2226
2227
2228
2229 public void registerParsingEnd() {
2230 parserCount_--;
2231 }
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245 public boolean isParsingHtmlSnippet() {
2246 return snippetParserCount_ > 0;
2247 }
2248
2249
2250
2251
2252
2253
2254 public void registerSnippetParsingStart() {
2255 snippetParserCount_++;
2256 }
2257
2258
2259
2260
2261
2262
2263 public void registerSnippetParsingEnd() {
2264 snippetParserCount_--;
2265 }
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277 public boolean isParsingInlineHtmlSnippet() {
2278 return inlineSnippetParserCount_ > 0;
2279 }
2280
2281
2282
2283
2284
2285
2286 public void registerInlineSnippetParsingStart() {
2287 inlineSnippetParserCount_++;
2288 }
2289
2290
2291
2292
2293
2294
2295 public void registerInlineSnippetParsingEnd() {
2296 inlineSnippetParserCount_--;
2297 }
2298
2299
2300
2301
2302
2303
2304 public Page refresh() throws IOException {
2305 return getWebClient().getPage(getWebResponse().getWebRequest());
2306 }
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316 public void writeInParsedStream(final String string) {
2317 getDOMBuilder().pushInputString(string);
2318 }
2319
2320
2321
2322
2323
2324
2325
2326 public void setDOMBuilder(final HTMLParserDOMBuilder htmlUnitDOMBuilder) {
2327 domBuilder_ = htmlUnitDOMBuilder;
2328 }
2329
2330
2331
2332
2333
2334
2335
2336 public HTMLParserDOMBuilder getDOMBuilder() {
2337 return domBuilder_;
2338 }
2339
2340
2341
2342
2343
2344
2345 public Map<String, String> getNamespaces() {
2346 final org.w3c.dom.NamedNodeMap attributes = getDocumentElement().getAttributes();
2347 final Map<String, String> namespaces = new HashMap<>();
2348 for (int i = 0; i < attributes.getLength(); i++) {
2349 final Attr attr = (Attr) attributes.item(i);
2350 String name = attr.getName();
2351 if (name.startsWith("xmlns")) {
2352 int startPos = 5;
2353 if (name.length() > 5 && name.charAt(5) == ':') {
2354 startPos = 6;
2355 }
2356 name = name.substring(startPos);
2357 namespaces.put(name, attr.getValue());
2358 }
2359 }
2360 return namespaces;
2361 }
2362
2363
2364
2365
2366 @Override
2367 public void setDocumentType(final DocumentType type) {
2368 super.setDocumentType(type);
2369 }
2370
2371
2372
2373
2374
2375
2376
2377
2378 public void save(final File file) throws IOException {
2379 new XmlSerializer().save(this, file);
2380 }
2381
2382
2383
2384
2385
2386 public boolean isQuirksMode() {
2387 return "BackCompat".equals(((HTMLDocument) getScriptableObject()).getCompatMode());
2388 }
2389
2390
2391
2392
2393
2394 @Override
2395 public boolean isAttachedToPage() {
2396 return true;
2397 }
2398
2399
2400
2401
2402 @Override
2403 public boolean isHtmlPage() {
2404 return true;
2405 }
2406
2407
2408
2409
2410
2411 public URL getBaseURL() {
2412 URL baseUrl;
2413 if (base_ == null) {
2414 baseUrl = getUrl();
2415 final WebWindow window = getEnclosingWindow();
2416 final boolean frame = window != null && window != window.getTopWindow();
2417 if (frame) {
2418 final boolean frameSrcIsNotSet = baseUrl == UrlUtils.URL_ABOUT_BLANK;
2419 final boolean frameSrcIsJs = "javascript".equals(baseUrl.getProtocol());
2420 if (frameSrcIsNotSet || frameSrcIsJs) {
2421 baseUrl = window.getTopWindow().getEnclosedPage().getWebResponse()
2422 .getWebRequest().getUrl();
2423 }
2424 }
2425 else if (baseUrl_ != null) {
2426 baseUrl = baseUrl_;
2427 }
2428 }
2429 else {
2430 final String href = base_.getHrefAttribute().trim();
2431 if (org.htmlunit.util.StringUtils.isEmptyOrNull(href)) {
2432 baseUrl = getUrl();
2433 }
2434 else {
2435 final URL url = getUrl();
2436 try {
2437 if (href.startsWith("http://") || href.startsWith("https://")) {
2438 baseUrl = new URL(href);
2439 }
2440 else if (href.startsWith("//")) {
2441 baseUrl = new URL("%s:%s".formatted(url.getProtocol(), href));
2442 }
2443 else if (href.length() > 0 && href.charAt(0) == '/') {
2444 final int port = Window.getPort(url);
2445 baseUrl = new URL("%s://%s:%d%s".formatted(url.getProtocol(), url.getHost(), port, href));
2446 }
2447 else if (url.toString().endsWith("/")) {
2448 baseUrl = new URL("%s%s".formatted(url, href));
2449 }
2450 else {
2451 baseUrl = new URL(UrlUtils.resolveUrl(url, href));
2452 }
2453 }
2454 catch (final MalformedURLException e) {
2455 notifyIncorrectness("Invalid base url: \"" + href + "\", ignoring it");
2456 baseUrl = url;
2457 }
2458 }
2459 }
2460
2461 return baseUrl;
2462 }
2463
2464
2465
2466
2467
2468
2469
2470 public void addAutoCloseable(final AutoCloseable autoCloseable) {
2471 if (autoCloseable == null) {
2472 return;
2473 }
2474
2475 if (autoCloseableList_ == null) {
2476 autoCloseableList_ = new ArrayList<>();
2477 }
2478 autoCloseableList_.add(autoCloseable);
2479 }
2480
2481
2482
2483
2484 @Override
2485 public boolean handles(final Event event) {
2486 if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
2487 return true;
2488 }
2489 return super.handles(event);
2490 }
2491
2492
2493
2494
2495
2496 public void setElementFromPointHandler(final ElementFromPointHandler elementFromPointHandler) {
2497 elementFromPointHandler_ = elementFromPointHandler;
2498 }
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509 public HtmlElement getElementFromPoint(final int x, final int y) {
2510 if (elementFromPointHandler_ == null) {
2511 if (LOG.isWarnEnabled()) {
2512 LOG.warn("ElementFromPointHandler was not specicifed for " + this);
2513 }
2514 if (x <= 0 || y <= 0) {
2515 return null;
2516 }
2517 return getBody();
2518 }
2519 return elementFromPointHandler_.getElementFromPoint(this, x, y);
2520 }
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530 public boolean setFocusedElement(final DomElement newElement) {
2531 return setFocusedElement(newElement, false);
2532 }
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543 public boolean setFocusedElement(final DomElement newElement, final boolean windowActivated) {
2544 if (elementWithFocus_ == newElement && !windowActivated) {
2545
2546 return true;
2547 }
2548
2549 final DomElement oldFocusedElement = elementWithFocus_;
2550 elementWithFocus_ = null;
2551
2552 if (!windowActivated) {
2553 if (oldFocusedElement != null) {
2554 oldFocusedElement.removeFocus();
2555 oldFocusedElement.fireEvent(Event.TYPE_BLUR);
2556
2557 oldFocusedElement.fireEvent(Event.TYPE_FOCUS_OUT);
2558 }
2559 }
2560
2561 elementWithFocus_ = newElement;
2562
2563
2564
2565 if (newElement != null) {
2566 newElement.focus();
2567 newElement.fireEvent(Event.TYPE_FOCUS);
2568
2569 newElement.fireEvent(Event.TYPE_FOCUS_IN);
2570 }
2571
2572
2573
2574 return this == getEnclosingWindow().getEnclosedPage();
2575 }
2576
2577
2578
2579
2580
2581
2582 public DomElement getFocusedElement() {
2583 return elementWithFocus_;
2584 }
2585
2586
2587
2588
2589
2590
2591
2592 public void setElementWithFocus(final DomElement elementWithFocus) {
2593 elementWithFocus_ = elementWithFocus;
2594 }
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604 public HtmlElement getActiveElement() {
2605 final DomElement activeElement = getFocusedElement();
2606 if (activeElement instanceof HtmlElement element) {
2607 return element;
2608 }
2609
2610 final HtmlElement body = getBody();
2611 if (body != null) {
2612 return body;
2613 }
2614 return null;
2615 }
2616
2617
2618
2619
2620
2621
2622
2623
2624 public List<SimpleRange> getSelectionRanges() {
2625 return selectionRanges_;
2626 }
2627
2628
2629
2630
2631
2632
2633
2634
2635 public void setSelectionRange(final SimpleRange selectionRange) {
2636 selectionRanges_.clear();
2637 selectionRanges_.add(selectionRange);
2638 }
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654 public ScriptResult executeJavaScriptFunction(final Object function, final Object thisObject,
2655 final Object[] args, final DomNode htmlElement) {
2656 if (!getWebClient().isJavaScriptEnabled()) {
2657 return new ScriptResult(null);
2658 }
2659
2660 final JavaScriptEngine engine = (JavaScriptEngine) getWebClient().getJavaScriptEngine();
2661 final Object result = engine.callFunction(this,
2662 (Function) function, (Scriptable) thisObject, args, htmlElement);
2663
2664 return new ScriptResult(result);
2665 }
2666
2667 private void writeObject(final ObjectOutputStream oos) throws IOException {
2668 oos.defaultWriteObject();
2669 oos.writeObject(originalCharset_ == null ? null : originalCharset_.name());
2670 }
2671
2672 private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
2673 ois.defaultReadObject();
2674 final String charsetName = (String) ois.readObject();
2675 if (charsetName != null) {
2676 originalCharset_ = Charset.forName(charsetName);
2677 }
2678 }
2679
2680
2681
2682
2683 @Override
2684 public void setNodeValue(final String value) {
2685
2686 }
2687
2688
2689
2690
2691 @Override
2692 public void setPrefix(final String prefix) {
2693
2694 }
2695
2696
2697
2698
2699 @Override
2700 public void clearComputedStyles() {
2701 if (computedStylesCache_ != null) {
2702 computedStylesCache_.clear();
2703 }
2704 }
2705
2706
2707
2708
2709 @Override
2710 public void clearComputedStyles(final DomElement element) {
2711 if (computedStylesCache_ != null) {
2712 computedStylesCache_.remove(element);
2713 }
2714 }
2715
2716
2717
2718
2719 @Override
2720 public void clearComputedStylesUpToRoot(final DomElement element) {
2721 if (computedStylesCache_ != null) {
2722 computedStylesCache_.remove(element);
2723
2724 DomNode parent = element.getParentNode();
2725 while (parent != null) {
2726 computedStylesCache_.remove(parent);
2727 parent = parent.getParentNode();
2728 }
2729 }
2730 }
2731
2732
2733
2734
2735
2736
2737
2738
2739 public ComputedCssStyleDeclaration getStyleFromCache(final DomElement element,
2740 final String normalizedPseudo) {
2741 return getCssPropertiesCache().get(element, normalizedPseudo);
2742 }
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752 public void putStyleIntoCache(final DomElement element, final String normalizedPseudo,
2753 final ComputedCssStyleDeclaration style) {
2754 getCssPropertiesCache().put(element, normalizedPseudo, style);
2755 }
2756
2757
2758
2759
2760
2761
2762
2763 public List<CssStyleSheet> getStyleSheets() {
2764 final List<CssStyleSheet> styles = new ArrayList<>();
2765 if (getWebClient().getOptions().isCssEnabled()) {
2766 for (final HtmlElement htmlElement : getHtmlElementDescendants()) {
2767 if (htmlElement instanceof HtmlStyle style) {
2768 styles.add(style.getSheet());
2769 continue;
2770 }
2771
2772 if (htmlElement instanceof HtmlLink link) {
2773 if (link.isStyleSheetLink()) {
2774 styles.add(link.getSheet());
2775 }
2776 }
2777 }
2778 }
2779 return styles;
2780 }
2781
2782
2783
2784
2785
2786
2787 private ComputedStylesCache getCssPropertiesCache() {
2788 if (computedStylesCache_ == null) {
2789 computedStylesCache_ = new ComputedStylesCache();
2790
2791
2792 final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl();
2793 addDomChangeListener(listener);
2794 addHtmlAttributeChangeListener(listener);
2795 }
2796 return computedStylesCache_;
2797 }
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833 private class DomHtmlAttributeChangeListenerImpl implements DomChangeListener, HtmlAttributeChangeListener {
2834
2835
2836
2837
2838 DomHtmlAttributeChangeListenerImpl() {
2839 super();
2840 }
2841
2842
2843
2844
2845 @Override
2846 public void nodeAdded(final DomChangeEvent event) {
2847 nodeChanged(event.getChangedNode(), null);
2848 }
2849
2850
2851
2852
2853 @Override
2854 public void nodeDeleted(final DomChangeEvent event) {
2855 nodeChanged(event.getChangedNode(), null);
2856 }
2857
2858
2859
2860
2861 @Override
2862 public void attributeAdded(final HtmlAttributeChangeEvent event) {
2863 nodeChanged(event.getHtmlElement(), event.getName());
2864 }
2865
2866
2867
2868
2869 @Override
2870 public void attributeRemoved(final HtmlAttributeChangeEvent event) {
2871 nodeChanged(event.getHtmlElement(), event.getName());
2872 }
2873
2874
2875
2876
2877 @Override
2878 public void attributeReplaced(final HtmlAttributeChangeEvent event) {
2879 nodeChanged(event.getHtmlElement(), event.getName());
2880 }
2881
2882 private void nodeChanged(final DomNode changedNode, final String attribName) {
2883
2884 if (changedNode instanceof HtmlStyle) {
2885 clearComputedStyles();
2886 return;
2887 }
2888 if (changedNode instanceof HtmlLink link) {
2889 if (link.isStyleSheetLink()) {
2890 clearComputedStyles();
2891 return;
2892 }
2893 }
2894
2895
2896
2897 final boolean clearParents = attribName == null || ATTRIBUTES_AFFECTING_PARENT.contains(attribName);
2898 if (computedStylesCache_ != null) {
2899 computedStylesCache_.nodeChanged(changedNode, clearParents);
2900 }
2901 }
2902 }
2903
2904
2905
2906
2907
2908
2909 private static final class ComputedStylesCache implements Serializable {
2910 private transient WeakHashMap<DomElement, Map<String, ComputedCssStyleDeclaration>>
2911 computedStyles_ = new WeakHashMap<>();
2912
2913
2914
2915
2916 ComputedStylesCache() {
2917 super();
2918 }
2919
2920 public synchronized ComputedCssStyleDeclaration get(final DomElement element,
2921 final String normalizedPseudo) {
2922 final Map<String, ComputedCssStyleDeclaration> elementMap = computedStyles_.get(element);
2923 if (elementMap != null) {
2924 return elementMap.get(normalizedPseudo);
2925 }
2926 return null;
2927 }
2928
2929 public synchronized void put(final DomElement element,
2930 final String normalizedPseudo, final ComputedCssStyleDeclaration style) {
2931 final Map<String, ComputedCssStyleDeclaration>
2932 elementMap = computedStyles_.computeIfAbsent(element, k -> new WeakHashMap<>());
2933 elementMap.put(normalizedPseudo, style);
2934 }
2935
2936 public synchronized void nodeChanged(final DomNode changed, final boolean clearParents) {
2937 final Iterator<Map.Entry<DomElement, Map<String, ComputedCssStyleDeclaration>>>
2938 i = computedStyles_.entrySet().iterator();
2939 while (i.hasNext()) {
2940 final Map.Entry<DomElement, Map<String, ComputedCssStyleDeclaration>> entry = i.next();
2941 final DomElement node = entry.getKey();
2942 if (changed == node
2943 || changed.getParentNode() == node.getParentNode()
2944 || changed.isAncestorOf(node)
2945 || clearParents && node.isAncestorOf(changed)) {
2946 i.remove();
2947 }
2948 }
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977 }
2978
2979 public synchronized void clear() {
2980 computedStyles_.clear();
2981 }
2982
2983 public synchronized Map<String, ComputedCssStyleDeclaration> remove(final DomNode element) {
2984 return computedStyles_.remove(element);
2985 }
2986
2987 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
2988 in.defaultReadObject();
2989 computedStyles_ = new WeakHashMap<>();
2990 }
2991 }
2992
2993 private static final class MappedElementIndexEntry implements Serializable {
2994 private final ArrayList<DomElement> elements_;
2995 private boolean sorted_;
2996
2997 MappedElementIndexEntry() {
2998
2999 elements_ = new ArrayList<>(2);
3000 sorted_ = true;
3001 }
3002
3003 void add(final DomElement element) {
3004 if (elements_.indexOf(element) == -1) {
3005 elements_.add(element);
3006 sorted_ = elements_.size() < 2;
3007 }
3008 }
3009
3010 DomElement first() {
3011 if (elements_.isEmpty()) {
3012 return null;
3013 }
3014
3015 if (sorted_) {
3016 return elements_.get(0);
3017 }
3018
3019 elements_.sort(DOCUMENT_POSITION_COMPERATOR);
3020 sorted_ = true;
3021
3022 return elements_.get(0);
3023 }
3024
3025 List<DomElement> elements() {
3026 if (sorted_) {
3027 return elements_;
3028 }
3029
3030 elements_.sort(DOCUMENT_POSITION_COMPERATOR);
3031 sorted_ = true;
3032
3033 return elements_;
3034 }
3035
3036 void remove(final DomElement element) {
3037 elements_.remove(element);
3038 sorted_ = elements_.size() < 2;
3039 }
3040 }
3041 }