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