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