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