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