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