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;
16  
17  import static java.nio.charset.StandardCharsets.ISO_8859_1;
18  import static java.nio.charset.StandardCharsets.UTF_8;
19  
20  import java.io.BufferedInputStream;
21  import java.io.File;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.ObjectInputStream;
26  import java.io.Serializable;
27  import java.lang.ref.Cleaner;
28  import java.lang.ref.Cleaner.Cleanable;
29  import java.lang.ref.WeakReference;
30  import java.net.MalformedURLException;
31  import java.net.URL;
32  import java.net.URLConnection;
33  import java.net.URLDecoder;
34  import java.nio.charset.Charset;
35  import java.nio.file.Files;
36  import java.util.ArrayList;
37  import java.util.Collections;
38  import java.util.ConcurrentModificationException;
39  import java.util.Date;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.Iterator;
43  import java.util.LinkedHashMap;
44  import java.util.LinkedHashSet;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.Map;
48  import java.util.Objects;
49  import java.util.Optional;
50  import java.util.Set;
51  import java.util.concurrent.ConcurrentLinkedDeque;
52  import java.util.concurrent.Executor;
53  import java.util.concurrent.ExecutorService;
54  import java.util.concurrent.Executors;
55  import java.util.concurrent.ThreadFactory;
56  import java.util.concurrent.ThreadPoolExecutor;
57  
58  import org.apache.commons.logging.Log;
59  import org.apache.commons.logging.LogFactory;
60  import org.apache.http.NoHttpResponseException;
61  import org.apache.http.client.CredentialsProvider;
62  import org.apache.http.cookie.MalformedCookieException;
63  import org.htmlunit.attachment.Attachment;
64  import org.htmlunit.attachment.AttachmentHandler;
65  import org.htmlunit.csp.Policy;
66  import org.htmlunit.csp.url.URI;
67  import org.htmlunit.css.ComputedCssStyleDeclaration;
68  import org.htmlunit.cssparser.parser.CSSErrorHandler;
69  import org.htmlunit.cssparser.parser.javacc.CSS3Parser;
70  import org.htmlunit.html.BaseFrameElement;
71  import org.htmlunit.html.DomElement;
72  import org.htmlunit.html.DomNode;
73  import org.htmlunit.html.FrameWindow;
74  import org.htmlunit.html.FrameWindow.PageDenied;
75  import org.htmlunit.html.HtmlElement;
76  import org.htmlunit.html.HtmlInlineFrame;
77  import org.htmlunit.html.HtmlPage;
78  import org.htmlunit.html.XHtmlPage;
79  import org.htmlunit.html.parser.HTMLParser;
80  import org.htmlunit.html.parser.HTMLParserListener;
81  import org.htmlunit.http.Cookie;
82  import org.htmlunit.http.HttpStatus;
83  import org.htmlunit.http.HttpUtils;
84  import org.htmlunit.httpclient.HttpClientConverter;
85  import org.htmlunit.javascript.AbstractJavaScriptEngine;
86  import org.htmlunit.javascript.DefaultJavaScriptErrorListener;
87  import org.htmlunit.javascript.HtmlUnitScriptable;
88  import org.htmlunit.javascript.JavaScriptEngine;
89  import org.htmlunit.javascript.JavaScriptErrorListener;
90  import org.htmlunit.javascript.background.JavaScriptJobManager;
91  import org.htmlunit.javascript.host.BroadcastChannel;
92  import org.htmlunit.javascript.host.Location;
93  import org.htmlunit.javascript.host.Window;
94  import org.htmlunit.javascript.host.dom.Node;
95  import org.htmlunit.javascript.host.event.Event;
96  import org.htmlunit.javascript.host.file.Blob;
97  import org.htmlunit.javascript.host.html.HTMLIFrameElement;
98  import org.htmlunit.protocol.data.DataURLConnection;
99  import org.htmlunit.util.HeaderUtils;
100 import org.htmlunit.util.MimeType;
101 import org.htmlunit.util.NameValuePair;
102 import org.htmlunit.util.StringUtils;
103 import org.htmlunit.util.UrlUtils;
104 import org.htmlunit.websocket.JettyWebSocketAdapter.JettyWebSocketAdapterFactory;
105 import org.htmlunit.websocket.WebSocketAdapter;
106 import org.htmlunit.websocket.WebSocketAdapterFactory;
107 import org.htmlunit.websocket.WebSocketListener;
108 import org.htmlunit.webstart.WebStartHandler;
109 
110 /**
111  * The main starting point in HtmlUnit: this class simulates a web browser.
112  * <p>
113  * A standard usage of HtmlUnit will start with using the {@link #getPage(String)} method
114  * (or {@link #getPage(URL)}) to load a first {@link Page}
115  * and will continue with further processing on this page depending on its type.
116  * </p>
117  * <b>Example:</b><br>
118  * <br>
119  * <code>
120  * final WebClient webClient = new WebClient();<br>
121  * final {@link HtmlPage} startPage = webClient.getPage("http://htmlunit.sf.net");<br>
122  * assertEquals("HtmlUnit - Welcome to HtmlUnit", startPage.{@link HtmlPage#getTitleText() getTitleText}());
123  * </code>
124  * <p>
125  * Note: a {@link WebClient} instance is <b>not thread safe</b>. It is intended to be used from a single thread.
126  * </p>
127  * @author Mike Bowler
128  * @author Mike J. Bresnahan
129  * @author Dominique Broeglin
130  * @author Noboru Sinohara
131  * @author Chen Jun
132  * @author David K. Taylor
133  * @author Christian Sell
134  * @author Ben Curren
135  * @author Marc Guillemot
136  * @author Chris Erskine
137  * @author Daniel Gredler
138  * @author Sergey Gorelkin
139  * @author Hans Donner
140  * @author Paul King
141  * @author Ahmed Ashour
142  * @author Bruce Chapman
143  * @author Sudhan Moghe
144  * @author Martin Tamme
145  * @author Amit Manjhi
146  * @author Nicolas Belisle
147  * @author Ronald Brill
148  * @author Frank Danek
149  * @author Joerg Werner
150  * @author Anton Demydenko
151  * @author Sergio Moreno
152  * @author Lai Quang Duong
153  * @author René Schwietzke
154  * @author Sven Strickroth
155  */
156 @SuppressWarnings("PMD.TooManyFields")
157 public class WebClient implements Serializable, AutoCloseable {
158 
159     /** Logging support. */
160     private static final Log LOG = LogFactory.getLog(WebClient.class);
161 
162     /** Like the Firefox default value for {@code network.http.redirection-limit}. */
163     private static final int ALLOWED_REDIRECTIONS_SAME_URL = 20;
164     private static final WebResponseData RESPONSE_DATA_NO_HTTP_RESPONSE = new WebResponseData(
165             0, "No HTTP Response", Collections.emptyList());
166 
167     static final Cleaner CLEANER = Cleaner.create();
168 
169     /**
170      * These response headers are not copied from a 304 response to the cached
171      * response headers. This list is based on Chromium http_response_headers.cc
172      */
173     private static final String[] DISCARDING_304_RESPONSE_HEADER_NAMES = {
174         "connection",
175         "proxy-connection",
176         "keep-alive",
177         "www-authenticate",
178         "proxy-authenticate",
179         "proxy-authorization",
180         "te",
181         "trailer",
182         "transfer-encoding",
183         "upgrade",
184         "content-location",
185         "content-md5",
186         "etag",
187         "content-encoding",
188         "content-range",
189         "content-type",
190         "content-length",
191         "x-frame-options",
192         "x-xss-protection",
193     };
194 
195     private static final String[] DISCARDING_304_HEADER_PREFIXES = {
196         "x-content-",
197         "x-webkit-"
198     };
199 
200     private transient WebConnection webConnection_;
201     private CredentialsProvider credentialsProvider_ = new DefaultCredentialsProvider();
202     private CookieManager cookieManager_ = new CookieManager();
203     private WebSocketAdapterFactory webSocketAdapterFactory_;
204     private transient AbstractJavaScriptEngine<?> scriptEngine_;
205     private transient List<LoadJob> loadQueue_;
206     private final Map<String, String> requestHeaders_ = Collections.synchronizedMap(new HashMap<>(89));
207     private IncorrectnessListener incorrectnessListener_ = new IncorrectnessListenerImpl();
208     private WebConsole webConsole_;
209     private transient ExecutorService executor_;
210 
211     private AlertHandler alertHandler_;
212     private ConfirmHandler confirmHandler_;
213     private PromptHandler promptHandler_;
214     private StatusHandler statusHandler_;
215     private AttachmentHandler attachmentHandler_;
216     private ClipboardHandler clipboardHandler_;
217     private PrintHandler printHandler_;
218     private WebStartHandler webStartHandler_;
219     private FrameContentHandler frameContentHandler_;
220 
221     private AjaxController ajaxController_ = new AjaxController();
222 
223     private final BrowserVersion browserVersion_;
224     private PageCreator pageCreator_ = new DefaultPageCreator();
225 
226     // we need a separate one to be sure the one is always informed as first
227     // one. Only then we can make sure our state is consistent when the others
228     // are informed.
229     private CurrentWindowTracker currentWindowTracker_;
230     private final Set<WebWindowListener> webWindowListeners_ = new HashSet<>(5);
231 
232     private final List<TopLevelWindow> topLevelWindows_ =
233             Collections.synchronizedList(new ArrayList<>()); // top-level windows
234     private final List<WebWindow> windows_ = Collections.synchronizedList(new ArrayList<>()); // all windows
235     private transient List<WeakReference<JavaScriptJobManager>> jobManagers_ =
236             Collections.synchronizedList(new ArrayList<>());
237     private WebWindow currentWindow_;
238 
239     private transient BlobUrlStore blobUrlStore_ = new BlobUrlStore();
240 
241     private HTMLParserListener htmlParserListener_;
242     private CSSErrorHandler cssErrorHandler_ = new DefaultCssErrorHandler();
243     private OnbeforeunloadHandler onbeforeunloadHandler_;
244     private Cache cache_ = new Cache();
245 
246     // mini pool to save resource when parsing CSS
247     private transient CSS3ParserPool css3ParserPool_ = new CSS3ParserPool();
248 
249     /** target "_blank". */
250     public static final String TARGET_BLANK = "_blank";
251 
252     /** target "_self". */
253     public static final String TARGET_SELF = "_self";
254 
255     /** target "_parent". */
256     private static final String TARGET_PARENT = "_parent";
257     /** target "_top". */
258     private static final String TARGET_TOP = "_top";
259 
260     private ScriptPreProcessor scriptPreProcessor_;
261 
262     private RefreshHandler refreshHandler_ = new NiceRefreshHandler(2);
263     private JavaScriptErrorListener javaScriptErrorListener_ = new DefaultJavaScriptErrorListener();
264 
265     private final WebClientOptions options_ = new WebClientOptions();
266     private final boolean javaScriptEngineEnabled_;
267     private final StorageHolder storageHolder_ = new StorageHolder();
268 
269     private transient Set<BroadcastChannel> broadcastChannel_ = new HashSet<>();
270 
271     /**
272      * Creates a web client instance using the browser version returned by
273      * {@link BrowserVersion#getDefault()}.
274      */
275     public WebClient() {
276         this(BrowserVersion.getDefault());
277     }
278 
279     /**
280      * Creates a web client instance using the specified {@link BrowserVersion}.
281      * @param browserVersion the browser version to simulate
282      */
283     public WebClient(final BrowserVersion browserVersion) {
284         this(browserVersion, null, -1);
285     }
286 
287     /**
288      * Creates an instance that will use the specified {@link BrowserVersion} and proxy server.
289      * @param browserVersion the browser version to simulate
290      * @param proxyHost the server that will act as proxy or null for no proxy
291      * @param proxyPort the port to use on the proxy server
292      */
293     public WebClient(final BrowserVersion browserVersion, final String proxyHost, final int proxyPort) {
294         this(browserVersion, true, proxyHost, proxyPort, null);
295     }
296 
297     /**
298      * Creates an instance that will use the specified {@link BrowserVersion} and proxy server.
299      * @param browserVersion the browser version to simulate
300      * @param proxyHost the server that will act as proxy or null for no proxy
301      * @param proxyPort the port to use on the proxy server
302      * @param proxyScheme the scheme http/https
303      */
304     public WebClient(final BrowserVersion browserVersion,
305             final String proxyHost, final int proxyPort, final String proxyScheme) {
306         this(browserVersion, true, proxyHost, proxyPort, proxyScheme);
307     }
308 
309     /**
310      * Creates an instance that will use the specified {@link BrowserVersion} and proxy server.
311      * @param browserVersion the browser version to simulate
312      * @param javaScriptEngineEnabled set to false if the simulated browser should not support javaScript
313      * @param proxyHost the server that will act as proxy or null for no proxy
314      * @param proxyPort the port to use on the proxy server
315      */
316     public WebClient(final BrowserVersion browserVersion, final boolean javaScriptEngineEnabled,
317             final String proxyHost, final int proxyPort) {
318         this(browserVersion, javaScriptEngineEnabled, proxyHost, proxyPort, null);
319     }
320 
321     /**
322      * Creates an instance that will use the specified {@link BrowserVersion} and proxy server.
323      * @param browserVersion the browser version to simulate
324      * @param javaScriptEngineEnabled set to false if the simulated browser should not support javaScript
325      * @param proxyHost the server that will act as proxy or null for no proxy
326      * @param proxyPort the port to use on the proxy server
327      * @param proxyScheme the scheme http/https
328      */
329     public WebClient(final BrowserVersion browserVersion, final boolean javaScriptEngineEnabled,
330             final String proxyHost, final int proxyPort, final String proxyScheme) {
331         WebAssert.notNull("browserVersion", browserVersion);
332 
333         browserVersion_ = browserVersion;
334         javaScriptEngineEnabled_ = javaScriptEngineEnabled;
335 
336         if (proxyHost == null) {
337             getOptions().setProxyConfig(new ProxyConfig());
338         }
339         else {
340             getOptions().setProxyConfig(new ProxyConfig(proxyHost, proxyPort, proxyScheme));
341         }
342 
343         webConnection_ = new HttpWebConnection(this); // this has to be done after the browser version was set
344         if (javaScriptEngineEnabled_) {
345             scriptEngine_ = new JavaScriptEngine(this);
346         }
347         loadQueue_ = new ArrayList<>();
348 
349         webSocketAdapterFactory_ = new JettyWebSocketAdapterFactory();
350 
351         // The window must be constructed AFTER the script engine.
352         currentWindowTracker_ = new CurrentWindowTracker(this, true);
353         currentWindow_ = new TopLevelWindow("", this);
354     }
355 
356     /**
357      * Our simple impl of a ThreadFactory (decorator) to be able to name
358      * our threads.
359      */
360     private static final class ThreadNamingFactory implements ThreadFactory {
361         private static int ID_ = 1;
362         private final ThreadFactory baseFactory_;
363 
364         ThreadNamingFactory(final ThreadFactory aBaseFactory) {
365             baseFactory_ = aBaseFactory;
366         }
367 
368         @Override
369         public Thread newThread(final Runnable aRunnable) {
370             final Thread thread = baseFactory_.newThread(aRunnable);
371             thread.setName("WebClient Thread " + ID_++);
372             return thread;
373         }
374     }
375 
376     /**
377      * Returns the object that will resolve all URL requests.
378      *
379      * @return the connection that will be used
380      */
381     public WebConnection getWebConnection() {
382         return webConnection_;
383     }
384 
385     /**
386      * Sets the object that will resolve all URL requests.
387      *
388      * @param webConnection the new web connection
389      */
390     public void setWebConnection(final WebConnection webConnection) {
391         WebAssert.notNull("webConnection", webConnection);
392         webConnection_ = webConnection;
393     }
394 
395     /**
396      * Send a request to a server and return a Page that represents the
397      * response from the server. This page will be used to populate the provided window.
398      * <p>
399      * The returned {@link Page} will be created by the {@link PageCreator}
400      * configured by {@link #setPageCreator(PageCreator)}, if any.
401      * </p>
402      * <p>
403      * The {@link DefaultPageCreator} will create a {@link Page} depending on the content type of the HTTP response,
404      * basically {@link HtmlPage} for HTML content, {@link org.htmlunit.xml.XmlPage} for XML content,
405      * {@link TextPage} for other text content and {@link UnexpectedPage} for anything else.
406      * </p>
407      *
408      * @param webWindow the WebWindow to load the result of the request into
409      * @param webRequest the web request
410      * @param <P> the page type
411      * @return the page returned by the server when the specified request was made in the specified window
412      * @throws IOException if an IO error occurs
413      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
414      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
415      *
416      * @see WebRequest
417      */
418     public <P extends Page> P getPage(final WebWindow webWindow, final WebRequest webRequest)
419             throws IOException, FailingHttpStatusCodeException {
420         return getPage(webWindow, webRequest, true);
421     }
422 
423     /**
424      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
425      *
426      * Send a request to a server and return a Page that represents the
427      * response from the server. This page will be used to populate the provided window.
428      * <p>
429      * The returned {@link Page} will be created by the {@link PageCreator}
430      * configured by {@link #setPageCreator(PageCreator)}, if any.
431      * </p>
432      * <p>
433      * The {@link DefaultPageCreator} will create a {@link Page} depending on the content type of the HTTP response,
434      * basically {@link HtmlPage} for HTML content, {@link org.htmlunit.xml.XmlPage} for XML content,
435      * {@link TextPage} for other text content and {@link UnexpectedPage} for anything else.
436      * </p>
437      *
438      * @param webWindow the WebWindow to load the result of the request into
439      * @param webRequest the web request
440      * @param addToHistory true if the page should be part of the history
441      * @param <P> the page type
442      * @return the page returned by the server when the specified request was made in the specified window
443      * @throws IOException if an IO error occurs
444      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
445      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
446      *
447      * @see WebRequest
448      */
449     @SuppressWarnings("unchecked")
450     <P extends Page> P getPage(final WebWindow webWindow, final WebRequest webRequest,
451             final boolean addToHistory)
452         throws IOException, FailingHttpStatusCodeException {
453 
454         final Page page = webWindow.getEnclosedPage();
455 
456         if (page != null) {
457             final URL prev = page.getUrl();
458             final URL current = webRequest.getUrl();
459             if (UrlUtils.sameFile(current, prev)
460                         && current.getRef() != null
461                         && !Objects.equals(current.getRef(), prev.getRef())) {
462                 // We're just navigating to an anchor within the current page.
463                 page.getWebResponse().getWebRequest().setUrl(current);
464                 if (addToHistory) {
465                     webWindow.getHistory().addPage(page);
466                 }
467 
468                 // clear the cache because the anchors are now matched by
469                 // the target pseudo style
470                 if (page instanceof HtmlPage htmlPage) {
471                     htmlPage.clearComputedStyles();
472                 }
473 
474                 final Window window = webWindow.getScriptableObject();
475                 if (window != null) { // js enabled
476                     window.getLocation().setHash(current.getRef());
477                 }
478                 return (P) page;
479             }
480 
481             if (page.isHtmlPage()) {
482                 final HtmlPage htmlPage = (HtmlPage) page;
483                 if (!htmlPage.isOnbeforeunloadAccepted()) {
484                     LOG.debug("The registered OnbeforeunloadHandler rejected to load a new page.");
485                     return (P) page;
486                 }
487             }
488         }
489 
490         if (LOG.isDebugEnabled()) {
491             LOG.debug("Get page for window named '" + webWindow.getName() + "', using " + webRequest);
492         }
493 
494         WebResponse webResponse;
495         final String protocol = webRequest.getUrl().getProtocol();
496         if ("javascript".equals(protocol)) {
497             webResponse = makeWebResponseForJavaScriptUrl(webWindow, webRequest.getUrl(), webRequest.getCharset());
498             if (webWindow.getEnclosedPage() != null && webWindow.getEnclosedPage().getWebResponse() == webResponse) {
499                 // a javascript:... url with result of type undefined didn't changed the page
500                 return (P) webWindow.getEnclosedPage();
501             }
502         }
503         else {
504             try {
505                 webResponse = loadWebResponse(webRequest);
506             }
507             catch (final NoHttpResponseException e) {
508                 webResponse = new WebResponse(RESPONSE_DATA_NO_HTTP_RESPONSE, webRequest, 0);
509             }
510         }
511 
512         printContentIfNecessary(webResponse);
513         loadWebResponseInto(webResponse, webWindow);
514 
515         // start execution here
516         // note: we have to do this also if the server reports an error!
517         //       e.g. if the server returns a 404 error page that includes javascript
518         if (scriptEngine_ != null) {
519             scriptEngine_.registerWindowAndMaybeStartEventLoop(webWindow);
520         }
521 
522         // check and report problems if needed
523         throwFailingHttpStatusCodeExceptionIfNecessary(webResponse);
524         return (P) webWindow.getEnclosedPage();
525     }
526 
527     /**
528      * Convenient method to build a URL and load it into the current WebWindow as it would be done
529      * by {@link #getPage(WebWindow, WebRequest)}.
530      * @param url the URL of the new content; in contrast to real browsers plain file url's are not supported.
531      *        You have to use the 'file', 'data', 'blob', 'http' or 'https' protocol.
532      * @param <P> the page type
533      * @return the new page
534      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
535      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true.
536      * @throws IOException if an IO problem occurs
537      * @throws MalformedURLException if no URL can be created from the provided string
538      */
539     public <P extends Page> P getPage(final String url) throws IOException, FailingHttpStatusCodeException,
540         MalformedURLException {
541         return getPage(UrlUtils.toUrlUnsafe(url));
542     }
543 
544     /**
545      * Convenient method to load a URL into the current top WebWindow as it would be done
546      * by {@link #getPage(WebWindow, WebRequest)}.
547      * @param url the URL of the new content; in contrast to real browsers plain file url's are not supported.
548      *        You have to use the 'file', 'data', 'blob', 'http' or 'https' protocol.
549      * @param <P> the page type
550      * @return the new page
551      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
552      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true.
553      * @throws IOException if an IO problem occurs
554      */
555     public <P extends Page> P getPage(final URL url) throws IOException, FailingHttpStatusCodeException {
556         final WebRequest request = new WebRequest(url, getBrowserVersion().getHtmlAcceptHeader(),
557                                                           getBrowserVersion().getAcceptEncodingHeader());
558         request.setCharset(UTF_8);
559         return getPage(getCurrentWindow().getTopWindow(), request);
560     }
561 
562     /**
563      * Convenient method to load a web request into the current top WebWindow.
564      * @param request the request parameters
565      * @param <P> the page type
566      * @return the new page
567      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
568      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true.
569      * @throws IOException if an IO problem occurs
570      * @see #getPage(WebWindow,WebRequest)
571      */
572     public <P extends Page> P getPage(final WebRequest request) throws IOException,
573         FailingHttpStatusCodeException {
574         return getPage(getCurrentWindow().getTopWindow(), request);
575     }
576 
577     /**
578      * <p>Creates a page based on the specified response and inserts it into the specified window. All page
579      * initialization and event notification is handled here.</p>
580      *
581      * <p>Note that if the page created is an attachment page, and an {@link AttachmentHandler} has been
582      * registered with this client, the page is <b>not</b> loaded into the specified window; in this case,
583      * the page is loaded into a new window, and attachment handling is delegated to the registered
584      * <code>AttachmentHandler</code>.</p>
585      *
586      * @param webResponse the response that will be used to create the new page
587      * @param webWindow the window that the new page will be placed within
588      * @throws IOException if an IO error occurs
589      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
590      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
591      * @return the newly created page
592      * @see #setAttachmentHandler(AttachmentHandler)
593      */
594     public Page loadWebResponseInto(final WebResponse webResponse, final WebWindow webWindow)
595         throws IOException, FailingHttpStatusCodeException {
596         return loadWebResponseInto(webResponse, webWindow, null);
597     }
598 
599     /**
600      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
601      *
602      * <p>Creates a page based on the specified response and inserts it into the specified window. All page
603      * initialization and event notification is handled here.</p>
604      *
605      * <p>Note that if the page created is an attachment page, and an {@link AttachmentHandler} has been
606      * registered with this client, the page is <b>not</b> loaded into the specified window; in this case,
607      * the page is loaded into a new window, and attachment handling is delegated to the registered
608      * <code>AttachmentHandler</code>.</p>
609      *
610      * @param webResponse the response that will be used to create the new page
611      * @param webWindow the window that the new page will be placed within
612      * @param forceAttachmentWithFilename if not {@code null}, handle this as an attachment with the specified name
613      *        or if an empty string ("") use the filename provided in the response
614      * @throws IOException if an IO error occurs
615      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
616      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
617      * @return the newly created page
618      * @see #setAttachmentHandler(AttachmentHandler)
619      */
620     public Page loadWebResponseInto(final WebResponse webResponse, final WebWindow webWindow,
621             String forceAttachmentWithFilename)
622             throws IOException, FailingHttpStatusCodeException {
623         WebAssert.notNull("webResponse", webResponse);
624         WebAssert.notNull("webWindow", webWindow);
625 
626         if (webResponse.getStatusCode() == HttpStatus.NO_CONTENT_204) {
627             return webWindow.getEnclosedPage();
628         }
629 
630         if (webStartHandler_ != null && "application/x-java-jnlp-file".equals(webResponse.getContentType())) {
631             webStartHandler_.handleJnlpResponse(webResponse);
632             return webWindow.getEnclosedPage();
633         }
634 
635         if (attachmentHandler_ != null
636                 && (forceAttachmentWithFilename != null || attachmentHandler_.isAttachment(webResponse))) {
637 
638             // check content disposition header for nothing provided
639             if (StringUtils.isEmptyOrNull(forceAttachmentWithFilename)) {
640                 final String disp = webResponse.getResponseHeaderValue(HttpHeader.CONTENT_DISPOSITION);
641                 forceAttachmentWithFilename = Attachment.getSuggestedFilename(disp);
642             }
643 
644             if (attachmentHandler_.handleAttachment(webResponse,
645                         StringUtils.isEmptyOrNull(forceAttachmentWithFilename) ? null : forceAttachmentWithFilename)) {
646                 // the handling is done by the attachment handler;
647                 // do not open a new window
648                 return webWindow.getEnclosedPage();
649             }
650 
651             final WebWindow w = openWindow(null, null, webWindow);
652             final Page page = pageCreator_.createPage(webResponse, w);
653             attachmentHandler_.handleAttachment(page,
654                                 StringUtils.isEmptyOrNull(forceAttachmentWithFilename)
655                                         ? null : forceAttachmentWithFilename);
656             return page;
657         }
658 
659         final Page oldPage = webWindow.getEnclosedPage();
660         if (oldPage != null) {
661             // Remove the old page before create new one.
662             oldPage.cleanUp();
663         }
664 
665         Page newPage = null;
666         FrameWindow.PageDenied pageDenied = PageDenied.NONE;
667         if (windows_.contains(webWindow)) {
668             if (webWindow instanceof FrameWindow window) {
669                 final String contentSecurityPolicy =
670                         webResponse.getResponseHeaderValue(HttpHeader.CONTENT_SECURIRY_POLICY);
671                 if (StringUtils.isNotBlank(contentSecurityPolicy)) {
672                     final URL origin = UrlUtils.getUrlWithoutPathRefQuery(
673                             window.getEnclosingPage().getUrl());
674                     final URL source = UrlUtils.getUrlWithoutPathRefQuery(webResponse.getWebRequest().getUrl());
675                     final Policy policy = Policy.parseSerializedCSP(contentSecurityPolicy,
676                                                     Policy.PolicyErrorConsumer.ignored);
677                     if (!policy.allowsFrameAncestor(
678                             Optional.of(URI.parseURI(source.toExternalForm()).orElse(null)),
679                             Optional.of(URI.parseURI(origin.toExternalForm()).orElse(null)))) {
680                         pageDenied = PageDenied.BY_CONTENT_SECURIRY_POLICY;
681 
682                         if (LOG.isWarnEnabled()) {
683                             LOG.warn("Load denied by Content-Security-Policy: '" + contentSecurityPolicy + "' - "
684                                     + webResponse.getWebRequest().getUrl() + "' does not permit framing.");
685                         }
686                     }
687                 }
688 
689                 if (pageDenied == PageDenied.NONE) {
690                     final String xFrameOptions = webResponse.getResponseHeaderValue(HttpHeader.X_FRAME_OPTIONS);
691                     if ("DENY".equalsIgnoreCase(xFrameOptions)) {
692                         pageDenied = PageDenied.BY_X_FRAME_OPTIONS;
693 
694                         if (LOG.isWarnEnabled()) {
695                             LOG.warn("Load denied by X-Frame-Options: DENY; - '"
696                                     + webResponse.getWebRequest().getUrl() + "' does not permit framing.");
697                         }
698                     }
699                 }
700             }
701 
702             if (pageDenied == PageDenied.NONE) {
703                 newPage = pageCreator_.createPage(webResponse, webWindow);
704             }
705             else {
706                 try {
707                     final WebResponse aboutBlank = loadWebResponse(WebRequest.newAboutBlankRequest());
708                     newPage = pageCreator_.createPage(aboutBlank, webWindow);
709                     // TODO - maybe we have to attach to original request/response to the page
710 
711                     ((FrameWindow) webWindow).setPageDenied(pageDenied);
712                 }
713                 catch (final IOException ignored) {
714                     // ignore
715                 }
716             }
717 
718             if (windows_.contains(webWindow)) {
719                 fireWindowContentChanged(new WebWindowEvent(webWindow, WebWindowEvent.CHANGE, oldPage, newPage));
720 
721                 // The page being loaded may already have been replaced by another page via JavaScript code.
722                 if (webWindow.getEnclosedPage() == newPage) {
723                     newPage.initialize();
724                     // hack: onload should be fired the same way for all type of pages
725                     // here is a hack to handle non HTML pages
726                     if (isJavaScriptEnabled()
727                             && webWindow instanceof FrameWindow fw && !newPage.isHtmlPage()) {
728                         final BaseFrameElement frame = fw.getFrameElement();
729                         if (frame.hasEventHandlers("onload")) {
730                             if (LOG.isDebugEnabled()) {
731                                 LOG.debug("Executing onload handler for " + frame);
732                             }
733                             final Event event = new Event(frame, Event.TYPE_LOAD);
734                             ((Node) frame.getScriptableObject()).executeEventLocally(event);
735                         }
736                     }
737                 }
738             }
739         }
740         return newPage;
741     }
742 
743     /**
744      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
745      *
746      * <p>Logs the response's content if its status code indicates a request failure and
747      * {@link WebClientOptions#isPrintContentOnFailingStatusCode()} returns {@code true}.
748      * </p>
749      *
750      * @param webResponse the response whose content may be logged
751      */
752     public void printContentIfNecessary(final WebResponse webResponse) {
753         if (getOptions().isPrintContentOnFailingStatusCode()
754                 && !webResponse.isSuccess() && LOG.isInfoEnabled()) {
755             final String contentType = webResponse.getContentType();
756             LOG.info("statusCode=[" + webResponse.getStatusCode() + "] contentType=[" + contentType + "]");
757             LOG.info(webResponse.getContentAsString());
758         }
759     }
760 
761     /**
762      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
763      *
764      * <p>Throws a {@link FailingHttpStatusCodeException} if the request's status code indicates a request
765      * failure and {@link WebClientOptions#isThrowExceptionOnFailingStatusCode()} returns {@code true}.
766      * </p>
767      *
768      * @param webResponse the response which may trigger a {@link FailingHttpStatusCodeException}
769      */
770     public void throwFailingHttpStatusCodeExceptionIfNecessary(final WebResponse webResponse) {
771         if (getOptions().isThrowExceptionOnFailingStatusCode() && !webResponse.isSuccessOrUseProxyOrNotModified()) {
772             throw new FailingHttpStatusCodeException(webResponse);
773         }
774     }
775 
776     /**
777      * Adds a header which will be sent with EVERY request from this client.
778      * This list is empty per default; use this to add specific headers for your
779      * case.
780      * @param name the name of the header to add
781      * @param value the value of the header to add
782      * @see #removeRequestHeader(String)
783      */
784     public void addRequestHeader(final String name, final String value) {
785         if (HttpHeader.COOKIE_LC.equalsIgnoreCase(name)) {
786             throw new IllegalArgumentException("Do not add 'Cookie' header, use .getCookieManager() instead");
787         }
788         requestHeaders_.put(name, value);
789     }
790 
791     /**
792      * Removes a header from being sent with EVERY request from this client.
793      * This list is empty per default; use this method to remove specific headers
794      * your have added using {{@link #addRequestHeader(String, String)} before.<br>
795      * You can't use this to avoid sending standard headers like "Accept-Language"
796      * or "Sec-Fetch-Dest".
797      * @param name the name of the header to remove
798      * @see #addRequestHeader
799      */
800     public void removeRequestHeader(final String name) {
801         requestHeaders_.remove(name);
802     }
803 
804     /**
805      * Sets the credentials provider that will provide authentication information when
806      * trying to access protected information on a web server. This information is
807      * required when the server is using Basic HTTP authentication, NTLM authentication,
808      * or Digest authentication.
809      * @param credentialsProvider the new credentials provider to use to authenticate
810      */
811     public void setCredentialsProvider(final CredentialsProvider credentialsProvider) {
812         WebAssert.notNull("credentialsProvider", credentialsProvider);
813         credentialsProvider_ = credentialsProvider;
814     }
815 
816     /**
817      * Returns the credentials provider for this client instance. By default, this
818      * method returns an instance of {@link DefaultCredentialsProvider}.
819      * @return the credentials provider for this client instance
820      */
821     public CredentialsProvider getCredentialsProvider() {
822         return credentialsProvider_;
823     }
824 
825     /**
826      * This method is intended for testing only - use at your own risk.
827      * @return the current JavaScript engine (never {@code null})
828      */
829     public AbstractJavaScriptEngine<?> getJavaScriptEngine() {
830         return scriptEngine_;
831     }
832 
833     /**
834      * This method is intended for testing only - use at your own risk.
835      *
836      * @param engine the new script engine to use
837      */
838     public void setJavaScriptEngine(final AbstractJavaScriptEngine<?> engine) {
839         if (engine == null) {
840             throw new IllegalArgumentException("Can't set JavaScriptEngine to null");
841         }
842         scriptEngine_ = engine;
843     }
844 
845     /**
846      * Returns the cookie manager used by this web client.
847      * @return the cookie manager used by this web client
848      */
849     public CookieManager getCookieManager() {
850         return cookieManager_;
851     }
852 
853     /**
854      * Sets the cookie manager used by this web client.
855      * @param cookieManager the cookie manager used by this web client
856      */
857     public void setCookieManager(final CookieManager cookieManager) {
858         WebAssert.notNull("cookieManager", cookieManager);
859         cookieManager_ = cookieManager;
860     }
861 
862     /**
863      * Sets the alert handler for this webclient.
864      * @param alertHandler the new alerthandler or null if none is specified
865      */
866     public void setAlertHandler(final AlertHandler alertHandler) {
867         alertHandler_ = alertHandler;
868     }
869 
870     /**
871      * Returns the alert handler for this webclient.
872      * @return the alert handler or null if one hasn't been set
873      */
874     public AlertHandler getAlertHandler() {
875         return alertHandler_;
876     }
877 
878     /**
879      * Sets the handler that will be executed when the JavaScript method Window.confirm() is called.
880      * @param handler the new handler or null if no handler is to be used
881      */
882     public void setConfirmHandler(final ConfirmHandler handler) {
883         confirmHandler_ = handler;
884     }
885 
886     /**
887      * Returns the confirm handler.
888      * @return the confirm handler or null if one hasn't been set
889      */
890     public ConfirmHandler getConfirmHandler() {
891         return confirmHandler_;
892     }
893 
894     /**
895      * Sets the handler that will be executed when the JavaScript method Window.prompt() is called.
896      * @param handler the new handler or null if no handler is to be used
897      */
898     public void setPromptHandler(final PromptHandler handler) {
899         promptHandler_ = handler;
900     }
901 
902     /**
903      * Returns the prompt handler.
904      * @return the prompt handler or null if one hasn't been set
905      */
906     public PromptHandler getPromptHandler() {
907         return promptHandler_;
908     }
909 
910     /**
911      * Sets the status handler for this webclient.
912      * @param statusHandler the new status handler or null if none is specified
913      */
914     public void setStatusHandler(final StatusHandler statusHandler) {
915         statusHandler_ = statusHandler;
916     }
917 
918     /**
919      * Returns the status handler for this {@link WebClient}.
920      * @return the status handler or null if one hasn't been set
921      */
922     public StatusHandler getStatusHandler() {
923         return statusHandler_;
924     }
925 
926     /**
927      * Returns the executor for this {@link WebClient}.
928      * @return the executor
929      */
930     public synchronized Executor getExecutor() {
931         if (executor_ == null) {
932             final ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
933             threadPoolExecutor.setThreadFactory(new ThreadNamingFactory(threadPoolExecutor.getThreadFactory()));
934             // threadPoolExecutor.prestartAllCoreThreads();
935             executor_ = threadPoolExecutor;
936         }
937 
938         return executor_;
939     }
940 
941     /**
942      * Changes the ExecutorService for this {@link WebClient}.
943      * You have to call this before the first use of the executor, otherwise
944      * an IllegalStateExceptions is thrown.
945      * @param executor the new Executor.
946      */
947     public synchronized void setExecutor(final ExecutorService executor) {
948         if (executor_ != null) {
949             throw new IllegalStateException("Can't change the executor after first use.");
950         }
951 
952         executor_ = executor;
953     }
954 
955     /**
956      * Sets the javascript error listener for this {@link WebClient}.
957      * When setting to null, the {@link DefaultJavaScriptErrorListener} is used.
958      * @param javaScriptErrorListener the new JavaScriptErrorListener or null if none is specified
959      */
960     public void setJavaScriptErrorListener(final JavaScriptErrorListener javaScriptErrorListener) {
961         if (javaScriptErrorListener == null) {
962             javaScriptErrorListener_ = new DefaultJavaScriptErrorListener();
963         }
964         else {
965             javaScriptErrorListener_ = javaScriptErrorListener;
966         }
967     }
968 
969     /**
970      * Returns the javascript error listener for this {@link WebClient}.
971      * @return the javascript error listener or null if one hasn't been set
972      */
973     public JavaScriptErrorListener getJavaScriptErrorListener() {
974         return javaScriptErrorListener_;
975     }
976 
977     /**
978      * Returns the current browser version.
979      * @return the current browser version
980      */
981     public BrowserVersion getBrowserVersion() {
982         return browserVersion_;
983     }
984 
985     /**
986      * Returns the "current" window for this client. This window (or its top window) will be used
987      * when <code>getPage(...)</code> is called without specifying a window.
988      * @return the "current" window for this client
989      */
990     public WebWindow getCurrentWindow() {
991         return currentWindow_;
992     }
993 
994     /**
995      * Sets the "current" window for this client. This is the window that will be used when
996      * <code>getPage(...)</code> is called without specifying a window.
997      * @param window the new "current" window for this client
998      */
999     public void setCurrentWindow(final WebWindow window) {
1000         WebAssert.notNull("window", window);
1001         if (currentWindow_ == window) {
1002             return;
1003         }
1004         // onBlur event is triggered for focused element of old current window
1005         if (currentWindow_ != null && !currentWindow_.isClosed()) {
1006             final Page enclosedPage = currentWindow_.getEnclosedPage();
1007             if (enclosedPage != null && enclosedPage.isHtmlPage()) {
1008                 final DomElement focusedElement = ((HtmlPage) enclosedPage).getFocusedElement();
1009                 if (focusedElement != null) {
1010                     focusedElement.fireEvent(Event.TYPE_BLUR);
1011                 }
1012             }
1013         }
1014         currentWindow_ = window;
1015 
1016         // when marking an iframe window as current we have no need to move the focus
1017         final boolean isIFrame = currentWindow_ instanceof FrameWindow fw
1018                 && fw.getFrameElement() instanceof HtmlInlineFrame;
1019         if (!isIFrame) {
1020             //1. activeElement becomes focused element for new current window
1021             //2. onFocus event is triggered for focusedElement of new current window
1022             final Page enclosedPage = currentWindow_.getEnclosedPage();
1023             if (enclosedPage != null && enclosedPage.isHtmlPage()) {
1024                 final HtmlPage enclosedHtmlPage = (HtmlPage) enclosedPage;
1025                 final HtmlElement activeElement = enclosedHtmlPage.getActiveElement();
1026                 if (activeElement != null) {
1027                     enclosedHtmlPage.setFocusedElement(activeElement, true);
1028                 }
1029             }
1030         }
1031     }
1032 
1033     /**
1034      * Returns the blob URL store for this client.
1035      * @return the {@link BlobUrlStore} for this client
1036      */
1037     public BlobUrlStore getBlobUrlStore() {
1038         return blobUrlStore_;
1039     }
1040 
1041     /**
1042      * Adds a listener for {@link WebWindowEvent}s. All events from all windows associated with this
1043      * client will be sent to the specified listener.
1044      * @param listener a listener
1045      */
1046     public void addWebWindowListener(final WebWindowListener listener) {
1047         WebAssert.notNull("listener", listener);
1048         webWindowListeners_.add(listener);
1049     }
1050 
1051     /**
1052      * Removes a listener for {@link WebWindowEvent}s.
1053      * @param listener a listener
1054      */
1055     public void removeWebWindowListener(final WebWindowListener listener) {
1056         WebAssert.notNull("listener", listener);
1057         webWindowListeners_.remove(listener);
1058     }
1059 
1060     private void fireWindowContentChanged(final WebWindowEvent event) {
1061         if (currentWindowTracker_ != null) {
1062             currentWindowTracker_.webWindowContentChanged(event);
1063         }
1064         for (final WebWindowListener listener : new ArrayList<>(webWindowListeners_)) {
1065             listener.webWindowContentChanged(event);
1066         }
1067 
1068         blobUrlStore_.removeForPage(event.getOldPage());
1069     }
1070 
1071     private void fireWindowOpened(final WebWindowEvent event) {
1072         if (currentWindowTracker_ != null) {
1073             currentWindowTracker_.webWindowOpened(event);
1074         }
1075         for (final WebWindowListener listener : new ArrayList<>(webWindowListeners_)) {
1076             listener.webWindowOpened(event);
1077         }
1078     }
1079 
1080     private void fireWindowClosed(final WebWindowEvent event) {
1081         if (currentWindowTracker_ != null) {
1082             currentWindowTracker_.webWindowClosed(event);
1083         }
1084 
1085         for (final WebWindowListener listener : new ArrayList<>(webWindowListeners_)) {
1086             listener.webWindowClosed(event);
1087         }
1088 
1089         blobUrlStore_.removeForPage(event.getOldPage());
1090 
1091         // to open a new top level window if all others are gone
1092         if (currentWindowTracker_ != null) {
1093             currentWindowTracker_.afterWebWindowClosedListenersProcessed(event);
1094         }
1095     }
1096 
1097     /**
1098      * Open a new window with the specified name. If the URL is non-null then attempt to load
1099      * a page from that location and put it in the new window.
1100      *
1101      * @param url the URL to load content from or null if no content is to be loaded
1102      * @param windowName the name of the new window
1103      * @return the new window
1104      */
1105     public WebWindow openWindow(final URL url, final String windowName) {
1106         WebAssert.notNull("windowName", windowName);
1107         return openWindow(url, windowName, getCurrentWindow());
1108     }
1109 
1110     /**
1111      * Open a new window with the specified name. If the URL is non-null then attempt to load
1112      * a page from that location and put it in the new window.
1113      *
1114      * @param url the URL to load content from or null if no content is to be loaded
1115      * @param windowName the name of the new window
1116      * @param opener the web window that is calling openWindow
1117      * @return the new window
1118      */
1119     public WebWindow openWindow(final URL url, final String windowName, final WebWindow opener) {
1120         final WebWindow window = openTargetWindow(opener, windowName, TARGET_BLANK);
1121         if (url == null) {
1122             initializeEmptyWindow(window, window.getEnclosedPage());
1123         }
1124         else {
1125             try {
1126                 final WebRequest request = new WebRequest(url, getBrowserVersion().getHtmlAcceptHeader(),
1127                                                                 getBrowserVersion().getAcceptEncodingHeader());
1128                 request.setCharset(UTF_8);
1129 
1130                 final Page openerPage = opener.getEnclosedPage();
1131                 if (openerPage != null && openerPage.getUrl() != null) {
1132                     request.setRefererHeader(openerPage.getUrl());
1133                 }
1134                 getPage(window, request);
1135             }
1136             catch (final IOException e) {
1137                 LOG.error("Error loading content into window", e);
1138             }
1139         }
1140         return window;
1141     }
1142 
1143     /**
1144      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1145      *
1146      * Open the window with the specified name. The name may be a special
1147      * target name of _self, _parent, _top, or _blank. An empty or null
1148      * name is set to the default. The special target names are relative to
1149      * the opener window.
1150      *
1151      * @param opener the web window that is calling openWindow
1152      * @param windowName the name of the new window
1153      * @param defaultName the default target if no name is given
1154      * @return the new window
1155      */
1156     public WebWindow openTargetWindow(
1157             final WebWindow opener, final String windowName, final String defaultName) {
1158 
1159         WebAssert.notNull("opener", opener);
1160         WebAssert.notNull("defaultName", defaultName);
1161 
1162         String windowToOpen = windowName;
1163         if (windowToOpen == null || windowToOpen.isEmpty()) {
1164             windowToOpen = defaultName;
1165         }
1166 
1167         WebWindow webWindow = resolveWindow(opener, windowToOpen);
1168 
1169         if (webWindow == null) {
1170             if (TARGET_BLANK.equals(windowToOpen)) {
1171                 windowToOpen = "";
1172             }
1173             webWindow = new TopLevelWindow(windowToOpen, this);
1174         }
1175 
1176         if (webWindow instanceof TopLevelWindow window && webWindow != opener.getTopWindow()) {
1177             window.setOpener(opener);
1178         }
1179 
1180         return webWindow;
1181     }
1182 
1183     private WebWindow resolveWindow(final WebWindow opener, final String name) {
1184         if (name == null || name.isEmpty() || TARGET_SELF.equals(name)) {
1185             return opener;
1186         }
1187 
1188         if (TARGET_PARENT.equals(name)) {
1189             return opener.getParentWindow();
1190         }
1191 
1192         if (TARGET_TOP.equals(name)) {
1193             return opener.getTopWindow();
1194         }
1195 
1196         if (TARGET_BLANK.equals(name)) {
1197             return null;
1198         }
1199 
1200         // first search for frame windows inside our window hierarchy
1201         WebWindow window = opener;
1202         while (true) {
1203             final Page page = window.getEnclosedPage();
1204             if (page != null && page.isHtmlPage()) {
1205                 try {
1206                     final FrameWindow frame = ((HtmlPage) page).getFrameByName(name);
1207                     final HtmlUnitScriptable scriptable = frame.getFrameElement().getScriptableObject();
1208                     if (scriptable instanceof HTMLIFrameElement element) {
1209                         element.onRefresh();
1210                     }
1211                     return frame;
1212                 }
1213                 catch (final ElementNotFoundException expected) {
1214                     // Fall through
1215                 }
1216             }
1217 
1218             if (window == window.getParentWindow()) {
1219                 // TODO: should getParentWindow() return null on top windows?
1220                 break;
1221             }
1222             window = window.getParentWindow();
1223         }
1224 
1225         try {
1226             return getWebWindowByName(name);
1227         }
1228         catch (final WebWindowNotFoundException expected) {
1229             // Fall through - a new window will be created below
1230         }
1231         return null;
1232     }
1233 
1234     /**
1235      * <p><span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span></p>
1236      *
1237      * Opens a new dialog window.
1238      * @param url the URL of the document to load and display
1239      * @param opener the web window that is opening the dialog
1240      * @param dialogArguments the object to make available inside the dialog via <code>window.dialogArguments</code>
1241      * @return the new dialog window
1242      * @throws IOException if there is an IO error
1243      */
1244     public DialogWindow openDialogWindow(final URL url, final WebWindow opener, final Object dialogArguments)
1245         throws IOException {
1246 
1247         WebAssert.notNull("url", url);
1248         WebAssert.notNull("opener", opener);
1249 
1250         final DialogWindow window = new DialogWindow(this, dialogArguments);
1251 
1252         final HtmlPage openerPage = (HtmlPage) opener.getEnclosedPage();
1253         final WebRequest request = new WebRequest(url, getBrowserVersion().getHtmlAcceptHeader(),
1254                                                         getBrowserVersion().getAcceptEncodingHeader());
1255         request.setCharset(UTF_8);
1256 
1257         if (openerPage != null) {
1258             request.setRefererHeader(openerPage.getUrl());
1259         }
1260 
1261         getPage(window, request);
1262 
1263         return window;
1264     }
1265 
1266     /**
1267      * Sets the object that will be used to create pages. Set this if you want
1268      * to customize the type of page that is returned for a given content type.
1269      *
1270      * @param pageCreator the new page creator
1271      */
1272     public void setPageCreator(final PageCreator pageCreator) {
1273         WebAssert.notNull("pageCreator", pageCreator);
1274         pageCreator_ = pageCreator;
1275     }
1276 
1277     /**
1278      * Returns the current page creator.
1279      *
1280      * @return the page creator
1281      */
1282     public PageCreator getPageCreator() {
1283         return pageCreator_;
1284     }
1285 
1286     /**
1287      * Returns the first {@link WebWindow} that matches the specified name.
1288      *
1289      * @param name the name to search for
1290      * @return the {@link WebWindow} with the specified name
1291      * @throws WebWindowNotFoundException if the {@link WebWindow} can't be found
1292      * @see #getWebWindows()
1293      * @see #getTopLevelWindows()
1294      */
1295     public WebWindow getWebWindowByName(final String name) throws WebWindowNotFoundException {
1296         WebAssert.notNull("name", name);
1297 
1298         for (final WebWindow webWindow : windows_) {
1299             if (name.equals(webWindow.getName())) {
1300                 return webWindow;
1301             }
1302         }
1303 
1304         throw new WebWindowNotFoundException(name);
1305     }
1306 
1307     /**
1308      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1309      *
1310      * Initializes a new web window for JavaScript.
1311      * @param webWindow the new WebWindow
1312      * @param page the page that will become the enclosing page
1313      */
1314     public void initialize(final WebWindow webWindow, final Page page) {
1315         WebAssert.notNull("webWindow", webWindow);
1316 
1317         if (isJavaScriptEngineEnabled()) {
1318             scriptEngine_.initialize(webWindow, page);
1319         }
1320     }
1321 
1322     /**
1323      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1324      *
1325      * Initializes a new empty window for JavaScript.
1326      *
1327      * @param webWindow the new WebWindow
1328      * @param page the page that will become the enclosing page
1329      */
1330     public void initializeEmptyWindow(final WebWindow webWindow, final Page page) {
1331         WebAssert.notNull("webWindow", webWindow);
1332 
1333         if (isJavaScriptEngineEnabled()) {
1334             initialize(webWindow, page);
1335             ((Window) webWindow.getScriptableObject()).initialize();
1336         }
1337     }
1338 
1339     /**
1340      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1341      *
1342      * Adds a new window to the list of available windows.
1343      *
1344      * @param webWindow the new WebWindow
1345      */
1346     public void registerWebWindow(final WebWindow webWindow) {
1347         WebAssert.notNull("webWindow", webWindow);
1348         if (windows_.add(webWindow)) {
1349             fireWindowOpened(new WebWindowEvent(webWindow, WebWindowEvent.OPEN, webWindow.getEnclosedPage(), null));
1350         }
1351         // register JobManager here but don't deregister in deregisterWebWindow as it can live longer
1352         jobManagers_.add(new WeakReference<>(webWindow.getJobManager()));
1353     }
1354 
1355     /**
1356      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1357      *
1358      * Removes a window from the list of available windows.
1359      *
1360      * @param webWindow the window to remove
1361      */
1362     public void deregisterWebWindow(final WebWindow webWindow) {
1363         WebAssert.notNull("webWindow", webWindow);
1364         if (windows_.remove(webWindow)) {
1365             fireWindowClosed(new WebWindowEvent(webWindow, WebWindowEvent.CLOSE, webWindow.getEnclosedPage(), null));
1366         }
1367     }
1368 
1369     /**
1370      * Registers an object and a cleaning action to run when the object
1371      * becomes phantom reachable. This forwards the call to our static {@link Cleaner}.
1372      *
1373      * @param obj   the object to monitor
1374      * @param action a {@code Runnable} to invoke when the object becomes phantom reachable
1375      * @return a {@code Cleanable} instance
1376      */
1377     public static Cleanable registerCleanerAction(final Object obj, final Runnable action) {
1378         return CLEANER.register(obj, action);
1379     }
1380 
1381     /**
1382      * Expands a relative URL relative to the specified base. In most situations
1383      * this is the same as <code>new URL(baseUrl, relativeUrl)</code> but
1384      * there are some cases that URL doesn't handle correctly. See
1385      * <a href="http://www.faqs.org/rfcs/rfc1808.html">RFC1808</a>
1386      * regarding Relative Uniform Resource Locators for more information.
1387      *
1388      * @param baseUrl the base URL
1389      * @param relativeUrl the relative URL
1390      * @return the expansion of the specified base and relative URLs
1391      * @throws MalformedURLException if an error occurred when creating a URL object
1392      */
1393     public static URL expandUrl(final URL baseUrl, final String relativeUrl) throws MalformedURLException {
1394         final String newUrl = UrlUtils.resolveUrl(baseUrl, relativeUrl);
1395         return UrlUtils.toUrlUnsafe(newUrl);
1396     }
1397 
1398     private WebResponse makeWebResponseForDataUrl(final WebRequest webRequest) throws IOException {
1399         final URL url = webRequest.getUrl();
1400 
1401         final DataURLConnection connection = new DataURLConnection(url);
1402 
1403         final List<NameValuePair> responseHeaders = new ArrayList<>();
1404         responseHeaders.add(new NameValuePair(HttpHeader.CONTENT_TYPE_LC,
1405             connection.getMediaType() + ";charset=" + connection.getCharset()));
1406 
1407         if (HttpMethod.HEAD.equals(webRequest.getHttpMethod())) {
1408             final WebResponseData data = new WebResponseData(200, "OK", responseHeaders);
1409             return new WebResponse(data, url, webRequest.getHttpMethod(), 0);
1410         }
1411 
1412         try (InputStream is = connection.getInputStream()) {
1413             final DownloadedContent downloadedContent =
1414                     HttpWebConnection.downloadContent(is,
1415                             getOptions().getMaxInMemory(),
1416                             getOptions().getTempFileDirectory());
1417             final WebResponseData data = new WebResponseData(downloadedContent, 200, "OK", responseHeaders);
1418             return new WebResponse(data, url, webRequest.getHttpMethod(), 0);
1419         }
1420     }
1421 
1422     private static WebResponse makeWebResponseForAboutUrl(final WebRequest webRequest) throws MalformedURLException {
1423         final URL url = webRequest.getUrl();
1424         if (UrlUtils.ABOUT.equals(url.getProtocol())) {
1425             if ("blank".equalsIgnoreCase(url.getPath())) {
1426                 if (url.getRef() == null && url.getQuery() == null) {
1427                     return new StringWebResponse("", UrlUtils.URL_ABOUT_BLANK);
1428                 }
1429                 return new StringWebResponse("", url);
1430             }
1431         }
1432 
1433         throw new MalformedURLException(url + " is not supported, only about:blank is supported at the moment.");
1434     }
1435 
1436     /**
1437      * Builds a WebResponse for a file URL.
1438      * This first implementation is basic.
1439      * It assumes that the file contains an HTML page encoded with the specified encoding.
1440      * @param webRequest the request
1441      * @return the web response
1442      * @throws IOException if an IO problem occurs
1443      */
1444     private WebResponse makeWebResponseForFileUrl(final WebRequest webRequest) throws IOException {
1445         URL cleanUrl = webRequest.getUrl();
1446         if (cleanUrl.getQuery() != null) {
1447             // Get rid of the query portion before trying to load the file.
1448             cleanUrl = UrlUtils.getUrlWithNewQuery(cleanUrl, null);
1449         }
1450         if (cleanUrl.getRef() != null) {
1451             // Get rid of the ref portion before trying to load the file.
1452             cleanUrl = UrlUtils.getUrlWithNewRef(cleanUrl, null);
1453         }
1454 
1455         final WebResponse fromCache = getCache().getCachedResponse(webRequest);
1456         if (fromCache != null) {
1457             return new WebResponseFromCache(fromCache, webRequest);
1458         }
1459 
1460         String fileUrl = cleanUrl.toExternalForm();
1461         fileUrl = URLDecoder.decode(fileUrl, UTF_8);
1462         final File file = new File(fileUrl.substring(5));
1463         if (!file.exists()) {
1464             // construct 404
1465             final List<NameValuePair> compiledHeaders = new ArrayList<>();
1466             compiledHeaders.add(new NameValuePair(HttpHeader.CONTENT_TYPE, MimeType.TEXT_HTML));
1467             final WebResponseData responseData =
1468                 new WebResponseData(
1469                         StringUtils
1470                             .toByteArray("File: " + file.getAbsolutePath(), UTF_8),
1471                     404, "Not Found", compiledHeaders);
1472             return new WebResponse(responseData, webRequest, 0);
1473         }
1474 
1475         final String contentType = guessContentType(file);
1476 
1477         final DownloadedContent content = new DownloadedContent.OnFile(file, false);
1478         final List<NameValuePair> compiledHeaders = new ArrayList<>();
1479         compiledHeaders.add(new NameValuePair(HttpHeader.CONTENT_TYPE, contentType));
1480         compiledHeaders.add(new NameValuePair(HttpHeader.LAST_MODIFIED,
1481                 HttpUtils.formatDate(new Date(file.lastModified()))));
1482         final WebResponseData responseData = new WebResponseData(content, 200, "OK", compiledHeaders);
1483         final WebResponse webResponse = new WebResponse(responseData, webRequest, 0);
1484         getCache().cacheIfPossible(webRequest, webResponse, null);
1485         return webResponse;
1486     }
1487 
1488     private WebResponse makeWebResponseForBlobUrl(final WebRequest webRequest) throws IOException {
1489         final Blob fileOrBlob = blobUrlStore_.resolve(webRequest.getUrl().toString());
1490         if (fileOrBlob == null) {
1491             throw new FileNotFoundException("No entry for '" + webRequest.getUrl() + "' in the BlobUrlStore.");
1492         }
1493 
1494         final List<NameValuePair> headers = new ArrayList<>();
1495         final String type = fileOrBlob.getType();
1496         if (!StringUtils.isEmptyOrNull(type)) {
1497             headers.add(new NameValuePair(HttpHeader.CONTENT_TYPE, fileOrBlob.getType()));
1498         }
1499         if (fileOrBlob instanceof org.htmlunit.javascript.host.file.File file) {
1500             final String fileName = file.getName();
1501             if (!StringUtils.isEmptyOrNull(fileName)) {
1502                 // https://datatracker.ietf.org/doc/html/rfc6266#autoid-10
1503                 headers.add(new NameValuePair(HttpHeader.CONTENT_DISPOSITION, "inline; filename=\"" + fileName + "\""));
1504             }
1505         }
1506 
1507         final DownloadedContent content = new DownloadedContent.InMemory(fileOrBlob.getBytes());
1508         final WebResponseData responseData = new WebResponseData(content, 200, "OK", headers);
1509         return new WebResponse(responseData, webRequest, 0);
1510     }
1511 
1512     /**
1513      * Tries to guess the content type of the file.<br>
1514      * This utility could be located in a helper class but we can compare this functionality
1515      * for instance with the "Helper Applications" settings of Mozilla and therefore see it as a
1516      * property of the "browser".
1517      * @param file the file
1518      * @return "application/octet-stream" if nothing could be guessed
1519      */
1520     public String guessContentType(final File file) {
1521         final String fileName = file.getName();
1522         final String fileNameLC = fileName.toLowerCase(Locale.ROOT);
1523         if (fileNameLC.endsWith(".xhtml")) {
1524             // Java's mime type map returns application/xml in JDK8.
1525             return MimeType.APPLICATION_XHTML;
1526         }
1527 
1528         // Java's mime type map does not know these in JDK8.
1529         if (fileNameLC.endsWith(".js")) {
1530             return MimeType.TEXT_JAVASCRIPT;
1531         }
1532 
1533         if (fileNameLC.endsWith(".css")) {
1534             return MimeType.TEXT_CSS;
1535         }
1536 
1537         String contentType = null;
1538         if (!fileNameLC.endsWith(".php")) {
1539             contentType = URLConnection.guessContentTypeFromName(fileName);
1540         }
1541         if (contentType == null) {
1542             try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
1543                 contentType = URLConnection.guessContentTypeFromStream(inputStream);
1544             }
1545             catch (final IOException ignored) {
1546                 // Ignore silently.
1547             }
1548         }
1549         if (contentType == null) {
1550             contentType = MimeType.APPLICATION_OCTET_STREAM;
1551         }
1552         return contentType;
1553     }
1554 
1555     private WebResponse makeWebResponseForJavaScriptUrl(final WebWindow webWindow, final URL url,
1556         final Charset charset) throws FailingHttpStatusCodeException, IOException {
1557 
1558         HtmlPage page = null;
1559         if (webWindow instanceof FrameWindow frameWindow) {
1560             page = (HtmlPage) frameWindow.getEnclosedPage();
1561         }
1562         else {
1563             final Page currentPage = webWindow.getEnclosedPage();
1564             if (currentPage instanceof HtmlPage htmlPage) {
1565                 page = htmlPage;
1566             }
1567         }
1568 
1569         if (page == null) {
1570             page = getPage(webWindow, WebRequest.newAboutBlankRequest());
1571         }
1572         final ScriptResult r = page.executeJavaScript(url.toExternalForm(), "JavaScript URL", 1);
1573         if (r.getJavaScriptResult() == null || ScriptResult.isUndefined(r)) {
1574             // No new WebResponse to produce.
1575             return webWindow.getEnclosedPage().getWebResponse();
1576         }
1577 
1578         final String contentString = r.getJavaScriptResult().toString();
1579         final StringWebResponse response = new StringWebResponse(contentString, charset, url);
1580         response.setFromJavascript(true);
1581         return response;
1582     }
1583 
1584     /**
1585      * Loads a {@link WebResponse} from the server.
1586      * @param webRequest the request
1587      * @throws IOException if an IO problem occurs
1588      * @return the WebResponse
1589      */
1590     public WebResponse loadWebResponse(final WebRequest webRequest) throws IOException {
1591         final String protocol = webRequest.getUrl().getProtocol();
1592         return switch (protocol) {
1593             case UrlUtils.ABOUT -> makeWebResponseForAboutUrl(webRequest);
1594             case "file" -> makeWebResponseForFileUrl(webRequest);
1595             case "data" -> makeWebResponseForDataUrl(webRequest);
1596             case "blob" -> makeWebResponseForBlobUrl(webRequest);
1597             case "http", "https" -> loadWebResponseFromWebConnection(webRequest, ALLOWED_REDIRECTIONS_SAME_URL);
1598             default -> throw new IOException("Unsupported protocol '" + protocol + "'");
1599         };
1600     }
1601 
1602     /**
1603      * Loads a {@link WebResponse} from the server through the WebConnection.
1604      * @param webRequest the request
1605      * @param allowedRedirects the number of allowed redirects remaining
1606      * @throws IOException if an IO problem occurs
1607      * @return the resultant {@link WebResponse}
1608      */
1609     private WebResponse loadWebResponseFromWebConnection(final WebRequest webRequest,
1610         final int allowedRedirects) throws IOException {
1611 
1612         URL url = webRequest.getUrl();
1613         final HttpMethod method = webRequest.getHttpMethod();
1614         final List<NameValuePair> parameters = webRequest.getRequestParameters();
1615 
1616         WebAssert.notNull("url", url);
1617         WebAssert.notNull("method", method);
1618         WebAssert.notNull("parameters", parameters);
1619 
1620         url = UrlUtils.encodeUrl(url, webRequest.getCharset());
1621         webRequest.setUrl(url);
1622 
1623         if (LOG.isDebugEnabled()) {
1624             LOG.debug("Load response for " + method + " " + url.toExternalForm());
1625         }
1626 
1627         // If the request settings don't specify a custom proxy, use the default client proxy...
1628         if (webRequest.getProxyHost() == null) {
1629             final ProxyConfig proxyConfig = getOptions().getProxyConfig();
1630             if (proxyConfig.getProxyAutoConfigUrl() != null) {
1631                 if (!UrlUtils.sameFile(new URL(proxyConfig.getProxyAutoConfigUrl()), url)) {
1632                     String content = proxyConfig.getProxyAutoConfigContent();
1633                     if (content == null) {
1634                         content = getPage(proxyConfig.getProxyAutoConfigUrl())
1635                             .getWebResponse().getContentAsString();
1636                         proxyConfig.setProxyAutoConfigContent(content);
1637                     }
1638                     final String allValue = JavaScriptEngine.evaluateProxyAutoConfig(getBrowserVersion(), content, url);
1639                     if (LOG.isDebugEnabled()) {
1640                         LOG.debug("Proxy Auto-Config: value '" + allValue + "' for URL " + url);
1641                     }
1642                     String value = allValue.split(";")[0].trim();
1643                     if (value.startsWith("PROXY")) {
1644                         value = value.substring(6);
1645                         final int colonIndex = value.indexOf(':');
1646                         webRequest.setSocksProxy(false);
1647                         webRequest.setProxyHost(value.substring(0, colonIndex));
1648                         webRequest.setProxyPort(Integer.parseInt(value.substring(colonIndex + 1)));
1649                     }
1650                     else if (value.startsWith("SOCKS")) {
1651                         value = value.substring(6);
1652                         final int colonIndex = value.indexOf(':');
1653                         webRequest.setSocksProxy(true);
1654                         webRequest.setProxyHost(value.substring(0, colonIndex));
1655                         webRequest.setProxyPort(Integer.parseInt(value.substring(colonIndex + 1)));
1656                     }
1657                 }
1658             }
1659             // ...unless the host needs to bypass the configured client proxy!
1660             else if (!proxyConfig.shouldBypassProxy(webRequest.getUrl().getHost())) {
1661                 webRequest.setProxyHost(proxyConfig.getProxyHost());
1662                 webRequest.setProxyPort(proxyConfig.getProxyPort());
1663                 webRequest.setProxyScheme(proxyConfig.getProxyScheme());
1664                 webRequest.setSocksProxy(proxyConfig.isSocksProxy());
1665             }
1666         }
1667 
1668         // Add the headers that are sent with every request.
1669         addDefaultHeaders(webRequest);
1670 
1671         // Retrieve the response, either from the cache or from the server.
1672         final WebResponse fromCache = getCache().getCachedResponse(webRequest);
1673         final WebResponse webResponse = getWebResponseOrUseCached(webRequest, fromCache);
1674 
1675         // Continue according to the HTTP status code.
1676         final int status = webResponse.getStatusCode();
1677         if (status == HttpStatus.USE_PROXY_305) {
1678             getIncorrectnessListener().notify("Ignoring HTTP status code [305] 'Use Proxy'", this);
1679         }
1680         else if (status >= HttpStatus.MOVED_PERMANENTLY_301
1681             && status <= HttpStatus.PERMANENT_REDIRECT_308
1682             && status != HttpStatus.NOT_MODIFIED_304
1683             && getOptions().isRedirectEnabled()) {
1684 
1685             final URL newUrl;
1686             String locationString = null;
1687             try {
1688                 locationString = webResponse.getResponseHeaderValue("Location");
1689                 if (locationString == null) {
1690                     return webResponse;
1691                 }
1692                 locationString = new String(locationString.getBytes(ISO_8859_1), UTF_8);
1693                 newUrl = expandUrl(url, locationString);
1694             }
1695             catch (final MalformedURLException e) {
1696                 getIncorrectnessListener().notify("Got a redirect status code [" + status + " "
1697                     + webResponse.getStatusMessage()
1698                     + "] but the location is not a valid URL [" + locationString
1699                     + "]. Skipping redirection processing.", this);
1700                 return webResponse;
1701             }
1702 
1703             if (LOG.isDebugEnabled()) {
1704                 LOG.debug("Got a redirect status code [" + status + "] new location = [" + locationString + "]");
1705             }
1706 
1707             if (allowedRedirects == 0) {
1708                 throw new FailingHttpStatusCodeException("Too many redirects for "
1709                     + webResponse.getWebRequest().getUrl(), webResponse);
1710             }
1711 
1712             if (status == HttpStatus.MOVED_PERMANENTLY_301
1713                     || status == HttpStatus.FOUND_302
1714                     || status == HttpStatus.SEE_OTHER_303) {
1715                 final WebRequest wrs = new WebRequest(newUrl, HttpMethod.GET);
1716                 wrs.setCharset(webRequest.getCharset());
1717 
1718                 if (HttpMethod.HEAD == webRequest.getHttpMethod()) {
1719                     wrs.setHttpMethod(HttpMethod.HEAD);
1720                 }
1721                 for (final Map.Entry<String, String> entry : webRequest.getAdditionalHeaders().entrySet()) {
1722                     wrs.setAdditionalHeader(entry.getKey(), entry.getValue());
1723                 }
1724                 wrs.setFetchDestination(webRequest.getFetchDestination());
1725                 wrs.setFetchModeOverride(webRequest.getFetchModeOverride());
1726                 wrs.setRequestingUrl(webRequest.getRequestingUrl());
1727 
1728                 return loadWebResponseFromWebConnection(wrs, allowedRedirects - 1);
1729             }
1730             else if (status == HttpStatus.TEMPORARY_REDIRECT_307
1731                         || status == HttpStatus.PERMANENT_REDIRECT_308) {
1732                 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307
1733                 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308
1734                 // reuse method and body
1735                 final WebRequest wrs = new WebRequest(newUrl, webRequest.getHttpMethod());
1736                 wrs.setCharset(webRequest.getCharset());
1737                 if (webRequest.getRequestBody() != null) {
1738                     if (HttpMethod.POST == webRequest.getHttpMethod()
1739                             || HttpMethod.PUT == webRequest.getHttpMethod()
1740                             || HttpMethod.PATCH == webRequest.getHttpMethod()) {
1741                         wrs.setRequestBody(webRequest.getRequestBody());
1742                         wrs.setEncodingType(webRequest.getEncodingType());
1743                     }
1744                 }
1745                 else {
1746                     wrs.setRequestParameters(parameters);
1747                 }
1748 
1749                 for (final Map.Entry<String, String> entry : webRequest.getAdditionalHeaders().entrySet()) {
1750                     wrs.setAdditionalHeader(entry.getKey(), entry.getValue());
1751                 }
1752 
1753                 return loadWebResponseFromWebConnection(wrs, allowedRedirects - 1);
1754             }
1755         }
1756 
1757         if (fromCache == null) {
1758             getCache().cacheIfPossible(webRequest, webResponse, null);
1759         }
1760         return webResponse;
1761     }
1762 
1763     /**
1764      * Returns the cached response provided for the request if usable otherwise makes the
1765      * request and returns the response.
1766      * @param webRequest the request
1767      * @param cached a previous cached response for the request, or {@code null}
1768      */
1769     private WebResponse getWebResponseOrUseCached(
1770             final WebRequest webRequest, final WebResponse cached) throws IOException {
1771         if (cached == null) {
1772             return getWebConnection().getResponse(webRequest);
1773         }
1774 
1775         if (!HeaderUtils.containsNoCache(cached)) {
1776             return new WebResponseFromCache(cached, webRequest);
1777         }
1778 
1779         // implementation based on rfc9111 https://www.rfc-editor.org/rfc/rfc9111#name-validation
1780         if (HeaderUtils.containsETag(cached)) {
1781             webRequest.setAdditionalHeader(HttpHeader.IF_NONE_MATCH, cached.getResponseHeaderValue(HttpHeader.ETAG));
1782         }
1783         if (HeaderUtils.containsLastModified(cached)) {
1784             webRequest.setAdditionalHeader(HttpHeader.IF_MODIFIED_SINCE,
1785                     cached.getResponseHeaderValue(HttpHeader.LAST_MODIFIED));
1786         }
1787 
1788         final WebResponse webResponse = getWebConnection().getResponse(webRequest);
1789 
1790         if (webResponse.getStatusCode() >= HttpStatus.INTERNAL_SERVER_ERROR_500) {
1791             return new WebResponseFromCache(cached, webRequest);
1792         }
1793 
1794         if (webResponse.getStatusCode() == HttpStatus.NOT_MODIFIED_304) {
1795             final Map<String, NameValuePair> header2NameValuePair = new LinkedHashMap<>();
1796             for (final NameValuePair pair : cached.getResponseHeaders()) {
1797                 header2NameValuePair.put(pair.getName(), pair);
1798             }
1799             for (final NameValuePair pair : webResponse.getResponseHeaders()) {
1800                 if (preferHeaderFrom304Response(pair.getName())) {
1801                     header2NameValuePair.put(pair.getName(), pair);
1802                 }
1803             }
1804             // WebResponse headers is unmodifiableList so we cannot update it directly
1805             // instead, create a new WebResponseFromCache with updated headers
1806             // then use it to replace the old cached value
1807             final WebResponse updatedCached =
1808                     new WebResponseFromCache(cached, new ArrayList<>(header2NameValuePair.values()), webRequest);
1809             getCache().cacheIfPossible(webRequest, updatedCached, null);
1810             return updatedCached;
1811         }
1812 
1813         getCache().cacheIfPossible(webRequest, webResponse, null);
1814         return webResponse;
1815     }
1816 
1817     /**
1818      * Returns true if the value of the specified header in a 304 Not Modified response should be
1819      * adopted over any previously cached value.
1820      */
1821     private static boolean preferHeaderFrom304Response(final String name) {
1822         final String lcName = name.toLowerCase(Locale.ROOT);
1823         for (final String header : DISCARDING_304_RESPONSE_HEADER_NAMES) {
1824             if (lcName.equals(header)) {
1825                 return false;
1826             }
1827         }
1828         for (final String prefix : DISCARDING_304_HEADER_PREFIXES) {
1829             if (lcName.startsWith(prefix)) {
1830                 return false;
1831             }
1832         }
1833         return true;
1834     }
1835 
1836     /**
1837      * Adds the headers that are sent with every request to the specified {@link WebRequest} instance.
1838      * @param wrs the <code>WebRequestSettings</code> instance to modify
1839      */
1840     private void addDefaultHeaders(final WebRequest wrs) {
1841         // Add user-specified headers to the web request if not present there yet.
1842         requestHeaders_.forEach((name, value) -> {
1843             if (!wrs.isAdditionalHeader(name)) {
1844                 wrs.setAdditionalHeader(name, value);
1845             }
1846         });
1847 
1848         // Add standard HtmlUnit headers to the web request if still not present there yet.
1849         if (!wrs.isAdditionalHeader(HttpHeader.ACCEPT_LANGUAGE)) {
1850             wrs.setAdditionalHeader(HttpHeader.ACCEPT_LANGUAGE, getBrowserVersion().getAcceptLanguageHeader());
1851         }
1852 
1853         // the sec- stuff is done later in the HttpWebConnection
1854         // this implies that stuff is not visible in the MockWebConnection
1855     }
1856 
1857     /**
1858      * Returns an immutable list of open web windows (whether they are top level windows or not).
1859      * This is a snapshot; future changes are not reflected by this list.
1860      * <p>
1861      * The list is ordered by age, the oldest one first.
1862      * </p>
1863      *
1864      * @return an immutable list of open web windows (whether they are top level windows or not)
1865      * @see #getWebWindowByName(String)
1866      * @see #getTopLevelWindows()
1867      */
1868     public List<WebWindow> getWebWindows() {
1869         return List.copyOf(windows_);
1870     }
1871 
1872     /**
1873      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1874      *
1875      * Returns true if the list of WebWindows contains the provided one.
1876      * This method is there to improve the performance of some internal checks because
1877      * calling getWebWindows().contains(.) creates some objects without any need.
1878      *
1879      * @param webWindow the window to check
1880      * @return true or false
1881      */
1882     public boolean containsWebWindow(final WebWindow webWindow) {
1883         return windows_.contains(webWindow);
1884     }
1885 
1886     /**
1887      * Returns an immutable list of open top level windows.
1888      * This is a snapshot; future changes are not reflected by this list.
1889      * <p>
1890      * The list is ordered by age, the oldest one first.
1891      * </p>
1892      *
1893      * @return an immutable list of open top level windows
1894      * @see #getWebWindowByName(String)
1895      * @see #getWebWindows()
1896      */
1897     public List<TopLevelWindow> getTopLevelWindows() {
1898         return List.copyOf(topLevelWindows_);
1899     }
1900 
1901     /**
1902      * Sets the handler to be used whenever a refresh is triggered. Refer
1903      * to the documentation for {@link RefreshHandler} for more details.
1904      * @param handler the new handler
1905      */
1906     public void setRefreshHandler(final RefreshHandler handler) {
1907         if (handler == null) {
1908             refreshHandler_ = new NiceRefreshHandler(2);
1909         }
1910         else {
1911             refreshHandler_ = handler;
1912         }
1913     }
1914 
1915     /**
1916      * Returns the current refresh handler.
1917      * The default refresh handler is a {@link NiceRefreshHandler NiceRefreshHandler(2)}.
1918      * @return the current RefreshHandler
1919      */
1920     public RefreshHandler getRefreshHandler() {
1921         return refreshHandler_;
1922     }
1923 
1924     /**
1925      * Sets the script pre processor for this {@link WebClient}.
1926      * @param scriptPreProcessor the new preprocessor or null if none is specified
1927      */
1928     public void setScriptPreProcessor(final ScriptPreProcessor scriptPreProcessor) {
1929         scriptPreProcessor_ = scriptPreProcessor;
1930     }
1931 
1932     /**
1933      * Returns the script pre processor for this {@link WebClient}.
1934      * @return the pre processor or null of one hasn't been set
1935      */
1936     public ScriptPreProcessor getScriptPreProcessor() {
1937         return scriptPreProcessor_;
1938     }
1939 
1940     /**
1941      * Sets the listener for messages generated by the HTML parser.
1942      * @param listener the new listener, {@code null} if messages should be totally ignored
1943      */
1944     public void setHTMLParserListener(final HTMLParserListener listener) {
1945         htmlParserListener_ = listener;
1946     }
1947 
1948     /**
1949      * Gets the configured listener for messages generated by the HTML parser.
1950      * @return {@code null} if no listener is defined (default value)
1951      */
1952     public HTMLParserListener getHTMLParserListener() {
1953         return htmlParserListener_;
1954     }
1955 
1956     /**
1957      * Returns the CSS error handler used by this web client when CSS problems are encountered.
1958      * @return the CSS error handler used by this web client when CSS problems are encountered
1959      * @see DefaultCssErrorHandler
1960      * @see SilentCssErrorHandler
1961      */
1962     public CSSErrorHandler getCssErrorHandler() {
1963         return cssErrorHandler_;
1964     }
1965 
1966     /**
1967      * Sets the CSS error handler used by this web client when CSS problems are encountered.
1968      * @param cssErrorHandler the CSS error handler used by this web client when CSS problems are encountered
1969      * @see DefaultCssErrorHandler
1970      * @see SilentCssErrorHandler
1971      */
1972     public void setCssErrorHandler(final CSSErrorHandler cssErrorHandler) {
1973         WebAssert.notNull("cssErrorHandler", cssErrorHandler);
1974         cssErrorHandler_ = cssErrorHandler;
1975     }
1976 
1977     /**
1978      * Sets the number of milliseconds that a script is allowed to execute before being terminated.
1979      * A value of 0 or less means no timeout.
1980      *
1981      * @param timeout the timeout value, in milliseconds
1982      */
1983     public void setJavaScriptTimeout(final long timeout) {
1984         scriptEngine_.setJavaScriptTimeout(timeout);
1985     }
1986 
1987     /**
1988      * Returns the number of milliseconds that a script is allowed to execute before being terminated.
1989      * A value of 0 or less means no timeout.
1990      *
1991      * @return the timeout value, in milliseconds
1992      */
1993     public long getJavaScriptTimeout() {
1994         return scriptEngine_.getJavaScriptTimeout();
1995     }
1996 
1997     /**
1998      * Gets the current listener for encountered incorrectness (except HTML parsing messages that
1999      * are handled by the HTML parser listener). Default value is an instance of
2000      * {@link IncorrectnessListenerImpl}.
2001      * @return the current listener (not {@code null})
2002      */
2003     public IncorrectnessListener getIncorrectnessListener() {
2004         return incorrectnessListener_;
2005     }
2006 
2007     /**
2008      * Returns the current HTML incorrectness listener.
2009      * @param listener the new value (not {@code null})
2010      */
2011     public void setIncorrectnessListener(final IncorrectnessListener listener) {
2012         if (listener == null) {
2013             throw new IllegalArgumentException("Null is not a valid IncorrectnessListener");
2014         }
2015         incorrectnessListener_ = listener;
2016     }
2017 
2018     /**
2019      * Returns the WebConsole.
2020      * @return the web console
2021      */
2022     public WebConsole getWebConsole() {
2023         if (webConsole_ == null) {
2024             webConsole_ = new WebConsole();
2025         }
2026         return webConsole_;
2027     }
2028 
2029     /**
2030      * Gets the current AJAX controller.
2031      * @return the controller
2032      */
2033     public AjaxController getAjaxController() {
2034         return ajaxController_;
2035     }
2036 
2037     /**
2038      * Sets the current AJAX controller.
2039      * @param newValue the controller
2040      */
2041     public void setAjaxController(final AjaxController newValue) {
2042         if (newValue == null) {
2043             throw new IllegalArgumentException("Null is not a valid AjaxController");
2044         }
2045         ajaxController_ = newValue;
2046     }
2047 
2048     /**
2049      * Sets the attachment handler.
2050      * @param handler the new attachment handler
2051      */
2052     public void setAttachmentHandler(final AttachmentHandler handler) {
2053         attachmentHandler_ = handler;
2054     }
2055 
2056     /**
2057      * Returns the current attachment handler.
2058      * @return the current attachment handler
2059      */
2060     public AttachmentHandler getAttachmentHandler() {
2061         return attachmentHandler_;
2062     }
2063 
2064     /**
2065      * Sets the WebStart handler.
2066      * @param handler the new WebStart handler
2067      */
2068     public void setWebStartHandler(final WebStartHandler handler) {
2069         webStartHandler_ = handler;
2070     }
2071 
2072     /**
2073      * Returns the current WebStart handler.
2074      * @return the current WebStart handler
2075      */
2076     public WebStartHandler getWebStartHandler() {
2077         return webStartHandler_;
2078     }
2079 
2080     /**
2081      * Returns the current clipboard handler.
2082      * @return the current clipboard handler
2083      */
2084     public ClipboardHandler getClipboardHandler() {
2085         return clipboardHandler_;
2086     }
2087 
2088     /**
2089      * Sets the clipboard handler.
2090      * @param handler the new clipboard handler
2091      */
2092     public void setClipboardHandler(final ClipboardHandler handler) {
2093         clipboardHandler_ = handler;
2094     }
2095 
2096     /**
2097      * Returns the current {@link PrintHandler}.
2098      * @return the current {@link PrintHandler} or null if print
2099      *         requests are ignored
2100      */
2101     public PrintHandler getPrintHandler() {
2102         return printHandler_;
2103     }
2104 
2105     /**
2106      * Sets the {@link PrintHandler} to be used if Windoe.print() is called
2107      * (<a href="https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#printing">Printing Spec</a>).
2108      *
2109      * @param handler the new {@link PrintHandler} or null if you like to
2110      *        ignore print requests (default is null)
2111      */
2112     public void setPrintHandler(final PrintHandler handler) {
2113         printHandler_ = handler;
2114     }
2115 
2116     /**
2117      * Returns the current FrameContent handler.
2118      * @return the current FrameContent handler
2119      */
2120     public FrameContentHandler getFrameContentHandler() {
2121         return frameContentHandler_;
2122     }
2123 
2124     /**
2125      * Sets the FrameContent handler.
2126      * @param handler the new FrameContent handler
2127      */
2128     public void setFrameContentHandler(final FrameContentHandler handler) {
2129         frameContentHandler_ = handler;
2130     }
2131 
2132     /**
2133      * Sets the onbeforeunload handler for this {@link WebClient}.
2134      * @param onbeforeunloadHandler the new onbeforeunloadHandler or null if none is specified
2135      */
2136     public void setOnbeforeunloadHandler(final OnbeforeunloadHandler onbeforeunloadHandler) {
2137         onbeforeunloadHandler_ = onbeforeunloadHandler;
2138     }
2139 
2140     /**
2141      * Returns the onbeforeunload handler for this {@link WebClient}.
2142      * @return the onbeforeunload handler or null if one hasn't been set
2143      */
2144     public OnbeforeunloadHandler getOnbeforeunloadHandler() {
2145         return onbeforeunloadHandler_;
2146     }
2147 
2148     /**
2149      * Gets the cache currently being used.
2150      * @return the cache (may not be null)
2151      */
2152     public Cache getCache() {
2153         return cache_;
2154     }
2155 
2156     /**
2157      * Sets the cache to use.
2158      * @param cache the new cache (must not be {@code null})
2159      */
2160     public void setCache(final Cache cache) {
2161         if (cache == null) {
2162             throw new IllegalArgumentException("cache should not be null!");
2163         }
2164         cache_ = cache;
2165     }
2166 
2167     /**
2168      * Keeps track of the current window. Inspired by WebTest's logic to track the current response.
2169      */
2170     private static final class CurrentWindowTracker implements WebWindowListener, Serializable {
2171         private final WebClient webClient_;
2172         private final boolean ensureOneTopLevelWindow_;
2173 
2174         CurrentWindowTracker(final WebClient webClient, final boolean ensureOneTopLevelWindow) {
2175             webClient_ = webClient;
2176             ensureOneTopLevelWindow_ = ensureOneTopLevelWindow;
2177         }
2178 
2179         /**
2180          * {@inheritDoc}
2181          */
2182         @Override
2183         public void webWindowClosed(final WebWindowEvent event) {
2184             final WebWindow window = event.getWebWindow();
2185             if (window instanceof TopLevelWindow) {
2186                 webClient_.topLevelWindows_.remove(window);
2187                 if (window == webClient_.getCurrentWindow()) {
2188                     if (!webClient_.topLevelWindows_.isEmpty()) {
2189                         // The current window is now the previous top-level window.
2190                         webClient_.setCurrentWindow(
2191                                 webClient_.topLevelWindows_.get(webClient_.topLevelWindows_.size() - 1));
2192                     }
2193                 }
2194             }
2195             else if (window == webClient_.getCurrentWindow()) {
2196                 // The current window is now the last top-level window.
2197                 if (webClient_.topLevelWindows_.isEmpty()) {
2198                     webClient_.setCurrentWindow(null);
2199                 }
2200                 else {
2201                     webClient_.setCurrentWindow(
2202                             webClient_.topLevelWindows_.get(webClient_.topLevelWindows_.size() - 1));
2203                 }
2204             }
2205         }
2206 
2207         /**
2208          * Postprocessing to make sure we have always one top level window open.
2209          */
2210         public void afterWebWindowClosedListenersProcessed(final WebWindowEvent event) {
2211             if (!ensureOneTopLevelWindow_) {
2212                 return;
2213             }
2214 
2215             if (webClient_.topLevelWindows_.isEmpty()) {
2216                 // Must always have at least window, and there are no top-level windows left; must create one.
2217                 final TopLevelWindow newWindow = new TopLevelWindow("", webClient_);
2218                 webClient_.setCurrentWindow(newWindow);
2219             }
2220         }
2221 
2222         /**
2223          * {@inheritDoc}
2224          */
2225         @Override
2226         public void webWindowContentChanged(final WebWindowEvent event) {
2227             final WebWindow window = event.getWebWindow();
2228             boolean use = false;
2229             if (window instanceof DialogWindow) {
2230                 use = true;
2231             }
2232             else if (window instanceof TopLevelWindow) {
2233                 use = event.getOldPage() == null;
2234             }
2235             else if (window instanceof FrameWindow fw) {
2236                 final String enclosingPageState = fw.getEnclosingPage().getDocumentElement().getReadyState();
2237                 final URL frameUrl = fw.getEnclosedPage().getUrl();
2238                 if (!DomNode.READY_STATE_COMPLETE.equals(enclosingPageState) || frameUrl == UrlUtils.URL_ABOUT_BLANK) {
2239                     return;
2240                 }
2241 
2242                 // now looks at the visibility of the frame window
2243                 final BaseFrameElement frameElement = fw.getFrameElement();
2244                 if (webClient_.isJavaScriptEnabled() && frameElement.isDisplayed()) {
2245                     final ComputedCssStyleDeclaration style = fw.getComputedStyle(frameElement, null);
2246                     use = style.getCalculatedWidth(false, false) != 0
2247                             && style.getCalculatedHeight(false, false) != 0;
2248                 }
2249             }
2250             if (use) {
2251                 webClient_.setCurrentWindow(window);
2252             }
2253         }
2254 
2255         /**
2256          * {@inheritDoc}
2257          */
2258         @Override
2259         public void webWindowOpened(final WebWindowEvent event) {
2260             final WebWindow window = event.getWebWindow();
2261             if (window instanceof TopLevelWindow tlw) {
2262                 webClient_.topLevelWindows_.add(tlw);
2263             }
2264             // Page is not loaded yet, don't set it now as current window.
2265         }
2266     }
2267 
2268     /**
2269      * Closes all opened windows, stopping all background JavaScript processing.
2270      * The WebClient is not really usable after this - you have to create a new one or
2271      * use WebClient.reset() instead.
2272      */
2273     @Override
2274     public void close() {
2275         // avoid attaching new windows to the js engine
2276         if (scriptEngine_ != null) {
2277             scriptEngine_.prepareShutdown();
2278         }
2279 
2280         // stop the CurrentWindowTracker from making sure there is still one window available
2281         currentWindowTracker_ = new CurrentWindowTracker(this, false);
2282 
2283         // Hint: a new TopLevelWindow may be opened by some JS script while we are closing the others
2284         // but the prepareShutdown() call will prevent the new window form getting js support
2285         List<WebWindow> windows = new ArrayList<>(windows_);
2286         for (final WebWindow window : windows) {
2287             if (window instanceof TopLevelWindow topLevelWindow) {
2288 
2289                 try {
2290                     topLevelWindow.close(true);
2291                 }
2292                 catch (final Exception e) {
2293                     LOG.error("Exception while closing a TopLevelWindow", e);
2294                 }
2295             }
2296             else if (window instanceof DialogWindow dialogWindow) {
2297 
2298                 try {
2299                     dialogWindow.close();
2300                 }
2301                 catch (final Exception e) {
2302                     LOG.error("Exception while closing a DialogWindow", e);
2303                 }
2304             }
2305         }
2306 
2307         // second round, none of the remaining windows should be registered to
2308         // the js engine because of prepareShutdown()
2309         windows = new ArrayList<>(windows_);
2310         for (final WebWindow window : windows) {
2311             if (window instanceof TopLevelWindow topLevelWindow) {
2312 
2313                 try {
2314                     topLevelWindow.close(true);
2315                 }
2316                 catch (final Exception e) {
2317                     LOG.error("Exception while closing a TopLevelWindow", e);
2318                 }
2319             }
2320             else if (window instanceof DialogWindow dialogWindow) {
2321 
2322                 try {
2323                     dialogWindow.close();
2324                 }
2325                 catch (final Exception e) {
2326                     LOG.error("Exception while closing a DialogWindow", e);
2327                 }
2328             }
2329         }
2330 
2331         // now both lists have to be empty
2332         if (!topLevelWindows_.isEmpty()) {
2333             LOG.error("Sill " + topLevelWindows_.size() + " top level windows are open. Please report this error!");
2334             topLevelWindows_.clear();
2335         }
2336 
2337         if (!windows_.isEmpty()) {
2338             LOG.error("Sill " + windows_.size() + " windows are open. Please report this error!");
2339             windows_.clear();
2340         }
2341         currentWindow_ = null;
2342 
2343         ThreadDeath toThrow = null;
2344         if (scriptEngine_ != null) {
2345             try {
2346                 scriptEngine_.shutdown();
2347             }
2348             catch (final ThreadDeath ex) {
2349                 // make sure the following cleanup is performed to avoid resource leaks
2350                 toThrow = ex;
2351             }
2352             catch (final Exception e) {
2353                 LOG.error("Exception while shutdown the scriptEngine", e);
2354             }
2355         }
2356         scriptEngine_ = null;
2357 
2358         if (webConnection_ != null) {
2359             try {
2360                 webConnection_.close();
2361             }
2362             catch (final Exception e) {
2363                 LOG.error("Exception while closing the connection", e);
2364             }
2365         }
2366         webConnection_ = null;
2367 
2368         synchronized (this) {
2369             if (executor_ != null) {
2370                 try {
2371                     executor_.shutdownNow();
2372                 }
2373                 catch (final Exception e) {
2374                     LOG.error("Exception while shutdown the executor service", e);
2375                 }
2376             }
2377         }
2378         executor_ = null;
2379 
2380         cache_.clear();
2381         if (toThrow != null) {
2382             throw toThrow;
2383         }
2384     }
2385 
2386     /**
2387      * <p><span style="color:red">Experimental API: May be changed in next release
2388      * and may not yet work perfectly!</span></p>
2389      *
2390      * <p>This shuts down the whole client and restarts with a new empty window.
2391      * Cookies and other states are preserved.
2392      * </p>
2393      */
2394     public void reset() {
2395         close();
2396 
2397         // this has to be done after the browser version was set
2398         webConnection_ = new HttpWebConnection(this);
2399         if (javaScriptEngineEnabled_) {
2400             scriptEngine_ = new JavaScriptEngine(this);
2401         }
2402 
2403         // The window must be constructed AFTER the script engine.
2404         currentWindowTracker_ = new CurrentWindowTracker(this, true);
2405         currentWindow_ = new TopLevelWindow("", this);
2406     }
2407 
2408     /**
2409      * <p>Blocks until all background JavaScript tasks have finished executing or until the specified
2410      * timeout is reached, whichever occurs first. Background JavaScript tasks include:</p>
2411      * <ul>
2412      *   <li>JavaScript scheduled via <code>window.setTimeout()</code></li>
2413      *   <li>JavaScript scheduled via <code>window.setInterval()</code></li>
2414      *   <li>Asynchronous <code>XMLHttpRequest</code> operations</li>
2415      *   <li>Other asynchronous JavaScript operations across all windows managed by this WebClient</li>
2416      * </ul>
2417      *
2418      * <p><strong>Timeout Behavior:</strong> If background tasks are scheduled to execute after
2419      * <code>(now + timeoutMillis)</code>, this method will wait for the full timeout duration
2420      * and then return the number of remaining jobs. The method guarantees it will never block
2421      * longer than the specified timeout.</p>
2422      *
2423      * <p><strong>Use Case:</strong> Use this method when you don't know the exact timing of when
2424      * background JavaScript will start, but you have a reasonable estimate of how long all
2425      * tasks should take to complete. For scenarios where you know when tasks should start
2426      * executing, consider using {@link #waitForBackgroundJavaScriptStartingBefore(long)} instead.</p>
2427      *
2428      * <p><strong>Thread Safety:</strong> This method is thread-safe and handles concurrent
2429      * modifications to the internal job manager list gracefully.</p>
2430      *
2431      * <p><strong>Example Usage:</strong></p>
2432      * <pre><code>
2433      * // Wait up to 5 seconds for all background JavaScript to complete
2434      * int remainingJobs = webClient.waitForBackgroundJavaScript(5000);
2435      * if (remainingJobs == 0) {
2436      *     log("All background JavaScript completed");
2437      * } else {
2438      *     log("Timeout reached, " + remainingJobs + " jobs still pending");
2439      * }
2440      * </code></pre>
2441      *
2442      * @param timeoutMillis the maximum amount of time to wait in milliseconds; must be positive
2443      * @return the number of background JavaScript jobs still executing or waiting to be executed
2444      *         when this method returns; returns <code>0</code> if all jobs completed successfully
2445      *         within the timeout period
2446      * @throws IllegalArgumentException if timeoutMillis is negative
2447      * @see #waitForBackgroundJavaScriptStartingBefore(long)
2448      * @see #waitForBackgroundJavaScriptStartingBefore(long, long)
2449      */
2450     public int waitForBackgroundJavaScript(final long timeoutMillis) {
2451         int count = 0;
2452         final long endTime = System.currentTimeMillis() + timeoutMillis;
2453         for (Iterator<WeakReference<JavaScriptJobManager>> i = jobManagers_.iterator(); i.hasNext();) {
2454             final JavaScriptJobManager jobManager;
2455             final WeakReference<JavaScriptJobManager> reference;
2456             try {
2457                 reference = i.next();
2458                 jobManager = reference.get();
2459                 if (jobManager == null) {
2460                     i.remove();
2461                     continue;
2462                 }
2463             }
2464             catch (final ConcurrentModificationException e) {
2465                 i = jobManagers_.iterator();
2466                 count = 0;
2467                 continue;
2468             }
2469 
2470             final long newTimeout = endTime - System.currentTimeMillis();
2471             count += jobManager.waitForJobs(newTimeout);
2472         }
2473         if (count != getAggregateJobCount()) {
2474             final long newTimeout = endTime - System.currentTimeMillis();
2475             return waitForBackgroundJavaScript(newTimeout);
2476         }
2477         return count;
2478     }
2479 
2480     /**
2481      * <p>Blocks until all background JavaScript tasks scheduled to start executing before
2482      * <code>(now + delayMillis)</code> have finished executing. Background JavaScript tasks include:</p>
2483      * <ul>
2484      *   <li>JavaScript scheduled via <code>window.setTimeout()</code></li>
2485      *   <li>JavaScript scheduled via <code>window.setInterval()</code></li>
2486      *   <li>Asynchronous <code>XMLHttpRequest</code> operations</li>
2487      *   <li>Other asynchronous JavaScript operations across all windows managed by this WebClient</li>
2488      * </ul>
2489      *
2490      * <p><strong>Method Behavior:</strong></p>
2491      * <ul>
2492      *   <li>If no background JavaScript tasks are currently executing and none are scheduled
2493      *       to start within <code>delayMillis</code>, this method returns immediately</li>
2494      *   <li>Tasks scheduled to execute after <code>(now + delayMillis)</code> are ignored
2495      *       and do not affect the waiting behavior</li>
2496      *   <li>The method waits for tasks to complete execution, not just to start</li>
2497      *   <li>This method waits indefinitely for qualifying tasks to complete (no timeout)</li>
2498      * </ul>
2499      *
2500      * <p><strong>Use Case:</strong> This method is ideal when you know approximately when
2501      * background JavaScript should start executing but are uncertain about execution duration.
2502      * Use this when you don't need a timeout and want to ensure all relevant tasks complete.
2503      * For scenarios where you need to wait for all background tasks regardless of timing,
2504      * use {@link #waitForBackgroundJavaScript(long)} instead. For timeout control, use
2505      * {@link #waitForBackgroundJavaScriptStartingBefore(long, long)} instead.</p>
2506      *
2507      * <p><strong>Thread Safety:</strong> This method is thread-safe and handles concurrent
2508      * modifications to the internal job manager list gracefully.</p>
2509      *
2510      * <p><strong>Example Usage:</strong></p>
2511      * <pre><code>
2512      * // Wait indefinitely for JavaScript tasks starting within 1 second
2513      * int remainingJobs = webClient.waitForBackgroundJavaScriptStartingBefore(1000);
2514      * if (remainingJobs == 0) {
2515      *     log("All relevant background JavaScript completed");
2516      * } else {
2517      *     log("Some tasks may still be pending: " + remainingJobs + " jobs");
2518      * }
2519      *
2520      * // Common pattern: wait for tasks that should start soon
2521      * // (useful after triggering an action that schedules JavaScript)
2522      * webClient.waitForBackgroundJavaScriptStartingBefore(500);
2523      * </code></pre>
2524      *
2525      * @param delayMillis the delay which determines the background tasks to wait for (in milliseconds);
2526      *                   must be non-negative
2527      * @return the number of background JavaScript jobs still executing or waiting to be executed
2528      *         when this method returns; returns <code>0</code> if all qualifying jobs completed
2529      *         successfully
2530      * @see #waitForBackgroundJavaScript(long)
2531      * @see #waitForBackgroundJavaScriptStartingBefore(long, long)
2532      */
2533     public int waitForBackgroundJavaScriptStartingBefore(final long delayMillis) {
2534         return waitForBackgroundJavaScriptStartingBefore(delayMillis, -1);
2535     }
2536 
2537     /**
2538      * <p>Blocks until all background JavaScript tasks scheduled to start executing before
2539      * <code>(now + delayMillis)</code> have finished executing, or until the specified timeout
2540      * is reached, whichever occurs first. Background JavaScript tasks include:</p>
2541      * <ul>
2542      *   <li>JavaScript scheduled via <code>window.setTimeout()</code></li>
2543      *   <li>JavaScript scheduled via <code>window.setInterval()</code></li>
2544      *   <li>Asynchronous <code>XMLHttpRequest</code> operations</li>
2545      *   <li>Other asynchronous JavaScript operations across all windows managed by this WebClient</li>
2546      * </ul>
2547      *
2548      * <p><strong>Method Behavior:</strong></p>
2549      * <ul>
2550      *   <li>If no background JavaScript tasks are currently executing and none are scheduled
2551      *       to start within <code>delayMillis</code>, this method returns immediately</li>
2552      *   <li>Tasks scheduled to execute after <code>(now + delayMillis)</code> are ignored
2553      *       and do not affect the waiting behavior</li>
2554      *   <li>The method waits for tasks to complete execution, not just to start</li>
2555      * </ul>
2556      *
2557      * <p><strong>Timeout Behavior:</strong></p>
2558      * <ul>
2559      *   <li>If <code>timeoutMillis</code> is negative or less than <code>delayMillis</code>,
2560      *       the timeout is ignored and the method waits indefinitely</li>
2561      *   <li>When a valid timeout is specified, the method will never block longer than
2562      *       <code>timeoutMillis</code> milliseconds</li>
2563      *   <li>The timeout applies to the total waiting time, not per task</li>
2564      * </ul>
2565      *
2566      * <p><strong>Use Case:</strong> This method is ideal when you know approximately when
2567      * background JavaScript should start executing but are uncertain about execution duration.
2568      * For scenarios where you need to wait for all background tasks regardless of timing,
2569      * use {@link #waitForBackgroundJavaScript(long)} instead.</p>
2570      *
2571      * <p><strong>Thread Safety:</strong> This method is thread-safe and handles concurrent
2572      * modifications to the internal job manager list gracefully.</p>
2573      *
2574      * <p><strong>Example Usage:</strong></p>
2575      * <pre><code>
2576      * // Wait for JavaScript tasks starting within 1 second, with 10 second max timeout
2577      * int remainingJobs = webClient.waitForBackgroundJavaScriptStartingBefore(1000, 10000);
2578      * if (remainingJobs == 0) {
2579      *     log("All relevant background JavaScript completed");
2580      * } else {
2581      *     log("Timeout reached or tasks still pending: " + remainingJobs + " jobs");
2582      * }
2583      *
2584      * // Wait indefinitely for tasks starting within 500ms (timeout ignored)
2585      * webClient.waitForBackgroundJavaScriptStartingBefore(500, 100); // timeout &lt; delay
2586      * </code></pre>
2587      *
2588      * @param delayMillis the delay which determines the background tasks to wait for (in milliseconds);
2589      *                   must be non-negative
2590      * @param timeoutMillis the maximum amount of time to wait (in milliseconds); if negative or
2591      *                     less than <code>delayMillis</code>, the timeout is ignored and the method
2592      *                     waits indefinitely for qualifying tasks to complete
2593      * @return the number of background JavaScript jobs still executing or waiting to be executed
2594      *         when this method returns; returns <code>0</code> if all qualifying jobs completed
2595      *         successfully within the specified constraints
2596      * @see #waitForBackgroundJavaScript(long)
2597      * @see #waitForBackgroundJavaScriptStartingBefore(long)
2598      */
2599     public int waitForBackgroundJavaScriptStartingBefore(final long delayMillis, final long timeoutMillis) {
2600         int count = 0;
2601         long now = System.currentTimeMillis();
2602         final long endTime = now + delayMillis;
2603         long endTimeout = now + timeoutMillis;
2604         if (timeoutMillis < 0 || timeoutMillis < delayMillis) {
2605             endTimeout = -1;
2606         }
2607 
2608         for (Iterator<WeakReference<JavaScriptJobManager>> i = jobManagers_.iterator(); i.hasNext();) {
2609             final JavaScriptJobManager jobManager;
2610             final WeakReference<JavaScriptJobManager> reference;
2611             try {
2612                 reference = i.next();
2613                 jobManager = reference.get();
2614                 if (jobManager == null) {
2615                     i.remove();
2616                     continue;
2617                 }
2618             }
2619             catch (final ConcurrentModificationException e) {
2620                 i = jobManagers_.iterator();
2621                 count = 0;
2622                 continue;
2623             }
2624             now = System.currentTimeMillis();
2625             final long newDelay = endTime - now;
2626             final long newTimeout = (endTimeout == -1) ? -1 : endTimeout - now;
2627             count += jobManager.waitForJobsStartingBefore(newDelay, newTimeout);
2628         }
2629         if (count != getAggregateJobCount()) {
2630             now = System.currentTimeMillis();
2631             final long newDelay = endTime - now;
2632             final long newTimeout = (endTimeout == -1) ? -1 : endTimeout - now;
2633             return waitForBackgroundJavaScriptStartingBefore(newDelay, newTimeout);
2634         }
2635         return count;
2636     }
2637 
2638     /**
2639      * Returns the aggregate background JavaScript job count across all windows.
2640      * @return the aggregate background JavaScript job count across all windows
2641      */
2642     private int getAggregateJobCount() {
2643         int count = 0;
2644         for (Iterator<WeakReference<JavaScriptJobManager>> i = jobManagers_.iterator(); i.hasNext();) {
2645             final JavaScriptJobManager jobManager;
2646             final WeakReference<JavaScriptJobManager> reference;
2647             try {
2648                 reference = i.next();
2649                 jobManager = reference.get();
2650                 if (jobManager == null) {
2651                     i.remove();
2652                     continue;
2653                 }
2654             }
2655             catch (final ConcurrentModificationException e) {
2656                 i = jobManagers_.iterator();
2657                 count = 0;
2658                 continue;
2659             }
2660             final int jobCount = jobManager.getJobCount();
2661             count += jobCount;
2662         }
2663         return count;
2664     }
2665 
2666     /**
2667      * When we deserialize, re-initializie transient fields.
2668      * @param in the object input stream
2669      * @throws IOException if an error occurs
2670      * @throws ClassNotFoundException if an error occurs
2671      */
2672     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
2673         in.defaultReadObject();
2674 
2675         webConnection_ = new HttpWebConnection(this);
2676         scriptEngine_ = new JavaScriptEngine(this);
2677         jobManagers_ = Collections.synchronizedList(new ArrayList<>());
2678         loadQueue_ = new ArrayList<>();
2679         css3ParserPool_ = new CSS3ParserPool();
2680         broadcastChannel_ = new HashSet<>();
2681         blobUrlStore_ = new BlobUrlStore();
2682     }
2683 
2684     private static class LoadJob {
2685         private final WebWindow requestingWindow_;
2686         private final String target_;
2687         private final WebResponse response_;
2688         private final WeakReference<Page> originalPage_;
2689         private final WebRequest request_;
2690         private final String forceAttachmentWithFilename_;
2691 
2692         // we can't us the WebRequest from the WebResponse because
2693         // we need the original request e.g. after a redirect
2694         LoadJob(final WebRequest request, final WebResponse response,
2695                 final WebWindow requestingWindow, final String target, final String forceAttachmentWithFilename) {
2696             request_ = request;
2697             requestingWindow_ = requestingWindow;
2698             target_ = target;
2699             response_ = response;
2700             originalPage_ = new WeakReference<>(requestingWindow.getEnclosedPage());
2701             forceAttachmentWithFilename_ = forceAttachmentWithFilename;
2702         }
2703 
2704         public boolean isOutdated() {
2705             if (target_ != null && !target_.isEmpty()) {
2706                 return false;
2707             }
2708 
2709             if (requestingWindow_.isClosed()) {
2710                 return true;
2711             }
2712 
2713             if (requestingWindow_.getEnclosedPage() != originalPage_.get()) {
2714                 return true;
2715             }
2716 
2717             return false;
2718         }
2719     }
2720 
2721     /**
2722      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2723      *
2724      * Perform the downloads and stores it for loading later into a window.
2725      * In the future downloads should be performed in parallel in separated threads.
2726      * TODO: refactor it before next release.
2727      * @param requestingWindow the window from which the request comes
2728      * @param target the name of the target window
2729      * @param request the request to perform
2730      * @param checkHash if true check for hashChenage
2731      * @param forceAttachmentWithFilename if not {@code null} the AttachmentHandler isAttachment() method is not called,
2732      *        the response has to be handled as attachment in any case
2733      * @param description information about the origin of the request. Useful for debugging.
2734      */
2735     public void download(final WebWindow requestingWindow, final String target,
2736         final WebRequest request, final boolean checkHash,
2737         final String forceAttachmentWithFilename, final String description) {
2738 
2739         final WebWindow targetWindow = resolveWindow(requestingWindow, target);
2740         final URL url = request.getUrl();
2741 
2742         if (targetWindow != null && HttpMethod.POST != request.getHttpMethod()) {
2743             final Page page = targetWindow.getEnclosedPage();
2744             if (page != null) {
2745                 if (page.isHtmlPage() && !((HtmlPage) page).isOnbeforeunloadAccepted()) {
2746                     return;
2747                 }
2748 
2749                 if (checkHash) {
2750                     final URL current = page.getUrl();
2751                     final boolean justHashJump =
2752                             HttpMethod.GET == request.getHttpMethod()
2753                             && UrlUtils.sameFile(url, current)
2754                             && null != url.getRef();
2755 
2756                     if (justHashJump) {
2757                         processOnlyHashChange(targetWindow, url);
2758                         return;
2759                     }
2760                 }
2761             }
2762         }
2763 
2764         synchronized (loadQueue_) {
2765             // verify if this load job doesn't already exist
2766             for (final LoadJob otherLoadJob : loadQueue_) {
2767                 if (otherLoadJob.response_ == null) {
2768                     continue;
2769                 }
2770                 final WebRequest otherRequest = otherLoadJob.request_;
2771                 final URL otherUrl = otherRequest.getUrl();
2772 
2773                 if (url.getPath().equals(otherUrl.getPath()) // fail fast
2774                     && url.toString().equals(otherUrl.toString())
2775                     && request.getRequestParameters().equals(otherRequest.getRequestParameters())
2776                     && Objects.equals(request.getRequestBody(), otherRequest.getRequestBody())) {
2777                     return; // skip it;
2778                 }
2779             }
2780         }
2781 
2782         final LoadJob loadJob;
2783         try {
2784             WebResponse response;
2785             try {
2786                 response = loadWebResponse(request);
2787             }
2788             catch (final NoHttpResponseException e) {
2789                 LOG.error("NoHttpResponseException while downloading; generating a NoHttpResponse", e);
2790                 response = new WebResponse(RESPONSE_DATA_NO_HTTP_RESPONSE, request, 0);
2791             }
2792             loadJob = new LoadJob(request, response, requestingWindow, target, forceAttachmentWithFilename);
2793         }
2794         catch (final IOException e) {
2795             throw new RuntimeException(e);
2796         }
2797 
2798         synchronized (loadQueue_) {
2799             loadQueue_.add(loadJob);
2800         }
2801     }
2802 
2803     /**
2804      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2805      *
2806      * Loads downloaded responses into the corresponding windows.
2807      * TODO: refactor it before next release.
2808      * @throws IOException in case of exception
2809      * @throws FailingHttpStatusCodeException in case of exception
2810      */
2811     public void loadDownloadedResponses() throws FailingHttpStatusCodeException, IOException {
2812         final List<LoadJob> queue;
2813 
2814         // synchronize access to the loadQueue_,
2815         // to be sure no job is ignored
2816         synchronized (loadQueue_) {
2817             if (loadQueue_.isEmpty()) {
2818                 return;
2819             }
2820             queue = new ArrayList<>(loadQueue_);
2821             loadQueue_.clear();
2822         }
2823 
2824         final HashSet<WebWindow> updatedWindows = new HashSet<>();
2825         for (int i = queue.size() - 1; i >= 0; --i) {
2826             final LoadJob loadJob = queue.get(i);
2827             if (loadJob.isOutdated()) {
2828                 if (LOG.isInfoEnabled()) {
2829                     LOG.info("No usage of download: " + loadJob);
2830                 }
2831                 continue;
2832             }
2833 
2834             final WebWindow window = resolveWindow(loadJob.requestingWindow_, loadJob.target_);
2835             if (updatedWindows.contains(window)) {
2836                 if (LOG.isInfoEnabled()) {
2837                     LOG.info("No usage of download: " + loadJob);
2838                 }
2839                 continue;
2840             }
2841 
2842             final WebWindow win = openTargetWindow(loadJob.requestingWindow_, loadJob.target_, TARGET_SELF);
2843             final Page pageBeforeLoad = win.getEnclosedPage();
2844             loadWebResponseInto(loadJob.response_, win, loadJob.forceAttachmentWithFilename_);
2845 
2846             // start execution here.
2847             if (scriptEngine_ != null) {
2848                 scriptEngine_.registerWindowAndMaybeStartEventLoop(win);
2849             }
2850 
2851             if (pageBeforeLoad != win.getEnclosedPage()) {
2852                 updatedWindows.add(win);
2853             }
2854 
2855             // check and report problems if needed
2856             throwFailingHttpStatusCodeExceptionIfNecessary(loadJob.response_);
2857         }
2858     }
2859 
2860     private static void processOnlyHashChange(final WebWindow window, final URL urlWithOnlyHashChange) {
2861         final Page page = window.getEnclosedPage();
2862         final String oldURL = page.getUrl().toExternalForm();
2863 
2864         // update request url
2865         final WebRequest req = page.getWebResponse().getWebRequest();
2866         req.setUrl(urlWithOnlyHashChange);
2867 
2868         // update location.hash
2869         final Window jsWindow = window.getScriptableObject();
2870         if (null != jsWindow) {
2871             final Location location = jsWindow.getLocation();
2872             location.setHash(oldURL, urlWithOnlyHashChange.getRef());
2873         }
2874 
2875         // add to history
2876         window.getHistory().addPage(page);
2877     }
2878 
2879     /**
2880      * Returns the options object of this WebClient.
2881      * @return the options object
2882      */
2883     public WebClientOptions getOptions() {
2884         return options_;
2885     }
2886 
2887     /**
2888      * Gets the holder for the different storages.
2889      * <p><span style="color:red">Experimental API: May be changed in next release!</span></p>
2890      * @return the holder
2891      */
2892     public StorageHolder getStorageHolder() {
2893         return storageHolder_;
2894     }
2895 
2896     /**
2897      * Returns the currently configured cookies applicable to the specified URL, in an unmodifiable set.
2898      * If disabled, this returns an empty set.
2899      * @param url the URL on which to filter the returned cookies
2900      * @return the currently configured cookies applicable to the specified URL, in an unmodifiable set
2901      */
2902     public synchronized Set<Cookie> getCookies(final URL url) {
2903         final CookieManager cookieManager = getCookieManager();
2904 
2905         if (!cookieManager.isCookiesEnabled()) {
2906             return Collections.emptySet();
2907         }
2908 
2909         final URL normalizedUrl = HttpClientConverter.replaceForCookieIfNecessary(url);
2910 
2911         final String host = normalizedUrl.getHost();
2912         // URLs like "about:blank" don't have cookies and we need to catch these
2913         // cases here before HttpClient complains
2914         if (host.isEmpty()) {
2915             return Collections.emptySet();
2916         }
2917 
2918         // discard expired cookies
2919         cookieManager.clearExpired(new Date());
2920 
2921         final Set<Cookie> matchingCookies = new LinkedHashSet<>();
2922         HttpClientConverter.addMatching(cookieManager.getCookies(), normalizedUrl,
2923                 getBrowserVersion(), matchingCookies);
2924         return Collections.unmodifiableSet(matchingCookies);
2925     }
2926 
2927     /**
2928      * Parses the given cookie and adds this to our cookie store.
2929      * @param cookieString the string to parse
2930      * @param pageUrl the url of the page that likes to set the cookie
2931      * @param origin the requester
2932      */
2933     public void addCookie(final String cookieString, final URL pageUrl, final Object origin) {
2934         final CookieManager cookieManager = getCookieManager();
2935         if (!cookieManager.isCookiesEnabled()) {
2936             if (LOG.isDebugEnabled()) {
2937                 LOG.debug("Skipped adding cookie: '" + cookieString
2938                         + "' because cookies are not enabled for the CookieManager.");
2939             }
2940             return;
2941         }
2942 
2943         try {
2944             final List<Cookie> cookies = HttpClientConverter.parseCookie(cookieString, pageUrl, getBrowserVersion());
2945             // final List<Cookie> cookies = CookieParser.parseCookie(cookieString, pageUrl, getBrowserVersion());
2946 
2947             for (final Cookie cookie : cookies) {
2948                 cookieManager.addCookie(cookie);
2949 
2950                 if (LOG.isDebugEnabled()) {
2951                     LOG.debug("Added cookie: '" + cookieString + "'");
2952                 }
2953             }
2954         }
2955         catch (final MalformedCookieException e) {
2956             if (LOG.isDebugEnabled()) {
2957                 LOG.warn("Adding cookie '" + cookieString + "' failed.", e);
2958             }
2959             getIncorrectnessListener().notify("Adding cookie '" + cookieString
2960                         + "' failed; reason: '" + e.getMessage() + "'.", origin);
2961         }
2962     }
2963 
2964     /**
2965      * Returns true if the javaScript support is enabled.
2966      * To disable the javascript support (eg. temporary)
2967      * you have to use the {@link WebClientOptions#setJavaScriptEnabled(boolean)} setter.
2968      * @see #isJavaScriptEngineEnabled()
2969      * @see WebClientOptions#isJavaScriptEnabled()
2970      * @return true if the javaScript engine and the javaScript support is enabled.
2971      */
2972     public boolean isJavaScriptEnabled() {
2973         return javaScriptEngineEnabled_ && getOptions().isJavaScriptEnabled();
2974     }
2975 
2976     /**
2977      * Returns true if the javaScript engine is enabled.
2978      * To disable the javascript engine you have to use the
2979      * {@link WebClient#WebClient(BrowserVersion, boolean, String, int)} constructor.
2980      * @return true if the javaScript engine is enabled.
2981      */
2982     public boolean isJavaScriptEngineEnabled() {
2983         return javaScriptEngineEnabled_;
2984     }
2985 
2986     /**
2987      * Parses the given XHtml code string and loads the resulting XHtmlPage into
2988      * the current window.
2989      *
2990      * @param htmlCode the html code as string
2991      * @return the HtmlPage
2992      * @throws IOException in case of error
2993      */
2994     public HtmlPage loadHtmlCodeIntoCurrentWindow(final String htmlCode) throws IOException {
2995         final HTMLParser htmlParser = getPageCreator().getHtmlParser();
2996         final WebWindow webWindow = getCurrentWindow();
2997 
2998         final StringWebResponse webResponse =
2999                 new StringWebResponse(htmlCode, new URL("https://www.htmlunit.org/dummy.html"));
3000         final HtmlPage page = new HtmlPage(webResponse, webWindow);
3001         webWindow.setEnclosedPage(page);
3002 
3003         htmlParser.parse(this, webResponse, page, false, false);
3004         return page;
3005     }
3006 
3007     /**
3008      * Parses the given XHtml code string and loads the resulting XHtmlPage into
3009      * the current window.
3010      *
3011      * @param xhtmlCode the xhtml code as string
3012      * @return the XHtmlPage
3013      * @throws IOException in case of error
3014      */
3015     public XHtmlPage loadXHtmlCodeIntoCurrentWindow(final String xhtmlCode) throws IOException {
3016         final HTMLParser htmlParser = getPageCreator().getHtmlParser();
3017         final WebWindow webWindow = getCurrentWindow();
3018 
3019         final StringWebResponse webResponse =
3020                 new StringWebResponse(xhtmlCode, new URL("https://www.htmlunit.org/dummy.html"));
3021         final XHtmlPage page = new XHtmlPage(webResponse, webWindow);
3022         webWindow.setEnclosedPage(page);
3023 
3024         htmlParser.parse(this, webResponse, page, true, false);
3025         return page;
3026     }
3027 
3028     /**
3029      * Creates a new {@link WebSocketAdapter}.
3030      *
3031      * @param webSocketListener the {@link WebSocketListener}
3032      * @return a new {@link WebSocketAdapter}
3033      */
3034     public WebSocketAdapter buildWebSocketAdapter(final WebSocketListener webSocketListener) {
3035         return webSocketAdapterFactory_.buildWebSocketAdapter(this, webSocketListener);
3036     }
3037 
3038     /**
3039      * Defines a new factory method to create a new WebSocketAdapter.
3040      *
3041      * @param factory a {@link WebSocketAdapterFactory}
3042      */
3043     public void setWebSocketAdapter(final WebSocketAdapterFactory factory) {
3044         webSocketAdapterFactory_ = factory;
3045     }
3046 
3047     /**
3048      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
3049      *
3050      * @return a CSS3Parser that will return to an internal pool for reuse if closed using the
3051      *         try-with-resource concept
3052      */
3053     public PooledCSS3Parser getCSS3Parser() {
3054         return this.css3ParserPool_.get();
3055     }
3056 
3057     /**
3058      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
3059      *
3060      * @return the set of known {@link BroadcastChannel}s
3061      */
3062     public Set<BroadcastChannel> getBroadcastChannels() {
3063         return broadcastChannel_;
3064     }
3065 
3066     /**
3067      * Our pool of CSS3Parsers. If you need a parser, get it from here and use the AutoCloseable
3068      * functionality with a try-with-resource block. If you don't want to do that at all, continue
3069      * to build CSS3Parsers the old fashioned way.
3070      * <p>
3071      * Fetching a parser is thread safe. This API is built to minimize synchronization overhead,
3072      * hence it is possible to miss a returned parser from another thread under heavy pressure,
3073      * but because that is unlikely, we keep it simple and efficient. Caches are not supposed
3074      * to give cutting-edge guarantees.
3075      * </p>
3076      * <p>
3077      * This concept avoids a resource leak when someone does not close the fetched
3078      * parser because the pool does not know anything about the parser unless
3079      * it returns. We are not running a checkout-checkin concept.
3080      * </p>
3081      * <p>
3082      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
3083      * </p>
3084      */
3085     static class CSS3ParserPool {
3086         /*
3087          * Our pool. We only hold data when it is available. In addition, synchronization against
3088          * this deque is cheap.
3089          */
3090         private final ConcurrentLinkedDeque<PooledCSS3Parser> parsers_ = new ConcurrentLinkedDeque<>();
3091 
3092         /**
3093          * Fetch a new or recycled CSS3parser. Make sure you use the try-with-resource concept
3094          * to automatically return it after use because a parser creation is expensive.
3095          * We won't get a leak, if you don't do so, but that will remove the advantage.
3096          *
3097          * @return a parser
3098          */
3099         public PooledCSS3Parser get() {
3100             // see if we have one, LIFO
3101             final PooledCSS3Parser parser = parsers_.pollLast();
3102 
3103             // if we don't have one, get us one
3104             return parser != null ? parser.markInUse(this) : new PooledCSS3Parser(this);
3105         }
3106 
3107         /**
3108          * Return a parser. Normally you don't have to use that method explicitly.
3109          * Prefer to user the AutoCloseable interface of the PooledParser by
3110          * using a try-with-resource statement.
3111          *
3112          * @param parser the parser to recycle
3113          */
3114         protected void recycle(final PooledCSS3Parser parser) {
3115             parsers_.addLast(parser);
3116         }
3117     }
3118 
3119     /**
3120      * This is a poolable CSS3Parser which can be reused automatically when closed.
3121      * A regular CSS3Parser is not thread-safe, hence also our pooled parser
3122      * is not thread-safe.
3123      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
3124      */
3125     public static class PooledCSS3Parser extends CSS3Parser implements AutoCloseable {
3126         /**
3127          * The pool we want to return us to. Because multiple threads can use this, we
3128          * have to ensure that we see the action here.
3129          */
3130         private CSS3ParserPool pool_;
3131 
3132         /**
3133          * Create a new poolable parser.
3134          *
3135          * @param pool the pool the parser should return to when it is closed
3136          */
3137         protected PooledCSS3Parser(final CSS3ParserPool pool) {
3138             super();
3139             this.pool_ = pool;
3140         }
3141 
3142         /**
3143          * Resets the parser's pool state so it can be safely returned again.
3144          *
3145          * @param pool the pool the parser should return to when it is closed
3146          * @return this parser for fluid programming
3147          */
3148         protected PooledCSS3Parser markInUse(final CSS3ParserPool pool) {
3149             // ensure we detect programming mistakes
3150             if (this.pool_ == null) {
3151                 this.pool_ = pool;
3152             }
3153             else {
3154                 throw new IllegalStateException("This PooledParser was not returned to the pool properly");
3155             }
3156 
3157             return this;
3158         }
3159 
3160         /**
3161          * Implements the AutoClosable interface. The return method ensures that
3162          * we are notified when we incorrectly close it twice which indicates a
3163          * programming flow defect.
3164          *
3165          * @throws IllegalStateException in case the parser is closed several times
3166          */
3167         @Override
3168         public void close() {
3169             if (this.pool_ != null) {
3170                 final CSS3ParserPool oldPool = this.pool_;
3171                 // set null first and recycle later to avoid exposing a broken state
3172                 // volatile guarantees visibility
3173                 this.pool_ = null;
3174 
3175                 // return
3176                 oldPool.recycle(this);
3177             }
3178             else {
3179                 throw new IllegalStateException("This PooledParser was returned already");
3180             }
3181         }
3182     }
3183 }