View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
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   * A representation of an HTML page returned from a server.
100  * <p>
101  * This class provides different methods to access the page's content like
102  * {@link #getForms()}, {@link #getAnchors()}, {@link #getElementById(String)}, ... as well as the
103  * very powerful inherited methods {@link #getByXPath(String)} and {@link #getFirstByXPath(String)}
104  * for fine grained user specific access to child nodes.
105  * </p>
106  * <p>
107  * Child elements allowing user interaction provide methods for this purpose like {@link HtmlAnchor#click()},
108  * {@link HtmlInput#type(String)}, {@link HtmlOption#setSelected(boolean)}, ...
109  * </p>
110  * <p>
111  * HtmlPage instances should not be instantiated directly. They will be returned by {@link WebClient#getPage(String)}
112  * when the content type of the server's response is <code>text/html</code> (or one of its variations).<br>
113  * <br>
114  * <b>Example:</b><br>
115  * <br>
116  * <code>
117  * final HtmlPage page = webClient.{@link WebClient#getPage(String) getPage}("http://mywebsite/some/page.html");
118  * </code>
119  * </p>
120  *
121  * @author Mike Bowler
122  * @author Alex Nikiforoff
123  * @author Noboru Sinohara
124  * @author David K. Taylor
125  * @author Andreas Hangler
126  * @author Christian Sell
127  * @author Chris Erskine
128  * @author Marc Guillemot
129  * @author Ahmed Ashour
130  * @author Daniel Gredler
131  * @author Dmitri Zoubkov
132  * @author Sudhan Moghe
133  * @author Ethan Glasser-Camp
134  * @author Tom Anderson
135  * @author Ronald Brill
136  * @author Frank Danek
137  * @author Joerg Werner
138  * @author Atsushi Nakagawa
139  * @author Rural Hunter
140  * @author Ronny Shapiro
141  * @author Lai Quang Duong
142  * @author Sven Strickroth
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(); // used for synchronization
154 
155     private Map<String, MappedElementIndexEntry> idMap_ = new ConcurrentHashMap<>();
156     private Map<String, MappedElementIndexEntry> nameMap_ = new ConcurrentHashMap<>();
157     // The id/name lookup index is built lazily on first use. Until then,
158     // notifyNodeAdded / fireAttributeChange skip the per-element index updates.
159     // Reads must call ensureMappedElementsBuilt() before consulting idMap_/nameMap_.
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     /** Definition of special cases for the smart DomHtmlAttributeChangeListenerImpl. */
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; // same node
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      * Creates an instance of HtmlPage.
211      * An HtmlPage instance is normally retrieved with {@link WebClient#getPage(String)}.
212      *
213      * @param webResponse the web response that was used to create this page
214      * @param webWindow the window that this page is being loaded into
215      */
216     public HtmlPage(final WebResponse webResponse, final WebWindow webWindow) {
217         super(webResponse, webWindow);
218     }
219 
220     /**
221      * {@inheritDoc}
222      */
223     @Override
224     public HtmlPage getPage() {
225         return this;
226     }
227 
228     /**
229      * {@inheritDoc}
230      */
231     @Override
232     public boolean hasCaseSensitiveTagNames() {
233         return false;
234     }
235 
236     /**
237      * Initialize this page.
238      * @throws IOException if an IO problem occurs
239      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
240      *         {@link org.htmlunit.WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set
241      *         to true.
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             // a frame contains first a faked "about:blank" before its real content specified by src gets loaded
249             if (enclosingWindow instanceof FrameWindow window
250                     && !window.getFrameElement().isContentLoaded()) {
251                 return;
252             }
253 
254             // save the URL that should be used to resolve relative URLs in this page
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         // postponed actions are more or less the async scripts,
274         // they are running in real browsers whenever the download is done
275         processPostponedActionsIfNeeded();
276 
277         loadFrames();
278 
279         // don't set the ready state if we really load the blank page into the window
280         // see Node.initInlineFrameIfNeeded()
281         if (!isAboutBlank) {
282             setReadyState(READY_STATE_COMPLETE);
283             getDocumentElement().setReadyState(READY_STATE_COMPLETE);
284             executeEventHandlersIfNeeded(Event.TYPE_READY_STATE_CHANGE);
285         }
286 
287         // frame initialization has a different order
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      * Adds an action that should be executed once the page has been loaded.
345      * @param action the action
346      */
347     void addAfterLoadAction(final PostponedAction action) {
348         afterLoadActions_.add(action);
349     }
350 
351     /**
352      * Clean up this page.
353      */
354     @Override
355     public void cleanUp() {
356         //To avoid endless recursion caused by window.close() in onUnload
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      * {@inheritDoc}
385      */
386     @Override
387     public HtmlElement getDocumentElement() {
388         return (HtmlElement) super.getDocumentElement();
389     }
390 
391     /**
392      * Returns the document's {@code body} element.
393      *
394      * @return the document's {@code body} element, or {@code null} if it does
395      *         not exist
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      * Returns the head element.
411      * @return the head element
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      * {@inheritDoc}
427      */
428     @Override
429     public Document getOwnerDocument() {
430         return null;
431     }
432 
433     /**
434      * {@inheritDoc}
435      * Not yet implemented.
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      * {@inheritDoc}
444      * Not yet implemented.
445      */
446     @Override
447     public String getInputEncoding() {
448         throw new UnsupportedOperationException("HtmlPage.getInputEncoding is not yet implemented.");
449     }
450 
451     /**
452      * {@inheritDoc}
453      */
454     @Override
455     public String getXmlEncoding() {
456         return null;
457     }
458 
459     /**
460      * {@inheritDoc}
461      */
462     @Override
463     public boolean getXmlStandalone() {
464         return false;
465     }
466 
467     /**
468      * {@inheritDoc}
469      * Not yet implemented.
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      * {@inheritDoc}
478      */
479     @Override
480     public String getXmlVersion() {
481         return null;
482     }
483 
484     /**
485      * {@inheritDoc}
486      * Not yet implemented.
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      * {@inheritDoc}
495      * Not yet implemented.
496      */
497     @Override
498     public boolean getStrictErrorChecking() {
499         throw new UnsupportedOperationException("HtmlPage.getStrictErrorChecking is not yet implemented.");
500     }
501 
502     /**
503      * {@inheritDoc}
504      * Not yet implemented.
505      */
506     @Override
507     public void setStrictErrorChecking(final boolean strictErrorChecking) {
508         throw new UnsupportedOperationException("HtmlPage.setStrictErrorChecking is not yet implemented.");
509     }
510 
511     /**
512      * {@inheritDoc}
513      * Not yet implemented.
514      */
515     @Override
516     public String getDocumentURI() {
517         throw new UnsupportedOperationException("HtmlPage.getDocumentURI is not yet implemented.");
518     }
519 
520     /**
521      * {@inheritDoc}
522      * Not yet implemented.
523      */
524     @Override
525     public void setDocumentURI(final String documentURI) {
526         throw new UnsupportedOperationException("HtmlPage.setDocumentURI is not yet implemented.");
527     }
528 
529     /**
530      * {@inheritDoc}
531      * Not yet implemented.
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      * {@inheritDoc}
540      * Not yet implemented.
541      */
542     @Override
543     public DOMConfiguration getDomConfig() {
544         throw new UnsupportedOperationException("HtmlPage.getDomConfig is not yet implemented.");
545     }
546 
547     /**
548      * {@inheritDoc}
549      * Not yet implemented.
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      * {@inheritDoc}
559      */
560     @Override
561     public Charset getCharset() {
562         if (originalCharset_ == null) {
563             originalCharset_ = getWebResponse().getContentCharset();
564         }
565         return originalCharset_;
566     }
567 
568     /**
569      * {@inheritDoc}
570      */
571     @Override
572     public String getContentType() {
573         return getWebResponse().getContentType();
574     }
575 
576     /**
577      * {@inheritDoc}
578      * Not yet implemented.
579      */
580     @Override
581     public DOMImplementation getImplementation() {
582         throw new UnsupportedOperationException("HtmlPage.getImplementation is not yet implemented.");
583     }
584 
585     /**
586      * {@inheritDoc}
587      * @param tagName the tag name, preferably in lowercase
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      * {@inheritDoc}
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      * {@inheritDoc}
610      * Not yet implemented.
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      * {@inheritDoc}
619      * Not yet implemented.
620      */
621     @Override
622     public EntityReference createEntityReference(final String id) {
623         throw new UnsupportedOperationException("HtmlPage.createEntityReference is not yet implemented.");
624     }
625 
626     /**
627      * {@inheritDoc}
628      * Not yet implemented.
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      * {@inheritDoc}
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      * Returns the {@link HtmlAnchor} with the specified name.
652      *
653      * @param name the name to search by
654      * @return the {@link HtmlAnchor} with the specified name
655      * @throws ElementNotFoundException if the anchor could not be found
656      */
657     public HtmlAnchor getAnchorByName(final String name) throws ElementNotFoundException {
658         return getDocumentElement().getOneHtmlElementByAttribute("a", DomElement.NAME_ATTRIBUTE, name);
659     }
660 
661     /**
662      * Returns the {@link HtmlAnchor} with the specified href.
663      *
664      * @param href the string to search by
665      * @return the HtmlAnchor
666      * @throws ElementNotFoundException if the anchor could not be found
667      */
668     public HtmlAnchor getAnchorByHref(final String href) throws ElementNotFoundException {
669         return getDocumentElement().getOneHtmlElementByAttribute("a", "href", href);
670     }
671 
672     /**
673      * Returns a list of all anchors contained in this page.
674      * @return the list of {@link HtmlAnchor} in this page
675      */
676     public List<HtmlAnchor> getAnchors() {
677         return getDocumentElement().getElementsByTagNameImpl("a");
678     }
679 
680     /**
681      * Returns the first anchor with the specified text.
682      * @param text the text to search for
683      * @return the first anchor that was found
684      * @throws ElementNotFoundException if no anchors are found with the specified text
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      * Returns the first form that matches the specified name.
699      * @param name the name to search for
700      * @return the first form
701      * @exception ElementNotFoundException If no forms match the specified result.
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      * Returns a list of all the forms in this page.
714      * @return all the forms in this page
715      */
716     public List<HtmlForm> getForms() {
717         return getDocumentElement().getElementsByTagNameImpl("form");
718     }
719 
720     /**
721      * Given a relative URL (ie <code>/foo</code>), returns a fully-qualified URL based on
722      * the URL that was used to load this page.
723      *
724      * @param relativeUrl the relative URL
725      * @return the fully-qualified URL for the specified relative URL
726      * @exception MalformedURLException if an error occurred when creating a URL object
727      */
728     public URL getFullyQualifiedUrl(String relativeUrl) throws MalformedURLException {
729         // to handle http: and http:/ in FF (Bug #474)
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      * Given a target attribute value, resolve the target using a base target for the page.
744      *
745      * @param elementTarget the target specified as an attribute of the element
746      * @return the resolved target to use for the element
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      * Returns a list of ids (strings) that correspond to the tabbable elements
764      * in this page. Return them in the same order specified in {@link #getTabbableElements}
765      *
766      * @return the list of id's
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      * Returns a list of all elements that are tabbable in the order that will
780      * be used for tabbing.
781      * <p>
782      * The rules for determining tab order are as follows:
783      * </p>
784      * <ol>
785      *   <li>Those elements that support the tabindex attribute and assign a
786      *   positive value to it are navigated first. Navigation proceeds from the
787      *   element with the lowest tabindex value to the element with the highest
788      *   value. Values need not be sequential nor must they begin with any
789      *   particular value. Elements that have identical tabindex values should
790      *   be navigated in the order they appear in the character stream.</li>
791      *   <li>Those elements that do not support the tabindex attribute or
792      *   support it and assign it a value of "0" are navigated next. These
793      *   elements are navigated in the order they appear in the character
794      *   stream.</li>
795      *   <li>Elements that are disabled do not participate in the tabbing
796      *   order.</li>
797      * </ol>
798      * <p>
799      * Additionally, the value of tabindex must be within 0 and 32767. Any
800      * values outside this range will be ignored.
801      * </p>
802      * <p>
803      * The following elements support the <code>tabindex</code> attribute:
804      * A, AREA, BUTTON, INPUT, OBJECT, SELECT, and TEXTAREA.
805      * </p>
806      *
807      * @return all the tabbable elements in proper tab order
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      * Returns the HTML element that is assigned to the specified access key. An
868      * access key (aka mnemonic key) is used for keyboard navigation of the
869      * page.
870      * <p>
871      * Only the following HTML elements may have <code>accesskey</code>s defined: A, AREA,
872      * BUTTON, INPUT, LABEL, LEGEND, and TEXTAREA.
873      * </p>
874      *
875      * @param accessKey the key to look for
876      * @return the HTML element that is assigned to the specified key or null
877      *      if no elements can be found that match the specified key.
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      * Returns all the HTML elements that are assigned to the specified access key. An
889      * access key (aka mnemonic key) is used for keyboard navigation of the
890      * page.
891      * <p>
892      * The HTML specification seems to indicate that one accesskey cannot be used
893      * for multiple elements however Internet Explorer does seem to support this.
894      * It's worth noting that Firefox does not support multiple elements with one
895      * access key so you are making your HTML browser specific if you rely on this
896      * feature.
897      * </p>
898      *
899      * <p>
900      * Only the following HTML elements may have <code>accesskey</code>s defined: A, AREA,
901      * BUTTON, INPUT, LABEL, LEGEND, and TEXTAREA.
902      * </p>
903      *
904      * @param accessKey the key to look for
905      * @return the elements that are assigned to the specified accesskey
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      * <p>Executes the specified JavaScript code within the page. The usage would be similar to what can
925      * be achieved to execute JavaScript in the current page by entering "javascript:...some JS code..."
926      * in the URL field of a native browser.</p>
927      * <p><b>Note:</b> the provided code won't be executed if JavaScript has been disabled on the WebClient
928      * (see {@link org.htmlunit.WebClient#isJavaScriptEnabled()}).</p>
929      * @param sourceCode the JavaScript code to execute
930      * @return a ScriptResult which will contain both the current page (which may be different from
931      *         the previous page) and a JavaScript result object
932      */
933     public ScriptResult executeJavaScript(final String sourceCode) {
934         return executeJavaScript(sourceCode, "injected script", 1);
935     }
936 
937     /**
938      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
939      * <p>
940      * Execute the specified JavaScript if a JavaScript engine was successfully
941      * instantiated. If this JavaScript causes the current page to be reloaded
942      * (through location="" or form.submit()) then return the new page, otherwise
943      * return the current page.
944      * </p>
945      * <p><b>Please note:</b> Although this method is public, it is not intended for
946      * general execution of JavaScript. Users of HtmlUnit should interact with the pages
947      * as a user would by clicking on buttons or links and having the JavaScript event
948      * handlers execute as needed.
949      * </p>
950      *
951      * @param sourceCode the JavaScript code to execute
952      * @param sourceName the name for this chunk of code (will be displayed in error messages)
953      * @param startLine the line at which the script source starts
954      * @return a ScriptResult which will contain both the current page (which may be different from
955      *         the previous page) and a JavaScript result object.
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     /** Various possible external JavaScript file loading results. */
979     enum JavaScriptLoadResult {
980         /** The load was aborted and nothing was done. */
981         NOOP,
982         /** The load was aborted and nothing was done. */
983         NO_CONTENT,
984         /** The external JavaScript file was downloaded and compiled successfully. */
985         SUCCESS,
986         /** The external JavaScript file was not downloaded successfully. */
987         DOWNLOAD_ERROR,
988         /** The external JavaScript file was downloaded but was not compiled successfully. */
989         COMPILATION_ERROR
990     }
991 
992     /**
993      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
994      *
995      * @param srcAttribute the source attribute from the script tag
996      * @param scriptCharset the charset from the script tag
997      * @return the result of loading the specified external JavaScript file
998      * @throws FailingHttpStatusCodeException if the request's status code indicates a request
999      *         failure and the {@link WebClient} was configured to throw exceptions on failing
1000      *         HTTP status codes
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      * Loads JavaScript from the specified URL. This method may return {@code null} if
1063      * there is a problem loading the code from the specified URL.
1064      *
1065      * @param url the URL of the script
1066      * @param scriptCharset the charset from the script tag
1067      * @return the content of the file, or {@code null} if we ran into a compile error
1068      * @throws IOException if there is a problem downloading the JavaScript file
1069      * @throws FailingHttpStatusCodeException if the request's status code indicates a request
1070      *         failure and the {@link WebClient} was configured to throw exceptions on failing
1071      *         HTTP status codes
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         // copy all headers from the referring request
1081         request.setAdditionalHeaders(new HashMap<>(referringRequest.getAdditionalHeaders()));
1082 
1083         // at least overwrite this headers
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         // use info from script tag or fall back to utf-8
1093         // https://www.rfc-editor.org/rfc/rfc9239#section-4.2
1094         if (scriptCharset != null) {
1095             request.setDefaultResponseContentCharset(scriptCharset);
1096         }
1097         else {
1098             request.setDefaultResponseContentCharset(StandardCharsets.UTF_8);
1099         }
1100 
1101         // our cache is a bit strange;
1102         // loadWebResponse check the cache for the web response
1103         // AND also fixes the request url for the following cache lookups
1104         final WebResponse response = client.loadWebResponse(request);
1105 
1106         // now we can look into the cache with the fixed request for
1107         // a cached script
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                 // no cleanup if the response is stored inside the cache
1152                 return script;
1153             }
1154 
1155             response.cleanUp();
1156             return script;
1157         }
1158 
1159         response.cleanUp();
1160         return null;
1161     }
1162 
1163     /**
1164      * Returns the title of this page or an empty string if the title wasn't specified.
1165      *
1166      * @return the title of this page or an empty string if the title wasn't specified
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      * Sets the text for the title of this page. If there is not a title element
1178      * on this page, then one has to be generated.
1179      * @param message the new text
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                 // perhaps should we create head too?
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      * Gets the first child of startElement that is an instance of the given class.
1205      * @param startElement the parent element
1206      * @param clazz the class to search for
1207      * @return {@code null} if no child found
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      * Gets the first child of startElement or it's children that is an instance of the given class.
1224      * @param startElement the parent element
1225      * @param clazz the class to search for
1226      * @return {@code null} if no child found
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      * Gets the title element for this page. Returns null if one is not found.
1247      *
1248      * @return the title element for this page or null if this is not one
1249      */
1250     private HtmlTitle getTitleElement() {
1251         return (HtmlTitle) getFirstChildElementRecursive(getDocumentElement(), HtmlTitle.class);
1252     }
1253 
1254     /**
1255      * Looks for and executes any appropriate event handlers. Looks for body and frame tags.
1256      * @param eventType either {@link Event#TYPE_LOAD}, {@link Event#TYPE_UNLOAD}, or {@link Event#TYPE_BEFORE_UNLOAD}
1257      * @return {@code true} if user accepted <code>onbeforeunload</code> (not relevant to other events)
1258      */
1259     private boolean executeEventHandlersIfNeeded(final String eventType) {
1260         // If JavaScript isn't enabled, there's nothing for us to do.
1261         if (!getWebClient().isJavaScriptEnabled()) {
1262             return true;
1263         }
1264 
1265         // Execute the specified event on the document element.
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             // This is the same as DomElement.fireEvent() and was copied
1277             // here so it could be used with HtmlPage.
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                 // The load/beforeunload/unload events target Document but paths Window only (tested in Chrome/FF)
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         // If this page was loaded in a frame, execute the version of the event specified on the frame tag.
1302         if (window instanceof FrameWindow fw) {
1303             final BaseFrameElement frame = fw.getFrameElement();
1304 
1305             // if part of a document fragment, then the load event is not triggered
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                     // This fires the "load" event for the <frame> element which, like all non-window
1323                     // load events, propagates up to Document but not Window.  The "load" event for
1324                     // <frameset> on the other hand, like that of <body>, is handled above where it is
1325                     // fired against Document and directed to Window.
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      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1340      *
1341      * @return true if the OnbeforeunloadHandler has accepted to change the page
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      * If a refresh has been specified either through a meta tag or an HTTP
1366      * response header, then perform that refresh.
1367      * @throws IOException if an IO problem occurs
1368      */
1369     private void executeRefreshIfNeeded() throws IOException {
1370         // If this page is not in a frame then a refresh has already happened,
1371         // most likely through the JavaScript onload handler, so we don't do a
1372         // second refresh.
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             // Format: <meta http-equiv='refresh' content='10'>
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             // Format: <meta http-equiv='refresh' content='10;url=http://www.blah.com'>
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                     //content='10; URL=' is treated as content='10'
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     // this is different from what is done in org.htmlunit.WebClient.loadWebResponseFromWebConnection(WebRequest, int)
1461     // because there we are directly replacing the response before loading the response into the window
1462     // here we are replacing the page in the window (maybe after some time)
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      * Returns an auto-refresh string if specified. This will look in both the meta
1500      * tags and inside the HTTP response headers.
1501      * @return the auto-refresh string
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      * Executes any deferred scripts, if necessary.
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         // don't call getElementsByTagName() here because it creates a live collection
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      * Deregister frames that are no longer in use.
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                 // seems quite silly, but for instance if the src attribute of an iframe is not
1552                 // set, the error only occurs when leaving the page
1553                 ((HtmlPage) page).deregisterFramesIfNeeded();
1554             }
1555         }
1556     }
1557 
1558     /**
1559      * Returns a list containing all the frames (from frame and iframe tags) in this page
1560      * in document order.
1561      * @return a list of {@link FrameWindow}
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      * Returns the first frame contained in this page with the specified name.
1576      * @param name the name to search for
1577      * @return the first frame found
1578      * @exception ElementNotFoundException If no frame exist in this page with the specified name.
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      * Simulate pressing an access key. This may change the focus, may click buttons and may invoke
1593      * JavaScript.
1594      *
1595      * @param accessKey the key that will be pressed
1596      * @return the element that has the focus after pressing this access key or null if no element
1597      *         has the focus.
1598      * @throws IOException if an IO error occurs during the processing of this access key (this
1599      *         would only happen if the access key triggered a button which in turn caused a page load)
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                     // The page was reloaded therefore no element on this page will have the focus.
1616                     getFocusedElement().blur();
1617                 }
1618             }
1619         }
1620 
1621         return getFocusedElement();
1622     }
1623 
1624     /**
1625      * Move the focus to the next element in the tab order. To determine the specified tab
1626      * order, refer to {@link HtmlPage#getTabbableElements()}
1627      *
1628      * @return the element that has focus after calling this method
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                 // The element with focus isn't on this page
1646                 elementToGiveFocus = elements.get(0);
1647             }
1648             else if (index == elements.size() - 1) {
1649                 // if at last jump to start
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      * Move the focus to the previous element in the tab order. To determine the specified tab
1663      * order, refer to {@link HtmlPage#getTabbableElements()}
1664      *
1665      * @return the element that has focus after calling this method
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                 // The element with focus isn't on this page
1683                 elementToGiveFocus = elements.get(elements.size() - 1);
1684             }
1685             else if (index == 0) {
1686                 // first; back to the last
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      * Returns the HTML element with the specified ID. If more than one element
1700      * has this ID (not allowed by the HTML spec), then this method returns the
1701      * first one.
1702      *
1703      * @param elementId the ID value to search for
1704      * @param <E> the element type
1705      * @return the HTML element with the specified ID
1706      * @throws ElementNotFoundException if no element was found matching the specified ID
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      * Returns the elements with the specified ID. If there are no elements
1719      * with the specified ID, this method returns an empty list. Please note that
1720      * the lists returned by this method are immutable.
1721      *
1722      * @param elementId the ID value to search for
1723      * @return the elements with the specified name attribute
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      * Returns the element with the specified name. If more than one element
1738      * has this name, then this method returns the first one.
1739      *
1740      * @param name the name value to search for
1741      * @param <E> the element type
1742      * @return the element with the specified name
1743      * @throws ElementNotFoundException if no element was found matching the specified name
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      * Returns the elements with the specified name attribute. If there are no elements
1759      * with the specified name, this method returns an empty list. Please note that
1760      * the lists returned by this method are immutable.
1761      *
1762      * @param name the name value to search for
1763      * @return the elements with the specified name attribute
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      * Returns the elements with the specified string for their name or ID. If there are
1778      * no elements with the specified name or ID, this method returns an empty list.
1779      *
1780      * @param idAndOrName the value to search for
1781      * @return the elements with the specified string for their name or ID
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      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1806      *
1807      * @param node the node that has just been added to the document
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      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1836      *
1837      * @param node the node that has just been removed from the tree
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      * Adds an element to the ID and name maps, if necessary.
1860      * @param element the element to be added to the ID and name maps
1861      * @param recurse indicates if children must be added too
1862      */
1863     void addMappedElement(final DomElement element, final boolean recurse) {
1864         // Index is built lazily; skip while not built. ensureMappedElementsBuilt()
1865         // walks the tree once and populates everything on first read.
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         // Flip the flag only after the maps are populated, so a partial
1885         // failure mid-walk leaves us with built_=false and the next read
1886         // tries again rather than seeing a half-populated index.
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             // poor man's approach - we don't use getChildElements()
1919             // to avoid a bunch of object constructions
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      * Removes an element and optionally its children from the ID and name maps, if necessary.
1932      * @param element the element to be removed from the ID and name maps
1933      * @param recurse indicates if children must be removed too
1934      * @param descendant indicates of the element was descendant of this HtmlPage, but now its parent might be null
1935      */
1936     void removeMappedElement(final DomElement element, final boolean recurse, final boolean descendant) {
1937         // see addMappedElement: while the index is unbuilt, removals are also no-ops.
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      * Indicates if the attribute name indicates that the owning element is mapped.
1978      * @param document the owning document
1979      * @param attributeName the name of the attribute to consider
1980      * @return {@code true} if the owning element should be mapped in its owning page
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      * Loads the content of the contained frames. This is done after the page is completely loaded, to allow script
2004      * contained in the frames to reference elements from the page located after the closing &lt;/frame&gt; tag.
2005      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
2006      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to {@code true}
2007      */
2008     void loadFrames() throws FailingHttpStatusCodeException {
2009         for (final BaseFrameElement frameElement : new ArrayList<>(frameElements_)) {
2010             // test if the frame should really be loaded:
2011             // if a script has already changed its content, it should be skipped
2012             // use == and not equals(...) to identify initial content (versus URL set to "about:blank")
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      * Gives a basic representation for debugging purposes.
2023      * @return a basic representation
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      * Gets the meta tag for a given {@code http-equiv} value.
2037      * @param httpEquiv the {@code http-equiv} value
2038      * @return a list of {@link HtmlMeta}
2039      */
2040     protected List<HtmlMeta> getMetaTags(final String httpEquiv) {
2041         if (getDocumentElement() == null) {
2042             return Collections.emptyList(); // weird case, for instance if document.documentElement has been removed
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      * Creates a clone of this instance, and clears cached state to be not shared with the original.
2056      *
2057      * @return a clone of this instance
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      * {@inheritDoc}
2073      */
2074     @Override
2075     public HtmlPage cloneNode(final boolean deep) {
2076         // we need the ScriptObject clone before cloning the kids.
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         // if deep, clone the kids too, and re initialize parts of the clone
2084         if (deep) {
2085             // this was previously synchronized but that makes not sense, why
2086             // lock the source against a copy only one has a reference too,
2087             // because result is a local reference
2088             result.attributeListeners_ = null;
2089 
2090             result.selectionRanges_ = new ArrayList<>(3);
2091             // the original one is synchronized so we should do that here too, shouldn't we?
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      * Adds an HtmlAttributeChangeListener to the listener list.
2103      * The listener is registered for all attributes of all HtmlElements contained in this page.
2104      *
2105      * @param listener the attribute change listener to be added
2106      * @see #removeHtmlAttributeChangeListener(HtmlAttributeChangeListener)
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      * Removes an HtmlAttributeChangeListener from the listener list.
2120      * This method should be used to remove HtmlAttributeChangeListener that were registered
2121      * for all attributes of all HtmlElements contained in this page.
2122      *
2123      * @param listener the attribute change listener to be removed
2124      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
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      * Notifies all registered listeners for the given event to add an attribute.
2137      * @param event the event to fire
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      * Notifies all registered listeners for the given event to replace an attribute.
2150      * @param event the event to fire
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      * Notifies all registered listeners for the given event to remove an attribute.
2163      * @param event the event to fire
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      * {@inheritDoc}
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      * Returns {@code true} if an HTML parser is operating on this page, adding content to it.
2209      * @return {@code true} if an HTML parser is operating on this page, adding content to it
2210      */
2211     public boolean isBeingParsed() {
2212         return parserCount_ > 0;
2213     }
2214 
2215     /**
2216      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2217      *
2218      * Called by the HTML parser to let the page know that it has started parsing some content for this page.
2219      */
2220     public void registerParsingStart() {
2221         parserCount_++;
2222     }
2223 
2224     /**
2225      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2226      *
2227      * Called by the HTML parser to let the page know that it has finished parsing some content for this page.
2228      */
2229     public void registerParsingEnd() {
2230         parserCount_--;
2231     }
2232 
2233     /**
2234      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2235      *
2236      * Returns {@code true} if an HTML parser is parsing a non-inline HTML snippet to add content
2237      * to this page. Non-inline content is content that is parsed for the page, but not in the
2238      * same stream as the page itself -- basically anything other than <code>document.write()</code>
2239      * or <code>document.writeln()</code>: <code>innerHTML</code>, <code>outerHTML</code>,
2240      * <code>document.createElement()</code>, etc.
2241      *
2242      * @return {@code true} if an HTML parser is parsing a non-inline HTML snippet to add content
2243      *         to this page
2244      */
2245     public boolean isParsingHtmlSnippet() {
2246         return snippetParserCount_ > 0;
2247     }
2248 
2249     /**
2250      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2251      *
2252      * Called by the HTML parser to let the page know that it has started parsing a non-inline HTML snippet.
2253      */
2254     public void registerSnippetParsingStart() {
2255         snippetParserCount_++;
2256     }
2257 
2258     /**
2259      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2260      *
2261      * Called by the HTML parser to let the page know that it has finished parsing a non-inline HTML snippet.
2262      */
2263     public void registerSnippetParsingEnd() {
2264         snippetParserCount_--;
2265     }
2266 
2267     /**
2268      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2269      *
2270      * Returns {@code true} if an HTML parser is parsing an inline HTML snippet to add content
2271      * to this page. Inline content is content inserted into the parser stream dynamically
2272      * while the page is being parsed (i.e. <code>document.write()</code> or <code>document.writeln()</code>).
2273      *
2274      * @return {@code true} if an HTML parser is parsing an inline HTML snippet to add content
2275      *         to this page
2276      */
2277     public boolean isParsingInlineHtmlSnippet() {
2278         return inlineSnippetParserCount_ > 0;
2279     }
2280 
2281     /**
2282      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2283      *
2284      * Called by the HTML parser to let the page know that it has started parsing an inline HTML snippet.
2285      */
2286     public void registerInlineSnippetParsingStart() {
2287         inlineSnippetParserCount_++;
2288     }
2289 
2290     /**
2291      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2292      *
2293      * Called by the HTML parser to let the page know that it has finished parsing an inline HTML snippet.
2294      */
2295     public void registerInlineSnippetParsingEnd() {
2296         inlineSnippetParserCount_--;
2297     }
2298 
2299     /**
2300      * Refreshes the page by sending the same parameters as previously sent to get this page.
2301      * @return the newly loaded page.
2302      * @throws IOException if an IO problem occurs
2303      */
2304     public Page refresh() throws IOException {
2305         return getWebClient().getPage(getWebResponse().getWebRequest());
2306     }
2307 
2308     /**
2309      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2310      * <p>
2311      * Parses the given string as would it belong to the content being parsed
2312      * at the current parsing position
2313      * </p>
2314      * @param string the HTML code to write in place
2315      */
2316     public void writeInParsedStream(final String string) {
2317         getDOMBuilder().pushInputString(string);
2318     }
2319 
2320     /**
2321      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2322      *
2323      * Sets the builder to allow page to send content from document.write(ln) calls.
2324      * @param htmlUnitDOMBuilder the builder
2325      */
2326     public void setDOMBuilder(final HTMLParserDOMBuilder htmlUnitDOMBuilder) {
2327         domBuilder_ = htmlUnitDOMBuilder;
2328     }
2329 
2330     /**
2331      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2332      *
2333      * Returns the current builder.
2334      * @return the current builder
2335      */
2336     public HTMLParserDOMBuilder getDOMBuilder() {
2337         return domBuilder_;
2338     }
2339 
2340     /**
2341      * <p>Returns all namespaces defined in the root element of this page.</p>
2342      * <p>The default namespace has a key of an empty string.</p>
2343      * @return all namespaces defined in the root element of this page
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      * {@inheritDoc}
2365      */
2366     @Override
2367     public void setDocumentType(final DocumentType type) {
2368         super.setDocumentType(type);
2369     }
2370 
2371     /**
2372      * Saves the current page, with all images, to the specified location.
2373      * The default behavior removes all script elements.
2374      *
2375      * @param file file to write this page into
2376      * @throws IOException If an error occurs
2377      */
2378     public void save(final File file) throws IOException {
2379         new XmlSerializer().save(this, file);
2380     }
2381 
2382     /**
2383      * Returns whether the current page mode is in {@code quirks mode} or in {@code standards mode}.
2384      * @return true for {@code quirks mode}, false for {@code standards mode}
2385      */
2386     public boolean isQuirksMode() {
2387         return "BackCompat".equals(((HTMLDocument) getScriptableObject()).getCompatMode());
2388     }
2389 
2390     /**
2391      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2392      * {@inheritDoc}
2393      */
2394     @Override
2395     public boolean isAttachedToPage() {
2396         return true;
2397     }
2398 
2399     /**
2400      * {@inheritDoc}
2401      */
2402     @Override
2403     public boolean isHtmlPage() {
2404         return true;
2405     }
2406 
2407     /**
2408      * The base URL used to resolve relative URLs.
2409      * @return the base URL
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      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2466      *
2467      * Adds an {@link AutoCloseable}, which would be closed during the {@link #cleanUp()}.
2468      * @param autoCloseable the autoclosable
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      * {@inheritDoc}
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      * Sets the {@link ElementFromPointHandler}.
2494      * @param elementFromPointHandler the handler
2495      */
2496     public void setElementFromPointHandler(final ElementFromPointHandler elementFromPointHandler) {
2497         elementFromPointHandler_ = elementFromPointHandler;
2498     }
2499 
2500     /**
2501      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2502      *
2503      * Returns the element for the specified x coordinate and the specified y coordinate.
2504      *
2505      * @param x the x offset, in pixels
2506      * @param y the y offset, in pixels
2507      * @return the element for the specified x coordinate and the specified y coordinate
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      * Moves the focus to the specified element. This will trigger any relevant JavaScript
2524      * event handlers.
2525      *
2526      * @param newElement the element that will receive the focus, use {@code null} to remove focus from any element
2527      * @return true if the specified element now has the focus
2528      * @see #getFocusedElement()
2529      */
2530     public boolean setFocusedElement(final DomElement newElement) {
2531         return setFocusedElement(newElement, false);
2532     }
2533 
2534     /**
2535      * Moves the focus to the specified element. This will trigger any relevant JavaScript
2536      * event handlers.
2537      *
2538      * @param newElement the element that will receive the focus, use {@code null} to remove focus from any element
2539      * @param windowActivated - whether the enclosing window got focus resulting in specified element getting focus
2540      * @return true if the specified element now has the focus
2541      * @see #getFocusedElement()
2542      */
2543     public boolean setFocusedElement(final DomElement newElement, final boolean windowActivated) {
2544         if (elementWithFocus_ == newElement && !windowActivated) {
2545             // nothing to do
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         // use newElement in the code below because element elementWithFocus_
2564         // might be changed by another thread
2565         if (newElement != null) {
2566             newElement.focus();
2567             newElement.fireEvent(Event.TYPE_FOCUS);
2568 
2569             newElement.fireEvent(Event.TYPE_FOCUS_IN);
2570         }
2571 
2572         // If a page reload happened as a result of the focus change then obviously this
2573         // element will not have the focus because its page has gone away.
2574         return this == getEnclosingWindow().getEnclosedPage();
2575     }
2576 
2577     /**
2578      * Returns the element with the focus or null if no element has the focus.
2579      * @return the element with focus or null
2580      * @see #setFocusedElement(DomElement)
2581      */
2582     public DomElement getFocusedElement() {
2583         return elementWithFocus_;
2584     }
2585 
2586     /**
2587      * <p><span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span></p>
2588      *
2589      * Sets the element with focus.
2590      * @param elementWithFocus the element with focus
2591      */
2592     public void setElementWithFocus(final DomElement elementWithFocus) {
2593         elementWithFocus_ = elementWithFocus;
2594     }
2595 
2596     /**
2597      * <p><span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span></p>
2598      * Returns the currently active element.
2599      *
2600      * @return the element that currently has focus, or the document's
2601      *         {@code body} element if no element has focus, or {@code null}
2602      *         if the document has no {@code body} element
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      * <p><span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span></p>
2619      *
2620      * <p>Returns the page's current selection ranges.</p>
2621      *
2622      * @return the page's current selection ranges
2623      */
2624     public List<SimpleRange> getSelectionRanges() {
2625         return selectionRanges_;
2626     }
2627 
2628     /**
2629      * <p><span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span></p>
2630      *
2631      * <p>Makes the specified selection range the *only* selection range on this page.</p>
2632      *
2633      * @param selectionRange the selection range
2634      */
2635     public void setSelectionRange(final SimpleRange selectionRange) {
2636         selectionRanges_.clear();
2637         selectionRanges_.add(selectionRange);
2638     }
2639 
2640     /**
2641      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2642      *
2643      * Execute a Function in the given context.
2644      *
2645      * @param function the JavaScript Function to call
2646      * @param thisObject the "this" object to be used during invocation
2647      * @param args the arguments to pass into the call
2648      * @param htmlElement the HTML element for which this script is being executed
2649      *        This element will be the context during the JavaScript execution. If null,
2650      *        the context will default to the page.
2651      * @return a ScriptResult which will contain both the current page (which may be different from
2652      *        the previous page) and a JavaScript result object.
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      * {@inheritDoc}
2682      */
2683     @Override
2684     public void setNodeValue(final String value) {
2685         // Default behavior is to do nothing, overridden in some subclasses
2686     }
2687 
2688     /**
2689      * {@inheritDoc}
2690      */
2691     @Override
2692     public void setPrefix(final String prefix) {
2693         // Empty.
2694     }
2695 
2696     /**
2697      * {@inheritDoc}
2698      */
2699     @Override
2700     public void clearComputedStyles() {
2701         if (computedStylesCache_ != null) {
2702             computedStylesCache_.clear();
2703         }
2704     }
2705 
2706     /**
2707      * {@inheritDoc}
2708      */
2709     @Override
2710     public void clearComputedStyles(final DomElement element) {
2711         if (computedStylesCache_ != null) {
2712             computedStylesCache_.remove(element);
2713         }
2714     }
2715 
2716     /**
2717      * {@inheritDoc}
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      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2734      *
2735      * @param element the element to clear its cache
2736      * @param normalizedPseudo the pseudo attribute
2737      * @return the cached ComputedCssStyleDeclaration object or null
2738      */
2739     public ComputedCssStyleDeclaration getStyleFromCache(final DomElement element,
2740             final String normalizedPseudo) {
2741         return getCssPropertiesCache().get(element, normalizedPseudo);
2742     }
2743 
2744     /**
2745      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2746      *
2747      * Caches a ComputedCssStyleDeclaration object.
2748      * @param element the element to clear its cache
2749      * @param normalizedPseudo the pseudo attribute
2750      * @param style the ComputedCssStyleDeclaration to cache
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      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2759      *
2760      * @return a list of all styles from this page (&lt;style&gt; and &lt;link rel=stylesheet&gt;).
2761      *         This returns an empty list if css support is disabled in the web client options.
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      * Returns the computed styles cache for this page.
2784      *
2785      * @return the computed styles cache for this page
2786      */
2787     private ComputedStylesCache getCssPropertiesCache() {
2788         if (computedStylesCache_ == null) {
2789             computedStylesCache_ = new ComputedStylesCache();
2790 
2791             // maintain the style cache
2792             final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl();
2793             addDomChangeListener(listener);
2794             addHtmlAttributeChangeListener(listener);
2795         }
2796         return computedStylesCache_;
2797     }
2798 
2799     /**
2800      * <p>Listens for changes anywhere in the document and evicts cached computed styles whenever something relevant
2801      * changes. Note that the very lazy way of doing this (completely clearing the cache every time something happens)
2802      * results in very meager performance gains. In order to get good (but still correct) performance, we need to be
2803      * a little smarter.</p>
2804      *
2805      * <p>CSS 2.1 has the following <a href="http://www.w3.org/TR/CSS21/selector.html">selector types</a> (where "SN" is
2806      * shorthand for "the selected node"):</p>
2807      *
2808      * <ol>
2809      *   <li><em>Universal</em> (i.e. "*"): Affected by the removal of SN from the document.</li>
2810      *   <li><em>Type</em> (i.e. "div"): Affected by the removal of SN from the document.</li>
2811      *   <li><em>Descendant</em> (i.e. "div span"): Affected by changes to SN or to any of its ancestors.</li>
2812      *   <li><em>Child</em> (i.e. "div &gt; span"): Affected by changes to SN or to its parent.</li>
2813      *   <li><em>Adjacent Sibling</em> (i.e. "table + p"): Affected by changes to SN or its previous sibling.</li>
2814      *   <li><em>Attribute</em> (i.e. "div.up, div[class~=up]"): Affected by changes to an attribute of SN.</li>
2815      *   <li><em>ID</em> (i.e. "#header"): Affected by changes to the <code>id</code> attribute of SN.</li>
2816      *   <li><em>Pseudo-Elements and Pseudo-Classes</em> (i.e. "p:first-child"): Affected by changes to parent.</li>
2817      * </ol>
2818      *
2819      * <p>Together, these rules dictate that the smart (but still lazy) way of removing elements from the computed style
2820      * cache is as follows -- whenever a node changes in any way, the cache needs to be cleared of styles for nodes
2821      * which:</p>
2822      *
2823      * <ul>
2824      *   <li>are actually the same node as the node that changed</li>
2825      *   <li>are siblings of the node that changed</li>
2826      *   <li>are descendants of the node that changed</li>
2827      * </ul>
2828      *
2829      * <p>Additionally, whenever a <code>style</code> node or a <code>link</code> node
2830      * with <code>rel=stylesheet</code> is added or
2831      * removed, all elements should be removed from the computed style cache.</p>
2832      */
2833     private class DomHtmlAttributeChangeListenerImpl implements DomChangeListener, HtmlAttributeChangeListener {
2834 
2835         /**
2836          * Ctor.
2837          */
2838         DomHtmlAttributeChangeListenerImpl() {
2839             super();
2840         }
2841 
2842         /**
2843          * {@inheritDoc}
2844          */
2845         @Override
2846         public void nodeAdded(final DomChangeEvent event) {
2847             nodeChanged(event.getChangedNode(), null);
2848         }
2849 
2850         /**
2851          * {@inheritDoc}
2852          */
2853         @Override
2854         public void nodeDeleted(final DomChangeEvent event) {
2855             nodeChanged(event.getChangedNode(), null);
2856         }
2857 
2858         /**
2859          * {@inheritDoc}
2860          */
2861         @Override
2862         public void attributeAdded(final HtmlAttributeChangeEvent event) {
2863             nodeChanged(event.getHtmlElement(), event.getName());
2864         }
2865 
2866         /**
2867          * {@inheritDoc}
2868          */
2869         @Override
2870         public void attributeRemoved(final HtmlAttributeChangeEvent event) {
2871             nodeChanged(event.getHtmlElement(), event.getName());
2872         }
2873 
2874         /**
2875          * {@inheritDoc}
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             // If a stylesheet was changed, all of our calculations could be off; clear the cache.
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             // Apparently it wasn't a stylesheet that changed; be semi-smart about what we evict and when.
2896             // null means that a node was added/removed; we always have to take care of this for the parents
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      * Cache computed styles when possible, because their calculation is very expensive.
2906      * We use a weak hash map because we don't want this cache to be the only reason
2907      * nodes are kept around in the JVM, if all other references to them are gone.
2908      */
2909     private static final class ComputedStylesCache implements Serializable {
2910         private transient WeakHashMap<DomElement, Map<String, ComputedCssStyleDeclaration>>
2911                     computedStyles_ = new WeakHashMap<>();
2912 
2913         /**
2914          * Ctor.
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             // maybe this is a better solution but I have to think a bit more about this
2951             //
2952             //            if (computedStyles_.isEmpty()) {
2953             //                return;
2954             //            }
2955             //
2956             //            // remove all siblings
2957             //            DomNode parent = changed.getParentNode();
2958             //            if (parent != null) {
2959             //                for (DomNode sibling : parent.getChildNodes()) {
2960             //                    computedStyles_.remove(sibling.getScriptableObject());
2961             //                }
2962             //
2963             //                if (clearParents) {
2964             //                    // remove all parents
2965             //                    while (parent != null) {
2966             //                        computedStyles_.remove(parent.getScriptableObject());
2967             //                        parent = parent.getParentNode();
2968             //                    }
2969             //                }
2970             //            }
2971             //
2972             //            // remove changed itself and all descendants
2973             //            computedStyles_.remove(changed.getScriptableObject());
2974             //            for (DomNode descendant : changed.getDescendants()) {
2975             //                computedStyles_.remove(descendant.getScriptableObject());
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             // we do not expect to many elements having the same id/name
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 }