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