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