From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- dom/xhr/XMLHttpRequest.cpp | 54 + dom/xhr/XMLHttpRequest.h | 139 + dom/xhr/XMLHttpRequestEventTarget.cpp | 38 + dom/xhr/XMLHttpRequestEventTarget.h | 47 + dom/xhr/XMLHttpRequestMainThread.cpp | 4154 ++++++++++++++++++++ dom/xhr/XMLHttpRequestMainThread.h | 873 ++++ dom/xhr/XMLHttpRequestString.cpp | 199 + dom/xhr/XMLHttpRequestString.h | 151 + dom/xhr/XMLHttpRequestUpload.cpp | 24 + dom/xhr/XMLHttpRequestUpload.h | 37 + dom/xhr/XMLHttpRequestWorker.cpp | 2233 +++++++++++ dom/xhr/XMLHttpRequestWorker.h | 257 ++ dom/xhr/moz.build | 39 + dom/xhr/tests/browser.ini | 13 + dom/xhr/tests/browser_blobFromFile.js | 64 + .../browser_sync_xhr_event_handing_switch_bcg.js | 144 + dom/xhr/tests/browser_temporaryFile.js | 71 + dom/xhr/tests/browser_xhr_onchange_leak.html | 25 + dom/xhr/tests/browser_xhr_onchange_leak.js | 31 + ...browser_xhr_substituted_protocol_responseURL.js | 27 + dom/xhr/tests/common_temporaryFileBlob.js | 126 + dom/xhr/tests/crashtests/1546185.html | 42 + dom/xhr/tests/crashtests/crashtests.list | 1 + dom/xhr/tests/echo.sjs | 26 + dom/xhr/tests/empty.html | 0 dom/xhr/tests/empty_parent.html | 0 dom/xhr/tests/file_XHRDocURI.html | 9 + dom/xhr/tests/file_XHRDocURI.html^headers^ | 3 + dom/xhr/tests/file_XHRDocURI.sjs | 12 + dom/xhr/tests/file_XHRDocURI.text | 1 + dom/xhr/tests/file_XHRDocURI.text^headers^ | 3 + dom/xhr/tests/file_XHRDocURI.xml | 1 + dom/xhr/tests/file_XHRDocURI.xml^headers^ | 3 + dom/xhr/tests/file_XHRResponseURL.js | 388 ++ dom/xhr/tests/file_XHRResponseURL.sjs | 13 + dom/xhr/tests/file_XHRResponseURL.text | 1 + dom/xhr/tests/file_XHRResponseURL.text^headers^ | 3 + dom/xhr/tests/file_XHRResponseURL_nocors.text | 1 + dom/xhr/tests/file_XHRSendData.sjs | 34 + dom/xhr/tests/file_XHRSendData_doc.xml | 2 + dom/xhr/tests/file_XHRSendData_doc.xml^headers^ | 1 + dom/xhr/tests/file_XHR_anon.sjs | 24 + dom/xhr/tests/file_XHR_binary1.bin | Bin 0 -> 12 bytes dom/xhr/tests/file_XHR_binary1.bin^headers^ | 1 + dom/xhr/tests/file_XHR_binary2.bin | Bin 0 -> 65536 bytes dom/xhr/tests/file_XHR_fail1.txt | 1 + dom/xhr/tests/file_XHR_fail1.txt^headers^ | 2 + dom/xhr/tests/file_XHR_fail1b.txt | 1 + dom/xhr/tests/file_XHR_header.sjs | 8 + dom/xhr/tests/file_XHR_pass1.xml | 1 + dom/xhr/tests/file_XHR_pass2.txt | 1 + dom/xhr/tests/file_XHR_pass3.txt | 1 + dom/xhr/tests/file_XHR_pass3.txt^headers^ | 2 + dom/xhr/tests/file_XHR_system_redirect.html | 5 + .../tests/file_XHR_system_redirect.html^headers^ | 2 + dom/xhr/tests/file_XHR_timeout.sjs | 16 + dom/xhr/tests/file_html_in_xhr.html | 16 + dom/xhr/tests/file_html_in_xhr.sjs | 19 + dom/xhr/tests/file_html_in_xhr2.html | 1 + dom/xhr/tests/file_html_in_xhr3.html | 1 + .../file_sync_xhr_document_write_with_iframe.html | 22 + .../tests/file_sync_xhr_event_handling_helper.html | 37 + dom/xhr/tests/file_sync_xhr_nested_helper.html | 30 + dom/xhr/tests/iframe_sync_xhr_unload.html | 17 + dom/xhr/tests/mochitest.ini | 161 + dom/xhr/tests/progressserver.sjs | 56 + dom/xhr/tests/relativeLoad_import.js | 5 + dom/xhr/tests/relativeLoad_worker.js | 31 + dom/xhr/tests/relativeLoad_worker2.js | 11 + dom/xhr/tests/responseIdentical.sjs | 19 + dom/xhr/tests/slow.sjs | 13 + dom/xhr/tests/subdir/relativeLoad_sub_import.js | 5 + dom/xhr/tests/subdir/relativeLoad_sub_worker.js | 28 + dom/xhr/tests/subdir/relativeLoad_sub_worker2.js | 11 + dom/xhr/tests/sync_xhr_unload.sjs | 16 + dom/xhr/tests/temporaryFileBlob.sjs | 45 + dom/xhr/tests/terminateSyncXHR_worker.js | 20 + dom/xhr/tests/test_XHR.html | 383 ++ dom/xhr/tests/test_XHRDocURI.html | 487 +++ dom/xhr/tests/test_XHRResponseURL.html | 69 + dom/xhr/tests/test_XHRSendData.html | 273 ++ dom/xhr/tests/test_XHR_anon.html | 180 + dom/xhr/tests/test_XHR_header.html | 32 + dom/xhr/tests/test_XHR_onuploadprogress.html | 40 + dom/xhr/tests/test_XHR_parameters.html | 97 + dom/xhr/tests/test_XHR_system.html | 99 + dom/xhr/tests/test_XHR_timeout.html | 59 + dom/xhr/tests/test_XHR_timeout.js | 398 ++ dom/xhr/tests/test_bug1070763.html | 58 + dom/xhr/tests/test_bug1300552.html | 29 + dom/xhr/tests/test_bug1697539.html | 27 + dom/xhr/tests/test_bug1752863.html | 32 + dom/xhr/tests/test_bug1752863_worker.js | 34 + dom/xhr/tests/test_bug1788125.html | 59 + dom/xhr/tests/test_event_listener_leaks.html | 45 + dom/xhr/tests/test_html_in_xhr.html | 97 + dom/xhr/tests/test_nestedSyncXHR.html | 101 + dom/xhr/tests/test_relativeLoad.html | 51 + dom/xhr/tests/test_sharedworker_xhr.html | 23 + .../test_sync_xhr_document_write_with_iframe.html | 28 + dom/xhr/tests/test_sync_xhr_event_handling.html | 39 + dom/xhr/tests/test_sync_xhr_nested.html | 47 + dom/xhr/tests/test_sync_xhr_timer.xhtml | 52 + dom/xhr/tests/test_sync_xhr_unload.html | 36 + dom/xhr/tests/test_temporaryFileBlob.html | 40 + dom/xhr/tests/test_worker_terminateSyncXHR.html | 44 + dom/xhr/tests/test_worker_xhr.html | 76 + dom/xhr/tests/test_worker_xhr2.html | 37 + dom/xhr/tests/test_worker_xhrAbort.html | 44 + dom/xhr/tests/test_worker_xhr_3rdparty.html | 51 + dom/xhr/tests/test_worker_xhr_cors_redirect.html | 35 + dom/xhr/tests/test_worker_xhr_doubleSend.html | 30 + dom/xhr/tests/test_worker_xhr_headers.html | 86 + dom/xhr/tests/test_worker_xhr_implicit_cancel.html | 43 + dom/xhr/tests/test_worker_xhr_parameters.html | 66 + dom/xhr/tests/test_worker_xhr_parameters.js | 84 + dom/xhr/tests/test_worker_xhr_responseURL.html | 76 + dom/xhr/tests/test_worker_xhr_system.html | 54 + dom/xhr/tests/test_worker_xhr_system.js | 30 + dom/xhr/tests/test_worker_xhr_timeout.html | 57 + dom/xhr/tests/test_xhr_abort_after_load.html | 96 + dom/xhr/tests/test_xhr_forbidden_headers.html | 96 + ...r_overridemimetype_throws_on_invalid_state.html | 62 + dom/xhr/tests/test_xhr_progressevents.html | 307 ++ dom/xhr/tests/test_xhr_send.html | 83 + dom/xhr/tests/test_xhr_send_readystate.html | 39 + dom/xhr/tests/test_xhr_withCredentials.html | 35 + dom/xhr/tests/window_worker_xhr_3rdparty.html | 75 + dom/xhr/tests/worker_bug1300552.js | 37 + dom/xhr/tests/worker_bug1697539.js | 19 + dom/xhr/tests/worker_file_getcookie.sjs | 15 + dom/xhr/tests/worker_temporaryFileBlob.js | 30 + dom/xhr/tests/worker_terminateSyncXHR_frame.html | 25 + dom/xhr/tests/worker_testXHR.txt | 1 + dom/xhr/tests/worker_xhr_cors_redirect.js | 10 + dom/xhr/tests/worker_xhr_cors_redirect.sjs | 10 + dom/xhr/tests/worker_xhr_headers_server.sjs | 69 + dom/xhr/tests/worker_xhr_headers_worker.js | 16 + dom/xhr/tests/xhr2_worker.js | 102 + dom/xhr/tests/xhrAbort_worker.js | 101 + dom/xhr/tests/xhr_implicit_cancel_worker.js | 10 + dom/xhr/tests/xhr_sharedworker.js | 105 + dom/xhr/tests/xhr_worker.js | 84 + dom/xhr/tests/xhr_worker_doubleSend.js | 11 + 144 files changed, 15085 insertions(+) create mode 100644 dom/xhr/XMLHttpRequest.cpp create mode 100644 dom/xhr/XMLHttpRequest.h create mode 100644 dom/xhr/XMLHttpRequestEventTarget.cpp create mode 100644 dom/xhr/XMLHttpRequestEventTarget.h create mode 100644 dom/xhr/XMLHttpRequestMainThread.cpp create mode 100644 dom/xhr/XMLHttpRequestMainThread.h create mode 100644 dom/xhr/XMLHttpRequestString.cpp create mode 100644 dom/xhr/XMLHttpRequestString.h create mode 100644 dom/xhr/XMLHttpRequestUpload.cpp create mode 100644 dom/xhr/XMLHttpRequestUpload.h create mode 100644 dom/xhr/XMLHttpRequestWorker.cpp create mode 100644 dom/xhr/XMLHttpRequestWorker.h create mode 100644 dom/xhr/moz.build create mode 100644 dom/xhr/tests/browser.ini create mode 100644 dom/xhr/tests/browser_blobFromFile.js create mode 100644 dom/xhr/tests/browser_sync_xhr_event_handing_switch_bcg.js create mode 100644 dom/xhr/tests/browser_temporaryFile.js create mode 100644 dom/xhr/tests/browser_xhr_onchange_leak.html create mode 100644 dom/xhr/tests/browser_xhr_onchange_leak.js create mode 100644 dom/xhr/tests/browser_xhr_substituted_protocol_responseURL.js create mode 100644 dom/xhr/tests/common_temporaryFileBlob.js create mode 100644 dom/xhr/tests/crashtests/1546185.html create mode 100644 dom/xhr/tests/crashtests/crashtests.list create mode 100644 dom/xhr/tests/echo.sjs create mode 100644 dom/xhr/tests/empty.html create mode 100644 dom/xhr/tests/empty_parent.html create mode 100644 dom/xhr/tests/file_XHRDocURI.html create mode 100644 dom/xhr/tests/file_XHRDocURI.html^headers^ create mode 100644 dom/xhr/tests/file_XHRDocURI.sjs create mode 100644 dom/xhr/tests/file_XHRDocURI.text create mode 100644 dom/xhr/tests/file_XHRDocURI.text^headers^ create mode 100644 dom/xhr/tests/file_XHRDocURI.xml create mode 100644 dom/xhr/tests/file_XHRDocURI.xml^headers^ create mode 100644 dom/xhr/tests/file_XHRResponseURL.js create mode 100644 dom/xhr/tests/file_XHRResponseURL.sjs create mode 100644 dom/xhr/tests/file_XHRResponseURL.text create mode 100644 dom/xhr/tests/file_XHRResponseURL.text^headers^ create mode 100644 dom/xhr/tests/file_XHRResponseURL_nocors.text create mode 100644 dom/xhr/tests/file_XHRSendData.sjs create mode 100644 dom/xhr/tests/file_XHRSendData_doc.xml create mode 100644 dom/xhr/tests/file_XHRSendData_doc.xml^headers^ create mode 100644 dom/xhr/tests/file_XHR_anon.sjs create mode 100644 dom/xhr/tests/file_XHR_binary1.bin create mode 100644 dom/xhr/tests/file_XHR_binary1.bin^headers^ create mode 100644 dom/xhr/tests/file_XHR_binary2.bin create mode 100644 dom/xhr/tests/file_XHR_fail1.txt create mode 100644 dom/xhr/tests/file_XHR_fail1.txt^headers^ create mode 100644 dom/xhr/tests/file_XHR_fail1b.txt create mode 100644 dom/xhr/tests/file_XHR_header.sjs create mode 100644 dom/xhr/tests/file_XHR_pass1.xml create mode 100644 dom/xhr/tests/file_XHR_pass2.txt create mode 100644 dom/xhr/tests/file_XHR_pass3.txt create mode 100644 dom/xhr/tests/file_XHR_pass3.txt^headers^ create mode 100644 dom/xhr/tests/file_XHR_system_redirect.html create mode 100644 dom/xhr/tests/file_XHR_system_redirect.html^headers^ create mode 100644 dom/xhr/tests/file_XHR_timeout.sjs create mode 100644 dom/xhr/tests/file_html_in_xhr.html create mode 100644 dom/xhr/tests/file_html_in_xhr.sjs create mode 100644 dom/xhr/tests/file_html_in_xhr2.html create mode 100644 dom/xhr/tests/file_html_in_xhr3.html create mode 100644 dom/xhr/tests/file_sync_xhr_document_write_with_iframe.html create mode 100644 dom/xhr/tests/file_sync_xhr_event_handling_helper.html create mode 100644 dom/xhr/tests/file_sync_xhr_nested_helper.html create mode 100644 dom/xhr/tests/iframe_sync_xhr_unload.html create mode 100644 dom/xhr/tests/mochitest.ini create mode 100644 dom/xhr/tests/progressserver.sjs create mode 100644 dom/xhr/tests/relativeLoad_import.js create mode 100644 dom/xhr/tests/relativeLoad_worker.js create mode 100644 dom/xhr/tests/relativeLoad_worker2.js create mode 100644 dom/xhr/tests/responseIdentical.sjs create mode 100644 dom/xhr/tests/slow.sjs create mode 100644 dom/xhr/tests/subdir/relativeLoad_sub_import.js create mode 100644 dom/xhr/tests/subdir/relativeLoad_sub_worker.js create mode 100644 dom/xhr/tests/subdir/relativeLoad_sub_worker2.js create mode 100644 dom/xhr/tests/sync_xhr_unload.sjs create mode 100644 dom/xhr/tests/temporaryFileBlob.sjs create mode 100644 dom/xhr/tests/terminateSyncXHR_worker.js create mode 100644 dom/xhr/tests/test_XHR.html create mode 100644 dom/xhr/tests/test_XHRDocURI.html create mode 100644 dom/xhr/tests/test_XHRResponseURL.html create mode 100644 dom/xhr/tests/test_XHRSendData.html create mode 100644 dom/xhr/tests/test_XHR_anon.html create mode 100644 dom/xhr/tests/test_XHR_header.html create mode 100644 dom/xhr/tests/test_XHR_onuploadprogress.html create mode 100644 dom/xhr/tests/test_XHR_parameters.html create mode 100644 dom/xhr/tests/test_XHR_system.html create mode 100644 dom/xhr/tests/test_XHR_timeout.html create mode 100644 dom/xhr/tests/test_XHR_timeout.js create mode 100644 dom/xhr/tests/test_bug1070763.html create mode 100644 dom/xhr/tests/test_bug1300552.html create mode 100644 dom/xhr/tests/test_bug1697539.html create mode 100644 dom/xhr/tests/test_bug1752863.html create mode 100644 dom/xhr/tests/test_bug1752863_worker.js create mode 100644 dom/xhr/tests/test_bug1788125.html create mode 100644 dom/xhr/tests/test_event_listener_leaks.html create mode 100644 dom/xhr/tests/test_html_in_xhr.html create mode 100644 dom/xhr/tests/test_nestedSyncXHR.html create mode 100644 dom/xhr/tests/test_relativeLoad.html create mode 100644 dom/xhr/tests/test_sharedworker_xhr.html create mode 100644 dom/xhr/tests/test_sync_xhr_document_write_with_iframe.html create mode 100644 dom/xhr/tests/test_sync_xhr_event_handling.html create mode 100644 dom/xhr/tests/test_sync_xhr_nested.html create mode 100644 dom/xhr/tests/test_sync_xhr_timer.xhtml create mode 100644 dom/xhr/tests/test_sync_xhr_unload.html create mode 100644 dom/xhr/tests/test_temporaryFileBlob.html create mode 100644 dom/xhr/tests/test_worker_terminateSyncXHR.html create mode 100644 dom/xhr/tests/test_worker_xhr.html create mode 100644 dom/xhr/tests/test_worker_xhr2.html create mode 100644 dom/xhr/tests/test_worker_xhrAbort.html create mode 100644 dom/xhr/tests/test_worker_xhr_3rdparty.html create mode 100644 dom/xhr/tests/test_worker_xhr_cors_redirect.html create mode 100644 dom/xhr/tests/test_worker_xhr_doubleSend.html create mode 100644 dom/xhr/tests/test_worker_xhr_headers.html create mode 100644 dom/xhr/tests/test_worker_xhr_implicit_cancel.html create mode 100644 dom/xhr/tests/test_worker_xhr_parameters.html create mode 100644 dom/xhr/tests/test_worker_xhr_parameters.js create mode 100644 dom/xhr/tests/test_worker_xhr_responseURL.html create mode 100644 dom/xhr/tests/test_worker_xhr_system.html create mode 100644 dom/xhr/tests/test_worker_xhr_system.js create mode 100644 dom/xhr/tests/test_worker_xhr_timeout.html create mode 100644 dom/xhr/tests/test_xhr_abort_after_load.html create mode 100644 dom/xhr/tests/test_xhr_forbidden_headers.html create mode 100644 dom/xhr/tests/test_xhr_overridemimetype_throws_on_invalid_state.html create mode 100644 dom/xhr/tests/test_xhr_progressevents.html create mode 100644 dom/xhr/tests/test_xhr_send.html create mode 100644 dom/xhr/tests/test_xhr_send_readystate.html create mode 100644 dom/xhr/tests/test_xhr_withCredentials.html create mode 100644 dom/xhr/tests/window_worker_xhr_3rdparty.html create mode 100644 dom/xhr/tests/worker_bug1300552.js create mode 100644 dom/xhr/tests/worker_bug1697539.js create mode 100644 dom/xhr/tests/worker_file_getcookie.sjs create mode 100644 dom/xhr/tests/worker_temporaryFileBlob.js create mode 100644 dom/xhr/tests/worker_terminateSyncXHR_frame.html create mode 100644 dom/xhr/tests/worker_testXHR.txt create mode 100644 dom/xhr/tests/worker_xhr_cors_redirect.js create mode 100644 dom/xhr/tests/worker_xhr_cors_redirect.sjs create mode 100644 dom/xhr/tests/worker_xhr_headers_server.sjs create mode 100644 dom/xhr/tests/worker_xhr_headers_worker.js create mode 100644 dom/xhr/tests/xhr2_worker.js create mode 100644 dom/xhr/tests/xhrAbort_worker.js create mode 100644 dom/xhr/tests/xhr_implicit_cancel_worker.js create mode 100644 dom/xhr/tests/xhr_sharedworker.js create mode 100644 dom/xhr/tests/xhr_worker.js create mode 100644 dom/xhr/tests/xhr_worker_doubleSend.js (limited to 'dom/xhr') diff --git a/dom/xhr/XMLHttpRequest.cpp b/dom/xhr/XMLHttpRequest.cpp new file mode 100644 index 0000000000..a331de6ffe --- /dev/null +++ b/dom/xhr/XMLHttpRequest.cpp @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequest.h" +#include "XMLHttpRequestMainThread.h" +#include "XMLHttpRequestWorker.h" +#include "mozilla/net/CookieJarSettings.h" +#include "nsGlobalWindowInner.h" + +namespace mozilla::dom { + +/* static */ +already_AddRefed XMLHttpRequest::Constructor( + const GlobalObject& aGlobal, const MozXMLHttpRequestParameters& aParams, + ErrorResult& aRv) { + if (NS_IsMainThread()) { + nsCOMPtr global = + do_QueryInterface(aGlobal.GetAsSupports()); + nsCOMPtr principal = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!global || !principal) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr cookieJarSettings; + nsCOMPtr window = do_QueryInterface(global); + if (window) { + Document* document = window->GetExtantDoc(); + if (NS_WARN_IF(!document)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + cookieJarSettings = document->CookieJarSettings(); + } else { + // We are here because this is a sandbox. + cookieJarSettings = + net::CookieJarSettings::Create(principal->GetPrincipal()); + } + + RefPtr req = new XMLHttpRequestMainThread(global); + req->Construct(principal->GetPrincipal(), cookieJarSettings, false); + req->InitParameters(aParams.mMozAnon, aParams.mMozSystem); + return req.forget(); + } + + return XMLHttpRequestWorker::Construct(aGlobal, aParams, aRv); +} + +} // namespace mozilla::dom diff --git a/dom/xhr/XMLHttpRequest.h b/dom/xhr/XMLHttpRequest.h new file mode 100644 index 0000000000..8726cd5840 --- /dev/null +++ b/dom/xhr/XMLHttpRequest.h @@ -0,0 +1,139 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequest_h +#define mozilla_dom_XMLHttpRequest_h + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/XMLHttpRequestEventTarget.h" +#include "mozilla/dom/XMLHttpRequestBinding.h" + +class nsIInputStream; + +namespace mozilla::dom { + +class Blob; +class DOMString; +class FormData; +class URLSearchParams; +class XMLHttpRequestUpload; + +class XMLHttpRequest : public XMLHttpRequestEventTarget { + public: + static already_AddRefed Constructor( + const GlobalObject& aGlobal, const MozXMLHttpRequestParameters& aParams, + ErrorResult& aRv); + + static already_AddRefed Constructor( + const GlobalObject& aGlobal, const nsAString& ignored, ErrorResult& aRv) { + // Pretend like someone passed null, so we can pick up the default values + MozXMLHttpRequestParameters params; + if (!params.Init(aGlobal.Context(), JS::NullHandleValue)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + return Constructor(aGlobal, params, aRv); + } + + IMPL_EVENT_HANDLER(readystatechange) + + virtual uint16_t ReadyState() const = 0; + + virtual void Open(const nsACString& aMethod, const nsAString& aUrl, + ErrorResult& aRv) = 0; + + virtual void Open(const nsACString& aMethod, const nsAString& aUrl, + bool aAsync, const nsAString& aUser, + const nsAString& aPassword, ErrorResult& aRv) = 0; + + virtual void SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, ErrorResult& aRv) = 0; + + virtual uint32_t Timeout() const = 0; + + virtual void SetTimeout(uint32_t aTimeout, ErrorResult& aRv) = 0; + + virtual bool WithCredentials() const = 0; + + virtual void SetWithCredentials(bool aWithCredentials, ErrorResult& aRv) = 0; + + virtual XMLHttpRequestUpload* GetUpload(ErrorResult& aRv) = 0; + + virtual void Send( + const Nullable< + DocumentOrBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString>& + aData, + ErrorResult& aRv) = 0; + + virtual void SendInputStream(nsIInputStream* aInputStream, + ErrorResult& aRv) = 0; + + virtual void Abort(ErrorResult& aRv) = 0; + + virtual void GetResponseURL(nsAString& aUrl) = 0; + + virtual uint32_t GetStatus(ErrorResult& aRv) = 0; + + virtual void GetStatusText(nsACString& aStatusText, ErrorResult& aRv) = 0; + + virtual void GetResponseHeader(const nsACString& aHeader, nsACString& aResult, + ErrorResult& aRv) = 0; + + virtual void GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) = 0; + + virtual void OverrideMimeType(const nsAString& aMimeType, + ErrorResult& aRv) = 0; + + virtual XMLHttpRequestResponseType ResponseType() const = 0; + + virtual void SetResponseType(XMLHttpRequestResponseType aType, + ErrorResult& aRv) = 0; + + virtual void GetResponse(JSContext* aCx, + JS::MutableHandle aResponse, + ErrorResult& aRv) = 0; + + virtual void GetResponseText(DOMString& aResponseText, ErrorResult& aRv) = 0; + + virtual Document* GetResponseXML(ErrorResult& aRv) = 0; + + virtual bool MozBackgroundRequest() const = 0; + + virtual void SetMozBackgroundRequest(bool aMozBackgroundRequest, + ErrorResult& aRv) = 0; + + virtual nsIChannel* GetChannel() const = 0; + + // We need a GetInterface callable from JS for chrome JS + virtual void GetInterface(JSContext* aCx, JS::Handle aIID, + JS::MutableHandle aRetval, + ErrorResult& aRv) = 0; + + virtual void SetOriginAttributes( + const mozilla::dom::OriginAttributesDictionary& aAttrs) = 0; + + virtual uint16_t ErrorCode() const = 0; + + virtual bool MozAnon() const = 0; + + virtual bool MozSystem() const = 0; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override { + return mozilla::dom::XMLHttpRequest_Binding::Wrap(aCx, this, aGivenProto); + } + + protected: + explicit XMLHttpRequest(nsIGlobalObject* aGlobalObject) + : XMLHttpRequestEventTarget(aGlobalObject) {} +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XMLHttpRequest_h diff --git a/dom/xhr/XMLHttpRequestEventTarget.cpp b/dom/xhr/XMLHttpRequestEventTarget.cpp new file mode 100644 index 0000000000..32bba7ac9c --- /dev/null +++ b/dom/xhr/XMLHttpRequestEventTarget.cpp @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestEventTarget.h" + +#include "mozilla/dom/DebuggerNotificationBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(XMLHttpRequestEventTarget) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XMLHttpRequestEventTarget, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XMLHttpRequestEventTarget, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XMLHttpRequestEventTarget) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(XMLHttpRequestEventTarget, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(XMLHttpRequestEventTarget, DOMEventTargetHelper) + +mozilla::Maybe +XMLHttpRequestEventTarget::GetDebuggerNotificationType() const { + return mozilla::Some(EventCallbackDebuggerNotificationType::Xhr); +} + +void XMLHttpRequestEventTarget::DisconnectFromOwner() { + DOMEventTargetHelper::DisconnectFromOwner(); +} + +} // namespace mozilla::dom diff --git a/dom/xhr/XMLHttpRequestEventTarget.h b/dom/xhr/XMLHttpRequestEventTarget.h new file mode 100644 index 0000000000..89c13e1338 --- /dev/null +++ b/dom/xhr/XMLHttpRequestEventTarget.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestEventTarget_h +#define mozilla_dom_XMLHttpRequestEventTarget_h + +#include "mozilla/DOMEventTargetHelper.h" + +namespace mozilla::dom { + +class XMLHttpRequestEventTarget : public DOMEventTargetHelper { + protected: + explicit XMLHttpRequestEventTarget(DOMEventTargetHelper* aOwner) + : DOMEventTargetHelper(aOwner) {} + + explicit XMLHttpRequestEventTarget(nsIGlobalObject* aGlobalObject) + : DOMEventTargetHelper(aGlobalObject) {} + + virtual ~XMLHttpRequestEventTarget() = default; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XMLHttpRequestEventTarget, + DOMEventTargetHelper) + + mozilla::Maybe + GetDebuggerNotificationType() const override; + + IMPL_EVENT_HANDLER(loadstart) + IMPL_EVENT_HANDLER(progress) + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(error) + IMPL_EVENT_HANDLER(load) + IMPL_EVENT_HANDLER(timeout) + IMPL_EVENT_HANDLER(loadend) + + nsISupports* GetParentObject() const { return GetOwner(); } + + virtual void DisconnectFromOwner() override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XMLHttpRequestEventTarget_h diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp new file mode 100644 index 0000000000..23161957d3 --- /dev/null +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -0,0 +1,4154 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestMainThread.h" + +#include +#ifndef XP_WIN +# include +#endif +#include "mozilla/ArrayUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Components.h" +#include "mozilla/dom/AutoSuppressEventHandlingAndSuspend.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/DOMString.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileBinding.h" +#include "mozilla/dom/FileCreatorHelper.h" +#include "mozilla/dom/FetchUtil.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/MutableBlobStorage.h" +#include "mozilla/dom/XMLDocument.h" +#include "mozilla/dom/URLSearchParams.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/WorkerError.h" +#include "mozilla/Encoding.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/LoadContext.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/PreloaderBase.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/dom/ProgressEvent.h" +#include "nsIJARChannel.h" +#include "nsIJARURI.h" +#include "nsLayoutCID.h" +#include "nsReadableUtils.h" +#include "nsSandboxFlags.h" + +#include "nsIURI.h" +#include "nsIURIMutator.h" +#include "nsILoadGroup.h" +#include "nsNetUtil.h" +#include "nsStringStream.h" +#include "nsIAuthPrompt.h" +#include "nsIAuthPrompt2.h" +#include "nsIClassOfService.h" +#include "nsIHttpChannel.h" +#include "nsISupportsPriority.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsIUploadChannel.h" +#include "nsIUploadChannel2.h" +#include "nsXPCOM.h" +#include "nsIDOMEventListener.h" +#include "nsVariant.h" +#include "nsIScriptError.h" +#include "nsICachingChannel.h" +#include "nsICookieJarSettings.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsError.h" +#include "nsIPromptFactory.h" +#include "nsIWindowWatcher.h" +#include "nsIConsoleService.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "nsStringBuffer.h" +#include "nsIFileChannel.h" +#include "mozilla/Telemetry.h" +#include "js/ArrayBuffer.h" // JS::{Create,Release}MappedArrayBufferContents,New{,Mapped}ArrayBufferWithContents +#include "js/JSON.h" // JS_ParseJSON +#include "js/MemoryFunctions.h" +#include "js/RootingAPI.h" // JS::{{,Mutable}Handle,Rooted} +#include "js/Value.h" // JS::{,Undefined}Value +#include "jsapi.h" // JS_ClearPendingException +#include "GeckoProfiler.h" +#include "mozilla/dom/XMLHttpRequestBinding.h" +#include "mozilla/Attributes.h" +#include "MultipartBlobImpl.h" +#include "nsIPermissionManager.h" +#include "nsMimeTypes.h" +#include "nsIHttpChannelInternal.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsStreamListenerWrapper.h" +#include "nsITimedChannel.h" +#include "nsWrapperCacheInlines.h" +#include "nsZipArchive.h" +#include "mozilla/Preferences.h" +#include "private/pprio.h" +#include "XMLHttpRequestUpload.h" + +// Undefine the macro of CreateFile to avoid FileCreatorHelper#CreateFile being +// replaced by FileCreatorHelper#CreateFileW. +#ifdef CreateFile +# undef CreateFile +#endif + +using namespace mozilla::net; + +namespace mozilla::dom { + +// Maximum size that we'll grow an ArrayBuffer instead of doubling, +// once doubling reaches this threshold +const uint32_t XML_HTTP_REQUEST_ARRAYBUFFER_MAX_GROWTH = 32 * 1024 * 1024; +// start at 32k to avoid lots of doubling right at the start +const uint32_t XML_HTTP_REQUEST_ARRAYBUFFER_MIN_SIZE = 32 * 1024; +// the maximum Content-Length that we'll preallocate. 1GB. Must fit +// in an int32_t! +const int32_t XML_HTTP_REQUEST_MAX_CONTENT_LENGTH_PREALLOCATE = + 1 * 1024 * 1024 * 1024LL; + +namespace { +const nsLiteralString ProgressEventTypeStrings[] = { + u"loadstart"_ns, u"progress"_ns, u"error"_ns, u"abort"_ns, + u"timeout"_ns, u"load"_ns, u"loadend"_ns}; +static_assert(MOZ_ARRAY_LENGTH(ProgressEventTypeStrings) == + size_t(XMLHttpRequestMainThread::ProgressEventType::ENUM_MAX), + "Mismatched lengths for ProgressEventTypeStrings and " + "ProgressEventType enums"); + +const nsString kLiteralString_readystatechange = u"readystatechange"_ns; +const nsString kLiteralString_xmlhttprequest = u"xmlhttprequest"_ns; +const nsString kLiteralString_DOMContentLoaded = u"DOMContentLoaded"_ns; +const nsCString kLiteralString_charset = "charset"_ns; +const nsCString kLiteralString_UTF_8 = "UTF-8"_ns; +} // namespace + +#define NS_PROGRESS_EVENT_INTERVAL 50 +#define MAX_SYNC_TIMEOUT_WHEN_UNLOADING 10000 /* 10 secs */ + +NS_IMPL_ISUPPORTS(nsXHRParseEndListener, nsIDOMEventListener) + +class nsResumeTimeoutsEvent : public Runnable { + public: + explicit nsResumeTimeoutsEvent(nsPIDOMWindowInner* aWindow) + : Runnable("dom::nsResumeTimeoutsEvent"), mWindow(aWindow) {} + + NS_IMETHOD Run() override { + mWindow->Resume(); + return NS_OK; + } + + private: + nsCOMPtr mWindow; +}; + +// This helper function adds the given load flags to the request's existing +// load flags. +static void AddLoadFlags(nsIRequest* request, nsLoadFlags newFlags) { + nsLoadFlags flags; + request->GetLoadFlags(&flags); + flags |= newFlags; + request->SetLoadFlags(flags); +} + +// We are in a sync event loop. +#define NOT_CALLABLE_IN_SYNC_SEND_RV \ + if (mFlagSyncLooping || mEventDispatchingSuspended) { \ + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT); \ + return; \ + } + +///////////////////////////////////////////// +// +// +///////////////////////////////////////////// + +bool XMLHttpRequestMainThread::sDontWarnAboutSyncXHR = false; + +XMLHttpRequestMainThread::XMLHttpRequestMainThread( + nsIGlobalObject* aGlobalObject) + : XMLHttpRequest(aGlobalObject), + mResponseBodyDecodedPos(0), + mResponseType(XMLHttpRequestResponseType::_empty), + mState(XMLHttpRequest_Binding::UNSENT), + mFlagSynchronous(false), + mFlagAborted(false), + mFlagParseBody(false), + mFlagSyncLooping(false), + mFlagBackgroundRequest(false), + mFlagHadUploadListenersOnSend(false), + mFlagACwithCredentials(false), + mFlagTimedOut(false), + mFlagDeleted(false), + mFlagSend(false), + mUploadTransferred(0), + mUploadTotal(0), + mUploadComplete(true), + mProgressSinceLastProgressEvent(false), + mRequestSentTime(0), + mTimeoutMilliseconds(0), + mErrorLoad(ErrorType::eOK), + mErrorParsingXML(false), + mWaitingForOnStopRequest(false), + mProgressTimerIsActive(false), + mIsHtml(false), + mWarnAboutSyncHtml(false), + mLoadTotal(-1), + mLoadTransferred(0), + mIsSystem(false), + mIsAnon(false), + mResultJSON(JS::UndefinedValue()), + mArrayBufferBuilder(new ArrayBufferBuilder()), + mResultArrayBuffer(nullptr), + mIsMappedArrayBuffer(false), + mXPCOMifier(nullptr), + mEventDispatchingSuspended(false), + mEofDecoded(false), + mDelayedDoneNotifier(nullptr) { + mozilla::HoldJSObjects(this); +} + +XMLHttpRequestMainThread::~XMLHttpRequestMainThread() { + MOZ_ASSERT( + !mDelayedDoneNotifier, + "How can we have mDelayedDoneNotifier, which owns us, in destructor?"); + + mFlagDeleted = true; + + if ((mState == XMLHttpRequest_Binding::OPENED && mFlagSend) || + mState == XMLHttpRequest_Binding::LOADING) { + Abort(); + } + + if (mParseEndListener) { + mParseEndListener->SetIsStale(); + mParseEndListener = nullptr; + } + + MOZ_ASSERT(!mFlagSyncLooping, "we rather crash than hang"); + mFlagSyncLooping = false; + + mozilla::DropJSObjects(this); +} + +void XMLHttpRequestMainThread::Construct( + nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings, + bool aForWorker, nsIURI* aBaseURI /* = nullptr */, + nsILoadGroup* aLoadGroup /* = nullptr */, + PerformanceStorage* aPerformanceStorage /* = nullptr */, + nsICSPEventListener* aCSPEventListener /* = nullptr */) { + MOZ_ASSERT(aPrincipal); + mPrincipal = aPrincipal; + mBaseURI = aBaseURI; + mLoadGroup = aLoadGroup; + mCookieJarSettings = aCookieJarSettings; + mForWorker = aForWorker; + mPerformanceStorage = aPerformanceStorage; + mCSPEventListener = aCSPEventListener; +} + +void XMLHttpRequestMainThread::InitParameters(bool aAnon, bool aSystem) { + if (!aAnon && !aSystem) { + return; + } + + // Check for permissions. + // Chrome is always allowed access, so do the permission check only + // for non-chrome pages. + if (!IsSystemXHR() && aSystem) { + nsIGlobalObject* global = GetOwnerGlobal(); + if (NS_WARN_IF(!global)) { + SetParameters(aAnon, false); + return; + } + + nsIPrincipal* principal = global->PrincipalOrNull(); + if (NS_WARN_IF(!principal)) { + SetParameters(aAnon, false); + return; + } + + nsCOMPtr permMgr = + components::PermissionManager::Service(); + if (NS_WARN_IF(!permMgr)) { + SetParameters(aAnon, false); + return; + } + + uint32_t permission; + nsresult rv = permMgr->TestPermissionFromPrincipal( + principal, "systemXHR"_ns, &permission); + if (NS_FAILED(rv) || permission != nsIPermissionManager::ALLOW_ACTION) { + SetParameters(aAnon, false); + return; + } + } + + SetParameters(aAnon, aSystem); +} + +void XMLHttpRequestMainThread::SetClientInfoAndController( + const ClientInfo& aClientInfo, + const Maybe& aController) { + mClientInfo.emplace(aClientInfo); + mController = aController; +} + +void XMLHttpRequestMainThread::ResetResponse() { + mResponseXML = nullptr; + mResponseBody.Truncate(); + TruncateResponseText(); + mResponseBlobImpl = nullptr; + mResponseBlob = nullptr; + mBlobStorage = nullptr; + mResultArrayBuffer = nullptr; + mArrayBufferBuilder = new ArrayBufferBuilder(); + mResultJSON.setUndefined(); + mLoadTransferred = 0; + mResponseBodyDecodedPos = 0; + mEofDecoded = false; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(XMLHttpRequestMainThread) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XMLHttpRequestMainThread, + XMLHttpRequestEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContext) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChannel) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResponseXML) + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mXMLParserStreamListener) + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResponseBlob) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationCallbacks) + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChannelEventSink) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mProgressEventSink) + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUpload) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XMLHttpRequestMainThread, + XMLHttpRequestEventTarget) + tmp->mResultArrayBuffer = nullptr; + tmp->mArrayBufferBuilder = nullptr; + tmp->mResultJSON.setUndefined(); + tmp->mResponseBlobImpl = nullptr; + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mContext) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mChannel) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mResponseXML) + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mXMLParserStreamListener) + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mResponseBlob) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationCallbacks) + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mChannelEventSink) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mProgressEventSink) + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mUpload) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(XMLHttpRequestMainThread, + XMLHttpRequestEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResultArrayBuffer) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResultJSON) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +bool XMLHttpRequestMainThread::IsCertainlyAliveForCC() const { + return mWaitingForOnStopRequest; +} + +// QueryInterface implementation for XMLHttpRequestMainThread +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XMLHttpRequestMainThread) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink) + NS_INTERFACE_MAP_ENTRY(nsIProgressEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY(nsISizeOfEventTarget) +NS_INTERFACE_MAP_END_INHERITING(XMLHttpRequestEventTarget) + +NS_IMPL_ADDREF_INHERITED(XMLHttpRequestMainThread, XMLHttpRequestEventTarget) +NS_IMPL_RELEASE_INHERITED(XMLHttpRequestMainThread, XMLHttpRequestEventTarget) + +void XMLHttpRequestMainThread::DisconnectFromOwner() { + XMLHttpRequestEventTarget::DisconnectFromOwner(); + Abort(); +} + +size_t XMLHttpRequestMainThread::SizeOfEventTargetIncludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + n += mResponseBody.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + + // Why is this safe? Because no-one else will report this string. The + // other possible sharers of this string are as follows. + // + // - The JS engine could hold copies if the JS code holds references, e.g. + // |var text = XHR.responseText|. However, those references will be via JS + // external strings, for which the JS memory reporter does *not* report the + // chars. + // + // - Binary extensions, but they're *extremely* unlikely to do any memory + // reporting. + // + n += mResponseText.SizeOfThis(aMallocSizeOf); + + return n; + + // Measurement of the following members may be added later if DMD finds it is + // worthwhile: + // - lots +} + +static void LogMessage( + const char* aWarning, nsPIDOMWindowInner* aWindow, + const nsTArray& aParams = nsTArray()) { + nsCOMPtr doc; + if (aWindow) { + doc = aWindow->GetExtantDoc(); + } + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, doc, + nsContentUtils::eDOM_PROPERTIES, aWarning, + aParams); +} + +Document* XMLHttpRequestMainThread::GetResponseXML(ErrorResult& aRv) { + if (mResponseType != XMLHttpRequestResponseType::_empty && + mResponseType != XMLHttpRequestResponseType::Document) { + aRv.ThrowInvalidStateError( + "responseXML is only available if responseType is '' or 'document'."); + return nullptr; + } + if (mWarnAboutSyncHtml) { + mWarnAboutSyncHtml = false; + LogMessage("HTMLSyncXHRWarning", GetOwner()); + } + if (mState != XMLHttpRequest_Binding::DONE) { + return nullptr; + } + return mResponseXML; +} + +/* + * This piece copied from XMLDocument, we try to get the charset + * from HTTP headers. + */ +nsresult XMLHttpRequestMainThread::DetectCharset() { + mDecoder = nullptr; + + if (mResponseType != XMLHttpRequestResponseType::_empty && + mResponseType != XMLHttpRequestResponseType::Text && + mResponseType != XMLHttpRequestResponseType::Json) { + return NS_OK; + } + + nsAutoCString charsetVal; + const Encoding* encoding; + bool ok = mChannel && NS_SUCCEEDED(mChannel->GetContentCharset(charsetVal)) && + (encoding = Encoding::ForLabel(charsetVal)); + if (!ok) { + // MS documentation states UTF-8 is default for responseText + encoding = UTF_8_ENCODING; + } + + if (mResponseType == XMLHttpRequestResponseType::Json && + encoding != UTF_8_ENCODING) { + // The XHR spec says only UTF-8 is supported for responseType == "json" + LogMessage("JSONCharsetWarning", GetOwner()); + encoding = UTF_8_ENCODING; + } + + // Only sniff the BOM for non-JSON responseTypes + if (mResponseType == XMLHttpRequestResponseType::Json) { + mDecoder = encoding->NewDecoderWithBOMRemoval(); + } else { + mDecoder = encoding->NewDecoder(); + } + + return NS_OK; +} + +nsresult XMLHttpRequestMainThread::AppendToResponseText( + Span aBuffer, bool aLast) { + // Call this with an empty buffer to send the decoder the signal + // that we have hit the end of the stream. + + NS_ENSURE_STATE(mDecoder); + + CheckedInt destBufferLen = + mDecoder->MaxUTF16BufferLength(aBuffer.Length()); + + { // scope for holding the mutex that protects mResponseText + XMLHttpRequestStringWriterHelper helper(mResponseText); + + uint32_t len = helper.Length(); + + destBufferLen += len; + if (!destBufferLen.isValid() || destBufferLen.value() > UINT32_MAX) { + return NS_ERROR_OUT_OF_MEMORY; + } + + auto handleOrErr = helper.BulkWrite(destBufferLen.value()); + if (handleOrErr.isErr()) { + return handleOrErr.unwrapErr(); + } + + auto handle = handleOrErr.unwrap(); + + uint32_t result; + size_t read; + size_t written; + std::tie(result, read, written, std::ignore) = + mDecoder->DecodeToUTF16(aBuffer, handle.AsSpan().From(len), aLast); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == aBuffer.Length()); + len += written; + MOZ_ASSERT(len <= destBufferLen.value()); + handle.Finish(len, false); + } // release mutex + + if (aLast) { + // Drop the finished decoder to avoid calling into a decoder + // that has finished. + mDecoder = nullptr; + mEofDecoded = true; + } + return NS_OK; +} + +void XMLHttpRequestMainThread::GetResponseText(DOMString& aResponseText, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(!mForWorker); + + XMLHttpRequestStringSnapshot snapshot; + GetResponseText(snapshot, aRv); + if (aRv.Failed()) { + return; + } + + if (!snapshot.GetAsString(aResponseText)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } +} + +void XMLHttpRequestMainThread::GetResponseText( + XMLHttpRequestStringSnapshot& aSnapshot, ErrorResult& aRv) { + aSnapshot.Reset(); + + if (mResponseType != XMLHttpRequestResponseType::_empty && + mResponseType != XMLHttpRequestResponseType::Text) { + aRv.ThrowInvalidStateError( + "responseText is only available if responseType is '' or 'text'."); + return; + } + + if (mState != XMLHttpRequest_Binding::LOADING && + mState != XMLHttpRequest_Binding::DONE) { + return; + } + + // Main Fetch step 18 requires to ignore body for head/connect methods. + if (mRequestMethod.EqualsLiteral("HEAD") || + mRequestMethod.EqualsLiteral("CONNECT")) { + return; + } + + // We only decode text lazily if we're also parsing to a doc. + // Also, if we've decoded all current data already, then no need to decode + // more. + if ((!mResponseXML && !mErrorParsingXML) || + (mResponseBodyDecodedPos == mResponseBody.Length() && + (mState != XMLHttpRequest_Binding::DONE || mEofDecoded))) { + mResponseText.CreateSnapshot(aSnapshot); + return; + } + + MatchCharsetAndDecoderToResponseDocument(); + + MOZ_ASSERT(mResponseBodyDecodedPos < mResponseBody.Length() || + mState == XMLHttpRequest_Binding::DONE, + "Unexpected mResponseBodyDecodedPos"); + Span span = mResponseBody; + aRv = AppendToResponseText(span.From(mResponseBodyDecodedPos), + mState == XMLHttpRequest_Binding::DONE); + if (aRv.Failed()) { + return; + } + + mResponseBodyDecodedPos = mResponseBody.Length(); + + if (mEofDecoded) { + // Free memory buffer which we no longer need + mResponseBody.Truncate(); + mResponseBodyDecodedPos = 0; + } + + mResponseText.CreateSnapshot(aSnapshot); +} + +nsresult XMLHttpRequestMainThread::CreateResponseParsedJSON(JSContext* aCx) { + if (!aCx) { + return NS_ERROR_FAILURE; + } + + nsAutoString string; + nsresult rv = GetResponseTextForJSON(string); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The Unicode converter has already zapped the BOM if there was one + JS::Rooted value(aCx); + if (!JS_ParseJSON(aCx, string.BeginReading(), string.Length(), &value)) { + return NS_ERROR_FAILURE; + } + + mResultJSON = value; + return NS_OK; +} + +void XMLHttpRequestMainThread::SetResponseType( + XMLHttpRequestResponseType aResponseType, ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + if (mState == XMLHttpRequest_Binding::LOADING || + mState == XMLHttpRequest_Binding::DONE) { + aRv.ThrowInvalidStateError( + "Cannot set 'responseType' property on XMLHttpRequest after 'send()' " + "(when its state is LOADING or DONE)."); + return; + } + + // sync request is not allowed setting responseType in window context + if (HasOrHasHadOwner() && mState != XMLHttpRequest_Binding::UNSENT && + mFlagSynchronous) { + LogMessage("ResponseTypeSyncXHRWarning", GetOwner()); + aRv.ThrowInvalidAccessError( + "synchronous XMLHttpRequests do not support timeout and responseType"); + return; + } + + // Set the responseType attribute's value to the given value. + SetResponseTypeRaw(aResponseType); +} + +void XMLHttpRequestMainThread::GetResponse( + JSContext* aCx, JS::MutableHandle aResponse, ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(!mForWorker); + + switch (mResponseType) { + case XMLHttpRequestResponseType::_empty: + case XMLHttpRequestResponseType::Text: { + DOMString str; + GetResponseText(str, aRv); + if (aRv.Failed()) { + return; + } + if (!xpc::StringToJsval(aCx, str, aResponse)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + } + return; + } + + case XMLHttpRequestResponseType::Arraybuffer: { + if (mState != XMLHttpRequest_Binding::DONE) { + aResponse.setNull(); + return; + } + + if (!mResultArrayBuffer) { + mResultArrayBuffer = mArrayBufferBuilder->TakeArrayBuffer(aCx); + if (!mResultArrayBuffer) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + aResponse.setObject(*mResultArrayBuffer); + return; + } + case XMLHttpRequestResponseType::Blob: { + if (mState != XMLHttpRequest_Binding::DONE) { + aResponse.setNull(); + return; + } + + if (!mResponseBlobImpl) { + aResponse.setNull(); + return; + } + + if (!mResponseBlob) { + mResponseBlob = Blob::Create(GetOwnerGlobal(), mResponseBlobImpl); + } + + if (!GetOrCreateDOMReflector(aCx, mResponseBlob, aResponse)) { + aResponse.setNull(); + } + + return; + } + case XMLHttpRequestResponseType::Document: { + if (!mResponseXML || mState != XMLHttpRequest_Binding::DONE) { + aResponse.setNull(); + return; + } + + aRv = + nsContentUtils::WrapNative(aCx, ToSupports(mResponseXML), aResponse); + return; + } + case XMLHttpRequestResponseType::Json: { + if (mState != XMLHttpRequest_Binding::DONE) { + aResponse.setNull(); + return; + } + + if (mResultJSON.isUndefined()) { + aRv = CreateResponseParsedJSON(aCx); + TruncateResponseText(); + if (aRv.Failed()) { + // Per spec, errors aren't propagated. null is returned instead. + aRv = NS_OK; + // It would be nice to log the error to the console. That's hard to + // do without calling window.onerror as a side effect, though. + JS_ClearPendingException(aCx); + mResultJSON.setNull(); + } + } + aResponse.set(mResultJSON); + return; + } + default: + NS_ERROR("Should not happen"); + } + + aResponse.setNull(); +} + +already_AddRefed XMLHttpRequestMainThread::GetResponseBlobImpl() { + MOZ_DIAGNOSTIC_ASSERT(mForWorker); + MOZ_DIAGNOSTIC_ASSERT(mResponseType == XMLHttpRequestResponseType::Blob); + + if (mState != XMLHttpRequest_Binding::DONE) { + return nullptr; + } + + RefPtr blobImpl = mResponseBlobImpl; + return blobImpl.forget(); +} + +already_AddRefed +XMLHttpRequestMainThread::GetResponseArrayBufferBuilder() { + MOZ_DIAGNOSTIC_ASSERT(mForWorker); + MOZ_DIAGNOSTIC_ASSERT(mResponseType == + XMLHttpRequestResponseType::Arraybuffer); + + if (mState != XMLHttpRequest_Binding::DONE) { + return nullptr; + } + + RefPtr builder = mArrayBufferBuilder; + return builder.forget(); +} + +nsresult XMLHttpRequestMainThread::GetResponseTextForJSON(nsAString& aString) { + if (mState != XMLHttpRequest_Binding::DONE) { + aString.SetIsVoid(true); + return NS_OK; + } + + if (!mResponseText.GetAsString(aString)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +bool XMLHttpRequestMainThread::IsCrossSiteCORSRequest() const { + if (!mChannel) { + return false; + } + + nsCOMPtr loadInfo = mChannel->LoadInfo(); + return loadInfo->GetTainting() == LoadTainting::CORS; +} + +bool XMLHttpRequestMainThread::IsDeniedCrossSiteCORSRequest() { + if (IsCrossSiteCORSRequest()) { + nsresult rv; + mChannel->GetStatus(&rv); + if (NS_FAILED(rv)) { + return true; + } + } + return false; +} + +void XMLHttpRequestMainThread::GetResponseURL(nsAString& aUrl) { + aUrl.Truncate(); + + if ((mState == XMLHttpRequest_Binding::UNSENT || + mState == XMLHttpRequest_Binding::OPENED) || + !mChannel) { + return; + } + + // Make sure we don't leak responseURL information from denied cross-site + // requests. + if (IsDeniedCrossSiteCORSRequest()) { + return; + } + + nsCOMPtr responseUrl; + if (NS_FAILED(NS_GetFinalChannelURI(mChannel, getter_AddRefs(responseUrl)))) { + return; + } + + nsAutoCString temp; + responseUrl->GetSpecIgnoringRef(temp); + CopyUTF8toUTF16(temp, aUrl); +} + +uint32_t XMLHttpRequestMainThread::GetStatus(ErrorResult& aRv) { + // Make sure we don't leak status information from denied cross-site + // requests. + if (IsDeniedCrossSiteCORSRequest()) { + return 0; + } + + if (mState == XMLHttpRequest_Binding::UNSENT || + mState == XMLHttpRequest_Binding::OPENED) { + return 0; + } + + if (mErrorLoad != ErrorType::eOK) { + // Let's simulate the http protocol for jar/app requests: + nsCOMPtr jarChannel = GetCurrentJARChannel(); + if (jarChannel) { + nsresult status; + mChannel->GetStatus(&status); + + if (status == NS_ERROR_FILE_NOT_FOUND) { + return 404; // Not Found + } else { + return 500; // Internal Error + } + } + + return 0; + } + + nsCOMPtr httpChannel = GetCurrentHttpChannel(); + if (!httpChannel) { + // Pretend like we got a 200 response, since our load was successful + return 200; + } + + uint32_t status; + nsresult rv = httpChannel->GetResponseStatus(&status); + if (NS_FAILED(rv)) { + status = 0; + } + + return status; +} + +void XMLHttpRequestMainThread::GetStatusText(nsACString& aStatusText, + ErrorResult& aRv) { + // Return an empty status text on all error loads. + aStatusText.Truncate(); + + // Make sure we don't leak status information from denied cross-site + // requests. + if (IsDeniedCrossSiteCORSRequest()) { + return; + } + + // Check the current XHR state to see if it is valid to obtain the statusText + // value. This check is to prevent the status text for redirects from being + // available before all the redirects have been followed and HTTP headers have + // been received. + if (mState == XMLHttpRequest_Binding::UNSENT || + mState == XMLHttpRequest_Binding::OPENED) { + return; + } + + if (mErrorLoad != ErrorType::eOK) { + return; + } + + nsCOMPtr httpChannel = GetCurrentHttpChannel(); + if (httpChannel) { + Unused << httpChannel->GetResponseStatusText(aStatusText); + } else { + aStatusText.AssignLiteral("OK"); + } +} + +void XMLHttpRequestMainThread::TerminateOngoingFetch() { + if ((mState == XMLHttpRequest_Binding::OPENED && mFlagSend) || + mState == XMLHttpRequest_Binding::HEADERS_RECEIVED || + mState == XMLHttpRequest_Binding::LOADING) { + CloseRequest(); + } +} + +void XMLHttpRequestMainThread::CloseRequest() { + mWaitingForOnStopRequest = false; + mErrorLoad = ErrorType::eTerminated; + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "XMLHttpRequestMainThread::CloseRequest"_ns); + } + if (mTimeoutTimer) { + mTimeoutTimer->Cancel(); + } +} + +void XMLHttpRequestMainThread::CloseRequestWithError( + const ProgressEventType aType) { + CloseRequest(); + + ResetResponse(); + + // If we're in the destructor, don't risk dispatching an event. + if (mFlagDeleted) { + mFlagSyncLooping = false; + return; + } + + if (mState != XMLHttpRequest_Binding::UNSENT && + !(mState == XMLHttpRequest_Binding::OPENED && !mFlagSend) && + mState != XMLHttpRequest_Binding::DONE) { + ChangeState(XMLHttpRequest_Binding::DONE, true); + + if (!mFlagSyncLooping) { + if (mUpload && !mUploadComplete) { + mUploadComplete = true; + DispatchProgressEvent(mUpload, aType, 0, -1); + } + DispatchProgressEvent(this, aType, 0, -1); + } + } + + // The ChangeState call above calls onreadystatechange handlers which + // if they load a new url will cause XMLHttpRequestMainThread::Open to clear + // the abort state bit. If this occurs we're not uninitialized (bug 361773). + if (mFlagAborted) { + ChangeState(XMLHttpRequest_Binding::UNSENT, false); // IE seems to do it + } + + mFlagSyncLooping = false; +} + +void XMLHttpRequestMainThread::RequestErrorSteps( + const ProgressEventType aEventType, const nsresult aOptionalException, + ErrorResult& aRv) { + // Step 1 + mState = XMLHttpRequest_Binding::DONE; + + StopProgressEventTimer(); + + // Step 2 + mFlagSend = false; + + // Step 3 + ResetResponse(); + + // If we're in the destructor, don't risk dispatching an event. + if (mFlagDeleted) { + mFlagSyncLooping = false; + return; + } + + // Step 4 + if (mFlagSynchronous && NS_FAILED(aOptionalException)) { + aRv.Throw(aOptionalException); + return; + } + + // Step 5 + FireReadystatechangeEvent(); + + // Step 6 + if (mUpload && !mUploadComplete) { + // Step 6-1 + mUploadComplete = true; + + // Step 6-2 + if (mFlagHadUploadListenersOnSend) { + // Steps 6-3, 6-4 (loadend is fired for us) + DispatchProgressEvent(mUpload, aEventType, 0, -1); + } + } + + // Steps 7 and 8 (loadend is fired for us) + DispatchProgressEvent(this, aEventType, 0, -1); +} + +void XMLHttpRequestMainThread::Abort(ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + AbortInternal(aRv); +} + +void XMLHttpRequestMainThread::AbortInternal(ErrorResult& aRv) { + mFlagAborted = true; + DisconnectDoneNotifier(); + + // Step 1 + TerminateOngoingFetch(); + + // Step 2 + if ((mState == XMLHttpRequest_Binding::OPENED && mFlagSend) || + mState == XMLHttpRequest_Binding::HEADERS_RECEIVED || + mState == XMLHttpRequest_Binding::LOADING) { + RequestErrorSteps(ProgressEventType::abort, NS_OK, aRv); + } + + // Step 3 + if (mState == XMLHttpRequest_Binding::DONE) { + ChangeState(XMLHttpRequest_Binding::UNSENT, + false); // no ReadystateChange event + } + + mFlagSyncLooping = false; +} + +/*Method that checks if it is safe to expose a header value to the client. +It is used to check what headers are exposed for CORS requests.*/ +bool XMLHttpRequestMainThread::IsSafeHeader( + const nsACString& aHeader, NotNull aHttpChannel) const { + // See bug #380418. Hide "Set-Cookie" headers from non-chrome scripts. + if (!IsSystemXHR() && nsContentUtils::IsForbiddenResponseHeader(aHeader)) { + NS_WARNING("blocked access to response header"); + return false; + } + // if this is not a CORS call all headers are safe + if (!IsCrossSiteCORSRequest()) { + return true; + } + // Check for dangerous headers + // Make sure we don't leak header information from denied cross-site + // requests. + if (mChannel) { + nsresult status; + mChannel->GetStatus(&status); + if (NS_FAILED(status)) { + return false; + } + } + const char* kCrossOriginSafeHeaders[] = { + "cache-control", "content-language", "content-type", "content-length", + "expires", "last-modified", "pragma"}; + for (uint32_t i = 0; i < ArrayLength(kCrossOriginSafeHeaders); ++i) { + if (aHeader.LowerCaseEqualsASCII(kCrossOriginSafeHeaders[i])) { + return true; + } + } + nsAutoCString headerVal; + // The "Access-Control-Expose-Headers" header contains a comma separated + // list of method names. + Unused << aHttpChannel->GetResponseHeader("Access-Control-Expose-Headers"_ns, + headerVal); + bool isSafe = false; + for (const nsACString& token : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (token.IsEmpty()) { + continue; + } + if (!NS_IsValidHTTPToken(token)) { + return false; + } + + if (token.EqualsLiteral("*") && !mFlagACwithCredentials) { + isSafe = true; + } else if (aHeader.Equals(token, nsCaseInsensitiveCStringComparator)) { + isSafe = true; + } + } + + return isSafe; +} + +void XMLHttpRequestMainThread::GetAllResponseHeaders( + nsACString& aResponseHeaders, ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + aResponseHeaders.Truncate(); + + // If the state is UNSENT or OPENED, + // return the empty string and terminate these steps. + if (mState == XMLHttpRequest_Binding::UNSENT || + mState == XMLHttpRequest_Binding::OPENED) { + return; + } + + if (mErrorLoad != ErrorType::eOK) { + return; + } + + if (nsCOMPtr httpChannel = GetCurrentHttpChannel()) { + RefPtr visitor = + new nsHeaderVisitor(*this, WrapNotNull(httpChannel)); + if (NS_SUCCEEDED(httpChannel->VisitResponseHeaders(visitor))) { + aResponseHeaders = visitor->Headers(); + } + return; + } + + if (!mChannel) { + return; + } + + // Even non-http channels supply content type. + nsAutoCString value; + if (NS_SUCCEEDED(mChannel->GetContentType(value))) { + aResponseHeaders.AppendLiteral("Content-Type: "); + aResponseHeaders.Append(value); + if (NS_SUCCEEDED(mChannel->GetContentCharset(value)) && !value.IsEmpty()) { + aResponseHeaders.AppendLiteral(";charset="); + aResponseHeaders.Append(value); + } + aResponseHeaders.AppendLiteral("\r\n"); + } + + // Don't provide Content-Length for data URIs + nsCOMPtr uri; + if (NS_FAILED(mChannel->GetURI(getter_AddRefs(uri))) || + !uri->SchemeIs("data")) { + int64_t length; + if (NS_SUCCEEDED(mChannel->GetContentLength(&length))) { + aResponseHeaders.AppendLiteral("Content-Length: "); + aResponseHeaders.AppendInt(length); + aResponseHeaders.AppendLiteral("\r\n"); + } + } +} + +void XMLHttpRequestMainThread::GetResponseHeader(const nsACString& header, + nsACString& _retval, + ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + _retval.SetIsVoid(true); + + nsCOMPtr httpChannel = GetCurrentHttpChannel(); + + if (!httpChannel) { + // If the state is UNSENT or OPENED, + // return null and terminate these steps. + if (mState == XMLHttpRequest_Binding::UNSENT || + mState == XMLHttpRequest_Binding::OPENED) { + return; + } + + // Even non-http channels supply content type and content length. + // Remember we don't leak header information from denied cross-site + // requests. However, we handle file: and blob: URLs for blob response + // types by canceling them with a specific error, so we have to allow + // them to pass through this check. + nsresult status; + if (!mChannel || NS_FAILED(mChannel->GetStatus(&status)) || + (NS_FAILED(status) && status != NS_ERROR_FILE_ALREADY_EXISTS)) { + return; + } + + // Content Type: + if (header.LowerCaseEqualsASCII("content-type")) { + if (NS_FAILED(mChannel->GetContentType(_retval))) { + // Means no content type + _retval.SetIsVoid(true); + return; + } + + nsCString value; + if (NS_SUCCEEDED(mChannel->GetContentCharset(value)) && + !value.IsEmpty()) { + _retval.AppendLiteral(";charset="); + _retval.Append(value); + } + } + + // Content Length: + else if (header.LowerCaseEqualsASCII("content-length")) { + int64_t length; + if (NS_SUCCEEDED(mChannel->GetContentLength(&length))) { + _retval.AppendInt(length); + } + } + + return; + } + + // Check for dangerous headers + if (!IsSafeHeader(header, WrapNotNull(httpChannel))) { + return; + } + + aRv = httpChannel->GetResponseHeader(header, _retval); + if (aRv.ErrorCodeIs(NS_ERROR_NOT_AVAILABLE)) { + // Means no header + _retval.SetIsVoid(true); + aRv.SuppressException(); + } +} + +already_AddRefed XMLHttpRequestMainThread::GetLoadGroup() const { + if (mFlagBackgroundRequest) { + return nullptr; + } + + if (mLoadGroup) { + nsCOMPtr ref = mLoadGroup; + return ref.forget(); + } + + Document* doc = GetDocumentIfCurrent(); + if (doc) { + return doc->GetDocumentLoadGroup(); + } + + return nullptr; +} + +nsresult XMLHttpRequestMainThread::FireReadystatechangeEvent() { + MOZ_ASSERT(mState != XMLHttpRequest_Binding::UNSENT); + RefPtr event = NS_NewDOMEvent(this, nullptr, nullptr); + event->InitEvent(kLiteralString_readystatechange, false, false); + // We assume anyone who managed to call CreateReadystatechangeEvent is trusted + event->SetTrusted(true); + DispatchOrStoreEvent(this, event); + return NS_OK; +} + +void XMLHttpRequestMainThread::DispatchProgressEvent( + DOMEventTargetHelper* aTarget, const ProgressEventType aType, + int64_t aLoaded, int64_t aTotal) { + NS_ASSERTION(aTarget, "null target"); + + if (NS_FAILED(CheckCurrentGlobalCorrectness()) || + (!AllowUploadProgress() && aTarget == mUpload)) { + return; + } + + // If blocked by CORS, zero-out the stats on progress events + // and never fire "progress" or "load" events at all. + if (IsDeniedCrossSiteCORSRequest()) { + if (aType == ProgressEventType::progress || + aType == ProgressEventType::load) { + return; + } + aLoaded = 0; + aTotal = -1; + } + + ProgressEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mLengthComputable = aTotal != -1; // XHR spec step 6.1 + init.mLoaded = aLoaded; + init.mTotal = (aTotal == -1) ? 0 : aTotal; + + const nsAString& typeString = ProgressEventTypeStrings[(uint8_t)aType]; + RefPtr event = + ProgressEvent::Constructor(aTarget, typeString, init); + event->SetTrusted(true); + + DispatchOrStoreEvent(aTarget, event); + + // If we're sending a load, error, timeout or abort event, then + // also dispatch the subsequent loadend event. + if (aType == ProgressEventType::load || aType == ProgressEventType::error || + aType == ProgressEventType::timeout || + aType == ProgressEventType::abort) { + DispatchProgressEvent(aTarget, ProgressEventType::loadend, aLoaded, aTotal); + } +} + +void XMLHttpRequestMainThread::DispatchOrStoreEvent( + DOMEventTargetHelper* aTarget, Event* aEvent) { + MOZ_ASSERT(aTarget); + MOZ_ASSERT(aEvent); + + if (NS_FAILED(CheckCurrentGlobalCorrectness())) { + return; + } + + if (mEventDispatchingSuspended) { + PendingEvent* event = mPendingEvents.AppendElement(); + event->mTarget = aTarget; + event->mEvent = aEvent; + return; + } + + aTarget->DispatchEvent(*aEvent); +} + +void XMLHttpRequestMainThread::SuspendEventDispatching() { + MOZ_ASSERT(!mEventDispatchingSuspended); + mEventDispatchingSuspended = true; +} + +void XMLHttpRequestMainThread::ResumeEventDispatching() { + MOZ_ASSERT(mEventDispatchingSuspended); + mEventDispatchingSuspended = false; + + nsTArray pendingEvents = std::move(mPendingEvents); + + if (NS_FAILED(CheckCurrentGlobalCorrectness())) { + return; + } + + for (uint32_t i = 0; i < pendingEvents.Length(); ++i) { + pendingEvents[i].mTarget->DispatchEvent(*pendingEvents[i].mEvent); + } +} + +already_AddRefed +XMLHttpRequestMainThread::GetCurrentHttpChannel() { + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + return httpChannel.forget(); +} + +already_AddRefed +XMLHttpRequestMainThread::GetCurrentJARChannel() { + nsCOMPtr appChannel = do_QueryInterface(mChannel); + return appChannel.forget(); +} + +bool XMLHttpRequestMainThread::IsSystemXHR() const { + return mIsSystem || mPrincipal->IsSystemPrincipal(); +} + +bool XMLHttpRequestMainThread::InUploadPhase() const { + // We're in the upload phase while our state is OPENED. + return mState == XMLHttpRequest_Binding::OPENED; +} + +// This case is hit when the async parameter is outright omitted, which +// should set it to true (and the username and password to null). +void XMLHttpRequestMainThread::Open(const nsACString& aMethod, + const nsAString& aUrl, ErrorResult& aRv) { + Open(aMethod, aUrl, true, VoidString(), VoidString(), aRv); +} + +// This case is hit when the async parameter is specified, even if the +// JS value was "undefined" (which due to legacy reasons should be +// treated as true, which is how it will already be passed in here). +void XMLHttpRequestMainThread::Open(const nsACString& aMethod, + const nsAString& aUrl, bool aAsync, + const nsAString& aUsername, + const nsAString& aPassword, + ErrorResult& aRv) { + Open(aMethod, NS_ConvertUTF16toUTF8(aUrl), aAsync, aUsername, aPassword, aRv); +} + +void XMLHttpRequestMainThread::Open(const nsACString& aMethod, + const nsACString& aUrl, bool aAsync, + const nsAString& aUsername, + const nsAString& aPassword, + ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + // Gecko-specific + if (!aAsync && !DontWarnAboutSyncXHR() && GetOwner() && + GetOwner()->GetExtantDoc()) { + GetOwner()->GetExtantDoc()->WarnOnceAbout( + DeprecatedOperations::eSyncXMLHttpRequest); + } + + Telemetry::Accumulate(Telemetry::XMLHTTPREQUEST_ASYNC_OR_SYNC, + aAsync ? 0 : 1); + + // Step 1 + nsCOMPtr responsibleDocument = GetDocumentIfCurrent(); + if (!responsibleDocument) { + // This could be because we're no longer current or because we're in some + // non-window context... + if (NS_WARN_IF(NS_FAILED(CheckCurrentGlobalCorrectness()))) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT); + return; + } + } + if (!mPrincipal) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + // Gecko-specific + if (!aAsync && responsibleDocument && GetOwner()) { + // We have no extant document during unload, so the above general + // syncXHR warning will not display. But we do want to display a + // recommendation to use sendBeacon instead of syncXHR during unload. + nsCOMPtr shell = responsibleDocument->GetDocShell(); + if (shell) { + bool inUnload = false; + shell->GetIsInUnload(&inUnload); + if (inUnload) { + LogMessage("UseSendBeaconDuringUnloadAndPagehideWarning", GetOwner()); + } + } + } + + // Steps 2-4 + nsAutoCString method; + aRv = FetchUtil::GetValidRequestMethod(aMethod, method); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // Steps 5-6 + nsIURI* baseURI = nullptr; + if (mBaseURI) { + baseURI = mBaseURI; + } else if (responsibleDocument) { + baseURI = responsibleDocument->GetBaseURI(); + } + + // Use the responsible document's encoding for the URL if we have one, + // except for dedicated workers. Use UTF-8 otherwise. + NotNull originCharset = UTF_8_ENCODING; + if (responsibleDocument && + responsibleDocument->NodePrincipal() == mPrincipal) { + originCharset = responsibleDocument->GetDocumentCharacterSet(); + } + + nsCOMPtr parsedURL; + nsresult rv = + NS_NewURI(getter_AddRefs(parsedURL), aUrl, originCharset, baseURI); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_MALFORMED_URI) { + aRv.Throw(NS_ERROR_DOM_MALFORMED_URI); + return; + } + aRv.Throw(rv); + return; + } + if (NS_WARN_IF(NS_FAILED(CheckCurrentGlobalCorrectness()))) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT); + return; + } + + // Step 7 + // This is already handled by the other Open() method, which passes + // username and password in as NullStrings. + + // Step 8 + nsAutoCString host; + parsedURL->GetHost(host); + if (!host.IsEmpty() && (!aUsername.IsVoid() || !aPassword.IsVoid())) { + auto mutator = NS_MutateURI(parsedURL); + if (!aUsername.IsVoid()) { + mutator.SetUsername(NS_ConvertUTF16toUTF8(aUsername)); + } + if (!aPassword.IsVoid()) { + mutator.SetPassword(NS_ConvertUTF16toUTF8(aPassword)); + } + Unused << mutator.Finalize(parsedURL); + } + + // Step 9 + if (!aAsync && HasOrHasHadOwner() && + (mTimeoutMilliseconds || + mResponseType != XMLHttpRequestResponseType::_empty)) { + if (mTimeoutMilliseconds) { + LogMessage("TimeoutSyncXHRWarning", GetOwner()); + } + if (mResponseType != XMLHttpRequestResponseType::_empty) { + LogMessage("ResponseTypeSyncXHRWarning", GetOwner()); + } + aRv.ThrowInvalidAccessError( + "synchronous XMLHttpRequests do not support timeout and responseType"); + return; + } + + // Step 10 + TerminateOngoingFetch(); + + // Step 11 + // timeouts are handled without a flag + DisconnectDoneNotifier(); + mFlagSend = false; + mRequestMethod.Assign(method); + mRequestURL = parsedURL; + mFlagSynchronous = !aAsync; + mAuthorRequestHeaders.Clear(); + ResetResponse(); + + // Gecko-specific + mFlagHadUploadListenersOnSend = false; + mFlagAborted = false; + mFlagTimedOut = false; + mDecoder = nullptr; + + // Per spec we should only create the channel on send(), but we have internal + // code that relies on the channel being created now, and that code is not + // always IsSystemXHR(). However, we're not supposed to throw channel-creation + // errors during open(), so we silently ignore those here. + CreateChannel(); + + // Step 12 + if (mState != XMLHttpRequest_Binding::OPENED) { + mState = XMLHttpRequest_Binding::OPENED; + FireReadystatechangeEvent(); + } +} + +void XMLHttpRequestMainThread::SetOriginAttributes( + const OriginAttributesDictionary& aAttrs) { + MOZ_ASSERT((mState == XMLHttpRequest_Binding::OPENED) && !mFlagSend); + + OriginAttributes attrs(aAttrs); + + nsCOMPtr loadInfo = mChannel->LoadInfo(); + loadInfo->SetOriginAttributes(attrs); +} + +/* + * "Copy" from a stream. + */ +nsresult XMLHttpRequestMainThread::StreamReaderFunc( + nsIInputStream* in, void* closure, const char* fromRawSegment, + uint32_t toOffset, uint32_t count, uint32_t* writeCount) { + XMLHttpRequestMainThread* xmlHttpRequest = + static_cast(closure); + if (!xmlHttpRequest || !writeCount) { + NS_WARNING( + "XMLHttpRequest cannot read from stream: no closure or writeCount"); + return NS_ERROR_FAILURE; + } + + nsresult rv = NS_OK; + + if (xmlHttpRequest->mResponseType == XMLHttpRequestResponseType::Blob) { + xmlHttpRequest->MaybeCreateBlobStorage(); + rv = xmlHttpRequest->mBlobStorage->Append(fromRawSegment, count); + } else if (xmlHttpRequest->mResponseType == + XMLHttpRequestResponseType::Arraybuffer && + !xmlHttpRequest->mIsMappedArrayBuffer) { + // get the initial capacity to something reasonable to avoid a bunch of + // reallocs right at the start + if (xmlHttpRequest->mArrayBufferBuilder->Capacity() == 0) + xmlHttpRequest->mArrayBufferBuilder->SetCapacity( + std::max(count, XML_HTTP_REQUEST_ARRAYBUFFER_MIN_SIZE)); + + if (NS_WARN_IF(!xmlHttpRequest->mArrayBufferBuilder->Append( + reinterpret_cast(fromRawSegment), count, + XML_HTTP_REQUEST_ARRAYBUFFER_MAX_GROWTH))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + } else if (xmlHttpRequest->mResponseType == + XMLHttpRequestResponseType::_empty && + xmlHttpRequest->mResponseXML) { + // Copy for our own use + if (!xmlHttpRequest->mResponseBody.Append(fromRawSegment, count, + fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } else if (xmlHttpRequest->mResponseType == + XMLHttpRequestResponseType::_empty || + xmlHttpRequest->mResponseType == + XMLHttpRequestResponseType::Text || + xmlHttpRequest->mResponseType == + XMLHttpRequestResponseType::Json) { + MOZ_ASSERT(!xmlHttpRequest->mResponseXML, + "We shouldn't be parsing a doc here"); + rv = xmlHttpRequest->AppendToResponseText( + AsBytes(Span(fromRawSegment, count))); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (xmlHttpRequest->mFlagParseBody) { + // Give the same data to the parser. + + // We need to wrap the data in a new lightweight stream and pass that + // to the parser, because calling ReadSegments() recursively on the same + // stream is not supported. + nsCOMPtr copyStream; + rv = NS_NewByteInputStream(getter_AddRefs(copyStream), + Span(fromRawSegment, count), + NS_ASSIGNMENT_DEPEND); + + if (NS_SUCCEEDED(rv) && xmlHttpRequest->mXMLParserStreamListener) { + NS_ASSERTION(copyStream, "NS_NewByteInputStream lied"); + nsresult parsingResult = + xmlHttpRequest->mXMLParserStreamListener->OnDataAvailable( + xmlHttpRequest->mChannel, copyStream, toOffset, count); + + // No use to continue parsing if we failed here, but we + // should still finish reading the stream + if (NS_FAILED(parsingResult)) { + xmlHttpRequest->mFlagParseBody = false; + } + } + } + + if (NS_SUCCEEDED(rv)) { + *writeCount = count; + } else { + *writeCount = 0; + } + + return rv; +} + +namespace { + +void GetBlobURIFromChannel(nsIRequest* aRequest, nsIURI** aURI) { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aURI); + + *aURI = nullptr; + + nsCOMPtr channel = do_QueryInterface(aRequest); + if (!channel) { + return; + } + + nsCOMPtr uri; + nsresult rv = channel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return; + } + + if (!dom::IsBlobURI(uri)) { + return; + } + + uri.forget(aURI); +} + +nsresult GetLocalFileFromChannel(nsIRequest* aRequest, nsIFile** aFile) { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aFile); + + *aFile = nullptr; + + nsCOMPtr fc = do_QueryInterface(aRequest); + if (!fc) { + return NS_OK; + } + + nsCOMPtr file; + nsresult rv = fc->GetFile(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + file.forget(aFile); + return NS_OK; +} + +nsresult DummyStreamReaderFunc(nsIInputStream* aInputStream, void* aClosure, + const char* aFromRawSegment, uint32_t aToOffset, + uint32_t aCount, uint32_t* aWriteCount) { + *aWriteCount = aCount; + return NS_OK; +} + +class FileCreationHandler final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + static void Create(Promise* aPromise, XMLHttpRequestMainThread* aXHR) { + MOZ_ASSERT(aPromise); + + RefPtr handler = new FileCreationHandler(aXHR); + aPromise->AppendNativeHandler(handler); + } + + void ResolvedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override { + if (NS_WARN_IF(!aValue.isObject())) { + mXHR->LocalFileToBlobCompleted(nullptr); + return; + } + + RefPtr blob; + if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Blob, &aValue.toObject(), blob)))) { + mXHR->LocalFileToBlobCompleted(nullptr); + return; + } + + mXHR->LocalFileToBlobCompleted(blob->Impl()); + } + + void RejectedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override { + mXHR->LocalFileToBlobCompleted(nullptr); + } + + private: + explicit FileCreationHandler(XMLHttpRequestMainThread* aXHR) : mXHR(aXHR) { + MOZ_ASSERT(aXHR); + } + + ~FileCreationHandler() = default; + + RefPtr mXHR; +}; + +NS_IMPL_ISUPPORTS0(FileCreationHandler) + +} // namespace + +void XMLHttpRequestMainThread::LocalFileToBlobCompleted(BlobImpl* aBlobImpl) { + MOZ_ASSERT(mState != XMLHttpRequest_Binding::DONE); + + mResponseBlobImpl = aBlobImpl; + mBlobStorage = nullptr; + NS_ASSERTION(mResponseBody.IsEmpty(), "mResponseBody should be empty"); + + ChangeStateToDone(mFlagSyncLooping); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, + uint32_t count) { + NS_ENSURE_ARG_POINTER(inStr); + + mProgressSinceLastProgressEvent = true; + XMLHttpRequest_Binding::ClearCachedResponseTextValue(this); + + nsresult rv; + + if (mResponseType == XMLHttpRequestResponseType::Blob) { + nsCOMPtr localFile; + nsCOMPtr blobURI; + GetBlobURIFromChannel(request, getter_AddRefs(blobURI)); + if (blobURI) { + RefPtr blobImpl; + rv = NS_GetBlobForBlobURI(blobURI, getter_AddRefs(blobImpl)); + if (NS_SUCCEEDED(rv)) { + mResponseBlobImpl = blobImpl; + } + } else { + rv = GetLocalFileFromChannel(request, getter_AddRefs(localFile)); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mResponseBlobImpl || localFile) { + mBlobStorage = nullptr; + NS_ASSERTION(mResponseBody.IsEmpty(), "mResponseBody should be empty"); + + // The nsIStreamListener contract mandates us to read from the stream + // before returning. + uint32_t totalRead; + rv = inStr->ReadSegments(DummyStreamReaderFunc, nullptr, count, + &totalRead); + NS_ENSURE_SUCCESS(rv, rv); + + ChangeState(XMLHttpRequest_Binding::LOADING); + + // Cancel() must be called with an error. We use + // NS_ERROR_FILE_ALREADY_EXISTS to know that we've aborted the operation + // just because we can retrieve the File from the channel directly. + return request->Cancel(NS_ERROR_FILE_ALREADY_EXISTS); + } + } + + uint32_t totalRead; + rv = inStr->ReadSegments(XMLHttpRequestMainThread::StreamReaderFunc, + (void*)this, count, &totalRead); + NS_ENSURE_SUCCESS(rv, rv); + + // Fire the first progress event/loading state change + if (mState == XMLHttpRequest_Binding::HEADERS_RECEIVED) { + ChangeState(XMLHttpRequest_Binding::LOADING); + if (!mFlagSynchronous) { + DispatchProgressEvent(this, ProgressEventType::progress, mLoadTransferred, + mLoadTotal); + } + mProgressSinceLastProgressEvent = false; + } + + if (!mFlagSynchronous && !mProgressTimerIsActive) { + StartProgressEventTimer(); + } + + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::OnStartRequest(nsIRequest* request) { + AUTO_PROFILER_LABEL("XMLHttpRequestMainThread::OnStartRequest", NETWORK); + + nsresult rv = NS_OK; + + if (request != mChannel) { + // Can this still happen? + return NS_OK; + } + + // Don't do anything if we have been aborted + if (mState == XMLHttpRequest_Binding::UNSENT) { + return NS_OK; + } + + // Don't do anything if we're in mid-abort, but let the request + // know (this can happen due to race conditions in valid XHRs, + // see bz1070763 for info). + if (mFlagAborted) { + return NS_BINDING_ABORTED; + } + + // Don't do anything if we have timed out. + if (mFlagTimedOut) { + return NS_OK; + } + + nsCOMPtr channel(do_QueryInterface(request)); + NS_ENSURE_TRUE(channel, NS_ERROR_UNEXPECTED); + + nsresult status; + request->GetStatus(&status); + if (mErrorLoad == ErrorType::eOK && NS_FAILED(status)) { + mErrorLoad = ErrorType::eRequest; + } + + // Upload phase is now over. If we were uploading anything, + // stop the timer and fire any final progress events. + if (mUpload && !mUploadComplete && mErrorLoad == ErrorType::eOK && + !mFlagSynchronous) { + StopProgressEventTimer(); + + mUploadTransferred = mUploadTotal; + + if (mProgressSinceLastProgressEvent) { + DispatchProgressEvent(mUpload, ProgressEventType::progress, + mUploadTransferred, mUploadTotal); + mProgressSinceLastProgressEvent = false; + } + + mUploadComplete = true; + DispatchProgressEvent(mUpload, ProgressEventType::load, mUploadTotal, + mUploadTotal); + } + + mFlagParseBody = true; + if (mErrorLoad == ErrorType::eOK) { + ChangeState(XMLHttpRequest_Binding::HEADERS_RECEIVED); + } + + ResetResponse(); + + if (!mOverrideMimeType.IsEmpty()) { + channel->SetContentType(NS_ConvertUTF16toUTF8(mOverrideMimeType)); + } + + // Fallback to 'application/octet-stream' + nsAutoCString type; + channel->GetContentType(type); + if (type.IsEmpty() || type.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) { + channel->SetContentType(nsLiteralCString(APPLICATION_OCTET_STREAM)); + } + + DetectCharset(); + + // Set up arraybuffer + if (mResponseType == XMLHttpRequestResponseType::Arraybuffer && + NS_SUCCEEDED(status)) { + if (mIsMappedArrayBuffer) { + nsCOMPtr jarChannel = do_QueryInterface(channel); + if (jarChannel) { + nsCOMPtr uri; + rv = channel->GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString file; + nsAutoCString scheme; + uri->GetScheme(scheme); + if (scheme.LowerCaseEqualsLiteral("jar")) { + nsCOMPtr jarURI = do_QueryInterface(uri); + if (jarURI) { + jarURI->GetJAREntry(file); + } + } + nsCOMPtr jarFile; + jarChannel->GetJarFile(getter_AddRefs(jarFile)); + if (!jarFile) { + mIsMappedArrayBuffer = false; + } else { + rv = mArrayBufferBuilder->MapToFileInPackage(file, jarFile); + // This can happen legitimately if there are compressed files + // in the jarFile. See bug #1357219. No need to warn on the error. + if (NS_FAILED(rv)) { + mIsMappedArrayBuffer = false; + } else { + channel->SetContentType("application/mem-mapped"_ns); + } + } + } + } + } + // If memory mapping failed, mIsMappedArrayBuffer would be set to false, + // and we want it fallback to the malloc way. + if (!mIsMappedArrayBuffer) { + int64_t contentLength; + rv = channel->GetContentLength(&contentLength); + if (NS_SUCCEEDED(rv) && contentLength > 0 && + contentLength < XML_HTTP_REQUEST_MAX_CONTENT_LENGTH_PREALLOCATE) { + mArrayBufferBuilder->SetCapacity(static_cast(contentLength)); + } + } + } + + // Set up responseXML + // Note: Main Fetch step 18 requires to ignore body for head/connect methods. + bool parseBody = (mResponseType == XMLHttpRequestResponseType::_empty || + mResponseType == XMLHttpRequestResponseType::Document) && + !(mRequestMethod.EqualsLiteral("HEAD") || + mRequestMethod.EqualsLiteral("CONNECT")); + + if (parseBody) { + // Do not try to parse documents if content-length = 0 + int64_t contentLength; + if (NS_SUCCEEDED(mChannel->GetContentLength(&contentLength)) && + contentLength == 0) { + parseBody = false; + } + } + + mIsHtml = false; + mWarnAboutSyncHtml = false; + if (parseBody && NS_SUCCEEDED(status)) { + // We can gain a huge performance win by not even trying to + // parse non-XML data. This also protects us from the situation + // where we have an XML document and sink, but HTML (or other) + // parser, which can produce unreliable results. + nsAutoCString type; + channel->GetContentType(type); + + if ((mResponseType == XMLHttpRequestResponseType::Document) && + type.EqualsLiteral("text/html")) { + // HTML parsing is only supported for responseType == "document" to + // avoid running the parser and, worse, populating responseXML for + // legacy users of XHR who use responseType == "" for retrieving the + // responseText of text/html resources. This legacy case is so common + // that it's not useful to emit a warning about it. + if (mFlagSynchronous) { + // We don't make cool new features available in the bad synchronous + // mode. The synchronous mode is for legacy only. + mWarnAboutSyncHtml = true; + mFlagParseBody = false; + } else { + mIsHtml = true; + } + } else if (!(type.EqualsLiteral("text/xml") || + type.EqualsLiteral("application/xml") || + StringEndsWith(type, "+xml"_ns))) { + // Follow https://xhr.spec.whatwg.org/ + // If final MIME type is not null, text/html, text/xml, application/xml, + // or does not end in +xml, return null. + mFlagParseBody = false; + } + } else { + // The request failed, so we shouldn't be parsing anyway + mFlagParseBody = false; + } + + if (mFlagParseBody) { + nsCOMPtr baseURI, docURI; + rv = mChannel->GetURI(getter_AddRefs(docURI)); + NS_ENSURE_SUCCESS(rv, rv); + baseURI = docURI; + + nsCOMPtr doc = GetDocumentIfCurrent(); + nsCOMPtr chromeXHRDocURI, chromeXHRDocBaseURI; + if (doc) { + chromeXHRDocURI = doc->GetDocumentURI(); + chromeXHRDocBaseURI = doc->GetBaseURI(); + } else { + // If we're no longer current, just kill the load, though it really should + // have been killed already. + if (NS_WARN_IF(NS_FAILED(CheckCurrentGlobalCorrectness()))) { + return NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT; + } + } + + // Create an empty document from it. + const auto& emptyStr = u""_ns; + nsIGlobalObject* global = DOMEventTargetHelper::GetParentObject(); + + nsCOMPtr requestingPrincipal; + rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + channel, getter_AddRefs(requestingPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewDOMDocument( + getter_AddRefs(mResponseXML), emptyStr, emptyStr, nullptr, docURI, + baseURI, requestingPrincipal, true, global, + mIsHtml ? DocumentFlavorHTML : DocumentFlavorLegacyGuess); + NS_ENSURE_SUCCESS(rv, rv); + mResponseXML->SetChromeXHRDocURI(chromeXHRDocURI); + mResponseXML->SetChromeXHRDocBaseURI(chromeXHRDocBaseURI); + + // suppress parsing failure messages to console for statuses which + // can have empty bodies (see bug 884693). + IgnoredErrorResult rv2; + uint32_t responseStatus = GetStatus(rv2); + if (!rv2.Failed() && (responseStatus == 201 || responseStatus == 202 || + responseStatus == 204 || responseStatus == 205 || + responseStatus == 304)) { + mResponseXML->SetSuppressParserErrorConsoleMessages(true); + } + + nsCOMPtr loadInfo = mChannel->LoadInfo(); + bool isCrossSite = false; + isCrossSite = loadInfo->GetTainting() != LoadTainting::Basic; + + if (isCrossSite) { + mResponseXML->DisableCookieAccess(); + } + + nsCOMPtr listener; + nsCOMPtr loadGroup; + channel->GetLoadGroup(getter_AddRefs(loadGroup)); + + // suppress nodes on XML document parse failure, but only + // for non-privileged code (including Web Extensions). See bug 289714. + if (!IsSystemXHR()) { + mResponseXML->SetSuppressParserErrorElement(true); + } + + rv = mResponseXML->StartDocumentLoad(kLoadAsData, channel, loadGroup, + nullptr, getter_AddRefs(listener), + !isCrossSite); + NS_ENSURE_SUCCESS(rv, rv); + + // the spec requires the response document.referrer to be the empty string + nsCOMPtr referrerInfo = + new ReferrerInfo(nullptr, mResponseXML->ReferrerPolicy()); + mResponseXML->SetReferrerInfo(referrerInfo); + + mXMLParserStreamListener = listener; + rv = mXMLParserStreamListener->OnStartRequest(request); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Download phase beginning; start the progress event timer if necessary. + if (NS_SUCCEEDED(rv) && HasListenersFor(nsGkAtoms::onprogress)) { + StartProgressEventTimer(); + } + + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::OnStopRequest(nsIRequest* request, nsresult status) { + AUTO_PROFILER_LABEL("XMLHttpRequestMainThread::OnStopRequest", NETWORK); + + if (request != mChannel) { + // Can this still happen? + return NS_OK; + } + + // Send the decoder the signal that we've hit the end of the stream, + // but only when decoding text eagerly. + if (mDecoder && ((mResponseType == XMLHttpRequestResponseType::Text) || + (mResponseType == XMLHttpRequestResponseType::Json) || + (mResponseType == XMLHttpRequestResponseType::_empty && + !mResponseXML))) { + AppendToResponseText(Span(), true); + } + + mWaitingForOnStopRequest = false; + + // make sure to notify the listener if we were aborted + // XXX in fact, why don't we do the cleanup below in this case?? + // UNSENT is for abort calls. See OnStartRequest above. + if (mState == XMLHttpRequest_Binding::UNSENT || mFlagTimedOut) { + if (mXMLParserStreamListener) + (void)mXMLParserStreamListener->OnStopRequest(request, status); + return NS_OK; + } + + // Is this good enough here? + if (mXMLParserStreamListener && mFlagParseBody) { + mXMLParserStreamListener->OnStopRequest(request, status); + } + + mXMLParserStreamListener = nullptr; + mContext = nullptr; + + // If window.stop() or other aborts were issued, handle as an abort + if (status == NS_BINDING_ABORTED) { + mFlagParseBody = false; + IgnoredErrorResult rv; + RequestErrorSteps(ProgressEventType::abort, NS_OK, rv); + ChangeState(XMLHttpRequest_Binding::UNSENT, false); + return NS_OK; + } + + // If we were just reading a blob URL, we're already done + if (status == NS_ERROR_FILE_ALREADY_EXISTS && mResponseBlobImpl) { + ChangeStateToDone(mFlagSyncLooping); + return NS_OK; + } + + bool waitingForBlobCreation = false; + + // If we have this error, we have to deal with a file: URL + responseType = + // blob. We have this error because we canceled the channel. The status will + // be set to NS_OK. + if (!mResponseBlobImpl && status == NS_ERROR_FILE_ALREADY_EXISTS && + mResponseType == XMLHttpRequestResponseType::Blob) { + nsCOMPtr file; + nsresult rv = GetLocalFileFromChannel(request, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (file) { + nsAutoCString contentType; + rv = mChannel->GetContentType(contentType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ChromeFilePropertyBag bag; + CopyUTF8toUTF16(contentType, bag.mType); + + nsCOMPtr global = GetOwnerGlobal(); + + ErrorResult error; + RefPtr promise = + FileCreatorHelper::CreateFile(global, file, bag, true, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + FileCreationHandler::Create(promise, this); + waitingForBlobCreation = true; + status = NS_OK; + + NS_ASSERTION(mResponseBody.IsEmpty(), "mResponseBody should be empty"); + NS_ASSERTION(mResponseText.IsEmpty(), "mResponseText should be empty"); + } + } + + if (NS_SUCCEEDED(status) && + mResponseType == XMLHttpRequestResponseType::Blob && + !waitingForBlobCreation) { + // Smaller files may be written in cache map instead of separate files. + // Also, no-store response cannot be written in persistent cache. + nsAutoCString contentType; + if (!mOverrideMimeType.IsEmpty()) { + contentType.Assign(NS_ConvertUTF16toUTF8(mOverrideMimeType)); + } else { + mChannel->GetContentType(contentType); + } + + // mBlobStorage can be null if the channel is non-file non-cacheable + // and if the response length is zero. + MaybeCreateBlobStorage(); + mBlobStorage->GetBlobImplWhenReady(contentType, this); + waitingForBlobCreation = true; + + NS_ASSERTION(mResponseBody.IsEmpty(), "mResponseBody should be empty"); + NS_ASSERTION(mResponseText.IsEmpty(), "mResponseText should be empty"); + } else if (NS_SUCCEEDED(status) && !mIsMappedArrayBuffer && + mResponseType == XMLHttpRequestResponseType::Arraybuffer) { + // set the capacity down to the actual length, to realloc back + // down to the actual size + if (!mArrayBufferBuilder->SetCapacity(mArrayBufferBuilder->Length())) { + // this should never happen! + status = NS_ERROR_UNEXPECTED; + } + } + + nsCOMPtr channel(do_QueryInterface(request)); + NS_ENSURE_TRUE(channel, NS_ERROR_UNEXPECTED); + + channel->SetNotificationCallbacks(nullptr); + mNotificationCallbacks = nullptr; + mChannelEventSink = nullptr; + mProgressEventSink = nullptr; + + bool wasSync = mFlagSyncLooping; + mFlagSyncLooping = false; + mRequestSentTime = 0; + + // update our charset and decoder to match mResponseXML, + // before it is possibly nulled out + MatchCharsetAndDecoderToResponseDocument(); + + if (NS_FAILED(status)) { + // This can happen if the server is unreachable. Other possible + // reasons are that the user leaves the page or hits the ESC key. + + mErrorLoad = ErrorType::eUnreachable; + mResponseXML = nullptr; + } + + // If we're uninitialized at this point, we encountered an error + // earlier and listeners have already been notified. Also we do + // not want to do this if we already completed. + if (mState == XMLHttpRequest_Binding::UNSENT || + mState == XMLHttpRequest_Binding::DONE) { + return NS_OK; + } + + if (!mResponseXML) { + mFlagParseBody = false; + + // We postpone the 'done' until the creation of the Blob is completed. + if (!waitingForBlobCreation) { + ChangeStateToDone(wasSync); + } + + return NS_OK; + } + + if (mIsHtml) { + NS_ASSERTION(!mFlagSyncLooping, + "We weren't supposed to support HTML parsing with XHR!"); + mParseEndListener = new nsXHRParseEndListener(this); + RefPtr eventTarget = mResponseXML; + EventListenerManager* manager = eventTarget->GetOrCreateListenerManager(); + manager->AddEventListenerByType(mParseEndListener, + kLiteralString_DOMContentLoaded, + TrustedEventsAtSystemGroupBubble()); + return NS_OK; + } else { + mFlagParseBody = false; + } + + // We might have been sent non-XML data. If that was the case, + // we should null out the document member. The idea in this + // check here is that if there is no document element it is not + // an XML document. We might need a fancier check... + if (!mResponseXML->GetRootElement()) { + mErrorParsingXML = true; + mResponseXML = nullptr; + } + ChangeStateToDone(wasSync); + return NS_OK; +} + +void XMLHttpRequestMainThread::OnBodyParseEnd() { + mFlagParseBody = false; + mParseEndListener = nullptr; + ChangeStateToDone(mFlagSyncLooping); +} + +void XMLHttpRequestMainThread::MatchCharsetAndDecoderToResponseDocument() { + if (mResponseXML && + (!mDecoder || + mDecoder->Encoding() != mResponseXML->GetDocumentCharacterSet())) { + TruncateResponseText(); + mResponseBodyDecodedPos = 0; + mEofDecoded = false; + mDecoder = mResponseXML->GetDocumentCharacterSet()->NewDecoder(); + } +} +void XMLHttpRequestMainThread::DisconnectDoneNotifier() { + if (mDelayedDoneNotifier) { + // Disconnect may release the last reference to 'this'. + RefPtr kungfuDeathGrip = this; + mDelayedDoneNotifier->Disconnect(); + mDelayedDoneNotifier = nullptr; + } +} + +void XMLHttpRequestMainThread::ChangeStateToDone(bool aWasSync) { + DisconnectDoneNotifier(); + + if (!mForWorker && !aWasSync && mChannel) { + // If the top level page is loading, try to postpone the handling of the + // final events. + nsLoadFlags loadFlags = 0; + mChannel->GetLoadFlags(&loadFlags); + if (loadFlags & nsIRequest::LOAD_BACKGROUND) { + nsPIDOMWindowInner* owner = GetOwner(); + BrowsingContext* bc = owner ? owner->GetBrowsingContext() : nullptr; + bc = bc ? bc->Top() : nullptr; + if (bc && bc->IsLoading()) { + MOZ_ASSERT(!mDelayedDoneNotifier); + RefPtr notifier = + new XMLHttpRequestDoneNotifier(this); + mDelayedDoneNotifier = notifier; + bc->AddDeprioritizedLoadRunner(notifier); + return; + } + } + } + + ChangeStateToDoneInternal(); +} + +void XMLHttpRequestMainThread::ChangeStateToDoneInternal() { + DisconnectDoneNotifier(); + StopProgressEventTimer(); + + MOZ_ASSERT(!mFlagParseBody, + "ChangeStateToDone() called before async HTML parsing is done."); + + mFlagSend = false; + + if (mTimeoutTimer) { + mTimeoutTimer->Cancel(); + } + + // Per spec, fire the last download progress event, if any, + // before readystatechange=4/done. (Note that 0-sized responses + // will have not sent a progress event yet, so one must be sent here). + if (!mFlagSynchronous && + (!mLoadTransferred || mProgressSinceLastProgressEvent)) { + DispatchProgressEvent(this, ProgressEventType::progress, mLoadTransferred, + mLoadTotal); + mProgressSinceLastProgressEvent = false; + } + + // Notify the document when an XHR request completes successfully. + // This is used by the password manager as a hint to observe DOM mutations. + // Call this prior to changing state to DONE to ensure we set up the + // observer before mutations occur. + if (mErrorLoad == ErrorType::eOK) { + Document* doc = GetDocumentIfCurrent(); + if (doc) { + doc->NotifyFetchOrXHRSuccess(); + } + } + + // Per spec, fire readystatechange=4/done before final error events. + ChangeState(XMLHttpRequest_Binding::DONE, true); + + // Per spec, if we failed in the upload phase, fire a final error + // and loadend events for the upload after readystatechange=4/done. + if (!mFlagSynchronous && mUpload && !mUploadComplete) { + DispatchProgressEvent(mUpload, ProgressEventType::error, 0, -1); + } + + // Per spec, fire download's load/error and loadend events after + // readystatechange=4/done (and of course all upload events). + if (mErrorLoad != ErrorType::eOK) { + DispatchProgressEvent(this, ProgressEventType::error, 0, -1); + } else { + DispatchProgressEvent(this, ProgressEventType::load, mLoadTransferred, + mLoadTotal); + } + + if (mErrorLoad != ErrorType::eOK) { + // By nulling out channel here we make it so that Send() can test + // for that and throw. Also calling the various status + // methods/members will not throw. + // This matches what IE does. + mChannel = nullptr; + } +} + +nsresult XMLHttpRequestMainThread::CreateChannel() { + // When we are called from JS we can find the load group for the page, + // and add ourselves to it. This way any pending requests + // will be automatically aborted if the user leaves the page. + nsCOMPtr loadGroup = GetLoadGroup(); + + nsSecurityFlags secFlags; + nsLoadFlags loadFlags = nsIRequest::LOAD_BACKGROUND; + uint32_t sandboxFlags = 0; + if (mPrincipal->IsSystemPrincipal()) { + // When chrome is loading we want to make sure to sandbox any potential + // result document. We also want to allow cross-origin loads. + secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + sandboxFlags = SANDBOXED_ORIGIN; + } else if (IsSystemXHR()) { + // For pages that have appropriate permissions, we want to still allow + // cross-origin loads, but make sure that the any potential result + // documents get the same principal as the loader. + secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT | + nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL; + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + } else { + // Otherwise use CORS. Again, make sure that potential result documents + // use the same principal as the loader. + secFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT | + nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL; + } + + if (mIsAnon) { + secFlags |= nsILoadInfo::SEC_COOKIES_OMIT; + } + + // Use the responsibleDocument if we have it, except for dedicated workers + // where it will be the parent document, which is not the one we want to use. + nsresult rv; + nsCOMPtr responsibleDocument = GetDocumentIfCurrent(); + if (responsibleDocument && + responsibleDocument->NodePrincipal() == mPrincipal) { + rv = NS_NewChannel(getter_AddRefs(mChannel), mRequestURL, + responsibleDocument, secFlags, + nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST, + nullptr, // aPerformanceStorage + loadGroup, + nullptr, // aCallbacks + loadFlags, nullptr, sandboxFlags); + } else if (mClientInfo.isSome()) { + rv = NS_NewChannel(getter_AddRefs(mChannel), mRequestURL, mPrincipal, + mClientInfo.ref(), mController, secFlags, + nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST, + mCookieJarSettings, + mPerformanceStorage, // aPerformanceStorage + loadGroup, + nullptr, // aCallbacks + loadFlags, nullptr, sandboxFlags); + } else { + // Otherwise use the principal. + rv = NS_NewChannel(getter_AddRefs(mChannel), mRequestURL, mPrincipal, + secFlags, nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST, + mCookieJarSettings, + mPerformanceStorage, // aPerformanceStorage + loadGroup, + nullptr, // aCallbacks + loadFlags, nullptr, sandboxFlags); + } + NS_ENSURE_SUCCESS(rv, rv); + + if (mCSPEventListener) { + nsCOMPtr loadInfo = mChannel->LoadInfo(); + rv = loadInfo->SetCspEventListener(mCSPEventListener); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr httpChannel(do_QueryInterface(mChannel)); + if (httpChannel) { + rv = httpChannel->SetRequestMethod(mRequestMethod); + NS_ENSURE_SUCCESS(rv, rv); + + httpChannel->SetSource(profiler_capture_backtrace()); + + // Set the initiator type + nsCOMPtr timedChannel(do_QueryInterface(httpChannel)); + if (timedChannel) { + timedChannel->SetInitiatorType(u"xmlhttprequest"_ns); + } + } + + return NS_OK; +} + +void XMLHttpRequestMainThread::MaybeLowerChannelPriority() { + nsCOMPtr doc = GetDocumentIfCurrent(); + if (!doc) { + return; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(GetOwnerGlobal())) { + return; + } + + JSContext* cx = jsapi.cx(); + + if (!doc->IsScriptTracking(cx)) { + return; + } + + if (StaticPrefs::network_http_tailing_enabled()) { + nsCOMPtr cos = do_QueryInterface(mChannel); + if (cos) { + // Adding TailAllowed to overrule the Unblocked flag, but to preserve + // the effect of Unblocked when tailing is off. + cos->AddClassFlags(nsIClassOfService::Throttleable | + nsIClassOfService::Tail | + nsIClassOfService::TailAllowed); + } + } + + nsCOMPtr p = do_QueryInterface(mChannel); + if (p) { + p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST); + } +} + +nsresult XMLHttpRequestMainThread::InitiateFetch( + already_AddRefed aUploadStream, int64_t aUploadLength, + nsACString& aUploadContentType) { + nsresult rv; + nsCOMPtr uploadStream = std::move(aUploadStream); + + if (!uploadStream) { + RefPtr preload = FindPreload(); + if (preload) { + // Because of bug 682305, we can't let listener be the XHR object itself + // because JS wouldn't be able to use it. So create a listener around + // 'this'. Make sure to hold a strong reference so that we don't leak the + // wrapper. + nsCOMPtr listener = + new net::nsStreamListenerWrapper(this); + rv = preload->AsyncConsume(listener); + if (NS_SUCCEEDED(rv)) { + mFromPreload = true; + + // May be null when the preload has already finished, but the XHR code + // is safe to live with it. + mChannel = preload->Channel(); + MOZ_ASSERT(mChannel); + EnsureChannelContentType(); + return NS_OK; + } + + preload = nullptr; + } + } + + // nsIRequest::LOAD_BACKGROUND prevents throbber from becoming active, which + // in turn keeps STOP button from becoming active. If the consumer passed in + // a progress event handler we must load with nsIRequest::LOAD_NORMAL or + // necko won't generate any progress notifications. + if (HasListenersFor(nsGkAtoms::onprogress) || + (mUpload && mUpload->HasListenersFor(nsGkAtoms::onprogress))) { + nsLoadFlags loadFlags; + mChannel->GetLoadFlags(&loadFlags); + loadFlags &= ~nsIRequest::LOAD_BACKGROUND; + loadFlags |= nsIRequest::LOAD_NORMAL; + mChannel->SetLoadFlags(loadFlags); + } + + nsCOMPtr httpChannel(do_QueryInterface(mChannel)); + if (httpChannel) { + // If the user hasn't overridden the Accept header, set it to */* per spec. + if (!mAuthorRequestHeaders.Has("accept")) { + mAuthorRequestHeaders.Set("accept", "*/*"_ns); + } + + mAuthorRequestHeaders.ApplyToChannel(httpChannel, false, false); + + if (!IsSystemXHR()) { + nsCOMPtr owner = GetOwner(); + nsCOMPtr doc = owner ? owner->GetExtantDoc() : nullptr; + nsCOMPtr referrerInfo = + ReferrerInfo::CreateForFetch(mPrincipal, doc); + Unused << httpChannel->SetReferrerInfoWithoutClone(referrerInfo); + } + + // Some extensions override the http protocol handler and provide their own + // implementation. The channels returned from that implementation don't + // always seem to implement the nsIUploadChannel2 interface, presumably + // because it's a new interface. Eventually we should remove this and simply + // require that http channels implement the new interface (see bug 529041). + nsCOMPtr uploadChannel2 = do_QueryInterface(httpChannel); + if (!uploadChannel2) { + nsCOMPtr consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService) { + consoleService->LogStringMessage( + u"Http channel implementation doesn't support nsIUploadChannel2. " + "An extension has supplied a non-functional http protocol handler. " + "This will break behavior and in future releases not work at all."); + } + } + + if (uploadStream) { + // If necessary, wrap the stream in a buffered stream so as to guarantee + // support for our upload when calling ExplicitSetUploadStream. + if (!NS_InputStreamIsBuffered(uploadStream)) { + nsCOMPtr bufferedStream; + rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), + uploadStream.forget(), 4096); + NS_ENSURE_SUCCESS(rv, rv); + + uploadStream = bufferedStream; + } + + // We want to use a newer version of the upload channel that won't + // ignore the necessary headers for an empty Content-Type. + nsCOMPtr uploadChannel2( + do_QueryInterface(httpChannel)); + // This assertion will fire if buggy extensions are installed + NS_ASSERTION(uploadChannel2, "http must support nsIUploadChannel2"); + if (uploadChannel2) { + uploadChannel2->ExplicitSetUploadStream( + uploadStream, aUploadContentType, mUploadTotal, mRequestMethod, + false); + } else { + // The http channel doesn't support the new nsIUploadChannel2. + // Emulate it as best we can using nsIUploadChannel. + if (aUploadContentType.IsEmpty()) { + aUploadContentType.AssignLiteral("application/octet-stream"); + } + nsCOMPtr uploadChannel = + do_QueryInterface(httpChannel); + uploadChannel->SetUploadStream(uploadStream, aUploadContentType, + mUploadTotal); + // Reset the method to its original value + rv = httpChannel->SetRequestMethod(mRequestMethod); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + } + + // Due to the chrome-only XHR.channel API, we need a hacky way to set the + // SEC_COOKIES_INCLUDE *after* the channel has been has been created, since + // .withCredentials can be called after open() is called. + // Not doing this for privileged system XHRs since those don't use CORS. + if (!IsSystemXHR() && !mIsAnon && mFlagACwithCredentials) { + nsCOMPtr loadInfo = mChannel->LoadInfo(); + static_cast(loadInfo.get())->SetIncludeCookiesSecFlag(); + } + + // We never let XHR be blocked by head CSS/JS loads to avoid potential + // deadlock where server generation of CSS/JS requires an XHR signal. + nsCOMPtr cos(do_QueryInterface(mChannel)); + if (cos) { + cos->AddClassFlags(nsIClassOfService::Unblocked); + + // Mark channel as urgent-start if the XHR is triggered by user input + // events. + if (UserActivation::IsHandlingUserInput()) { + cos->AddClassFlags(nsIClassOfService::UrgentStart); + } + } + + // Disable Necko-internal response timeouts. + nsCOMPtr internalHttpChannel( + do_QueryInterface(mChannel)); + if (internalHttpChannel) { + rv = internalHttpChannel->SetResponseTimeoutEnabled(false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (!mIsAnon) { + AddLoadFlags(mChannel, nsIChannel::LOAD_EXPLICIT_CREDENTIALS); + } + + // Bypass the network cache in cases where it makes no sense: + // POST responses are always unique, and we provide no API that would + // allow our consumers to specify a "cache key" to access old POST + // responses, so they are not worth caching. + if (mRequestMethod.EqualsLiteral("POST")) { + AddLoadFlags(mChannel, nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE | + nsIRequest::INHIBIT_CACHING); + } else { + // When we are sync loading, we need to bypass the local cache when it would + // otherwise block us waiting for exclusive access to the cache. If we + // don't do this, then we could dead lock in some cases (see bug 309424). + // + // Also don't block on the cache entry on async if it is busy - favoring + // parallelism over cache hit rate for xhr. This does not disable the cache + // everywhere - only in cases where more than one channel for the same URI + // is accessed simultanously. + AddLoadFlags(mChannel, nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY); + } + + EnsureChannelContentType(); + + // Set up the preflight if needed + if (!IsSystemXHR()) { + nsTArray CORSUnsafeHeaders; + mAuthorRequestHeaders.GetCORSUnsafeHeaders(CORSUnsafeHeaders); + nsCOMPtr loadInfo = mChannel->LoadInfo(); + loadInfo->SetCorsPreflightInfo(CORSUnsafeHeaders, + mFlagHadUploadListenersOnSend); + } + + // Hook us up to listen to redirects and the like. Only do this very late + // since this creates a cycle between the channel and us. This cycle has + // to be manually broken if anything below fails. + mChannel->GetNotificationCallbacks(getter_AddRefs(mNotificationCallbacks)); + mChannel->SetNotificationCallbacks(this); + + if (internalHttpChannel) { + internalHttpChannel->SetBlockAuthPrompt(ShouldBlockAuthPrompt()); + } + + // Because of bug 682305, we can't let listener be the XHR object itself + // because JS wouldn't be able to use it. So create a listener around 'this'. + // Make sure to hold a strong reference so that we don't leak the wrapper. + nsCOMPtr listener = new net::nsStreamListenerWrapper(this); + + // Check if this XHR is created from a tracking script. + // If yes, lower the channel's priority. + if (StaticPrefs::privacy_trackingprotection_lower_network_priority()) { + MaybeLowerChannelPriority(); + } + + // Associate any originating stack with the channel. + NotifyNetworkMonitorAlternateStack(mChannel, std::move(mOriginStack)); + + // Start reading from the channel + rv = mChannel->AsyncOpen(listener); + listener = nullptr; + if (NS_WARN_IF(NS_FAILED(rv))) { + // Drop our ref to the channel to avoid cycles. Also drop channel's + // ref to us to be extra safe. + mChannel->SetNotificationCallbacks(mNotificationCallbacks); + mChannel = nullptr; + + mErrorLoad = ErrorType::eChannelOpen; + + // Per spec, we throw on sync errors, but not async. + if (mFlagSynchronous) { + mState = XMLHttpRequest_Binding::DONE; + return NS_ERROR_DOM_NETWORK_ERR; + } + } + + return NS_OK; +} + +already_AddRefed XMLHttpRequestMainThread::FindPreload() { + Document* doc = GetDocumentIfCurrent(); + if (!doc) { + return nullptr; + } + if (mPrincipal->IsSystemPrincipal() || IsSystemXHR()) { + return nullptr; + } + if (!mRequestMethod.EqualsLiteral("GET")) { + // Preload can only do GET. + return nullptr; + } + if (!mAuthorRequestHeaders.IsEmpty()) { + // Preload can't set headers. + return nullptr; + } + + // mIsAnon overrules mFlagACwithCredentials. + CORSMode cors = (mIsAnon || !mFlagACwithCredentials) + ? CORSMode::CORS_ANONYMOUS + : CORSMode::CORS_USE_CREDENTIALS; + nsCOMPtr referrerInfo = + ReferrerInfo::CreateForFetch(mPrincipal, doc); + auto key = PreloadHashKey::CreateAsFetch(mRequestURL, cors); + RefPtr preload = doc->Preloads().LookupPreload(key); + if (!preload) { + return nullptr; + } + + preload->RemoveSelf(doc); + preload->NotifyUsage(doc, PreloaderBase::LoadBackground::Keep); + + return preload.forget(); +} + +void XMLHttpRequestMainThread::EnsureChannelContentType() { + MOZ_ASSERT(mChannel); + + // Since we expect XML data, set the type hint accordingly + // if the channel doesn't know any content type. + // This means that we always try to parse local files as XML + // ignoring return value, as this is not critical. Use text/xml as fallback + // MIME type. + nsAutoCString contentType; + if (NS_FAILED(mChannel->GetContentType(contentType)) || + contentType.IsEmpty() || + contentType.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) { + mChannel->SetContentType("text/xml"_ns); + } +} + +void XMLHttpRequestMainThread::ResumeTimeout() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mFlagSynchronous); + + if (mResumeTimeoutRunnable) { + DispatchToMainThread(mResumeTimeoutRunnable.forget()); + mResumeTimeoutRunnable = nullptr; + } +} + +void XMLHttpRequestMainThread::Send( + const Nullable< + DocumentOrBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString>& + aData, + ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + if (!CanSend(aRv)) { + return; + } + + if (aData.IsNull()) { + SendInternal(nullptr, false, aRv); + return; + } + + if (aData.Value().IsDocument()) { + BodyExtractor body(&aData.Value().GetAsDocument()); + SendInternal(&body, true, aRv); + return; + } + + if (aData.Value().IsBlob()) { + BodyExtractor body(&aData.Value().GetAsBlob()); + SendInternal(&body, false, aRv); + return; + } + + if (aData.Value().IsArrayBuffer()) { + BodyExtractor body(&aData.Value().GetAsArrayBuffer()); + SendInternal(&body, false, aRv); + return; + } + + if (aData.Value().IsArrayBufferView()) { + BodyExtractor body( + &aData.Value().GetAsArrayBufferView()); + SendInternal(&body, false, aRv); + return; + } + + if (aData.Value().IsFormData()) { + BodyExtractor body(&aData.Value().GetAsFormData()); + SendInternal(&body, false, aRv); + return; + } + + if (aData.Value().IsURLSearchParams()) { + BodyExtractor body( + &aData.Value().GetAsURLSearchParams()); + SendInternal(&body, false, aRv); + return; + } + + if (aData.Value().IsUSVString()) { + BodyExtractor body(&aData.Value().GetAsUSVString()); + SendInternal(&body, true, aRv); + return; + } +} + +nsresult XMLHttpRequestMainThread::MaybeSilentSendFailure(nsresult aRv) { + // Per spec, silently fail on async request failures; throw for sync. + if (mFlagSynchronous) { + mState = XMLHttpRequest_Binding::DONE; + return NS_ERROR_DOM_NETWORK_ERR; + } + + // Defer the actual sending of async events just in case listeners + // are attached after the send() method is called. + Unused << NS_WARN_IF( + NS_FAILED(DispatchToMainThread(NewRunnableMethod( + "dom::XMLHttpRequestMainThread::CloseRequestWithError", this, + &XMLHttpRequestMainThread::CloseRequestWithError, + ProgressEventType::error)))); + return NS_OK; +} + +bool XMLHttpRequestMainThread::CanSend(ErrorResult& aRv) { + if (!mPrincipal) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return false; + } + + // Step 1 + if (mState != XMLHttpRequest_Binding::OPENED) { + aRv.ThrowInvalidStateError("XMLHttpRequest state must be OPENED."); + return false; + } + + // Step 2 + if (mFlagSend) { + aRv.ThrowInvalidStateError("XMLHttpRequest must not be sending."); + return false; + } + + if (NS_FAILED(CheckCurrentGlobalCorrectness())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT); + return false; + } + + return true; +} + +void XMLHttpRequestMainThread::SendInternal(const BodyExtractorBase* aBody, + bool aBodyIsDocumentOrString, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + // We expect that CanSend has been called before we get here! + // We cannot move the remaining two checks below there because + // MaybeSilentSendFailure can cause unexpected side effects if called + // too early. + + // If open() failed to create the channel, then throw a network error + // as per spec. We really should create the channel here in send(), but + // we have internal code relying on the channel being created in open(). + if (!mChannel) { + mErrorLoad = ErrorType::eChannelOpen; + mFlagSend = true; // so CloseRequestWithError sets us to DONE. + aRv = MaybeSilentSendFailure(NS_ERROR_DOM_NETWORK_ERR); + return; + } + + // non-GET requests aren't allowed for blob. + if (IsBlobURI(mRequestURL) && !mRequestMethod.EqualsLiteral("GET")) { + mErrorLoad = ErrorType::eChannelOpen; + mFlagSend = true; // so CloseRequestWithError sets us to DONE. + aRv = MaybeSilentSendFailure(NS_ERROR_DOM_NETWORK_ERR); + return; + } + + // XXX We should probably send a warning to the JS console + // if there are no event listeners set and we are doing + // an asynchronous call. + + mUploadTransferred = 0; + mUploadTotal = 0; + // By default we don't have any upload, so mark upload complete. + mUploadComplete = true; + mErrorLoad = ErrorType::eOK; + mLoadTotal = -1; + nsCOMPtr uploadStream; + nsAutoCString uploadContentType; + nsCOMPtr httpChannel(do_QueryInterface(mChannel)); + if (aBody && httpChannel && !mRequestMethod.EqualsLiteral("GET") && + !mRequestMethod.EqualsLiteral("HEAD")) { + nsAutoCString charset; + nsAutoCString defaultContentType; + uint64_t size_u64; + aRv = aBody->GetAsStream(getter_AddRefs(uploadStream), &size_u64, + defaultContentType, charset); + if (aRv.Failed()) { + return; + } + + // make sure it fits within js MAX_SAFE_INTEGER + mUploadTotal = + net::InScriptableRange(size_u64) ? static_cast(size_u64) : -1; + + if (uploadStream) { + // If author set no Content-Type, use the default from GetAsStream(). + mAuthorRequestHeaders.Get("content-type", uploadContentType); + if (uploadContentType.IsVoid()) { + uploadContentType = defaultContentType; + } else if (aBodyIsDocumentOrString && + StaticPrefs::dom_xhr_standard_content_type_normalization()) { + UniquePtr contentTypeRecord = + CMimeType::Parse(uploadContentType); + nsAutoCString charset; + if (contentTypeRecord && + contentTypeRecord->GetParameterValue(kLiteralString_charset, + charset) && + !charset.EqualsIgnoreCase("utf-8")) { + contentTypeRecord->SetParameterValue(kLiteralString_charset, + kLiteralString_UTF_8); + contentTypeRecord->Serialize(uploadContentType); + } + } else if (!charset.IsEmpty()) { + // We don't want to set a charset for streams. + // Replace all case-insensitive matches of the charset in the + // content-type with the correct case. + RequestHeaders::CharsetIterator iter(uploadContentType); + while (iter.Next()) { + if (!iter.Equals(charset, nsCaseInsensitiveCStringComparator)) { + iter.Replace(charset); + } + } + } + + mUploadComplete = false; + } + } + + ResetResponse(); + + // Check if we should enable cross-origin upload listeners. + if (mUpload && mUpload->HasListeners()) { + mFlagHadUploadListenersOnSend = true; + } + + mIsMappedArrayBuffer = false; + if (mResponseType == XMLHttpRequestResponseType::Arraybuffer && + StaticPrefs::dom_mapped_arraybuffer_enabled()) { + nsCOMPtr uri; + nsAutoCString scheme; + + aRv = mChannel->GetURI(getter_AddRefs(uri)); + if (!aRv.Failed()) { + uri->GetScheme(scheme); + if (scheme.LowerCaseEqualsLiteral("jar")) { + mIsMappedArrayBuffer = true; + } + } + } + + aRv = InitiateFetch(uploadStream.forget(), mUploadTotal, uploadContentType); + if (aRv.Failed()) { + return; + } + + // Start our timeout + mRequestSentTime = PR_Now(); + StartTimeoutTimer(); + + mWaitingForOnStopRequest = true; + + // Step 8 + mFlagSend = true; + + // If we're synchronous, spin an event loop here and wait + RefPtr suspendedDoc; + if (mFlagSynchronous) { + auto scopeExit = MakeScopeExit([&] { + CancelSyncTimeoutTimer(); + ResumeTimeout(); + ResumeEventDispatching(); + }); + Maybe autoSuppress; + + mFlagSyncLooping = true; + + if (GetOwner()) { + if (nsCOMPtr topWindow = + GetOwner()->GetOuterWindow()->GetInProcessTop()) { + if (nsCOMPtr topInner = + topWindow->GetCurrentInnerWindow()) { + suspendedDoc = topWindow->GetExtantDoc(); + autoSuppress.emplace(topWindow->GetBrowsingContext()); + topInner->Suspend(); + mResumeTimeoutRunnable = new nsResumeTimeoutsEvent(topInner); + } + } + } + + SuspendEventDispatching(); + StopProgressEventTimer(); + + SyncTimeoutType syncTimeoutType = MaybeStartSyncTimeoutTimer(); + if (syncTimeoutType == eErrorOrExpired) { + Abort(); + aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); + return; + } + + nsAutoSyncOperation sync(suspendedDoc, + SyncOperationBehavior::eSuspendInput); + if (!SpinEventLoopUntil("XMLHttpRequestMainThread::SendInternal"_ns, + [&]() { return !mFlagSyncLooping; })) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + // Time expired... We should throw. + if (syncTimeoutType == eTimerStarted && !mSyncTimeoutTimer) { + aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); + return; + } + } else { + // Now that we've successfully opened the channel, we can change state. Note + // that this needs to come after the AsyncOpen() and rv check, because this + // can run script that would try to restart this request, and that could end + // up doing our AsyncOpen on a null channel if the reentered AsyncOpen + // fails. + StopProgressEventTimer(); + + // Upload phase beginning; start the progress event timer if necessary. + if (mUpload && mUpload->HasListenersFor(nsGkAtoms::onprogress)) { + StartProgressEventTimer(); + } + // Dispatch loadstart events + DispatchProgressEvent(this, ProgressEventType::loadstart, 0, -1); + if (mUpload && !mUploadComplete) { + DispatchProgressEvent(mUpload, ProgressEventType::loadstart, 0, + mUploadTotal); + } + } + + if (!mChannel) { + aRv = MaybeSilentSendFailure(NS_ERROR_DOM_NETWORK_ERR); + } +} + +// http://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#dom-xmlhttprequest-setrequestheader +void XMLHttpRequestMainThread::SetRequestHeader(const nsACString& aName, + const nsACString& aValue, + ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + // Step 1 + if (mState != XMLHttpRequest_Binding::OPENED) { + aRv.ThrowInvalidStateError("XMLHttpRequest state must be OPENED."); + return; + } + + // Step 2 + if (mFlagSend) { + aRv.ThrowInvalidStateError("XMLHttpRequest must not be sending."); + return; + } + + // Step 3 + nsAutoCString value; + NS_TrimHTTPWhitespace(aValue, value); + + // Step 4 + if (!NS_IsValidHTTPToken(aName) || !NS_IsReasonableHTTPHeaderValue(value)) { + aRv.Throw(NS_ERROR_DOM_INVALID_HEADER_NAME); + return; + } + + // Step 5 + bool isPrivilegedCaller = IsSystemXHR(); + bool isForbiddenHeader = + nsContentUtils::IsForbiddenRequestHeader(aName, aValue); + if (!isPrivilegedCaller && isForbiddenHeader) { + AutoTArray params; + CopyUTF8toUTF16(aName, *params.AppendElement()); + LogMessage("ForbiddenHeaderWarning", GetOwner(), params); + return; + } + + // Step 6.1 + // Skipping for now, as normalizing the case of header names may not be + // web-compatible. See bug 1285036. + + // Step 6.2-6.3 + // Gecko-specific: invalid headers can be set by privileged + // callers, but will not merge. + if (isPrivilegedCaller && isForbiddenHeader) { + mAuthorRequestHeaders.Set(aName, value); + } else { + mAuthorRequestHeaders.MergeOrSet(aName, value); + } +} + +void XMLHttpRequestMainThread::SetTimeout(uint32_t aTimeout, ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + if (mFlagSynchronous && mState != XMLHttpRequest_Binding::UNSENT && + HasOrHasHadOwner()) { + /* Timeout is not supported for synchronous requests with an owning window, + per XHR2 spec. */ + LogMessage("TimeoutSyncXHRWarning", GetOwner()); + aRv.ThrowInvalidAccessError( + "synchronous XMLHttpRequests do not support timeout and responseType"); + return; + } + + mTimeoutMilliseconds = aTimeout; + if (mRequestSentTime) { + StartTimeoutTimer(); + } +} + +nsIEventTarget* XMLHttpRequestMainThread::GetTimerEventTarget() { + if (nsCOMPtr global = GetOwnerGlobal()) { + return global->EventTargetFor(TaskCategory::Other); + } + return nullptr; +} + +nsresult XMLHttpRequestMainThread::DispatchToMainThread( + already_AddRefed aRunnable) { + if (nsCOMPtr global = GetOwnerGlobal()) { + nsCOMPtr target = + global->EventTargetFor(TaskCategory::Other); + MOZ_ASSERT(target); + + return target->Dispatch(std::move(aRunnable), NS_DISPATCH_NORMAL); + } + + return NS_DispatchToMainThread(std::move(aRunnable)); +} + +void XMLHttpRequestMainThread::StartTimeoutTimer() { + MOZ_ASSERT( + mRequestSentTime, + "StartTimeoutTimer mustn't be called before the request was sent!"); + if (mState == XMLHttpRequest_Binding::DONE) { + // do nothing! + return; + } + + if (mTimeoutTimer) { + mTimeoutTimer->Cancel(); + } + + if (!mTimeoutMilliseconds) { + return; + } + + if (!mTimeoutTimer) { + mTimeoutTimer = NS_NewTimer(GetTimerEventTarget()); + } + uint32_t elapsed = + (uint32_t)((PR_Now() - mRequestSentTime) / PR_USEC_PER_MSEC); + mTimeoutTimer->InitWithCallback( + this, mTimeoutMilliseconds > elapsed ? mTimeoutMilliseconds - elapsed : 0, + nsITimer::TYPE_ONE_SHOT); +} + +uint16_t XMLHttpRequestMainThread::ReadyState() const { return mState; } + +void XMLHttpRequestMainThread::OverrideMimeType(const nsAString& aMimeType, + ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + if (mState == XMLHttpRequest_Binding::LOADING || + mState == XMLHttpRequest_Binding::DONE) { + aRv.ThrowInvalidStateError( + "Cannot call 'overrideMimeType()' on XMLHttpRequest after 'send()' " + "(when its state is LOADING or DONE)."); + return; + } + + UniquePtr parsed = MimeType::Parse(aMimeType); + if (parsed) { + parsed->Serialize(mOverrideMimeType); + } else { + mOverrideMimeType.AssignLiteral(APPLICATION_OCTET_STREAM); + } +} + +bool XMLHttpRequestMainThread::MozBackgroundRequest() const { + return mFlagBackgroundRequest; +} + +void XMLHttpRequestMainThread::SetMozBackgroundRequestExternal( + bool aMozBackgroundRequest, ErrorResult& aRv) { + if (!IsSystemXHR()) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + if (mState != XMLHttpRequest_Binding::UNSENT) { + // Can't change this while we're in the middle of something. + aRv.ThrowInvalidStateError("XMLHttpRequest must not be sending."); + return; + } + + mFlagBackgroundRequest = aMozBackgroundRequest; +} + +void XMLHttpRequestMainThread::SetMozBackgroundRequest( + bool aMozBackgroundRequest, ErrorResult& aRv) { + // No errors for this webIDL method on main-thread. + SetMozBackgroundRequestExternal(aMozBackgroundRequest, IgnoreErrors()); +} + +void XMLHttpRequestMainThread::SetOriginStack( + UniquePtr aOriginStack) { + mOriginStack = std::move(aOriginStack); +} + +void XMLHttpRequestMainThread::SetSource( + UniquePtr aSource) { + if (!mChannel) { + return; + } + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + + if (httpChannel) { + httpChannel->SetSource(std::move(aSource)); + } +} + +bool XMLHttpRequestMainThread::WithCredentials() const { + return mFlagACwithCredentials; +} + +void XMLHttpRequestMainThread::SetWithCredentials(bool aWithCredentials, + ErrorResult& aRv) { + NOT_CALLABLE_IN_SYNC_SEND_RV + + // Return error if we're already processing a request. Note that we can't use + // ReadyState() here, because it can't differentiate between "opened" and + // "sent", so we use mState directly. + + if ((mState != XMLHttpRequest_Binding::UNSENT && + mState != XMLHttpRequest_Binding::OPENED) || + mFlagSend || mIsAnon) { + aRv.ThrowInvalidStateError("XMLHttpRequest must not be sending."); + return; + } + + mFlagACwithCredentials = aWithCredentials; +} + +nsresult XMLHttpRequestMainThread::ChangeState(uint16_t aState, + bool aBroadcast) { + mState = aState; + nsresult rv = NS_OK; + + if (aState != XMLHttpRequest_Binding::HEADERS_RECEIVED && + aState != XMLHttpRequest_Binding::LOADING) { + StopProgressEventTimer(); + } + + if (aBroadcast && + (!mFlagSynchronous || aState == XMLHttpRequest_Binding::OPENED || + aState == XMLHttpRequest_Binding::DONE)) { + rv = FireReadystatechangeEvent(); + } + + return rv; +} + +///////////////////////////////////////////////////// +// nsIChannelEventSink methods: +// +NS_IMETHODIMP +XMLHttpRequestMainThread::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + MOZ_ASSERT(aNewChannel, "Redirect without a channel?"); + + // Prepare to receive callback + mRedirectCallback = callback; + mNewRedirectChannel = aNewChannel; + + if (mChannelEventSink) { + nsCOMPtr fwd = EnsureXPCOMifier(); + + nsresult rv = mChannelEventSink->AsyncOnChannelRedirect( + aOldChannel, aNewChannel, aFlags, fwd); + if (NS_FAILED(rv)) { + mRedirectCallback = nullptr; + mNewRedirectChannel = nullptr; + } + return rv; + } + + // we need to strip Authentication headers for cross-origin requests + // Ref: https://fetch.spec.whatwg.org/#http-redirect-fetch + bool stripAuth = + StaticPrefs::network_fetch_redirect_stripAuthHeader() && + NS_ShouldRemoveAuthHeaderOnRedirect(aOldChannel, aNewChannel, aFlags); + + OnRedirectVerifyCallback(NS_OK, stripAuth); + + return NS_OK; +} + +nsresult XMLHttpRequestMainThread::OnRedirectVerifyCallback(nsresult result, + bool aStripAuth) { + NS_ASSERTION(mRedirectCallback, "mRedirectCallback not set in callback"); + NS_ASSERTION(mNewRedirectChannel, "mNewRedirectChannel not set in callback"); + + if (NS_SUCCEEDED(result)) { + bool rewriteToGET = false; + nsCOMPtr oldHttpChannel = GetCurrentHttpChannel(); + // Fetch 4.4.11 + Unused << oldHttpChannel->ShouldStripRequestBodyHeader(mRequestMethod, + &rewriteToGET); + + mChannel = mNewRedirectChannel; + + nsCOMPtr newHttpChannel(do_QueryInterface(mChannel)); + if (newHttpChannel) { + // Ensure all original headers are duplicated for the new channel (bug + // #553888) + mAuthorRequestHeaders.ApplyToChannel(newHttpChannel, rewriteToGET, + aStripAuth); + } + } else { + mErrorLoad = ErrorType::eRedirect; + } + + mNewRedirectChannel = nullptr; + + mRedirectCallback->OnRedirectVerifyCallback(result); + mRedirectCallback = nullptr; + + // It's important that we return success here. If we return the result code + // that we were passed, JavaScript callers who cancel the redirect will wind + // up throwing an exception in the process. + return NS_OK; +} + +///////////////////////////////////////////////////// +// nsIProgressEventSink methods: +// + +NS_IMETHODIMP +XMLHttpRequestMainThread::OnProgress(nsIRequest* aRequest, int64_t aProgress, + int64_t aProgressMax) { + // When uploading, OnProgress reports also headers in aProgress and + // aProgressMax. So, try to remove the headers, if possible. + bool lengthComputable = (aProgressMax != -1); + if (InUploadPhase()) { + int64_t loaded = aProgress; + if (lengthComputable) { + int64_t headerSize = aProgressMax - mUploadTotal; + loaded -= headerSize; + } + mUploadTransferred = loaded; + mProgressSinceLastProgressEvent = true; + + if (!mFlagSynchronous && !mProgressTimerIsActive) { + StartProgressEventTimer(); + } + } else { + mLoadTotal = aProgressMax; + mLoadTransferred = aProgress; + // OnDataAvailable() handles mProgressSinceLastProgressEvent + // for the download phase. + } + + if (mProgressEventSink) { + mProgressEventSink->OnProgress(aRequest, aProgress, aProgressMax); + } + + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::OnStatus(nsIRequest* aRequest, nsresult aStatus, + const char16_t* aStatusArg) { + if (mProgressEventSink) { + mProgressEventSink->OnStatus(aRequest, aStatus, aStatusArg); + } + + return NS_OK; +} + +bool XMLHttpRequestMainThread::AllowUploadProgress() { + return !IsCrossSiteCORSRequest() || mFlagHadUploadListenersOnSend; +} + +///////////////////////////////////////////////////// +// nsIInterfaceRequestor methods: +// +NS_IMETHODIMP +XMLHttpRequestMainThread::GetInterface(const nsIID& aIID, void** aResult) { + nsresult rv; + + // Make sure to return ourselves for the channel event sink interface and + // progress event sink interface, no matter what. We can forward these to + // mNotificationCallbacks if it wants to get notifications for them. But we + // need to see these notifications for proper functioning. + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + mChannelEventSink = do_GetInterface(mNotificationCallbacks); + *aResult = static_cast(EnsureXPCOMifier().take()); + return NS_OK; + } else if (aIID.Equals(NS_GET_IID(nsIProgressEventSink))) { + mProgressEventSink = do_GetInterface(mNotificationCallbacks); + *aResult = static_cast(EnsureXPCOMifier().take()); + return NS_OK; + } + + // Now give mNotificationCallbacks (if non-null) a chance to return the + // desired interface. + if (mNotificationCallbacks) { + rv = mNotificationCallbacks->GetInterface(aIID, aResult); + if (NS_SUCCEEDED(rv)) { + NS_ASSERTION(*aResult, "Lying nsIInterfaceRequestor implementation!"); + return rv; + } + } + + if (!mFlagBackgroundRequest && (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) || + aIID.Equals(NS_GET_IID(nsIAuthPrompt2)))) { + nsCOMPtr wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the an auth prompter for our window so that the parenting + // of the dialogs works as it should when using tabs. + nsCOMPtr window; + if (GetOwner()) { + window = GetOwner()->GetOuterWindow(); + } + return wwatch->GetPrompt(window, aIID, reinterpret_cast(aResult)); + } + + // Now check for the various XHR non-DOM interfaces, except + // nsIProgressEventSink and nsIChannelEventSink which we already + // handled above. + if (aIID.Equals(NS_GET_IID(nsIStreamListener))) { + *aResult = static_cast(EnsureXPCOMifier().take()); + return NS_OK; + } + if (aIID.Equals(NS_GET_IID(nsIRequestObserver))) { + *aResult = static_cast(EnsureXPCOMifier().take()); + return NS_OK; + } + if (aIID.Equals(NS_GET_IID(nsITimerCallback))) { + *aResult = static_cast(EnsureXPCOMifier().take()); + return NS_OK; + } + + return QueryInterface(aIID, aResult); +} + +void XMLHttpRequestMainThread::GetInterface( + JSContext* aCx, JS::Handle aIID, + JS::MutableHandle aRetval, ErrorResult& aRv) { + dom::GetInterface(aCx, this, aIID, aRetval, aRv); +} + +XMLHttpRequestUpload* XMLHttpRequestMainThread::GetUpload(ErrorResult& aRv) { + if (!mUpload) { + mUpload = new XMLHttpRequestUpload(this); + } + return mUpload; +} + +bool XMLHttpRequestMainThread::MozAnon() const { return mIsAnon; } + +bool XMLHttpRequestMainThread::MozSystem() const { return IsSystemXHR(); } + +void XMLHttpRequestMainThread::HandleTimeoutCallback() { + if (mState == XMLHttpRequest_Binding::DONE) { + MOZ_ASSERT_UNREACHABLE( + "XMLHttpRequestMainThread::HandleTimeoutCallback " + "with completed request"); + // do nothing! + return; + } + + mFlagTimedOut = true; + CloseRequestWithError(ProgressEventType::timeout); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::Notify(nsITimer* aTimer) { + if (mProgressNotifier == aTimer) { + HandleProgressTimerCallback(); + return NS_OK; + } + + if (mTimeoutTimer == aTimer) { + HandleTimeoutCallback(); + return NS_OK; + } + + if (mSyncTimeoutTimer == aTimer) { + HandleSyncTimeoutTimer(); + return NS_OK; + } + + // Just in case some JS user wants to QI to nsITimerCallback and play with + // us... + NS_WARNING("Unexpected timer!"); + return NS_ERROR_INVALID_POINTER; +} + +void XMLHttpRequestMainThread::HandleProgressTimerCallback() { + // Don't fire the progress event if mLoadTotal is 0, see XHR spec step 6.1 + if (!mLoadTotal && mLoadTransferred) { + return; + } + + mProgressTimerIsActive = false; + + if (!mProgressSinceLastProgressEvent || mErrorLoad != ErrorType::eOK) { + return; + } + + if (InUploadPhase()) { + if (mUpload && !mUploadComplete && mFlagHadUploadListenersOnSend) { + DispatchProgressEvent(mUpload, ProgressEventType::progress, + mUploadTransferred, mUploadTotal); + } + } else { + FireReadystatechangeEvent(); + DispatchProgressEvent(this, ProgressEventType::progress, mLoadTransferred, + mLoadTotal); + } + + mProgressSinceLastProgressEvent = false; + + StartProgressEventTimer(); +} + +void XMLHttpRequestMainThread::StopProgressEventTimer() { + if (mProgressNotifier) { + mProgressTimerIsActive = false; + mProgressNotifier->Cancel(); + } +} + +void XMLHttpRequestMainThread::StartProgressEventTimer() { + if (!mProgressNotifier) { + mProgressNotifier = NS_NewTimer(GetTimerEventTarget()); + } + if (mProgressNotifier) { + mProgressTimerIsActive = true; + mProgressNotifier->Cancel(); + mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL, + nsITimer::TYPE_ONE_SHOT); + } +} + +XMLHttpRequestMainThread::SyncTimeoutType +XMLHttpRequestMainThread::MaybeStartSyncTimeoutTimer() { + MOZ_ASSERT(mFlagSynchronous); + + Document* doc = GetDocumentIfCurrent(); + if (!doc || !doc->GetPageUnloadingEventTimeStamp()) { + return eNoTimerNeeded; + } + + // If we are in a beforeunload or a unload event, we must force a timeout. + TimeDuration diff = + (TimeStamp::NowLoRes() - doc->GetPageUnloadingEventTimeStamp()); + if (diff.ToMilliseconds() > MAX_SYNC_TIMEOUT_WHEN_UNLOADING) { + return eErrorOrExpired; + } + + mSyncTimeoutTimer = NS_NewTimer(GetTimerEventTarget()); + if (!mSyncTimeoutTimer) { + return eErrorOrExpired; + } + + uint32_t timeout = MAX_SYNC_TIMEOUT_WHEN_UNLOADING - diff.ToMilliseconds(); + nsresult rv = mSyncTimeoutTimer->InitWithCallback(this, timeout, + nsITimer::TYPE_ONE_SHOT); + return NS_FAILED(rv) ? eErrorOrExpired : eTimerStarted; +} + +void XMLHttpRequestMainThread::HandleSyncTimeoutTimer() { + MOZ_ASSERT(mSyncTimeoutTimer); + MOZ_ASSERT(mFlagSyncLooping); + + CancelSyncTimeoutTimer(); + Abort(); +} + +void XMLHttpRequestMainThread::CancelSyncTimeoutTimer() { + if (mSyncTimeoutTimer) { + mSyncTimeoutTimer->Cancel(); + mSyncTimeoutTimer = nullptr; + } +} + +already_AddRefed +XMLHttpRequestMainThread::EnsureXPCOMifier() { + if (!mXPCOMifier) { + mXPCOMifier = new nsXMLHttpRequestXPCOMifier(this); + } + RefPtr newRef(mXPCOMifier); + return newRef.forget(); +} + +bool XMLHttpRequestMainThread::ShouldBlockAuthPrompt() { + // Verify that it's ok to prompt for credentials here, per spec + // http://xhr.spec.whatwg.org/#the-send%28%29-method + + if (mAuthorRequestHeaders.Has("authorization")) { + return true; + } + + nsCOMPtr uri; + nsresult rv = mChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + // Also skip if a username and/or password is provided in the URI. + nsCString username; + rv = uri->GetUsername(username); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsCString password; + rv = uri->GetPassword(password); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + if (!username.IsEmpty() || !password.IsEmpty()) { + return true; + } + + return false; +} + +void XMLHttpRequestMainThread::TruncateResponseText() { + mResponseText.Truncate(); + XMLHttpRequest_Binding::ClearCachedResponseTextValue(this); +} + +NS_IMPL_ISUPPORTS(XMLHttpRequestMainThread::nsHeaderVisitor, + nsIHttpHeaderVisitor) + +NS_IMETHODIMP XMLHttpRequestMainThread::nsHeaderVisitor::VisitHeader( + const nsACString& header, const nsACString& value) { + if (mXHR.IsSafeHeader(header, mHttpChannel)) { + nsAutoCString lowerHeader(header); + ToLowerCase(lowerHeader); + if (!mHeaderList.InsertElementSorted(HeaderEntry(lowerHeader, value), + fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + return NS_OK; +} + +XMLHttpRequestMainThread::nsHeaderVisitor::nsHeaderVisitor( + const XMLHttpRequestMainThread& aXMLHttpRequest, + NotNull aHttpChannel) + : mXHR(aXMLHttpRequest), mHttpChannel(aHttpChannel) {} + +XMLHttpRequestMainThread::nsHeaderVisitor::~nsHeaderVisitor() = default; + +void XMLHttpRequestMainThread::MaybeCreateBlobStorage() { + MOZ_ASSERT(mResponseType == XMLHttpRequestResponseType::Blob); + + if (mBlobStorage) { + return; + } + + MutableBlobStorage::MutableBlobStorageType storageType = + BasePrincipal::Cast(mPrincipal)->PrivateBrowsingId() == 0 + ? MutableBlobStorage::eCouldBeInTemporaryFile + : MutableBlobStorage::eOnlyInMemory; + + nsCOMPtr eventTarget; + if (nsCOMPtr global = GetOwnerGlobal()) { + eventTarget = global->EventTargetFor(TaskCategory::Other); + } + + mBlobStorage = new MutableBlobStorage(storageType, eventTarget); +} + +void XMLHttpRequestMainThread::BlobStoreCompleted( + MutableBlobStorage* aBlobStorage, BlobImpl* aBlobImpl, nsresult aRv) { + // Ok, the state is changed... + if (mBlobStorage != aBlobStorage || NS_FAILED(aRv)) { + return; + } + + MOZ_ASSERT(mState != XMLHttpRequest_Binding::DONE); + + mResponseBlobImpl = aBlobImpl; + mBlobStorage = nullptr; + + ChangeStateToDone(mFlagSyncLooping); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetName(nsACString& aName) { + aName.AssignLiteral("XMLHttpRequest"); + return NS_OK; +} + +// nsXMLHttpRequestXPCOMifier implementation +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXMLHttpRequestXPCOMifier) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink) + NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback) + NS_INTERFACE_MAP_ENTRY(nsIProgressEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXMLHttpRequestXPCOMifier) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXMLHttpRequestXPCOMifier) + +// Can't NS_IMPL_CYCLE_COLLECTION( because mXHR has ambiguous +// inheritance from nsISupports. +NS_IMPL_CYCLE_COLLECTION_CLASS(nsXMLHttpRequestXPCOMifier) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXMLHttpRequestXPCOMifier) + if (tmp->mXHR) { + tmp->mXHR->mXPCOMifier = nullptr; + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mXHR) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXMLHttpRequestXPCOMifier) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mXHR) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMETHODIMP +nsXMLHttpRequestXPCOMifier::GetInterface(const nsIID& aIID, void** aResult) { + // Return ourselves for the things we implement (except + // nsIInterfaceRequestor) and the XHR for the rest. + if (!aIID.Equals(NS_GET_IID(nsIInterfaceRequestor))) { + nsresult rv = QueryInterface(aIID, aResult); + if (NS_SUCCEEDED(rv)) { + return rv; + } + } + + return mXHR->GetInterface(aIID, aResult); +} + +ArrayBufferBuilder::ArrayBufferBuilder() + : mMutex("ArrayBufferBuilder"), + mDataPtr(nullptr), + mCapacity(0), + mLength(0), + mMapPtr(nullptr), + mNeutered(false) {} + +ArrayBufferBuilder::~ArrayBufferBuilder() { + if (mDataPtr) { + JS_free(nullptr, mDataPtr); + } + + if (mMapPtr) { + JS::ReleaseMappedArrayBufferContents(mMapPtr, mLength); + mMapPtr = nullptr; + } + + mDataPtr = nullptr; + mCapacity = mLength = 0; +} + +bool ArrayBufferBuilder::SetCapacity(uint32_t aNewCap) { + MutexAutoLock lock(mMutex); + return SetCapacityInternal(aNewCap, lock); +} + +bool ArrayBufferBuilder::SetCapacityInternal( + uint32_t aNewCap, const MutexAutoLock& aProofOfLock) { + MOZ_ASSERT(!mMapPtr); + MOZ_ASSERT(!mNeutered); + + // To ensure that realloc won't free mDataPtr, use a size of 1 + // instead of 0. + uint8_t* newdata = (uint8_t*)js_realloc(mDataPtr, aNewCap ? aNewCap : 1); + + if (!newdata) { + return false; + } + + if (aNewCap > mCapacity) { + memset(newdata + mCapacity, 0, aNewCap - mCapacity); + } + + mDataPtr = newdata; + mCapacity = aNewCap; + if (mLength > aNewCap) { + mLength = aNewCap; + } + + return true; +} + +bool ArrayBufferBuilder::Append(const uint8_t* aNewData, uint32_t aDataLen, + uint32_t aMaxGrowth) { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mMapPtr); + MOZ_ASSERT(!mNeutered); + + CheckedUint32 neededCapacity = mLength; + neededCapacity += aDataLen; + if (!neededCapacity.isValid()) { + return false; + } + if (mLength + aDataLen > mCapacity) { + CheckedUint32 newcap = mCapacity; + // Double while under aMaxGrowth or if not specified. + if (!aMaxGrowth || mCapacity < aMaxGrowth) { + newcap *= 2; + } else { + newcap += aMaxGrowth; + } + + if (!newcap.isValid()) { + return false; + } + + // But make sure there's always enough to satisfy our request. + if (newcap.value() < neededCapacity.value()) { + newcap = neededCapacity; + } + + if (!SetCapacityInternal(newcap.value(), lock)) { + return false; + } + } + + // Assert that the region isn't overlapping so we can memcpy. + MOZ_ASSERT( + !AreOverlappingRegions(aNewData, aDataLen, mDataPtr + mLength, aDataLen)); + + memcpy(mDataPtr + mLength, aNewData, aDataLen); + mLength += aDataLen; + + return true; +} + +uint32_t ArrayBufferBuilder::Length() { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mNeutered); + return mLength; +} + +uint32_t ArrayBufferBuilder::Capacity() { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mNeutered); + return mCapacity; +} + +JSObject* ArrayBufferBuilder::TakeArrayBuffer(JSContext* aCx) { + MutexAutoLock lock(mMutex); + MOZ_DIAGNOSTIC_ASSERT(!mNeutered); + + if (mMapPtr) { + JSObject* obj = JS::NewMappedArrayBufferWithContents(aCx, mLength, mMapPtr); + if (!obj) { + JS::ReleaseMappedArrayBufferContents(mMapPtr, mLength); + } + + mMapPtr = nullptr; + mNeutered = true; + + // The memory-mapped contents will be released when the ArrayBuffer becomes + // detached or is GC'd. + return obj; + } + + // we need to check for mLength == 0, because nothing may have been + // added + if (mCapacity > mLength || mLength == 0) { + if (!SetCapacityInternal(mLength, lock)) { + return nullptr; + } + } + + JSObject* obj = JS::NewArrayBufferWithContents(aCx, mLength, mDataPtr); + if (!obj) { + return nullptr; + } + + mDataPtr = nullptr; + mCapacity = mLength = 0; + + mNeutered = true; + return obj; +} + +nsresult ArrayBufferBuilder::MapToFileInPackage(const nsCString& aFile, + nsIFile* aJarFile) { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mNeutered); + + nsresult rv; + + // Open Jar file to get related attributes of target file. + RefPtr zip = nsZipArchive::OpenArchive(aJarFile); + if (!zip) { + return NS_ERROR_FAILURE; + } + nsZipItem* zipItem = zip->GetItem(aFile.get()); + if (!zipItem) { + return NS_ERROR_FILE_NOT_FOUND; + } + + // If file was added to the package as stored(uncompressed), map to the + // offset of file in zip package. + if (!zipItem->Compression()) { + uint32_t offset = zip->GetDataOffset(zipItem); + uint32_t size = zipItem->RealSize(); + mozilla::AutoFDClose pr_fd; + rv = aJarFile->OpenNSPRFileDesc(PR_RDONLY, 0, &pr_fd.rwget()); + if (NS_FAILED(rv)) { + return rv; + } + mMapPtr = JS::CreateMappedArrayBufferContents( + PR_FileDesc2NativeHandle(pr_fd), offset, size); + if (mMapPtr) { + mLength = size; + return NS_OK; + } + } + return NS_ERROR_FAILURE; +} + +/* static */ +bool ArrayBufferBuilder::AreOverlappingRegions(const uint8_t* aStart1, + uint32_t aLength1, + const uint8_t* aStart2, + uint32_t aLength2) { + const uint8_t* end1 = aStart1 + aLength1; + const uint8_t* end2 = aStart2 + aLength2; + + const uint8_t* max_start = aStart1 > aStart2 ? aStart1 : aStart2; + const uint8_t* min_end = end1 < end2 ? end1 : end2; + + return max_start < min_end; +} + +RequestHeaders::RequestHeader* RequestHeaders::Find(const nsACString& aName) { + for (RequestHeaders::RequestHeader& header : mHeaders) { + if (header.mName.Equals(aName, nsCaseInsensitiveCStringComparator)) { + return &header; + } + } + return nullptr; +} + +bool RequestHeaders::IsEmpty() const { return mHeaders.IsEmpty(); } + +bool RequestHeaders::Has(const char* aName) { + return Has(nsDependentCString(aName)); +} + +bool RequestHeaders::Has(const nsACString& aName) { return !!Find(aName); } + +void RequestHeaders::Get(const char* aName, nsACString& aValue) { + Get(nsDependentCString(aName), aValue); +} + +void RequestHeaders::Get(const nsACString& aName, nsACString& aValue) { + RequestHeader* header = Find(aName); + if (header) { + aValue = header->mValue; + } else { + aValue.SetIsVoid(true); + } +} + +void RequestHeaders::Set(const char* aName, const nsACString& aValue) { + Set(nsDependentCString(aName), aValue); +} + +void RequestHeaders::Set(const nsACString& aName, const nsACString& aValue) { + RequestHeader* header = Find(aName); + if (header) { + header->mValue.Assign(aValue); + } else { + RequestHeader newHeader = {nsCString(aName), nsCString(aValue)}; + mHeaders.AppendElement(newHeader); + } +} + +void RequestHeaders::MergeOrSet(const char* aName, const nsACString& aValue) { + MergeOrSet(nsDependentCString(aName), aValue); +} + +void RequestHeaders::MergeOrSet(const nsACString& aName, + const nsACString& aValue) { + RequestHeader* header = Find(aName); + if (header) { + header->mValue.AppendLiteral(", "); + header->mValue.Append(aValue); + } else { + RequestHeader newHeader = {nsCString(aName), nsCString(aValue)}; + mHeaders.AppendElement(newHeader); + } +} + +void RequestHeaders::Clear() { mHeaders.Clear(); } + +void RequestHeaders::ApplyToChannel(nsIHttpChannel* aChannel, + bool aStripRequestBodyHeader, + bool aStripAuthHeader) const { + for (const RequestHeader& header : mHeaders) { + if (aStripRequestBodyHeader && + (header.mName.LowerCaseEqualsASCII("content-type") || + header.mName.LowerCaseEqualsASCII("content-encoding") || + header.mName.LowerCaseEqualsASCII("content-language") || + header.mName.LowerCaseEqualsASCII("content-location"))) { + continue; + } + + if (aStripAuthHeader && + header.mName.LowerCaseEqualsASCII("authorization")) { + continue; + } + + // Update referrerInfo to override referrer header in system privileged. + if (header.mName.LowerCaseEqualsASCII("referer")) { + DebugOnly rv = aChannel->SetNewReferrerInfo( + header.mValue, nsIReferrerInfo::ReferrerPolicyIDL::UNSAFE_URL, true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + if (header.mValue.IsEmpty()) { + DebugOnly rv = aChannel->SetEmptyRequestHeader(header.mName); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + DebugOnly rv = + aChannel->SetRequestHeader(header.mName, header.mValue, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } +} + +void RequestHeaders::GetCORSUnsafeHeaders(nsTArray& aArray) const { + for (const RequestHeader& header : mHeaders) { + if (!nsContentUtils::IsCORSSafelistedRequestHeader(header.mName, + header.mValue)) { + aArray.AppendElement(header.mName); + } + } +} + +RequestHeaders::CharsetIterator::CharsetIterator(nsACString& aSource) + : mValid(false), + mCurPos(-1), + mCurLen(-1), + mCutoff(aSource.Length()), + mSource(aSource) {} + +bool RequestHeaders::CharsetIterator::Equals( + const nsACString& aOther, const nsCStringComparator& aCmp) const { + if (mValid) { + return Substring(mSource, mCurPos, mCurLen).Equals(aOther, aCmp); + } else { + return false; + } +} + +void RequestHeaders::CharsetIterator::Replace(const nsACString& aReplacement) { + if (mValid) { + mSource.Replace(mCurPos, mCurLen, aReplacement); + mCurLen = aReplacement.Length(); + } +} + +bool RequestHeaders::CharsetIterator::Next() { + int32_t start, end; + nsAutoCString charset; + + // Look for another charset declaration in the string, limiting the + // search to only the characters before the parts we've already searched + // (before mCutoff), so that we don't find the same charset twice. + NS_ExtractCharsetFromContentType(Substring(mSource, 0, mCutoff), charset, + &mValid, &start, &end); + + if (!mValid) { + return false; + } + + // Everything after the = sign is the part of the charset we want. + mCurPos = mSource.FindChar('=', start) + 1; + mCurLen = end - mCurPos; + + // Special case: the extracted charset is quoted with single quotes. + // For the purpose of preserving what was set we want to handle them + // as delimiters (although they aren't really). + if (charset.Length() >= 2 && charset.First() == '\'' && + charset.Last() == '\'') { + ++mCurPos; + mCurLen -= 2; + } + + mCutoff = start; + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/xhr/XMLHttpRequestMainThread.h b/dom/xhr/XMLHttpRequestMainThread.h new file mode 100644 index 0000000000..b248e03664 --- /dev/null +++ b/dom/xhr/XMLHttpRequestMainThread.h @@ -0,0 +1,873 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestMainThread_h +#define mozilla_dom_XMLHttpRequestMainThread_h + +#include +#include "nsISupportsUtils.h" +#include "nsIURI.h" +#include "mozilla/dom/Document.h" +#include "nsIStreamListener.h" +#include "nsIChannelEventSink.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIDOMEventListener.h" +#include "nsIInterfaceRequestor.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIProgressEventSink.h" +#include "nsJSUtils.h" +#include "nsTArray.h" +#include "nsITimer.h" +#include "nsIPrincipal.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsISizeOfEventTarget.h" +#include "nsIInputStream.h" +#include "nsIContentSecurityPolicy.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Maybe.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/NotNull.h" +#include "mozilla/dom/MutableBlobStorage.h" +#include "mozilla/dom/BodyExtractor.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/MimeType.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/dom/URLSearchParams.h" +#include "mozilla/dom/XMLHttpRequest.h" +#include "mozilla/dom/XMLHttpRequestBinding.h" +#include "mozilla/dom/XMLHttpRequestEventTarget.h" +#include "mozilla/dom/XMLHttpRequestString.h" +#include "mozilla/Encoding.h" + +#ifdef Status +/* Xlib headers insist on this for some reason... Nuke it because + it'll override our member name */ +typedef Status __StatusTmp; +# undef Status +typedef __StatusTmp Status; +#endif + +class nsIHttpChannel; +class nsIJARChannel; +class nsILoadGroup; + +namespace mozilla { +class ProfileChunkedBuffer; + +namespace dom { + +class DOMString; +class XMLHttpRequestUpload; +class SerializedStackHolder; +struct OriginAttributesDictionary; + +// A helper for building up an ArrayBuffer object's data +// before creating the ArrayBuffer itself. Will do doubling +// based reallocation, up to an optional maximum growth given. +// +// When all the data has been appended, call GetArrayBuffer, +// passing in the JSContext* for which the ArrayBuffer object +// is to be created. This also implicitly resets the builder. +class ArrayBufferBuilder { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ArrayBufferBuilder); + + ArrayBufferBuilder(); + + // Will truncate if aNewCap is < Length(). + bool SetCapacity(uint32_t aNewCap); + + // Append aDataLen bytes from data to the current buffer. If we + // need to grow the buffer, grow by doubling the size up to a + // maximum of aMaxGrowth (if given). If aDataLen is greater than + // what the new capacity would end up as, then grow by aDataLen. + // + // The data parameter must not overlap with anything beyond the + // builder's current valid contents [0..length) + bool Append(const uint8_t* aNewData, uint32_t aDataLen, + uint32_t aMaxGrowth = 0); + + uint32_t Length(); + uint32_t Capacity(); + + JSObject* TakeArrayBuffer(JSContext* aCx); + + // Memory mapping to starting position of file(aFile) in the zip + // package(aJarFile). + // + // The file in the zip package has to be uncompressed and the starting + // position of the file must be aligned according to array buffer settings + // in JS engine. + nsresult MapToFileInPackage(const nsCString& aFile, nsIFile* aJarFile); + + private: + ~ArrayBufferBuilder(); + + ArrayBufferBuilder(const ArrayBufferBuilder&) = delete; + ArrayBufferBuilder& operator=(const ArrayBufferBuilder&) = delete; + ArrayBufferBuilder& operator=(const ArrayBufferBuilder&&) = delete; + + bool SetCapacityInternal(uint32_t aNewCap, const MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(mMutex); + + static bool AreOverlappingRegions(const uint8_t* aStart1, uint32_t aLength1, + const uint8_t* aStart2, uint32_t aLength2); + + Mutex mMutex; + + // All of these are protected by mMutex. + uint8_t* mDataPtr MOZ_GUARDED_BY(mMutex); + uint32_t mCapacity MOZ_GUARDED_BY(mMutex); + uint32_t mLength MOZ_GUARDED_BY(mMutex); + void* mMapPtr MOZ_GUARDED_BY(mMutex); + + // This is used in assertions only. + bool mNeutered; +}; + +class nsXMLHttpRequestXPCOMifier; + +class RequestHeaders { + struct RequestHeader { + nsCString mName; + nsCString mValue; + }; + nsTArray mHeaders; + RequestHeader* Find(const nsACString& aName); + + public: + class CharsetIterator { + bool mValid; + int32_t mCurPos, mCurLen, mCutoff; + nsACString& mSource; + + public: + explicit CharsetIterator(nsACString& aSource); + bool Equals(const nsACString& aOther, + const nsCStringComparator& aCmp) const; + void Replace(const nsACString& aReplacement); + bool Next(); + }; + + bool IsEmpty() const; + bool Has(const char* aName); + bool Has(const nsACString& aName); + void Get(const char* aName, nsACString& aValue); + void Get(const nsACString& aName, nsACString& aValue); + void Set(const char* aName, const nsACString& aValue); + void Set(const nsACString& aName, const nsACString& aValue); + void MergeOrSet(const char* aName, const nsACString& aValue); + void MergeOrSet(const nsACString& aName, const nsACString& aValue); + void Clear(); + void ApplyToChannel(nsIHttpChannel* aChannel, bool aStripRequestBodyHeader, + bool aStripAuth) const; + void GetCORSUnsafeHeaders(nsTArray& aArray) const; +}; + +class nsXHRParseEndListener; +class XMLHttpRequestDoneNotifier; + +// Make sure that any non-DOM interfaces added here are also added to +// nsXMLHttpRequestXPCOMifier. +class XMLHttpRequestMainThread final : public XMLHttpRequest, + public nsIStreamListener, + public nsIChannelEventSink, + public nsIProgressEventSink, + public nsIInterfaceRequestor, + public nsITimerCallback, + public nsISizeOfEventTarget, + public nsINamed, + public MutableBlobStorageCallback { + friend class nsXHRParseEndListener; + friend class nsXMLHttpRequestXPCOMifier; + friend class XMLHttpRequestDoneNotifier; + + public: + enum class ProgressEventType : uint8_t { + loadstart, + progress, + error, + abort, + timeout, + load, + loadend, + ENUM_MAX + }; + + // Make sure that any additions done to ErrorType enum are also mirrored in + // XHR_ERROR_TYPE enum of TelemetrySend.sys.mjs. + enum class ErrorType : uint16_t { + eOK, + eRequest, + eUnreachable, + eChannelOpen, + eRedirect, + eTerminated, + ENUM_MAX + }; + + explicit XMLHttpRequestMainThread(nsIGlobalObject* aGlobalObject); + + void Construct(nsIPrincipal* aPrincipal, + nsICookieJarSettings* aCookieJarSettings, bool aForWorker, + nsIURI* aBaseURI = nullptr, nsILoadGroup* aLoadGroup = nullptr, + PerformanceStorage* aPerformanceStorage = nullptr, + nsICSPEventListener* aCSPEventListener = nullptr); + + void InitParameters(bool aAnon, bool aSystem); + + void SetParameters(bool aAnon, bool aSystem) { + mIsAnon = aAnon || aSystem; + mIsSystem = aSystem; + } + + void SetClientInfoAndController( + const ClientInfo& aClientInfo, + const Maybe& aController); + + NS_DECL_ISUPPORTS_INHERITED + + // nsIStreamListener + NS_DECL_NSISTREAMLISTENER + + // nsIRequestObserver + NS_DECL_NSIREQUESTOBSERVER + + // nsIChannelEventSink + NS_DECL_NSICHANNELEVENTSINK + + // nsIProgressEventSink + NS_DECL_NSIPROGRESSEVENTSINK + + // nsIInterfaceRequestor + NS_DECL_NSIINTERFACEREQUESTOR + + // nsITimerCallback + NS_DECL_NSITIMERCALLBACK + + // nsINamed + NS_DECL_NSINAMED + + // nsISizeOfEventTarget + virtual size_t SizeOfEventTargetIncludingThis( + MallocSizeOf aMallocSizeOf) const override; + + // states + virtual uint16_t ReadyState() const override; + + // request + nsresult CreateChannel(); + nsresult InitiateFetch(already_AddRefed aUploadStream, + int64_t aUploadLength, nsACString& aUploadContentType); + + virtual void Open(const nsACString& aMethod, const nsAString& aUrl, + ErrorResult& aRv) override; + + virtual void Open(const nsACString& aMethod, const nsAString& aUrl, + bool aAsync, const nsAString& aUsername, + const nsAString& aPassword, ErrorResult& aRv) override; + + void Open(const nsACString& aMethod, const nsACString& aUrl, bool aAsync, + const nsAString& aUsername, const nsAString& aPassword, + ErrorResult& aRv); + + virtual void SetRequestHeader(const nsACString& aName, + const nsACString& aValue, + ErrorResult& aRv) override; + + virtual uint32_t Timeout() const override { return mTimeoutMilliseconds; } + + virtual void SetTimeout(uint32_t aTimeout, ErrorResult& aRv) override; + + virtual bool WithCredentials() const override; + + virtual void SetWithCredentials(bool aWithCredentials, + ErrorResult& aRv) override; + + virtual XMLHttpRequestUpload* GetUpload(ErrorResult& aRv) override; + + private: + virtual ~XMLHttpRequestMainThread(); + + nsresult MaybeSilentSendFailure(nsresult aRv); + void SendInternal(const BodyExtractorBase* aBody, + bool aBodyIsDocumentOrString, ErrorResult& aRv); + + bool IsCrossSiteCORSRequest() const; + bool IsDeniedCrossSiteCORSRequest(); + + void ResumeTimeout(); + + void MaybeLowerChannelPriority(); + + public: + bool CanSend(ErrorResult& aRv); + + virtual void Send( + const Nullable< + DocumentOrBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString>& + aData, + ErrorResult& aRv) override; + + virtual void SendInputStream(nsIInputStream* aInputStream, + ErrorResult& aRv) override { + if (!CanSend(aRv)) { + return; + } + BodyExtractor body(aInputStream); + SendInternal(&body, false, aRv); + } + + void RequestErrorSteps(const ProgressEventType aEventType, + const nsresult aOptionalException, ErrorResult& aRv); + + void Abort() { + IgnoredErrorResult rv; + AbortInternal(rv); + MOZ_ASSERT(!rv.Failed()); + } + + virtual void Abort(ErrorResult& aRv) override; + + // response + virtual void GetResponseURL(nsAString& aUrl) override; + + virtual uint32_t GetStatus(ErrorResult& aRv) override; + + virtual void GetStatusText(nsACString& aStatusText, + ErrorResult& aRv) override; + + virtual void GetResponseHeader(const nsACString& aHeader, nsACString& aResult, + ErrorResult& aRv) override; + + void GetResponseHeader(const nsAString& aHeader, nsAString& aResult, + ErrorResult& aRv) { + nsAutoCString result; + GetResponseHeader(NS_ConvertUTF16toUTF8(aHeader), result, aRv); + if (result.IsVoid()) { + aResult.SetIsVoid(true); + } else { + // The result value should be inflated: + CopyASCIItoUTF16(result, aResult); + } + } + + virtual void GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) override; + + bool IsSafeHeader(const nsACString& aHeaderName, + NotNull aHttpChannel) const; + + virtual void OverrideMimeType(const nsAString& aMimeType, + ErrorResult& aRv) override; + + virtual XMLHttpRequestResponseType ResponseType() const override { + return XMLHttpRequestResponseType(mResponseType); + } + + virtual void SetResponseType(XMLHttpRequestResponseType aType, + ErrorResult& aRv) override; + + void SetResponseTypeRaw(XMLHttpRequestResponseType aType) { + mResponseType = aType; + } + + virtual void GetResponse(JSContext* aCx, + JS::MutableHandle aResponse, + ErrorResult& aRv) override; + + virtual void GetResponseText(DOMString& aResponseText, + ErrorResult& aRv) override; + + // GetResponse* for workers: + already_AddRefed GetResponseBlobImpl(); + already_AddRefed GetResponseArrayBufferBuilder(); + nsresult GetResponseTextForJSON(nsAString& aString); + void GetResponseText(XMLHttpRequestStringSnapshot& aSnapshot, + ErrorResult& aRv); + + virtual Document* GetResponseXML(ErrorResult& aRv) override; + + virtual bool MozBackgroundRequest() const override; + + void SetMozBackgroundRequestExternal(bool aMozBackgroundRequest, + ErrorResult& aRv); + + virtual void SetMozBackgroundRequest(bool aMozBackgroundRequest, + ErrorResult& aRv) override; + + void SetOriginStack(UniquePtr aOriginStack); + + void SetSource(UniquePtr aSource); + + virtual uint16_t ErrorCode() const override { + return static_cast(mErrorLoad); + } + + virtual bool MozAnon() const override; + + virtual bool MozSystem() const override; + + virtual nsIChannel* GetChannel() const override { return mChannel; } + + // We need a GetInterface callable from JS for chrome JS + virtual void GetInterface(JSContext* aCx, JS::Handle aIID, + JS::MutableHandle aRetval, + ErrorResult& aRv) override; + + // This fires a trusted readystatechange event, which is not cancelable and + // doesn't bubble. + nsresult FireReadystatechangeEvent(); + void DispatchProgressEvent(DOMEventTargetHelper* aTarget, + const ProgressEventType aType, int64_t aLoaded, + int64_t aTotal); + + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + XMLHttpRequestMainThread, XMLHttpRequest) + virtual bool IsCertainlyAliveForCC() const override; + + bool AllowUploadProgress(); + + virtual void DisconnectFromOwner() override; + + static void SetDontWarnAboutSyncXHR(bool aVal) { + sDontWarnAboutSyncXHR = aVal; + } + static bool DontWarnAboutSyncXHR() { return sDontWarnAboutSyncXHR; } + + virtual void SetOriginAttributes( + const mozilla::dom::OriginAttributesDictionary& aAttrs) override; + + void BlobStoreCompleted(MutableBlobStorage* aBlobStorage, BlobImpl* aBlobImpl, + nsresult aResult) override; + + void LocalFileToBlobCompleted(BlobImpl* aBlobImpl); + + protected: + nsresult DetectCharset(); + nsresult AppendToResponseText(Span aBuffer, + bool aLast = false); + static nsresult StreamReaderFunc(nsIInputStream* in, void* closure, + const char* fromRawSegment, + uint32_t toOffset, uint32_t count, + uint32_t* writeCount); + nsresult CreateResponseParsedJSON(JSContext* aCx); + // Change the state of the object with this. The broadcast argument + // determines if the onreadystatechange listener should be called. + nsresult ChangeState(uint16_t aState, bool aBroadcast = true); + already_AddRefed GetLoadGroup() const; + + // Finds a preload for this XHR. If it is found it's removed from the preload + // table of the document and marked as used. The called doesn't need to do + // any more comparative checks. + already_AddRefed FindPreload(); + // If no or unknown mime type is set on the channel this method ensures it's + // set to "text/xml". + void EnsureChannelContentType(); + + already_AddRefed GetCurrentHttpChannel(); + already_AddRefed GetCurrentJARChannel(); + + void TruncateResponseText(); + + bool IsSystemXHR() const; + bool InUploadPhase() const; + + void OnBodyParseEnd(); + void ChangeStateToDone(bool aWasSync); + void ChangeStateToDoneInternal(); + + void StartProgressEventTimer(); + void StopProgressEventTimer(); + + void MaybeCreateBlobStorage(); + + nsresult OnRedirectVerifyCallback(nsresult result, bool stripAuth = false); + + nsIEventTarget* GetTimerEventTarget(); + + nsresult DispatchToMainThread(already_AddRefed aRunnable); + + void DispatchOrStoreEvent(DOMEventTargetHelper* aTarget, Event* aEvent); + + already_AddRefed EnsureXPCOMifier(); + + void SuspendEventDispatching(); + void ResumeEventDispatching(); + + void AbortInternal(ErrorResult& aRv); + + struct PendingEvent { + RefPtr mTarget; + RefPtr mEvent; + }; + + nsTArray mPendingEvents; + + nsCOMPtr mContext; + nsCOMPtr mPrincipal; + nsCOMPtr mChannel; + nsCString mRequestMethod; + nsCOMPtr mRequestURL; + RefPtr mResponseXML; + + nsCOMPtr mXMLParserStreamListener; + + nsCOMPtr mCookieJarSettings; + + RefPtr mPerformanceStorage; + nsCOMPtr mCSPEventListener; + + // used to implement getAllResponseHeaders() + class nsHeaderVisitor : public nsIHttpHeaderVisitor { + struct HeaderEntry final { + nsCString mName; + nsCString mValue; + + HeaderEntry(const nsACString& aName, const nsACString& aValue) + : mName(aName), mValue(aValue) {} + + bool operator==(const HeaderEntry& aOther) const { + return mName == aOther.mName; + } + + bool operator<(const HeaderEntry& aOther) const { + uint32_t selfLen = mName.Length(); + uint32_t otherLen = aOther.mName.Length(); + uint32_t min = XPCOM_MIN(selfLen, otherLen); + for (uint32_t i = 0; i < min; ++i) { + unsigned char self = mName[i]; + unsigned char other = aOther.mName[i]; + MOZ_ASSERT(!(self >= 'A' && self <= 'Z')); + MOZ_ASSERT(!(other >= 'A' && other <= 'Z')); + if (self == other) { + continue; + } + if (self >= 'a' && self <= 'z') { + self -= 0x20; + } + if (other >= 'a' && other <= 'z') { + other -= 0x20; + } + return self < other; + } + return selfLen < otherLen; + } + }; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPHEADERVISITOR + nsHeaderVisitor(const XMLHttpRequestMainThread& aXMLHttpRequest, + NotNull aHttpChannel); + const nsACString& Headers() { + for (uint32_t i = 0; i < mHeaderList.Length(); i++) { + HeaderEntry& header = mHeaderList.ElementAt(i); + + mHeaders.Append(header.mName); + mHeaders.AppendLiteral(": "); + mHeaders.Append(header.mValue); + mHeaders.AppendLiteral("\r\n"); + } + return mHeaders; + } + + private: + virtual ~nsHeaderVisitor(); + + nsTArray mHeaderList; + nsCString mHeaders; + const XMLHttpRequestMainThread& mXHR; + NotNull> mHttpChannel; + }; + + // The bytes of our response body. Only used for DEFAULT, ARRAYBUFFER and + // BLOB responseTypes + nsCString mResponseBody; + + // The text version of our response body. This is incrementally decoded into + // as we receive network data. However for the DEFAULT responseType we + // lazily decode into this from mResponseBody only when .responseText is + // accessed. + // Only used for DEFAULT and TEXT responseTypes. + XMLHttpRequestString mResponseText; + + // For DEFAULT responseType we use this to keep track of how far we've + // lazily decoded from mResponseBody to mResponseText + uint32_t mResponseBodyDecodedPos; + + // Decoder used for decoding into mResponseText + // Only used for DEFAULT, TEXT and JSON responseTypes. + // In cases where we've only received half a surrogate, the decoder itself + // carries the state to remember this. Next time we receive more data we + // simply feed the new data into the decoder which will handle the second + // part of the surrogate. + mozilla::UniquePtr mDecoder; + + void MatchCharsetAndDecoderToResponseDocument(); + + XMLHttpRequestResponseType mResponseType; + + RefPtr mResponseBlobImpl; + + // This is the cached blob-response, created only at the first GetResponse() + // call. + RefPtr mResponseBlob; + + // We stream data to mBlobStorage when response type is "blob". + RefPtr mBlobStorage; + + nsString mOverrideMimeType; + + /** + * The notification callbacks the channel had when Send() was + * called. We want to forward things here as needed. + */ + nsCOMPtr mNotificationCallbacks; + /** + * Sink interfaces that we implement that mNotificationCallbacks may + * want to also be notified for. These are inited lazily if we're + * asked for the relevant interface. + */ + nsCOMPtr mChannelEventSink; + nsCOMPtr mProgressEventSink; + + nsCOMPtr mBaseURI; + nsCOMPtr mLoadGroup; + + Maybe mClientInfo; + Maybe mController; + + uint16_t mState; + + // If true, this object is used by the worker's XMLHttpRequest. + bool mForWorker; + + bool mFlagSynchronous; + bool mFlagAborted; + bool mFlagParseBody; + bool mFlagSyncLooping; + bool mFlagBackgroundRequest; + bool mFlagHadUploadListenersOnSend; + bool mFlagACwithCredentials; + bool mFlagTimedOut; + bool mFlagDeleted; + + // The XHR2 spec's send() flag. Set when the XHR begins uploading, until it + // finishes downloading (or an error/abort has occurred during either phase). + // Used to guard against the user trying to alter headers/etc when it's too + // late, and ensure the XHR only handles one in-flight request at once. + bool mFlagSend; + + RefPtr mUpload; + int64_t mUploadTransferred; + int64_t mUploadTotal; + bool mUploadComplete; + bool mProgressSinceLastProgressEvent; + + // Timeout support + PRTime mRequestSentTime; + uint32_t mTimeoutMilliseconds; + nsCOMPtr mTimeoutTimer; + void StartTimeoutTimer(); + void HandleTimeoutCallback(); + + nsCOMPtr mResumeTimeoutRunnable; + + nsCOMPtr mSyncTimeoutTimer; + + enum SyncTimeoutType { eErrorOrExpired, eTimerStarted, eNoTimerNeeded }; + + SyncTimeoutType MaybeStartSyncTimeoutTimer(); + void HandleSyncTimeoutTimer(); + void CancelSyncTimeoutTimer(); + + ErrorType mErrorLoad; + bool mErrorParsingXML; + bool mWaitingForOnStopRequest; + bool mProgressTimerIsActive; + bool mIsHtml; + bool mWarnAboutSyncHtml; + int64_t mLoadTotal; // -1 if not known. + // Number of HTTP message body bytes received so far. This quantity is + // in the same units as Content-Length and mLoadTotal, and hence counts + // compressed bytes when the channel has gzip Content-Encoding. If the + // channel does not have Content-Encoding, this will be the same as + // mDataReceived except between the OnProgress that changes mLoadTransferred + // and the corresponding OnDataAvailable (which changes mDataReceived). + // Ordering of OnProgress and OnDataAvailable is undefined. + int64_t mLoadTransferred; + nsCOMPtr mProgressNotifier; + void HandleProgressTimerCallback(); + + bool mIsSystem; + bool mIsAnon; + + /** + * Close the XMLHttpRequest's channels. + */ + void CloseRequest(); + + void TerminateOngoingFetch(); + + /** + * Close the XMLHttpRequest's channels and dispatch appropriate progress + * events. + * + * @param aType The progress event type. + */ + void CloseRequestWithError(const ProgressEventType aType); + + nsCOMPtr mRedirectCallback; + nsCOMPtr mNewRedirectChannel; + + JS::Heap mResultJSON; + + RefPtr mArrayBufferBuilder; + JS::Heap mResultArrayBuffer; + bool mIsMappedArrayBuffer; + + void ResetResponse(); + + bool ShouldBlockAuthPrompt(); + + RequestHeaders mAuthorRequestHeaders; + + // Helper object to manage our XPCOM scriptability bits + nsXMLHttpRequestXPCOMifier* mXPCOMifier; + + // When this is set to true, the event dispatching is suspended. This is + // useful to change the correct state when XHR is working sync. + bool mEventDispatchingSuspended; + + // True iff mDecoder has processed the end of the stream. + // Used in lazy decoding to distinguish between having + // processed all the bytes but not the EOF and having + // processed all the bytes and the EOF. + bool mEofDecoded; + + // This flag will be set in `Send()` when we successfully reuse a "fetch" + // preload to satisfy this XHR. + bool mFromPreload = false; + + // Our parse-end listener, if we are parsing. + RefPtr mParseEndListener; + + XMLHttpRequestDoneNotifier* mDelayedDoneNotifier; + void DisconnectDoneNotifier(); + + // Any stack information for the point the XHR was opened. This is deleted + // after the XHR is opened, to avoid retaining references to the worker. + UniquePtr mOriginStack; + + static bool sDontWarnAboutSyncXHR; +}; + +class MOZ_STACK_CLASS AutoDontWarnAboutSyncXHR { + public: + AutoDontWarnAboutSyncXHR() + : mOldVal(XMLHttpRequestMainThread::DontWarnAboutSyncXHR()) { + XMLHttpRequestMainThread::SetDontWarnAboutSyncXHR(true); + } + + ~AutoDontWarnAboutSyncXHR() { + XMLHttpRequestMainThread::SetDontWarnAboutSyncXHR(mOldVal); + } + + private: + bool mOldVal; +}; + +// A shim class designed to expose the non-DOM interfaces of +// XMLHttpRequest via XPCOM stuff. +class nsXMLHttpRequestXPCOMifier final : public nsIStreamListener, + public nsIChannelEventSink, + public nsIAsyncVerifyRedirectCallback, + public nsIProgressEventSink, + public nsIInterfaceRequestor, + public nsITimerCallback, + public nsINamed { + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXMLHttpRequestXPCOMifier, + nsIStreamListener) + + explicit nsXMLHttpRequestXPCOMifier(XMLHttpRequestMainThread* aXHR) + : mXHR(aXHR) {} + + private: + ~nsXMLHttpRequestXPCOMifier() { + if (mXHR) { + mXHR->mXPCOMifier = nullptr; + } + } + + public: + NS_FORWARD_NSISTREAMLISTENER(mXHR->) + NS_FORWARD_NSIREQUESTOBSERVER(mXHR->) + NS_FORWARD_NSICHANNELEVENTSINK(mXHR->) + NS_FORWARD_NSIASYNCVERIFYREDIRECTCALLBACK(mXHR->) + NS_FORWARD_NSIPROGRESSEVENTSINK(mXHR->) + NS_FORWARD_NSITIMERCALLBACK(mXHR->) + NS_FORWARD_NSINAMED(mXHR->) + + NS_DECL_NSIINTERFACEREQUESTOR + + private: + RefPtr mXHR; +}; + +class XMLHttpRequestDoneNotifier : public Runnable { + public: + explicit XMLHttpRequestDoneNotifier(XMLHttpRequestMainThread* aXHR) + : Runnable("XMLHttpRequestDoneNotifier"), mXHR(aXHR) {} + + NS_IMETHOD Run() override { + if (mXHR) { + RefPtr xhr = mXHR; + // ChangeStateToDoneInternal ends up calling Disconnect(); + xhr->ChangeStateToDoneInternal(); + MOZ_ASSERT(!mXHR); + } + return NS_OK; + } + + void Disconnect() { mXHR = nullptr; } + + private: + RefPtr mXHR; +}; + +class nsXHRParseEndListener : public nsIDOMEventListener { + public: + NS_DECL_ISUPPORTS + NS_IMETHOD HandleEvent(Event* event) override { + if (mXHR) { + mXHR->OnBodyParseEnd(); + } + mXHR = nullptr; + return NS_OK; + } + + explicit nsXHRParseEndListener(XMLHttpRequestMainThread* aXHR) : mXHR(aXHR) {} + + void SetIsStale() { mXHR = nullptr; } + + private: + virtual ~nsXHRParseEndListener() = default; + + XMLHttpRequestMainThread* mXHR; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XMLHttpRequestMainThread_h diff --git a/dom/xhr/XMLHttpRequestString.cpp b/dom/xhr/XMLHttpRequestString.cpp new file mode 100644 index 0000000000..d1fd2ccdc1 --- /dev/null +++ b/dom/xhr/XMLHttpRequestString.cpp @@ -0,0 +1,199 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestString.h" +#include "nsISupportsImpl.h" +#include "mozilla/dom/DOMString.h" + +namespace mozilla::dom { + +class XMLHttpRequestStringBuffer final { + friend class XMLHttpRequestStringWriterHelper; + friend class XMLHttpRequestStringSnapshotReaderHelper; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XMLHttpRequestStringBuffer) + NS_DECL_OWNINGTHREAD + + XMLHttpRequestStringBuffer() : mMutex("XMLHttpRequestStringBuffer::mMutex") {} + + uint32_t Length() { + MutexAutoLock lock(mMutex); + return mData.Length(); + } + + uint32_t UnsafeLength() const MOZ_NO_THREAD_SAFETY_ANALYSIS { + return mData.Length(); + } + + mozilla::Result, nsresult> UnsafeBulkWrite( + uint32_t aCapacity) MOZ_NO_THREAD_SAFETY_ANALYSIS { + return mData.BulkWrite(aCapacity, UnsafeLength(), false); + } + + void Append(const nsAString& aString) { + NS_ASSERT_OWNINGTHREAD(XMLHttpRequestStringBuffer); + + MutexAutoLock lock(mMutex); + mData.Append(aString); + } + + [[nodiscard]] bool GetAsString(nsAString& aString) { + MutexAutoLock lock(mMutex); + return aString.Assign(mData, mozilla::fallible); + } + + size_t SizeOfThis(MallocSizeOf aMallocSizeOf) { + MutexAutoLock lock(mMutex); + return mData.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + + [[nodiscard]] bool GetAsString(DOMString& aString, uint32_t aLength) { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(aLength <= mData.Length()); + + // XXX: Bug 1408793 suggests encapsulating the following sequence within + // DOMString. + nsStringBuffer* buf = nsStringBuffer::FromString(mData); + if (buf) { + // We have to use SetStringBuffer, because once we release our mutex mData + // can get mutated from some other thread while the DOMString is still + // alive. + aString.SetStringBuffer(buf, aLength); + return true; + } + + // We can get here if mData is empty. In that case it won't have an + // nsStringBuffer.... + MOZ_ASSERT(mData.IsEmpty()); + return aString.AsAString().Assign(mData.BeginReading(), aLength, + mozilla::fallible); + } + + void CreateSnapshot(XMLHttpRequestStringSnapshot& aSnapshot) { + MutexAutoLock lock(mMutex); + aSnapshot.Set(this, mData.Length()); + } + + private: + ~XMLHttpRequestStringBuffer() = default; + + nsString& UnsafeData() { return mData; } + + Mutex mMutex; + + // The following member variable is protected by mutex. + nsString mData MOZ_GUARDED_BY(mMutex); +}; + +// --------------------------------------------------------------------------- +// XMLHttpRequestString + +XMLHttpRequestString::XMLHttpRequestString() + : mBuffer(new XMLHttpRequestStringBuffer()) {} + +XMLHttpRequestString::~XMLHttpRequestString() = default; + +void XMLHttpRequestString::Truncate() { + mBuffer = new XMLHttpRequestStringBuffer(); +} + +uint32_t XMLHttpRequestString::Length() const { return mBuffer->Length(); } + +void XMLHttpRequestString::Append(const nsAString& aString) { + mBuffer->Append(aString); +} + +bool XMLHttpRequestString::GetAsString(nsAString& aString) const { + return mBuffer->GetAsString(aString); +} + +size_t XMLHttpRequestString::SizeOfThis(MallocSizeOf aMallocSizeOf) const { + return mBuffer->SizeOfThis(aMallocSizeOf); +} + +bool XMLHttpRequestString::IsEmpty() const { return !mBuffer->Length(); } + +void XMLHttpRequestString::CreateSnapshot( + XMLHttpRequestStringSnapshot& aSnapshot) { + mBuffer->CreateSnapshot(aSnapshot); +} + +// --------------------------------------------------------------------------- +// XMLHttpRequestStringSnapshot + +XMLHttpRequestStringSnapshot::XMLHttpRequestStringSnapshot() + : mLength(0), mVoid(false) {} + +XMLHttpRequestStringSnapshot::~XMLHttpRequestStringSnapshot() = default; + +void XMLHttpRequestStringSnapshot::ResetInternal(bool aIsVoid) { + mBuffer = nullptr; + mLength = 0; + mVoid = aIsVoid; +} + +void XMLHttpRequestStringSnapshot::Set(XMLHttpRequestStringBuffer* aBuffer, + uint32_t aLength) { + MOZ_ASSERT(aBuffer); + MOZ_ASSERT(aLength <= aBuffer->UnsafeLength()); + + mBuffer = aBuffer; + mLength = aLength; + mVoid = false; +} + +bool XMLHttpRequestStringSnapshot::GetAsString(DOMString& aString) const { + if (mBuffer) { + MOZ_ASSERT(!mVoid); + return mBuffer->GetAsString(aString, mLength); + } + + if (mVoid) { + aString.SetNull(); + } + + return true; +} + +// --------------------------------------------------------------------------- +// XMLHttpRequestStringWriterHelper + +XMLHttpRequestStringWriterHelper::XMLHttpRequestStringWriterHelper( + XMLHttpRequestString& aString) + : mBuffer(aString.mBuffer), mLock(aString.mBuffer->mMutex) {} + +XMLHttpRequestStringWriterHelper::~XMLHttpRequestStringWriterHelper() = default; + +uint32_t XMLHttpRequestStringWriterHelper::Length() const { + return mBuffer->UnsafeLength(); +} + +mozilla::Result, nsresult> +XMLHttpRequestStringWriterHelper::BulkWrite(uint32_t aCapacity) { + return mBuffer->UnsafeBulkWrite(aCapacity); +} + +// --------------------------------------------------------------------------- +// XMLHttpRequestStringReaderHelper + +XMLHttpRequestStringSnapshotReaderHelper:: + XMLHttpRequestStringSnapshotReaderHelper( + XMLHttpRequestStringSnapshot& aSnapshot) + : mBuffer(aSnapshot.mBuffer), mLock(aSnapshot.mBuffer->mMutex) {} + +XMLHttpRequestStringSnapshotReaderHelper:: + ~XMLHttpRequestStringSnapshotReaderHelper() = default; + +const char16_t* XMLHttpRequestStringSnapshotReaderHelper::Buffer() const { + return mBuffer->UnsafeData().BeginReading(); +} + +uint32_t XMLHttpRequestStringSnapshotReaderHelper::Length() const { + return mBuffer->UnsafeLength(); +} + +} // namespace mozilla::dom diff --git a/dom/xhr/XMLHttpRequestString.h b/dom/xhr/XMLHttpRequestString.h new file mode 100644 index 0000000000..76e072d78b --- /dev/null +++ b/dom/xhr/XMLHttpRequestString.h @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestString_h +#define mozilla_dom_XMLHttpRequestString_h + +#include "mozilla/Mutex.h" +#include "nsString.h" + +namespace mozilla::dom { + +class ArrayBufferBuilder; +class BlobImpl; +class DOMString; +class XMLHttpRequestStringBuffer; +class XMLHttpRequestStringSnapshot; +class XMLHttpRequestStringWriterHelper; +class XMLHttpRequestStringSnapshotReaderHelper; + +// We want to avoid the dup of strings when XHR in workers has access to +// responseText for events dispatched during the loading state. For this reason +// we use this class, able to create snapshots of the current size of itself +// without making extra copies. +class XMLHttpRequestString final { + friend class XMLHttpRequestStringWriterHelper; + + public: + XMLHttpRequestString(); + ~XMLHttpRequestString(); + + void Truncate(); + + uint32_t Length() const; + + void Append(const nsAString& aString); + + // This method should be called only when the string is really needed because + // it can cause the duplication of the strings in case the loading of the XHR + // is not completed yet. + [[nodiscard]] bool GetAsString(nsAString& aString) const; + + size_t SizeOfThis(MallocSizeOf aMallocSizeOf) const; + + const char16_t* Data() const; + + bool IsEmpty() const; + + void CreateSnapshot(XMLHttpRequestStringSnapshot& aSnapshot); + + private: + XMLHttpRequestString(const XMLHttpRequestString&) = delete; + XMLHttpRequestString& operator=(const XMLHttpRequestString&) = delete; + XMLHttpRequestString& operator=(const XMLHttpRequestString&&) = delete; + + RefPtr mBuffer; +}; + +// This class locks the buffer and allows the callee to write data into it. +class MOZ_STACK_CLASS XMLHttpRequestStringWriterHelper final { + public: + explicit XMLHttpRequestStringWriterHelper(XMLHttpRequestString& aString); + ~XMLHttpRequestStringWriterHelper(); + + /** + * The existing length of the string. Do not call during BulkWrite(). + */ + uint32_t Length() const; + + mozilla::Result, nsresult> BulkWrite( + uint32_t aCapacity); + + private: + XMLHttpRequestStringWriterHelper(const XMLHttpRequestStringWriterHelper&) = + delete; + XMLHttpRequestStringWriterHelper& operator=( + const XMLHttpRequestStringWriterHelper&) = delete; + XMLHttpRequestStringWriterHelper& operator=( + const XMLHttpRequestStringWriterHelper&&) = delete; + + RefPtr mBuffer; + MutexAutoLock mLock; +}; + +// This class is the internal XMLHttpRequestStringBuffer of the +// XMLHttpRequestString plus the current length. GetAsString will return the +// string with that particular length also if the XMLHttpRequestStringBuffer is +// grown in the meantime. +class XMLHttpRequestStringSnapshot final { + friend class XMLHttpRequestStringBuffer; + friend class XMLHttpRequestStringSnapshotReaderHelper; + + public: + XMLHttpRequestStringSnapshot(); + ~XMLHttpRequestStringSnapshot(); + + XMLHttpRequestStringSnapshot& operator=(const XMLHttpRequestStringSnapshot&) = + delete; + + void Reset() { ResetInternal(false /* isVoid */); } + + void SetVoid() { ResetInternal(true /* isVoid */); } + + bool IsVoid() const { return mVoid; } + + bool IsEmpty() const { return !mLength; } + + [[nodiscard]] bool GetAsString(DOMString& aString) const; + + private: + XMLHttpRequestStringSnapshot(const XMLHttpRequestStringSnapshot&) = delete; + XMLHttpRequestStringSnapshot& operator=( + const XMLHttpRequestStringSnapshot&&) = delete; + + void Set(XMLHttpRequestStringBuffer* aBuffer, uint32_t aLength); + + void ResetInternal(bool aIsVoid); + + RefPtr mBuffer; + uint32_t mLength; + bool mVoid; +}; + +// This class locks the buffer and allows the callee to read data from it. +class MOZ_STACK_CLASS XMLHttpRequestStringSnapshotReaderHelper final { + public: + explicit XMLHttpRequestStringSnapshotReaderHelper( + XMLHttpRequestStringSnapshot& aSnapshot); + ~XMLHttpRequestStringSnapshotReaderHelper(); + + const char16_t* Buffer() const; + + uint32_t Length() const; + + private: + XMLHttpRequestStringSnapshotReaderHelper( + const XMLHttpRequestStringSnapshotReaderHelper&) = delete; + XMLHttpRequestStringSnapshotReaderHelper& operator=( + const XMLHttpRequestStringSnapshotReaderHelper&) = delete; + XMLHttpRequestStringSnapshotReaderHelper& operator=( + const XMLHttpRequestStringSnapshotReaderHelper&&) = delete; + + RefPtr mBuffer; + MutexAutoLock mLock; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XMLHttpRequestString_h diff --git a/dom/xhr/XMLHttpRequestUpload.cpp b/dom/xhr/XMLHttpRequestUpload.cpp new file mode 100644 index 0000000000..dd908d7f1f --- /dev/null +++ b/dom/xhr/XMLHttpRequestUpload.cpp @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestUpload.h" +#include "mozilla/dom/XMLHttpRequestUploadBinding.h" + +namespace mozilla::dom { + +NS_INTERFACE_MAP_BEGIN(XMLHttpRequestUpload) +NS_INTERFACE_MAP_END_INHERITING(XMLHttpRequestEventTarget) + +NS_IMPL_ADDREF_INHERITED(XMLHttpRequestUpload, XMLHttpRequestEventTarget) +NS_IMPL_RELEASE_INHERITED(XMLHttpRequestUpload, XMLHttpRequestEventTarget) + +/* virtual */ +JSObject* XMLHttpRequestUpload::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return XMLHttpRequestUpload_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/xhr/XMLHttpRequestUpload.h b/dom/xhr/XMLHttpRequestUpload.h new file mode 100644 index 0000000000..126944ff88 --- /dev/null +++ b/dom/xhr/XMLHttpRequestUpload.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestUpload_h +#define mozilla_dom_XMLHttpRequestUpload_h + +#include "mozilla/dom/XMLHttpRequestEventTarget.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "mozilla/EventListenerManager.h" + +namespace mozilla::dom { + +class XMLHttpRequestUpload final : public XMLHttpRequestEventTarget { + public: + explicit XMLHttpRequestUpload(DOMEventTargetHelper* aOwner) + : XMLHttpRequestEventTarget(aOwner) {} + + NS_DECL_ISUPPORTS_INHERITED + + virtual JSObject* WrapObject(JSContext* cx, + JS::Handle aGivenProto) override; + + bool HasListeners() { + return mListenerManager && mListenerManager->HasListeners(); + } + + private: + virtual ~XMLHttpRequestUpload() = default; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XMLHttpRequestUpload_h diff --git a/dom/xhr/XMLHttpRequestWorker.cpp b/dom/xhr/XMLHttpRequestWorker.cpp new file mode 100644 index 0000000000..3916ec3a92 --- /dev/null +++ b/dom/xhr/XMLHttpRequestWorker.cpp @@ -0,0 +1,2233 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestWorker.h" + +#include "nsIDOMEventListener.h" + +#include "GeckoProfiler.h" +#include "jsapi.h" // JS::RootedValueArray +#include "jsfriendapi.h" +#include "js/ArrayBuffer.h" // JS::Is{,Detached}ArrayBufferObject +#include "js/GCPolicyAPI.h" +#include "js/JSON.h" +#include "js/RootingAPI.h" // JS::{Handle,Heap},PersistentRooted +#include "js/TracingAPI.h" +#include "js/Value.h" // JS::{Undefined,}Value +#include "mozilla/ArrayUtils.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/ProgressEvent.h" +#include "mozilla/dom/SerializedStackHolder.h" +#include "mozilla/dom/StreamBlobImpl.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/URLSearchParams.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/XMLHttpRequestBinding.h" +#include "mozilla/Telemetry.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsJSUtils.h" +#include "nsThreadUtils.h" + +#include "XMLHttpRequestMainThread.h" +#include "XMLHttpRequestUpload.h" + +#include "mozilla/UniquePtr.h" + +namespace mozilla::dom { + +/** + * XMLHttpRequest in workers + * + * XHR in workers is implemented by proxying calls/events/etc between the + * worker thread and an XMLHttpRequest on the main thread. The glue + * object here is the Proxy, which lives on both threads. All other objects + * live on either the main thread (the XMLHttpRequest) or the worker thread + * (the worker and XHR private objects). + * + * The main thread XHR is always operated in async mode, even for sync XHR + * in workers. Calls made on the worker thread are proxied to the main thread + * synchronously (meaning the worker thread is blocked until the call + * returns). Each proxied call spins up a sync queue, which captures any + * synchronously dispatched events and ensures that they run synchronously + * on the worker as well. Asynchronously dispatched events are posted to the + * worker thread to run asynchronously. Some of the XHR state is mirrored on + * the worker thread to avoid needing a cross-thread call on every property + * access. + * + * The XHR private is stored in the private slot of the XHR JSObject on the + * worker thread. It is destroyed when that JSObject is GCd. The private + * roots its JSObject while network activity is in progress. It also + * adds itself as a feature to the worker to give itself a chance to clean up + * if the worker goes away during an XHR call. It is important that the + * rooting and feature registration (collectively called pinning) happens at + * the proper times. If we pin for too long we can cause memory leaks or even + * shutdown hangs. If we don't pin for long enough we introduce a GC hazard. + * + * The XHR is pinned from the time Send is called to roughly the time loadend + * is received. There are some complications involved with Abort and XHR + * reuse. We maintain a counter on the main thread of how many times Send was + * called on this XHR, and we decrement the counter every time we receive a + * loadend event. When the counter reaches zero we dispatch a runnable to the + * worker thread to unpin the XHR. We only decrement the counter if the + * dispatch was successful, because the worker may no longer be accepting + * regular runnables. In the event that we reach Proxy::Teardown and there + * the outstanding Send count is still non-zero, we dispatch a control + * runnable which is guaranteed to run. + * + * NB: Some of this could probably be simplified now that we have the + * inner/outer channel ids. + */ + +class Proxy final : public nsIDOMEventListener { + public: + // Read on multiple threads. + WorkerPrivate* mWorkerPrivate; + const ClientInfo mClientInfo; + const Maybe mController; + + // Only ever dereferenced and/or checked on the worker thread. Cleared + // explicitly on the worker thread inside XMLHttpRequestWorker::ReleaseProxy. + WeakPtr mXMLHttpRequestPrivate; + + // XHR Params: + bool mMozAnon; + bool mMozSystem; + + // Only touched on the main thread. + RefPtr mXHR; + RefPtr mXHRUpload; + nsCOMPtr mSyncLoopTarget; + nsCOMPtr mSyncEventResponseTarget; + uint32_t mInnerEventStreamId; + uint32_t mInnerChannelId; + uint32_t mOutstandingSendCount; + + // Only touched on the worker thread. + uint32_t mOuterEventStreamId; + uint32_t mOuterChannelId; + uint32_t mOpenCount; + uint64_t mLastLoaded; + uint64_t mLastTotal; + uint64_t mLastUploadLoaded; + uint64_t mLastUploadTotal; + bool mIsSyncXHR; + bool mLastLengthComputable; + bool mLastUploadLengthComputable; + bool mSeenLoadStart; + bool mSeenUploadLoadStart; + bool mDispatchPrematureAbortEvent; + bool mDispatchPrematureAbortEventToUpload; + + // Only touched on the main thread. + bool mUploadEventListenersAttached; + bool mMainThreadSeenLoadStart; + bool mInOpen; + + public: + Proxy(XMLHttpRequestWorker* aXHRPrivate, const ClientInfo& aClientInfo, + const Maybe& aController, bool aMozAnon, + bool aMozSystem) + : mWorkerPrivate(nullptr), + mClientInfo(aClientInfo), + mController(aController), + mXMLHttpRequestPrivate(aXHRPrivate), + mMozAnon(aMozAnon), + mMozSystem(aMozSystem), + mInnerEventStreamId(0), + mInnerChannelId(0), + mOutstandingSendCount(0), + mOuterEventStreamId(0), + mOuterChannelId(0), + mOpenCount(0), + mLastLoaded(0), + mLastTotal(0), + mLastUploadLoaded(0), + mLastUploadTotal(0), + mIsSyncXHR(false), + mLastLengthComputable(false), + mLastUploadLengthComputable(false), + mSeenLoadStart(false), + mSeenUploadLoadStart(false), + mDispatchPrematureAbortEvent(false), + mDispatchPrematureAbortEventToUpload(false), + mUploadEventListenersAttached(false), + mMainThreadSeenLoadStart(false), + mInOpen(false) {} + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + bool Init(); + + void Teardown(); + + bool AddRemoveEventListeners(bool aUpload, bool aAdd); + + void Reset() { + AssertIsOnMainThread(); + + if (mUploadEventListenersAttached) { + AddRemoveEventListeners(true, false); + } + } + + already_AddRefed GetEventTarget() { + AssertIsOnMainThread(); + + nsCOMPtr target = + mSyncEventResponseTarget ? mSyncEventResponseTarget : mSyncLoopTarget; + return target.forget(); + } + + private: + ~Proxy() { + MOZ_ASSERT(!mXHR); + MOZ_ASSERT(!mXHRUpload); + MOZ_ASSERT(!mOutstandingSendCount); + } +}; + +class WorkerThreadProxySyncRunnable : public WorkerMainThreadRunnable { + protected: + RefPtr mProxy; + + private: + // mErrorCode is set on the main thread by MainThreadRun and it's used to at + // the end of the Dispatch() to return the error code. + nsresult mErrorCode; + + public: + WorkerThreadProxySyncRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy) + : WorkerMainThreadRunnable(aWorkerPrivate, "XHR"_ns), + mProxy(aProxy), + mErrorCode(NS_OK) { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aProxy); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + void Dispatch(WorkerStatus aFailStatus, ErrorResult& aRv) { + WorkerMainThreadRunnable::Dispatch(aFailStatus, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (NS_FAILED(mErrorCode)) { + aRv.Throw(mErrorCode); + } + } + + protected: + virtual ~WorkerThreadProxySyncRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) = 0; + + private: + virtual bool MainThreadRun() override; +}; + +class SendRunnable final : public WorkerThreadProxySyncRunnable { + RefPtr mBlobImpl; + nsCOMPtr mSyncLoopTarget; + bool mHasUploadListeners; + + public: + SendRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + BlobImpl* aBlobImpl) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mBlobImpl(aBlobImpl), + mHasUploadListeners(false) {} + + void SetHaveUploadListeners(bool aHasUploadListeners) { + mHasUploadListeners = aHasUploadListeners; + } + + void SetSyncLoopTarget(nsIEventTarget* aSyncLoopTarget) { + mSyncLoopTarget = aSyncLoopTarget; + } + + private: + ~SendRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override; +}; + +namespace { + +enum { + STRING_abort = 0, + STRING_error, + STRING_load, + STRING_loadstart, + STRING_progress, + STRING_timeout, + STRING_loadend, + STRING_readystatechange, + + STRING_COUNT, + + STRING_LAST_XHR = STRING_readystatechange, + STRING_LAST_EVENTTARGET = STRING_loadend +}; + +static_assert(STRING_LAST_XHR >= STRING_LAST_EVENTTARGET, "Bad string setup!"); +static_assert(STRING_LAST_XHR == STRING_COUNT - 1, "Bad string setup!"); + +const char* const sEventStrings[] = { + // XMLHttpRequestEventTarget event types, supported by both XHR and Upload. + "abort", + "error", + "load", + "loadstart", + "progress", + "timeout", + "loadend", + + // XMLHttpRequest event types, supported only by XHR. + "readystatechange", +}; + +static_assert(MOZ_ARRAY_LENGTH(sEventStrings) == STRING_COUNT, + "Bad string count!"); + +class MainThreadProxyRunnable : public MainThreadWorkerSyncRunnable { + protected: + RefPtr mProxy; + + MainThreadProxyRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy) + : MainThreadWorkerSyncRunnable(aWorkerPrivate, aProxy->GetEventTarget()), + mProxy(aProxy) { + MOZ_ASSERT(aProxy); + } + + virtual ~MainThreadProxyRunnable() = default; +}; + +class AsyncTeardownRunnable final : public Runnable { + RefPtr mProxy; + + public: + explicit AsyncTeardownRunnable(Proxy* aProxy) + : Runnable("dom::AsyncTeardownRunnable"), mProxy(aProxy) { + MOZ_ASSERT(aProxy); + } + + private: + ~AsyncTeardownRunnable() = default; + + NS_IMETHOD + Run() override { + AssertIsOnMainThread(); + + mProxy->Teardown(); + mProxy = nullptr; + + return NS_OK; + } +}; + +class LoadStartDetectionRunnable final : public Runnable, + public nsIDOMEventListener { + WorkerPrivate* mWorkerPrivate; + RefPtr mProxy; + RefPtr mXHR; + nsString mEventType; + uint32_t mChannelId; + bool mReceivedLoadStart; + + class ProxyCompleteRunnable final : public MainThreadProxyRunnable { + uint32_t mChannelId; + + public: + ProxyCompleteRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + uint32_t aChannelId) + : MainThreadProxyRunnable(aWorkerPrivate, aProxy), + mChannelId(aChannelId) {} + + private: + ~ProxyCompleteRunnable() = default; + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + if (mChannelId != mProxy->mOuterChannelId) { + // Threads raced, this event is now obsolete. + return true; + } + + if (mSyncLoopTarget) { + aWorkerPrivate->StopSyncLoop(mSyncLoopTarget, NS_OK); + } + + XMLHttpRequestWorker* xhrw = mProxy->mXMLHttpRequestPrivate.get(); + if (xhrw && xhrw->SendInProgress()) { + xhrw->Unpin(); + } + + return true; + } + + nsresult Cancel() override { + // We need to check first if cancel is called twice + nsresult rv = MainThreadProxyRunnable::Cancel(); + NS_ENSURE_SUCCESS(rv, rv); + + // On the first cancel, this must run! + return Run(); + } + }; + + public: + explicit LoadStartDetectionRunnable(Proxy* aProxy) + : Runnable("dom::LoadStartDetectionRunnable"), + mWorkerPrivate(aProxy->mWorkerPrivate), + mProxy(aProxy), + mXHR(aProxy->mXHR), + mChannelId(mProxy->mInnerChannelId), + mReceivedLoadStart(false) { + AssertIsOnMainThread(); + mEventType.AssignASCII(sEventStrings[STRING_loadstart]); + } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSIDOMEVENTLISTENER + + bool RegisterAndDispatch() { + AssertIsOnMainThread(); + + if (NS_FAILED(mXHR->AddEventListener(mEventType, this, false, false))) { + NS_WARNING("Failed to add event listener!"); + return false; + } + + return NS_SUCCEEDED(mWorkerPrivate->DispatchToMainThread(this)); + } + + private: + ~LoadStartDetectionRunnable() { AssertIsOnMainThread(); } +}; + +class EventRunnable final : public MainThreadProxyRunnable { + nsString mType; + UniquePtr mResponseData; + nsString mResponseURL; + nsCString mStatusText; + uint64_t mLoaded; + uint64_t mTotal; + uint32_t mEventStreamId; + uint32_t mStatus; + uint16_t mReadyState; + bool mUploadEvent; + bool mProgressEvent; + bool mLengthComputable; + nsresult mStatusResult; + // mScopeObj is used in PreDispatch only. We init it in our constructor, and + // reset() in PreDispatch, to ensure that it's not still linked into the + // runtime once we go off-thread. + JS::PersistentRooted mScopeObj; + + public: + EventRunnable(Proxy* aProxy, bool aUploadEvent, const nsString& aType, + bool aLengthComputable, uint64_t aLoaded, uint64_t aTotal, + JS::Handle aScopeObj) + : MainThreadProxyRunnable(aProxy->mWorkerPrivate, aProxy), + mType(aType), + mResponseData(new XMLHttpRequestWorker::ResponseData()), + mLoaded(aLoaded), + mTotal(aTotal), + mEventStreamId(aProxy->mInnerEventStreamId), + mStatus(0), + mReadyState(0), + mUploadEvent(aUploadEvent), + mProgressEvent(true), + mLengthComputable(aLengthComputable), + mStatusResult(NS_OK), + mScopeObj(RootingCx(), aScopeObj) {} + + EventRunnable(Proxy* aProxy, bool aUploadEvent, const nsString& aType, + JS::Handle aScopeObj) + : MainThreadProxyRunnable(aProxy->mWorkerPrivate, aProxy), + mType(aType), + mResponseData(new XMLHttpRequestWorker::ResponseData()), + mLoaded(0), + mTotal(0), + mEventStreamId(aProxy->mInnerEventStreamId), + mStatus(0), + mReadyState(0), + mUploadEvent(aUploadEvent), + mProgressEvent(false), + mLengthComputable(0), + mStatusResult(NS_OK), + mScopeObj(RootingCx(), aScopeObj) {} + + private: + ~EventRunnable() = default; + + bool PreDispatch(WorkerPrivate* /* unused */) final; + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; +}; + +class SyncTeardownRunnable final : public WorkerThreadProxySyncRunnable { + public: + SyncTeardownRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy) {} + + private: + ~SyncTeardownRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + mProxy->Teardown(); + MOZ_ASSERT(!mProxy->mSyncLoopTarget); + } +}; + +class SetBackgroundRequestRunnable final + : public WorkerThreadProxySyncRunnable { + bool mValue; + + public: + SetBackgroundRequestRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + bool aValue) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), mValue(aValue) {} + + private: + ~SetBackgroundRequestRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + // XXXedgar, do we intend to ignore the errors? + mProxy->mXHR->SetMozBackgroundRequest(mValue, aRv); + } +}; + +class SetWithCredentialsRunnable final : public WorkerThreadProxySyncRunnable { + bool mValue; + + public: + SetWithCredentialsRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + bool aValue) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), mValue(aValue) {} + + private: + ~SetWithCredentialsRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + mProxy->mXHR->SetWithCredentials(mValue, aRv); + } +}; + +class SetResponseTypeRunnable final : public WorkerThreadProxySyncRunnable { + XMLHttpRequestResponseType mResponseType; + + public: + SetResponseTypeRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + XMLHttpRequestResponseType aResponseType) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mResponseType(aResponseType) {} + + XMLHttpRequestResponseType ResponseType() { return mResponseType; } + + private: + ~SetResponseTypeRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + mProxy->mXHR->SetResponseTypeRaw(mResponseType); + mResponseType = mProxy->mXHR->ResponseType(); + } +}; + +class SetTimeoutRunnable final : public WorkerThreadProxySyncRunnable { + uint32_t mTimeout; + + public: + SetTimeoutRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + uint32_t aTimeout) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mTimeout(aTimeout) {} + + private: + ~SetTimeoutRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + mProxy->mXHR->SetTimeout(mTimeout, aRv); + } +}; + +class AbortRunnable final : public WorkerThreadProxySyncRunnable { + public: + AbortRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy) {} + + private: + ~AbortRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override; +}; + +class GetAllResponseHeadersRunnable final + : public WorkerThreadProxySyncRunnable { + nsCString& mResponseHeaders; + + public: + GetAllResponseHeadersRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + nsCString& aResponseHeaders) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mResponseHeaders(aResponseHeaders) {} + + private: + ~GetAllResponseHeadersRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + mProxy->mXHR->GetAllResponseHeaders(mResponseHeaders, aRv); + } +}; + +class GetResponseHeaderRunnable final : public WorkerThreadProxySyncRunnable { + const nsCString mHeader; + nsCString& mValue; + + public: + GetResponseHeaderRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsACString& aHeader, nsCString& aValue) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mHeader(aHeader), + mValue(aValue) {} + + private: + ~GetResponseHeaderRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + mProxy->mXHR->GetResponseHeader(mHeader, mValue, aRv); + } +}; + +class OpenRunnable final : public WorkerThreadProxySyncRunnable { + nsCString mMethod; + nsString mURL; + Optional mUser; + nsString mUserStr; + Optional mPassword; + nsString mPasswordStr; + bool mBackgroundRequest; + bool mWithCredentials; + uint32_t mTimeout; + XMLHttpRequestResponseType mResponseType; + const nsString mMimeTypeOverride; + + // Remember the worker thread's stack when the XHR was opened, so that it can + // be passed on to the net monitor. + UniquePtr mOriginStack; + + // Remember the worker thread's stack when the XHR was opened for profiling + // purposes. + UniquePtr mSource; + + public: + OpenRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsACString& aMethod, const nsAString& aURL, + const Optional& aUser, + const Optional& aPassword, bool aBackgroundRequest, + bool aWithCredentials, uint32_t aTimeout, + XMLHttpRequestResponseType aResponseType, + const nsString& aMimeTypeOverride, + UniquePtr aOriginStack, + UniquePtr aSource = nullptr) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mMethod(aMethod), + mURL(aURL), + mBackgroundRequest(aBackgroundRequest), + mWithCredentials(aWithCredentials), + mTimeout(aTimeout), + mResponseType(aResponseType), + mMimeTypeOverride(aMimeTypeOverride), + mOriginStack(std::move(aOriginStack)), + mSource(std::move(aSource)) { + if (aUser.WasPassed()) { + mUserStr = aUser.Value(); + mUser = &mUserStr; + } + if (aPassword.WasPassed()) { + mPasswordStr = aPassword.Value(); + mPassword = &mPasswordStr; + } + } + + private: + ~OpenRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + WorkerPrivate* oldWorker = mProxy->mWorkerPrivate; + mProxy->mWorkerPrivate = mWorkerPrivate; + + MainThreadRunInternal(aRv); + + mProxy->mWorkerPrivate = oldWorker; + } + + void MainThreadRunInternal(ErrorResult& aRv); +}; + +class SetRequestHeaderRunnable final : public WorkerThreadProxySyncRunnable { + nsCString mHeader; + nsCString mValue; + + public: + SetRequestHeaderRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsACString& aHeader, const nsACString& aValue) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mHeader(aHeader), + mValue(aValue) {} + + private: + ~SetRequestHeaderRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + mProxy->mXHR->SetRequestHeader(mHeader, mValue, aRv); + } +}; + +class OverrideMimeTypeRunnable final : public WorkerThreadProxySyncRunnable { + nsString mMimeType; + + public: + OverrideMimeTypeRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsAString& aMimeType) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mMimeType(aMimeType) {} + + private: + ~OverrideMimeTypeRunnable() = default; + + virtual void RunOnMainThread(ErrorResult& aRv) override { + mProxy->mXHR->OverrideMimeType(mMimeType, aRv); + } +}; + +class AutoUnpinXHR { + XMLHttpRequestWorker* mXMLHttpRequestPrivate; + + public: + explicit AutoUnpinXHR(XMLHttpRequestWorker* aXMLHttpRequestPrivate) + : mXMLHttpRequestPrivate(aXMLHttpRequestPrivate) { + MOZ_ASSERT(aXMLHttpRequestPrivate); + } + + ~AutoUnpinXHR() { + if (mXMLHttpRequestPrivate) { + mXMLHttpRequestPrivate->Unpin(); + } + } + + void Clear() { mXMLHttpRequestPrivate = nullptr; } +}; + +} // namespace + +bool Proxy::Init() { + AssertIsOnMainThread(); + MOZ_ASSERT(mWorkerPrivate); + + if (mXHR) { + return true; + } + + nsPIDOMWindowInner* ownerWindow = mWorkerPrivate->GetWindow(); + if (ownerWindow && !ownerWindow->IsCurrentInnerWindow()) { + NS_WARNING("Window has navigated, cannot create XHR here."); + return false; + } + + mXHR = new XMLHttpRequestMainThread(ownerWindow ? ownerWindow->AsGlobal() + : nullptr); + mXHR->Construct(mWorkerPrivate->GetPrincipal(), + mWorkerPrivate->CookieJarSettings(), true, + mWorkerPrivate->GetBaseURI(), mWorkerPrivate->GetLoadGroup(), + mWorkerPrivate->GetPerformanceStorage(), + mWorkerPrivate->CSPEventListener()); + + mXHR->SetParameters(mMozAnon, mMozSystem); + mXHR->SetClientInfoAndController(mClientInfo, mController); + + ErrorResult rv; + mXHRUpload = mXHR->GetUpload(rv); + if (NS_WARN_IF(rv.Failed())) { + mXHR = nullptr; + return false; + } + + if (!AddRemoveEventListeners(false, true)) { + mXHR = nullptr; + mXHRUpload = nullptr; + return false; + } + + return true; +} + +void Proxy::Teardown() { + AssertIsOnMainThread(); + + if (mXHR) { + Reset(); + + // NB: We are intentionally dropping events coming from xhr.abort on the + // floor. + AddRemoveEventListeners(false, false); + + ErrorResult rv; + mXHR->Abort(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + + if (mOutstandingSendCount) { + if (mSyncLoopTarget) { + // We have an unclosed sync loop. Fix that now. + RefPtr runnable = + new MainThreadStopSyncLoopRunnable( + mWorkerPrivate, std::move(mSyncLoopTarget), NS_ERROR_FAILURE); + MOZ_ALWAYS_TRUE(runnable->Dispatch()); + } + + mOutstandingSendCount = 0; + } + + mWorkerPrivate = nullptr; + mXHRUpload = nullptr; + mXHR = nullptr; + } + + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(!mSyncLoopTarget); + // If there are rare edge cases left that violate our invariants + // just ensure that they won't harm us too much. + mWorkerPrivate = nullptr; + mSyncLoopTarget = nullptr; +} + +bool Proxy::AddRemoveEventListeners(bool aUpload, bool aAdd) { + AssertIsOnMainThread(); + + NS_ASSERTION(!aUpload || (mUploadEventListenersAttached && !aAdd) || + (!mUploadEventListenersAttached && aAdd), + "Messed up logic for upload listeners!"); + + RefPtr targetHelper = + aUpload ? static_cast(mXHRUpload.get()) + : static_cast(mXHR.get()); + MOZ_ASSERT(targetHelper, "This should never fail!"); + + uint32_t lastEventType = aUpload ? STRING_LAST_EVENTTARGET : STRING_LAST_XHR; + + nsAutoString eventType; + for (uint32_t index = 0; index <= lastEventType; index++) { + eventType = NS_ConvertASCIItoUTF16(sEventStrings[index]); + if (aAdd) { + if (NS_FAILED(targetHelper->AddEventListener(eventType, this, false))) { + return false; + } + } else { + targetHelper->RemoveEventListener(eventType, this, false); + } + } + + if (aUpload) { + mUploadEventListenersAttached = aAdd; + } + + return true; +} + +NS_IMPL_ISUPPORTS(Proxy, nsIDOMEventListener) + +NS_IMETHODIMP +Proxy::HandleEvent(Event* aEvent) { + AssertIsOnMainThread(); + + // EventRunnable::WorkerRun will bail out if mXMLHttpRequestWorker is null, + // so we do not need to prevent the dispatch from the main thread such that + // we do not need to touch it off-worker-thread. + if (!mWorkerPrivate) { + NS_ERROR("Shouldn't get here!"); + return NS_OK; + } + + nsAutoString type; + aEvent->GetType(type); + + bool isUploadTarget = mXHR != aEvent->GetTarget(); + ProgressEvent* progressEvent = aEvent->AsProgressEvent(); + + if (mInOpen && type.EqualsASCII(sEventStrings[STRING_readystatechange])) { + if (mXHR->ReadyState() == 1) { + mInnerEventStreamId++; + } + } + + { + AutoJSAPI jsapi; + JSObject* junkScope = xpc::UnprivilegedJunkScope(fallible); + if (!junkScope || !jsapi.Init(junkScope)) { + return NS_ERROR_FAILURE; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted value(cx); + if (!GetOrCreateDOMReflectorNoWrap(cx, mXHR, &value)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted scope(cx, &value.toObject()); + + RefPtr runnable; + if (progressEvent) { + if (!mIsSyncXHR || !type.EqualsASCII(sEventStrings[STRING_progress])) { + runnable = new EventRunnable( + this, isUploadTarget, type, progressEvent->LengthComputable(), + progressEvent->Loaded(), progressEvent->Total(), scope); + } + } else { + runnable = new EventRunnable(this, isUploadTarget, type, scope); + } + + if (runnable) { + runnable->Dispatch(); + } + } + + if (!isUploadTarget) { + if (type.EqualsASCII(sEventStrings[STRING_loadstart])) { + mMainThreadSeenLoadStart = true; + } else if (mMainThreadSeenLoadStart && + type.EqualsASCII(sEventStrings[STRING_loadend])) { + mMainThreadSeenLoadStart = false; + + RefPtr runnable = + new LoadStartDetectionRunnable(this); + if (!runnable->RegisterAndDispatch()) { + NS_WARNING("Failed to dispatch LoadStartDetectionRunnable!"); + } + } + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED(LoadStartDetectionRunnable, Runnable, + nsIDOMEventListener) + +NS_IMETHODIMP +LoadStartDetectionRunnable::Run() { + AssertIsOnMainThread(); + + mXHR->RemoveEventListener(mEventType, this, false); + + if (!mReceivedLoadStart) { + if (mProxy->mOutstandingSendCount > 1) { + mProxy->mOutstandingSendCount--; + } else if (mProxy->mOutstandingSendCount == 1) { + mProxy->Reset(); + + RefPtr runnable = + new ProxyCompleteRunnable(mWorkerPrivate, mProxy, mChannelId); + if (runnable->Dispatch()) { + mProxy->mWorkerPrivate = nullptr; + mProxy->mSyncLoopTarget = nullptr; + mProxy->mOutstandingSendCount--; + } + } + } + + mProxy = nullptr; + mXHR = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +LoadStartDetectionRunnable::HandleEvent(Event* aEvent) { + AssertIsOnMainThread(); + +#ifdef DEBUG + { + nsAutoString type; + aEvent->GetType(type); + MOZ_ASSERT(type == mEventType); + } +#endif + + mReceivedLoadStart = true; + return NS_OK; +} + +bool EventRunnable::PreDispatch(WorkerPrivate* /* unused */) { + AssertIsOnMainThread(); + + AutoJSAPI jsapi; + DebugOnly ok = jsapi.Init(xpc::NativeGlobal(mScopeObj)); + MOZ_ASSERT(ok); + JSContext* cx = jsapi.cx(); + // Now keep the mScopeObj alive for the duration + JS::Rooted scopeObj(cx, mScopeObj); + // And reset mScopeObj now, before we have a chance to run its destructor on + // some background thread. + mScopeObj.reset(); + + RefPtr& xhr = mProxy->mXHR; + MOZ_ASSERT(xhr); + + ErrorResult rv; + + XMLHttpRequestResponseType type = xhr->ResponseType(); + + // We want to take the result data only if this is available. + if (mType.EqualsASCII(sEventStrings[STRING_readystatechange])) { + switch (type) { + case XMLHttpRequestResponseType::_empty: + case XMLHttpRequestResponseType::Text: { + xhr->GetResponseText(mResponseData->mResponseText, rv); + mResponseData->mResponseResult = rv.StealNSResult(); + break; + } + + case XMLHttpRequestResponseType::Blob: { + mResponseData->mResponseBlobImpl = xhr->GetResponseBlobImpl(); + break; + } + + case XMLHttpRequestResponseType::Arraybuffer: { + mResponseData->mResponseArrayBufferBuilder = + xhr->GetResponseArrayBufferBuilder(); + break; + } + + case XMLHttpRequestResponseType::Json: { + mResponseData->mResponseResult = + xhr->GetResponseTextForJSON(mResponseData->mResponseJSON); + break; + } + + default: + MOZ_ASSERT_UNREACHABLE("Invalid response type"); + return false; + } + } + + mStatus = xhr->GetStatus(rv); + mStatusResult = rv.StealNSResult(); + + xhr->GetStatusText(mStatusText, rv); + MOZ_ASSERT(!rv.Failed()); + + mReadyState = xhr->ReadyState(); + + xhr->GetResponseURL(mResponseURL); + + return true; +} + +bool EventRunnable::WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { + if (mEventStreamId != mProxy->mOuterEventStreamId) { + // Threads raced, this event is now obsolete. + return true; + } + + if (!mProxy->mXMLHttpRequestPrivate) { + // Object was finalized, bail. + return true; + } + + if (mType.EqualsASCII(sEventStrings[STRING_loadstart])) { + if (mUploadEvent) { + mProxy->mSeenUploadLoadStart = true; + } else { + mProxy->mSeenLoadStart = true; + } + } else if (mType.EqualsASCII(sEventStrings[STRING_loadend])) { + if (mUploadEvent) { + mProxy->mSeenUploadLoadStart = false; + if (mProxy->mDispatchPrematureAbortEventToUpload) { + // We've already dispatched premature abort events. + return true; + } + } else { + mProxy->mSeenLoadStart = false; + if (mProxy->mDispatchPrematureAbortEvent) { + // We've already dispatched premature abort events. + return true; + } + } + } else if (mType.EqualsASCII(sEventStrings[STRING_abort])) { + if ((mUploadEvent && mProxy->mDispatchPrematureAbortEventToUpload) || + (!mUploadEvent && mProxy->mDispatchPrematureAbortEvent)) { + // We've already dispatched premature abort events. + return true; + } + } + + if (mProgressEvent) { + // Cache these for premature abort events. + if (mUploadEvent) { + mProxy->mLastUploadLengthComputable = mLengthComputable; + mProxy->mLastUploadLoaded = mLoaded; + mProxy->mLastUploadTotal = mTotal; + } else { + mProxy->mLastLengthComputable = mLengthComputable; + mProxy->mLastLoaded = mLoaded; + mProxy->mLastTotal = mTotal; + } + } + + UniquePtr state( + new XMLHttpRequestWorker::StateData()); + + state->mStatusResult = mStatusResult; + state->mStatus = mStatus; + + state->mStatusText = mStatusText; + + state->mReadyState = mReadyState; + + state->mResponseURL = mResponseURL; + + XMLHttpRequestWorker* xhr = mProxy->mXMLHttpRequestPrivate; + xhr->UpdateState(std::move(state), + mType.EqualsASCII(sEventStrings[STRING_readystatechange]) + ? std::move(mResponseData) + : nullptr); + + if (mUploadEvent && !xhr->GetUploadObjectNoCreate()) { + return true; + } + + XMLHttpRequestEventTarget* target; + if (mUploadEvent) { + target = xhr->GetUploadObjectNoCreate(); + } else { + target = xhr; + } + + MOZ_ASSERT(target); + + RefPtr event; + if (mProgressEvent) { + ProgressEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mLengthComputable = mLengthComputable; + init.mLoaded = mLoaded; + init.mTotal = mTotal; + + event = ProgressEvent::Constructor(target, mType, init); + } else { + event = NS_NewDOMEvent(target, nullptr, nullptr); + + if (event) { + event->InitEvent(mType, false, false); + } + } + + if (!event) { + return false; + } + + event->SetTrusted(true); + + target->DispatchEvent(*event); + + return true; +} + +bool WorkerThreadProxySyncRunnable::MainThreadRun() { + AssertIsOnMainThread(); + + nsCOMPtr tempTarget = mSyncLoopTarget; + + mProxy->mSyncEventResponseTarget.swap(tempTarget); + + ErrorResult rv; + RunOnMainThread(rv); + mErrorCode = rv.StealNSResult(); + + mProxy->mSyncEventResponseTarget.swap(tempTarget); + + return true; +} + +void AbortRunnable::RunOnMainThread(ErrorResult& aRv) { + mProxy->mInnerEventStreamId++; + + WorkerPrivate* oldWorker = mProxy->mWorkerPrivate; + mProxy->mWorkerPrivate = mWorkerPrivate; + + mProxy->mXHR->Abort(aRv); + + mProxy->mWorkerPrivate = oldWorker; + + mProxy->Reset(); +} + +void OpenRunnable::MainThreadRunInternal(ErrorResult& aRv) { + if (!mProxy->Init()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mBackgroundRequest) { + mProxy->mXHR->SetMozBackgroundRequestExternal(mBackgroundRequest, aRv); + if (aRv.Failed()) { + return; + } + } + + if (mOriginStack) { + mProxy->mXHR->SetOriginStack(std::move(mOriginStack)); + } + + if (mWithCredentials) { + mProxy->mXHR->SetWithCredentials(mWithCredentials, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + if (mTimeout) { + mProxy->mXHR->SetTimeout(mTimeout, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + if (!mMimeTypeOverride.IsVoid()) { + mProxy->mXHR->OverrideMimeType(mMimeTypeOverride, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + MOZ_ASSERT(!mProxy->mInOpen); + mProxy->mInOpen = true; + + mProxy->mXHR->Open( + mMethod, mURL, true, mUser.WasPassed() ? mUser.Value() : VoidString(), + mPassword.WasPassed() ? mPassword.Value() : VoidString(), aRv); + + MOZ_ASSERT(mProxy->mInOpen); + mProxy->mInOpen = false; + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (mSource) { + mProxy->mXHR->SetSource(std::move(mSource)); + } + + mProxy->mXHR->SetResponseType(mResponseType, aRv); +} + +void SendRunnable::RunOnMainThread(ErrorResult& aRv) { + // Before we change any state let's check if we can send. + if (!mProxy->mXHR->CanSend(aRv)) { + return; + } + + Nullable< + DocumentOrBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString> + payload; + + if (!mBlobImpl) { + payload.SetNull(); + } else { + JS::Rooted globalObject(RootingCx(), + xpc::UnprivilegedJunkScope(fallible)); + if (NS_WARN_IF(!globalObject)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr parent = xpc::NativeGlobal(globalObject); + if (NS_WARN_IF(!parent)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr blob = Blob::Create(parent, mBlobImpl); + MOZ_ASSERT(blob); + + DocumentOrBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString& + ref = payload.SetValue(); + ref.SetAsBlob() = blob; + } + + // Send() has been already called, reset the proxy. + if (mProxy->mWorkerPrivate) { + mProxy->Reset(); + } + + mProxy->mWorkerPrivate = mWorkerPrivate; + + MOZ_ASSERT(!mProxy->mSyncLoopTarget); + mProxy->mSyncLoopTarget.swap(mSyncLoopTarget); + + if (mHasUploadListeners) { + // Send() can be called more than once before failure, + // so don't attach the upload listeners more than once. + if (!mProxy->mUploadEventListenersAttached && + !mProxy->AddRemoveEventListeners(true, true)) { + MOZ_ASSERT(false, "This should never fail!"); + } + } + + mProxy->mInnerChannelId++; + + mProxy->mXHR->Send(payload, aRv); + + if (!aRv.Failed()) { + mProxy->mOutstandingSendCount++; + + if (!mHasUploadListeners) { + // Send() can be called more than once before failure, + // so don't attach the upload listeners more than once. + if (!mProxy->mUploadEventListenersAttached && + !mProxy->AddRemoveEventListeners(true, true)) { + MOZ_ASSERT(false, "This should never fail!"); + } + } + } else { + // In case of failure we just break the sync loop + mProxy->mSyncLoopTarget = nullptr; + mSyncLoopTarget = nullptr; + } +} + +XMLHttpRequestWorker::XMLHttpRequestWorker(WorkerPrivate* aWorkerPrivate, + nsIGlobalObject* aGlobalObject) + : XMLHttpRequest(aGlobalObject), + mWorkerPrivate(aWorkerPrivate), + mResponseType(XMLHttpRequestResponseType::_empty), + mStateData(new StateData()), + mResponseData(new ResponseData()), + mResponseArrayBufferValue(nullptr), + mResponseJSONValue(JS::UndefinedValue()), + mTimeout(0), + mBackgroundRequest(false), + mWithCredentials(false), + mCanceled(false), + mFlagSendActive(false), + mMozAnon(false), + mMozSystem(false), + mMimeTypeOverride(VoidString()) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + mozilla::HoldJSObjects(this); +} + +XMLHttpRequestWorker::~XMLHttpRequestWorker() { + mWorkerPrivate->AssertIsOnWorkerThread(); + + ReleaseProxy(XHRIsGoingAway); + + MOZ_ASSERT(!mWorkerRef); + + mozilla::DropJSObjects(this); +} + +NS_IMPL_ADDREF_INHERITED(XMLHttpRequestWorker, XMLHttpRequestEventTarget) +NS_IMPL_RELEASE_INHERITED(XMLHttpRequestWorker, XMLHttpRequestEventTarget) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XMLHttpRequestWorker) +NS_INTERFACE_MAP_END_INHERITING(XMLHttpRequestEventTarget) + +NS_IMPL_CYCLE_COLLECTION_CLASS(XMLHttpRequestWorker) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XMLHttpRequestWorker, + XMLHttpRequestEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUpload) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResponseBlob) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XMLHttpRequestWorker, + XMLHttpRequestEventTarget) + tmp->ReleaseProxy(XHRIsGoingAway); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mUpload) + tmp->mResponseData = nullptr; + tmp->mResponseBlob = nullptr; + tmp->mResponseArrayBufferValue = nullptr; + tmp->mResponseJSONValue.setUndefined(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(XMLHttpRequestWorker, + XMLHttpRequestEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResponseArrayBufferValue) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResponseJSONValue) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +/* static */ +already_AddRefed XMLHttpRequestWorker::Construct( + const GlobalObject& aGlobal, const MozXMLHttpRequestParameters& aParams, + ErrorResult& aRv) { + JSContext* cx = aGlobal.Context(); + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); + MOZ_ASSERT(workerPrivate); + + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr xhr = + new XMLHttpRequestWorker(workerPrivate, global); + + if (workerPrivate->XHRParamsAllowed()) { + if (aParams.mMozSystem) + xhr->mMozAnon = true; + else + xhr->mMozAnon = aParams.mMozAnon; + xhr->mMozSystem = aParams.mMozSystem; + } + + return xhr.forget(); +} + +void XMLHttpRequestWorker::ReleaseProxy(ReleaseType aType) { + // Can't assert that we're on the worker thread here because mWorkerPrivate + // may be gone. + + if (mProxy) { + if (aType == XHRIsGoingAway) { + // Coming here means the XHR was GC'd, so we can't be pinned. + MOZ_ASSERT(!mProxy->mXMLHttpRequestPrivate || + !mProxy->mXMLHttpRequestPrivate->mPinnedSelfRef); + + // We need to clear our weak pointer on the worker thread, let's do it now + // before doing it implicitly in the Proxy dtor on the wrong thread. + mProxy->mXMLHttpRequestPrivate = nullptr; + + // We're in a GC finalizer, so we can't do a sync call here (and we don't + // need to). + RefPtr runnable = + new AsyncTeardownRunnable(mProxy); + mProxy = nullptr; + + if (NS_FAILED(mWorkerPrivate->DispatchToMainThread(runnable.forget()))) { + NS_ERROR("Failed to dispatch teardown runnable!"); + } + } else { + // This isn't necessary if the worker is going away or the XHR is going + // away. + if (aType == Default) { + // Don't let any more events run. + mProxy->mOuterEventStreamId++; + } + + // Ensure we are unpinned before we clear the weak reference. + RefPtr self = this; + if (mPinnedSelfRef) { + Unpin(); + } + mProxy->mXMLHttpRequestPrivate = nullptr; + + // We need to make a sync call here. + RefPtr runnable = + new SyncTeardownRunnable(mWorkerPrivate, mProxy); + mProxy = nullptr; + + IgnoredErrorResult forAssertionsOnly; + // This runnable _must_ be executed. + runnable->Dispatch(Dead, forAssertionsOnly); + MOZ_DIAGNOSTIC_ASSERT(!forAssertionsOnly.Failed()); + } + } +} + +void XMLHttpRequestWorker::MaybePin(ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mWorkerRef) { + return; + } + + RefPtr self = this; + mWorkerRef = + StrongWorkerRef::Create(mWorkerPrivate, "XMLHttpRequestWorker", [self]() { + if (!self->mCanceled) { + self->mCanceled = true; + self->ReleaseProxy(WorkerIsGoingAway); + } + }); + if (NS_WARN_IF(!mWorkerRef)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + mPinnedSelfRef = this; +} + +void XMLHttpRequestWorker::MaybeDispatchPrematureAbortEvents(ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mProxy); + + // Only send readystatechange event when state changed. + bool isStateChanged = false; + if ((mStateData->mReadyState == 1 && mStateData->mFlagSend) || + mStateData->mReadyState == 2 || mStateData->mReadyState == 3) { + isStateChanged = true; + mStateData->mReadyState = 4; + } + + if (mProxy->mSeenUploadLoadStart) { + MOZ_ASSERT(mUpload); + + DispatchPrematureAbortEvent(mUpload, u"abort"_ns, true, aRv); + if (aRv.Failed()) { + return; + } + + DispatchPrematureAbortEvent(mUpload, u"loadend"_ns, true, aRv); + if (aRv.Failed()) { + return; + } + + // Similarly to null check in ::Open, mProxy may have been cleared here. + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mProxy->mSeenUploadLoadStart = false; + mProxy->mDispatchPrematureAbortEventToUpload = true; + } + + if (mProxy->mSeenLoadStart) { + if (isStateChanged) { + DispatchPrematureAbortEvent(this, u"readystatechange"_ns, false, aRv); + if (aRv.Failed()) { + return; + } + } + + DispatchPrematureAbortEvent(this, u"abort"_ns, false, aRv); + if (aRv.Failed()) { + return; + } + + DispatchPrematureAbortEvent(this, u"loadend"_ns, false, aRv); + if (aRv.Failed()) { + return; + } + + // Similarly to null check in ::Open, mProxy may have been cleared here. + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mProxy->mSeenLoadStart = false; + mProxy->mDispatchPrematureAbortEvent = true; + } +} + +void XMLHttpRequestWorker::DispatchPrematureAbortEvent( + EventTarget* aTarget, const nsAString& aEventType, bool aUploadTarget, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aTarget); + + if (!mProxy) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr event; + if (aEventType.EqualsLiteral("readystatechange")) { + event = NS_NewDOMEvent(aTarget, nullptr, nullptr); + event->InitEvent(aEventType, false, false); + } else { + if (mProxy->mIsSyncXHR && + aEventType.EqualsASCII(sEventStrings[STRING_progress])) { + return; + } + + ProgressEventInit init; + init.mBubbles = false; + init.mCancelable = false; + if (aUploadTarget) { + init.mLengthComputable = mProxy->mLastUploadLengthComputable; + init.mLoaded = mProxy->mLastUploadLoaded; + init.mTotal = mProxy->mLastUploadTotal; + } else { + init.mLengthComputable = mProxy->mLastLengthComputable; + init.mLoaded = mProxy->mLastLoaded; + init.mTotal = mProxy->mLastTotal; + } + event = ProgressEvent::Constructor(aTarget, aEventType, init); + } + + if (!event) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +void XMLHttpRequestWorker::Unpin() { + mWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_ASSERT(mWorkerRef, "Mismatched calls to Unpin!"); + mWorkerRef = nullptr; + + mPinnedSelfRef = nullptr; +} + +void XMLHttpRequestWorker::SendInternal(const BodyExtractorBase* aBody, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + // We don't really need to keep the same body-type when we proxy the send() + // call to the main-thread XHR. Let's extract the nsIInputStream from the + // aBody and let's wrap it into a StreamBlobImpl. + + RefPtr blobImpl; + + if (aBody) { + nsAutoCString charset; + nsAutoCString defaultContentType; + nsCOMPtr uploadStream; + + uint64_t size_u64; + aRv = aBody->GetAsStream(getter_AddRefs(uploadStream), &size_u64, + defaultContentType, charset); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + blobImpl = StreamBlobImpl::Create(uploadStream.forget(), + NS_ConvertUTF8toUTF16(defaultContentType), + size_u64, u"StreamBlobImpl"_ns); + MOZ_ASSERT(blobImpl); + } + + RefPtr sendRunnable = + new SendRunnable(mWorkerPrivate, mProxy, blobImpl); + + // No send() calls when open is running. + if (mProxy->mOpenCount) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + bool hasUploadListeners = mUpload ? mUpload->HasListeners() : false; + + MaybePin(aRv); + if (aRv.Failed()) { + return; + } + + RefPtr selfRef = this; + AutoUnpinXHR autoUnpin(this); + Maybe autoSyncLoop; + + nsCOMPtr syncLoopTarget; + bool isSyncXHR = mProxy->mIsSyncXHR; + if (isSyncXHR) { + autoSyncLoop.emplace(mWorkerPrivate, Canceling); + syncLoopTarget = autoSyncLoop->GetSerialEventTarget(); + if (!syncLoopTarget) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + } + + mProxy->mOuterChannelId++; + mProxy->mDispatchPrematureAbortEvent = false; + mProxy->mDispatchPrematureAbortEventToUpload = false; + + sendRunnable->SetSyncLoopTarget(syncLoopTarget); + sendRunnable->SetHaveUploadListeners(hasUploadListeners); + + mStateData->mFlagSend = true; + + sendRunnable->Dispatch(Canceling, aRv); + if (aRv.Failed()) { + // Dispatch() may have spun the event loop and we may have already unrooted. + // If so we don't want autoUnpin to try again. + if (!mWorkerRef) { + autoUnpin.Clear(); + } + return; + } + + if (!isSyncXHR) { + autoUnpin.Clear(); + MOZ_ASSERT(!autoSyncLoop); + return; + } + + autoUnpin.Clear(); + + bool succeeded = NS_SUCCEEDED(autoSyncLoop->Run()); + mStateData->mFlagSend = false; + + // Don't clobber an existing exception that we may have thrown on aRv + // already... though can there really be one? In any case, it seems to me + // that this autoSyncLoop->Run() can never fail, since the StopSyncLoop call + // for it will come from ProxyCompleteRunnable and that always passes true for + // the second arg. + if (!succeeded && !aRv.Failed()) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +void XMLHttpRequestWorker::Open(const nsACString& aMethod, + const nsAString& aUrl, bool aAsync, + const Optional& aUser, + const Optional& aPassword, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + bool alsoOverrideMimeType = false; + if (mProxy) { + MaybeDispatchPrematureAbortEvents(aRv); + if (aRv.Failed()) { + return; + } + } else { + Maybe clientInfo( + mWorkerPrivate->GlobalScope()->GetClientInfo()); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + mProxy = new Proxy(this, clientInfo.ref(), + mWorkerPrivate->GlobalScope()->GetController(), mMozAnon, + mMozSystem); + alsoOverrideMimeType = true; + } + + mProxy->mOuterEventStreamId++; + + UniquePtr stack; + if (mWorkerPrivate->IsWatchedByDevTools()) { + if (JSContext* cx = nsContentUtils::GetCurrentJSContext()) { + stack = GetCurrentStackForNetMonitor(cx); + } + } + + RefPtr runnable = new OpenRunnable( + mWorkerPrivate, mProxy, aMethod, aUrl, aUser, aPassword, + mBackgroundRequest, mWithCredentials, mTimeout, mResponseType, + alsoOverrideMimeType ? mMimeTypeOverride : VoidString(), std::move(stack), + profiler_capture_backtrace()); + + ++mProxy->mOpenCount; + runnable->Dispatch(Canceling, aRv); + if (aRv.Failed()) { + if (mProxy && !--mProxy->mOpenCount) { + ReleaseProxy(); + } + + return; + } + + // We have been released in one of the nested Open() calls. + if (!mProxy) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + --mProxy->mOpenCount; + mProxy->mIsSyncXHR = !aAsync; +} + +void XMLHttpRequestWorker::SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr runnable = + new SetRequestHeaderRunnable(mWorkerPrivate, mProxy, aHeader, aValue); + runnable->Dispatch(Canceling, aRv); +} + +void XMLHttpRequestWorker::SetTimeout(uint32_t aTimeout, ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + mTimeout = aTimeout; + + if (!mProxy) { + // Open may not have been called yet, in which case we'll handle the + // timeout in OpenRunnable. + return; + } + + RefPtr runnable = + new SetTimeoutRunnable(mWorkerPrivate, mProxy, aTimeout); + runnable->Dispatch(Canceling, aRv); +} + +void XMLHttpRequestWorker::SetWithCredentials(bool aWithCredentials, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + mWithCredentials = aWithCredentials; + + if (!mProxy) { + // Open may not have been called yet, in which case we'll handle the + // credentials in OpenRunnable. + return; + } + + RefPtr runnable = + new SetWithCredentialsRunnable(mWorkerPrivate, mProxy, aWithCredentials); + runnable->Dispatch(Canceling, aRv); +} + +void XMLHttpRequestWorker::SetMozBackgroundRequest(bool aBackgroundRequest, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + mBackgroundRequest = aBackgroundRequest; + + if (!mProxy) { + // Open may not have been called yet, in which case we'll handle the + // background request in OpenRunnable. + return; + } + + RefPtr runnable = + new SetBackgroundRequestRunnable(mWorkerPrivate, mProxy, + aBackgroundRequest); + runnable->Dispatch(Canceling, aRv); +} + +XMLHttpRequestUpload* XMLHttpRequestWorker::GetUpload(ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return nullptr; + } + + if (!mUpload) { + mUpload = new XMLHttpRequestUpload(this); + } + + return mUpload; +} + +void XMLHttpRequestWorker::Send( + const Nullable< + DocumentOrBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString>& + aData, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mFlagSendActive) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT); + return; + } + mFlagSendActive = true; + auto clearRecursionFlag = MakeScopeExit([&]() { + // No one else should have touched this flag. + MOZ_ASSERT(mFlagSendActive); + mFlagSendActive = false; + }); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (mStateData->mReadyState != XMLHttpRequest_Binding::OPENED) { + aRv.ThrowInvalidStateError("XMLHttpRequest state must be OPENED."); + return; + } + + if (!mProxy || !mProxy->mXMLHttpRequestPrivate || mStateData->mFlagSend) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (aData.IsNull()) { + SendInternal(nullptr, aRv); + return; + } + + if (aData.Value().IsDocument()) { + MOZ_ASSERT_UNREACHABLE("Documents are not exposed to workers."); + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (aData.Value().IsBlob()) { + BodyExtractor body(&aData.Value().GetAsBlob()); + SendInternal(&body, aRv); + return; + } + + if (aData.Value().IsArrayBuffer()) { + BodyExtractor body(&aData.Value().GetAsArrayBuffer()); + SendInternal(&body, aRv); + return; + } + + if (aData.Value().IsArrayBufferView()) { + BodyExtractor body( + &aData.Value().GetAsArrayBufferView()); + SendInternal(&body, aRv); + return; + } + + if (aData.Value().IsFormData()) { + BodyExtractor body(&aData.Value().GetAsFormData()); + SendInternal(&body, aRv); + return; + } + + if (aData.Value().IsURLSearchParams()) { + BodyExtractor body( + &aData.Value().GetAsURLSearchParams()); + SendInternal(&body, aRv); + return; + } + + if (aData.Value().IsUSVString()) { + BodyExtractor body(&aData.Value().GetAsUSVString()); + SendInternal(&body, aRv); + return; + } +} + +void XMLHttpRequestWorker::Abort(ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + return; + } + + // Set our status to 0 and statusText to "" if we + // will be aborting an ongoing fetch, so the upcoming + // abort events we dispatch have the correct info. + if ((mStateData->mReadyState == XMLHttpRequest_Binding::OPENED && + mStateData->mFlagSend) || + mStateData->mReadyState == XMLHttpRequest_Binding::HEADERS_RECEIVED || + mStateData->mReadyState == XMLHttpRequest_Binding::LOADING || + mStateData->mReadyState == XMLHttpRequest_Binding::DONE) { + mStateData->mStatus = 0; + mStateData->mStatusText.Truncate(); + } + + MaybeDispatchPrematureAbortEvents(aRv); + if (aRv.Failed()) { + return; + } + + if (mStateData->mReadyState == 4) { + // No one did anything to us while we fired abort events, so reset our state + // to "unsent" + mStateData->mReadyState = 0; + } + + mProxy->mOuterEventStreamId++; + + RefPtr runnable = new AbortRunnable(mWorkerPrivate, mProxy); + runnable->Dispatch(Canceling, aRv); +} + +void XMLHttpRequestWorker::GetResponseHeader(const nsACString& aHeader, + nsACString& aResponseHeader, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsCString responseHeader; + RefPtr runnable = new GetResponseHeaderRunnable( + mWorkerPrivate, mProxy, aHeader, responseHeader); + runnable->Dispatch(Canceling, aRv); + if (aRv.Failed()) { + return; + } + aResponseHeader = responseHeader; +} + +void XMLHttpRequestWorker::GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsCString responseHeaders; + RefPtr runnable = + new GetAllResponseHeadersRunnable(mWorkerPrivate, mProxy, + responseHeaders); + runnable->Dispatch(Canceling, aRv); + if (aRv.Failed()) { + return; + } + + aResponseHeaders = responseHeaders; +} + +void XMLHttpRequestWorker::OverrideMimeType(const nsAString& aMimeType, + ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + // We're supposed to throw if the state is LOADING or DONE. + if (mStateData->mReadyState == XMLHttpRequest_Binding::LOADING || + mStateData->mReadyState == XMLHttpRequest_Binding::DONE) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mMimeTypeOverride = aMimeType; + + if (mProxy) { + RefPtr runnable = + new OverrideMimeTypeRunnable(mWorkerPrivate, mProxy, aMimeType); + runnable->Dispatch(Canceling, aRv); + } +} + +void XMLHttpRequestWorker::SetResponseType( + XMLHttpRequestResponseType aResponseType, ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + // "document" is fine for the main thread but not for a worker. Short-circuit + // that here. + if (aResponseType == XMLHttpRequestResponseType::Document) { + return; + } + + if (!mProxy) { + // Open() has not been called yet. We store the responseType and we will use + // it later in Open(). + mResponseType = aResponseType; + return; + } + + if (mStateData->mReadyState == XMLHttpRequest_Binding::LOADING || + mStateData->mReadyState == XMLHttpRequest_Binding::DONE) { + aRv.ThrowInvalidStateError( + "Cannot set 'responseType' property on XMLHttpRequest after 'send()' " + "(when its state is LOADING or DONE)."); + return; + } + + RefPtr runnable = + new SetResponseTypeRunnable(mWorkerPrivate, mProxy, aResponseType); + runnable->Dispatch(Canceling, aRv); + if (aRv.Failed()) { + return; + } + + mResponseType = runnable->ResponseType(); +} + +void XMLHttpRequestWorker::GetResponse(JSContext* aCx, + JS::MutableHandle aResponse, + ErrorResult& aRv) { + if (NS_FAILED(mResponseData->mResponseResult)) { + aRv.Throw(mResponseData->mResponseResult); + return; + } + + switch (mResponseType) { + case XMLHttpRequestResponseType::_empty: + case XMLHttpRequestResponseType::Text: { + JSString* str; + + if (mResponseData->mResponseText.IsEmpty()) { + aResponse.set(JS_GetEmptyStringValue(aCx)); + return; + } + + XMLHttpRequestStringSnapshotReaderHelper helper( + mResponseData->mResponseText); + + str = JS_NewUCStringCopyN(aCx, helper.Buffer(), helper.Length()); + if (!str) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + aResponse.setString(str); + return; + } + + case XMLHttpRequestResponseType::Arraybuffer: { + if (!mResponseData->mResponseArrayBufferBuilder) { + aResponse.setNull(); + return; + } + + if (!mResponseArrayBufferValue) { + mResponseArrayBufferValue = + mResponseData->mResponseArrayBufferBuilder->TakeArrayBuffer(aCx); + if (!mResponseArrayBufferValue) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + aResponse.setObject(*mResponseArrayBufferValue); + return; + } + + case XMLHttpRequestResponseType::Blob: { + if (!mResponseData->mResponseBlobImpl) { + aResponse.setNull(); + return; + } + + if (!mResponseBlob) { + mResponseBlob = + Blob::Create(GetOwnerGlobal(), mResponseData->mResponseBlobImpl); + } + + if (!mResponseBlob || + !GetOrCreateDOMReflector(aCx, mResponseBlob, aResponse)) { + aResponse.setNull(); + } + + return; + } + + case XMLHttpRequestResponseType::Json: { + if (mResponseData->mResponseJSON.IsVoid()) { + aResponse.setNull(); + return; + } + + if (mResponseJSONValue.isUndefined()) { + // The Unicode converter has already zapped the BOM if there was one + JS::Rooted value(aCx); + if (!JS_ParseJSON(aCx, mResponseData->mResponseJSON.BeginReading(), + mResponseData->mResponseJSON.Length(), &value)) { + JS_ClearPendingException(aCx); + mResponseJSONValue.setNull(); + } else { + mResponseJSONValue = value; + } + + mResponseData->mResponseJSON.Truncate(); + } + + aResponse.set(mResponseJSONValue); + return; + } + + default: + MOZ_ASSERT_UNREACHABLE("Invalid type"); + aResponse.setNull(); + return; + } +} + +void XMLHttpRequestWorker::GetResponseText(DOMString& aResponseText, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(mResponseData); + + if (mResponseType != XMLHttpRequestResponseType::_empty && + mResponseType != XMLHttpRequestResponseType::Text) { + aRv.ThrowInvalidStateError( + "responseText is only available if responseType is '' or 'text'."); + return; + } + + if (!mResponseData->mResponseText.GetAsString(aResponseText)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } +} + +void XMLHttpRequestWorker::UpdateState( + UniquePtr&& aStateData, + UniquePtr&& aResponseData) { + mStateData = std::move(aStateData); + + UniquePtr responseData = std::move(aResponseData); + if (responseData) { + ResetResponseData(); + mResponseData = std::move(responseData); + } + + XMLHttpRequest_Binding::ClearCachedResponseTextValue(this); +} + +void XMLHttpRequestWorker::ResetResponseData() { + mResponseBlob = nullptr; + mResponseArrayBufferValue = nullptr; + mResponseJSONValue.setUndefined(); +} + +} // namespace mozilla::dom diff --git a/dom/xhr/XMLHttpRequestWorker.h b/dom/xhr/XMLHttpRequestWorker.h new file mode 100644 index 0000000000..0d4c92f04f --- /dev/null +++ b/dom/xhr/XMLHttpRequestWorker.h @@ -0,0 +1,257 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestWorker_h +#define mozilla_dom_XMLHttpRequestWorker_h + +#include "XMLHttpRequest.h" +#include "XMLHttpRequestString.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/BodyExtractor.h" +#include "mozilla/dom/TypedArray.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "mozilla/dom/BlobImpl.h" + +namespace mozilla::dom { + +class Proxy; +class DOMString; +class SendRunnable; +class StrongWorkerRef; +class WorkerPrivate; + +class XMLHttpRequestWorker final : public SupportsWeakPtr, + public XMLHttpRequest { + public: + // This defines the xhr.response value. + struct ResponseData { + nsresult mResponseResult; + + // responseType is empty or text. + XMLHttpRequestStringSnapshot mResponseText; + + // responseType is blob + RefPtr mResponseBlobImpl; + + // responseType is arrayBuffer; + RefPtr mResponseArrayBufferBuilder; + + // responseType is json + nsString mResponseJSON; + + ResponseData() : mResponseResult(NS_OK) {} + }; + + struct StateData { + nsString mResponseURL; + uint32_t mStatus; + nsCString mStatusText; + uint16_t mReadyState; + bool mFlagSend; + nsresult mStatusResult; + + StateData() + : mStatus(0), mReadyState(0), mFlagSend(false), mStatusResult(NS_OK) {} + }; + + private: + RefPtr mUpload; + WorkerPrivate* mWorkerPrivate; + RefPtr mWorkerRef; + RefPtr mPinnedSelfRef; + RefPtr mProxy; + + XMLHttpRequestResponseType mResponseType; + + UniquePtr mStateData; + + UniquePtr mResponseData; + RefPtr mResponseBlob; + JS::Heap mResponseArrayBufferValue; + JS::Heap mResponseJSONValue; + + uint32_t mTimeout; + + bool mBackgroundRequest; + bool mWithCredentials; + bool mCanceled; + bool mFlagSendActive; + + bool mMozAnon; + bool mMozSystem; + + nsString mMimeTypeOverride; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(XMLHttpRequestWorker, + XMLHttpRequest) + + static already_AddRefed Construct( + const GlobalObject& aGlobal, const MozXMLHttpRequestParameters& aParams, + ErrorResult& aRv); + + void Unpin(); + + virtual uint16_t ReadyState() const override { + return mStateData->mReadyState; + } + + virtual void Open(const nsACString& aMethod, const nsAString& aUrl, + ErrorResult& aRv) override { + Open(aMethod, aUrl, true, Optional(), Optional(), + aRv); + } + + virtual void Open(const nsACString& aMethod, const nsAString& aUrl, + bool aAsync, const nsAString& aUsername, + const nsAString& aPassword, ErrorResult& aRv) override { + Optional username; + username = &aUsername; + Optional password; + password = &aPassword; + Open(aMethod, aUrl, aAsync, username, password, aRv); + } + + void Open(const nsACString& aMethod, const nsAString& aUrl, bool aAsync, + const Optional& aUser, + const Optional& aPassword, ErrorResult& aRv); + + virtual void SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, + ErrorResult& aRv) override; + + virtual uint32_t Timeout() const override { return mTimeout; } + + virtual void SetTimeout(uint32_t aTimeout, ErrorResult& aRv) override; + + virtual bool WithCredentials() const override { return mWithCredentials; } + + virtual void SetWithCredentials(bool aWithCredentials, + ErrorResult& aRv) override; + + virtual bool MozBackgroundRequest() const override { + return mBackgroundRequest; + } + + virtual void SetMozBackgroundRequest(bool aBackgroundRequest, + ErrorResult& aRv) override; + + virtual nsIChannel* GetChannel() const override { + MOZ_CRASH("This method cannot be called on workers."); + } + + virtual XMLHttpRequestUpload* GetUpload(ErrorResult& aRv) override; + + virtual void Send( + const Nullable< + DocumentOrBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString>& + aData, + ErrorResult& aRv) override; + + virtual void SendInputStream(nsIInputStream* aInputStream, + ErrorResult& aRv) override { + MOZ_CRASH("nsIInputStream is not a valid argument for XHR in workers."); + } + + virtual void Abort(ErrorResult& aRv) override; + + virtual void GetResponseURL(nsAString& aUrl) override { + aUrl = mStateData->mResponseURL; + } + + uint32_t GetStatus(ErrorResult& aRv) override { + aRv = mStateData->mStatusResult; + return mStateData->mStatus; + } + + virtual void GetStatusText(nsACString& aStatusText, + ErrorResult& aRv) override { + aStatusText = mStateData->mStatusText; + } + + virtual void GetResponseHeader(const nsACString& aHeader, + nsACString& aResponseHeader, + ErrorResult& aRv) override; + + virtual void GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) override; + + virtual void OverrideMimeType(const nsAString& aMimeType, + ErrorResult& aRv) override; + + virtual XMLHttpRequestResponseType ResponseType() const override { + return mResponseType; + } + + virtual void SetResponseType(XMLHttpRequestResponseType aResponseType, + ErrorResult& aRv) override; + + virtual void GetResponse(JSContext* /* unused */, + JS::MutableHandle aResponse, + ErrorResult& aRv) override; + + virtual void GetResponseText(DOMString& aResponseText, + ErrorResult& aRv) override; + + virtual Document* GetResponseXML(ErrorResult& aRv) override { + MOZ_CRASH("This method should not be called."); + } + + virtual void GetInterface(JSContext* aCx, JS::Handle aIID, + JS::MutableHandle aRetval, + ErrorResult& aRv) override { + aRv.Throw(NS_ERROR_FAILURE); + } + + virtual void SetOriginAttributes( + const mozilla::dom::OriginAttributesDictionary& aAttrs) override { + MOZ_CRASH("This method cannot be called on workers."); + } + + XMLHttpRequestUpload* GetUploadObjectNoCreate() const { return mUpload; } + + void UpdateState(UniquePtr&& aStateData, + UniquePtr&& aResponseData); + + virtual uint16_t ErrorCode() const override { + return 0; // eOK + } + + virtual bool MozAnon() const override { return mMozAnon; } + + virtual bool MozSystem() const override { return mMozSystem; } + + bool SendInProgress() const { return !!mWorkerRef; } + + private: + XMLHttpRequestWorker(WorkerPrivate* aWorkerPrivate, + nsIGlobalObject* aGlobalObject); + ~XMLHttpRequestWorker(); + + enum ReleaseType { Default, XHRIsGoingAway, WorkerIsGoingAway }; + + void ReleaseProxy(ReleaseType aType = Default); + + void MaybePin(ErrorResult& aRv); + + void MaybeDispatchPrematureAbortEvents(ErrorResult& aRv); + + void DispatchPrematureAbortEvent(EventTarget* aTarget, + const nsAString& aEventType, + bool aUploadTarget, ErrorResult& aRv); + + void Send(JSContext* aCx, JS::Handle aBody, ErrorResult& aRv); + + void SendInternal(const BodyExtractorBase* aBody, ErrorResult& aRv); + + void ResetResponseData(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_workers_xmlhttprequest_h__ diff --git a/dom/xhr/moz.build b/dom/xhr/moz.build new file mode 100644 index 0000000000..1c9b5c63fb --- /dev/null +++ b/dom/xhr/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Networking") + +EXPORTS.mozilla.dom += [ + "XMLHttpRequest.h", + "XMLHttpRequestEventTarget.h", + "XMLHttpRequestMainThread.h", + "XMLHttpRequestString.h", + "XMLHttpRequestUpload.h", +] + +UNIFIED_SOURCES += [ + "XMLHttpRequest.cpp", + "XMLHttpRequestEventTarget.cpp", + "XMLHttpRequestMainThread.cpp", + "XMLHttpRequestString.cpp", + "XMLHttpRequestUpload.cpp", + "XMLHttpRequestWorker.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/file", + "/netwerk/base", +] + +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/xhr/tests/browser.ini b/dom/xhr/tests/browser.ini new file mode 100644 index 0000000000..4a25bf3210 --- /dev/null +++ b/dom/xhr/tests/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + browser_xhr_onchange_leak.html +[browser_blobFromFile.js] +[browser_xhr_onchange_leak.js] +[browser_xhr_substituted_protocol_responseURL.js] +[browser_sync_xhr_event_handing_switch_bcg.js] +support-files = + empty.html + empty_parent.html + slow.sjs +[browser_temporaryFile.js] +support-files = temporaryFileBlob.sjs diff --git a/dom/xhr/tests/browser_blobFromFile.js b/dom/xhr/tests/browser_blobFromFile.js new file mode 100644 index 0000000000..deaec142ae --- /dev/null +++ b/dom/xhr/tests/browser_blobFromFile.js @@ -0,0 +1,64 @@ +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.remote.separateFileUriProcess", true]], + }); + + let fileData = ""; + for (var i = 0; i < 100; ++i) { + fileData += "hello world!"; + } + + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("file.txt"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + let outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + file, + 0x02 | 0x08 | 0x20, // write, create, truncate + // eslint-disable-next-line no-octal + 0666, + 0 + ); + outStream.write(fileData, fileData.length); + outStream.close(); + + let fileHandler = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + + let fileURL = fileHandler.getURLSpecFromActualFile(file); + + info("Opening url: " + fileURL); + let tab = BrowserTestUtils.addTab(gBrowser, fileURL); + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let blob = await SpecialPowers.spawn( + browser, + [file.leafName], + function (fileName) { + return new content.window.Promise(resolve => { + let xhr = new content.window.XMLHttpRequest(); + xhr.responseType = "blob"; + xhr.open("GET", fileName); + xhr.send(); + xhr.onload = function () { + resolve(xhr.response); + }; + }); + } + ); + + ok(File.isInstance(blob), "We have a file"); + + is(blob.size, file.fileSize, "The size matches"); + is(blob.name, file.leafName, "The name is correct"); + + file.remove(false); + + gBrowser.removeTab(tab); +}); diff --git a/dom/xhr/tests/browser_sync_xhr_event_handing_switch_bcg.js b/dom/xhr/tests/browser_sync_xhr_event_handing_switch_bcg.js new file mode 100644 index 0000000000..d7966b2dac --- /dev/null +++ b/dom/xhr/tests/browser_sync_xhr_event_handing_switch_bcg.js @@ -0,0 +1,144 @@ +const baseURL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const childURL = `${baseURL}empty.html`; +const parentURL = `${baseURL}empty_parent.html`; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.input_events.canSuspendInBCG.enabled", true]], + }); + if (!Services.appinfo.fissionAutostart) { + // Make sure the tab that is opened with noopener + // also in the same process as the parent. + await SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount", 1]], + }); + } +}); + +async function checkInputManagerStatus(openChildInSameBCG) { + let childTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + childURL, + true, + true + ); + + let xhrTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + parentURL, + true + ); + + let xhrTabIsHidden = BrowserTestUtils.waitForContentEvent( + xhrTab.linkedBrowser, + "visibilitychange" + ); + + await SpecialPowers.spawn( + xhrTab.linkedBrowser.browsingContext, + [openChildInSameBCG, childURL], + async function (sameBCG, url) { + if (sameBCG) { + content.open(url); + } else { + content.open(url, "", "noopener"); + } + } + ); + + await childTabPromise; + await xhrTabIsHidden; + + let xhrRequestIsReady = BrowserTestUtils.waitForContentEvent( + xhrTab.linkedBrowser, + "xhrRequestIsReady" + ); + + let xhrRequest = SpecialPowers.spawn( + xhrTab.linkedBrowser.browsingContext, + [], + () => { + var xhr = new content.XMLHttpRequest(); + xhr.open("GET", "slow.sjs", false); + content.document.dispatchEvent( + new content.Event("xhrRequestIsReady", { bubbles: true }) + ); + xhr.send(null); + } + ); + + // Need to wait for the xhrIsReady event because spawn is async, + // so the content needs to give us a signal that the sync XHR request + // has started + await xhrRequestIsReady; + + let childTab = gBrowser.tabs[2]; + + // Since the xhrTab has started the sync XHR request, + // the InputTaskManager should be suspended here + // if it is in the same browsing context as the opener + let isSuspendedBeforeSwitch = await SpecialPowers.spawn( + childTab.linkedBrowser.browsingContext, + [], + () => { + var utils = SpecialPowers.getDOMWindowUtils(content); + return utils.isInputTaskManagerSuspended; + } + ); + + is( + isSuspendedBeforeSwitch, + openChildInSameBCG, + "InputTaskManager should be suspended before tab switching" + ); + + // Switching away from the childTab and switching back to + // test the status of InputTaskManager gets updated accordingly + // based on whether the childTab is in the same BCG as the xhrTab or not. + await BrowserTestUtils.switchTab(gBrowser, xhrTab); + await BrowserTestUtils.switchTab(gBrowser, childTab); + + let isSuspendedAfterTabSwitch = await SpecialPowers.spawn( + childTab.linkedBrowser.browsingContext, + [], + () => { + var utils = SpecialPowers.getDOMWindowUtils(content); + return utils.isInputTaskManagerSuspended; + } + ); + + is( + isSuspendedAfterTabSwitch, + openChildInSameBCG, + "InputTaskManager should be either suspended or not suspended based whether childTab was opened in the same BCG" + ); + + await xhrRequest; + + let isSuspendedAfterXHRRequest = await SpecialPowers.spawn( + xhrTab.linkedBrowser.browsingContext, + [], + () => { + var utils = SpecialPowers.getDOMWindowUtils(content); + return utils.isInputTaskManagerSuspended; + } + ); + + is( + isSuspendedAfterXHRRequest, + false, + "InputTaskManager should not be suspended before after the sync XHR is done" + ); + + gBrowser.removeTab(xhrTab); + gBrowser.removeTab(childTab); +} + +add_task(async function switchBCG() { + await checkInputManagerStatus(true); + await checkInputManagerStatus(false); +}); diff --git a/dom/xhr/tests/browser_temporaryFile.js b/dom/xhr/tests/browser_temporaryFile.js new file mode 100644 index 0000000000..c3694d9237 --- /dev/null +++ b/dom/xhr/tests/browser_temporaryFile.js @@ -0,0 +1,71 @@ +add_task(async _ => { + await new Promise(resolve => { + var xhr = new XMLHttpRequest(); + xhr.open( + "POST", + "http://mochi.test:8888/browser/dom/xhr/tests/temporaryFileBlob.sjs" + ); + xhr.responseType = "blob"; + xhr.send(""); + xhr.onloadend = __ => { + is(xhr.response.blobImplType, "EmptyBlobImpl", "We want a EmptyBlobImpl"); + resolve(); + }; + }); +}); + +add_task(async _ => { + var data = new Array(2).join("1234567890ABCDEF"); + + await new Promise(resolve => { + var xhr = new XMLHttpRequest(); + xhr.open( + "POST", + "http://mochi.test:8888/browser/dom/xhr/tests/temporaryFileBlob.sjs" + ); + xhr.responseType = "blob"; + xhr.send({ + toString() { + return data; + }, + }); + xhr.onloadend = __ => { + is( + xhr.response.blobImplType, + "MemoryBlobImpl", + "We want a MemoryBlobImpl" + ); + resolve(); + }; + }); +}); + +add_task(async _ => { + await SpecialPowers.pushPrefEnv({ + set: [["dom.blob.memoryToTemporaryFile", 1]], + }); + + var data = new Array(2).join("1234567890ABCDEF"); + + await new Promise(resolve => { + var xhr = new XMLHttpRequest(); + xhr.open( + "POST", + "http://mochi.test:8888/browser/dom/xhr/tests/temporaryFileBlob.sjs" + ); + xhr.responseType = "blob"; + xhr.send({ + toString() { + return data; + }, + }); + xhr.onloadend = __ => { + is( + xhr.response.blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We want a StreamBlobImpl holding a TemporaryFileBlobImpl on the parent side" + ); + resolve(); + }; + }); +}); diff --git a/dom/xhr/tests/browser_xhr_onchange_leak.html b/dom/xhr/tests/browser_xhr_onchange_leak.html new file mode 100644 index 0000000000..56eb455330 --- /dev/null +++ b/dom/xhr/tests/browser_xhr_onchange_leak.html @@ -0,0 +1,25 @@ + + + + + + Test page for Bug 1336811 + + +

sample page

+ + + diff --git a/dom/xhr/tests/browser_xhr_onchange_leak.js b/dom/xhr/tests/browser_xhr_onchange_leak.js new file mode 100644 index 0000000000..03e8fbdb67 --- /dev/null +++ b/dom/xhr/tests/browser_xhr_onchange_leak.js @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// Bug 1336811 - An XHR that has a .onreadystatechange waiting should +// not leak forever once the tab is closed. CC optimizations need to be +// turned off once it is closed. + +add_task(async function test() { + // We need to reuse the content process when we navigate so the entire process + // with the possible-leaking window doesn't get torn down. + await SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.keepProcessesAlive.webIsolated.perOrigin", 1]], + }); + + const url = + "http://mochi.test:8888/browser/dom/xhr/tests/browser_xhr_onchange_leak.html"; + let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let browser = gBrowser.selectedBrowser; + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow", + true + ); + BrowserTestUtils.loadURIString(browser, "http://mochi.test:8888/"); + await pageShowPromise; + + ok(pageShowPromise, "need to check something"); + BrowserTestUtils.removeTab(newTab); +}); diff --git a/dom/xhr/tests/browser_xhr_substituted_protocol_responseURL.js b/dom/xhr/tests/browser_xhr_substituted_protocol_responseURL.js new file mode 100644 index 0000000000..b5556d549f --- /dev/null +++ b/dom/xhr/tests/browser_xhr_substituted_protocol_responseURL.js @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// Bug 1411725 - An XHR using a SubstitutingProtocolHandler channel +// (web-extension:, resource:, etc) should return the original URL, +// not the jar/file it was actually substituted for. + +const TEST_URL = "resource://gre/modules/XPCOMUtils.sys.mjs"; + +add_task(async function test() { + await new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.responseType = "text"; + xhr.open("get", TEST_URL); + xhr.addEventListener("loadend", () => { + is( + xhr.responseURL, + TEST_URL, + "original URL is given instead of substitution" + ); + resolve(); + }); + xhr.send(); + }); +}); diff --git a/dom/xhr/tests/common_temporaryFileBlob.js b/dom/xhr/tests/common_temporaryFileBlob.js new file mode 100644 index 0000000000..a84d58dd77 --- /dev/null +++ b/dom/xhr/tests/common_temporaryFileBlob.js @@ -0,0 +1,126 @@ +// This file expects next() to be defined in the scope it is imported into. +/* global next */ +var data = new Array(256).join("1234567890ABCDEF"); + +function createXHR() { + var xhr = new XMLHttpRequest(); + xhr.open("POST", "temporaryFileBlob.sjs"); + xhr.responseType = "blob"; + xhr.send({ + toString() { + return data; + }, + }); + return xhr; +} + +function test_simple() { + info("Simple test"); + + var xhr = createXHR(); + + xhr.onloadend = function () { + ok(xhr.response instanceof Blob, "We have a blob!"); + ok(!(xhr.response instanceof File), "Our blob is not a file!"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(xhr.response).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + is(xhr.response.size, data.length, "Data length matches"); + + var fr = new FileReader(); + fr.readAsText(xhr.response); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + }; +} + +function test_abort() { + info("Aborting during onloading"); + + var xhr = createXHR(); + + xhr.onprogress = function () { + xhr.abort(); + }; + + xhr.onloadend = function () { + ok(!xhr.response, "We should not have a Blob!"); + next(); + }; +} + +function test_reuse() { + info("Reuse test"); + + var xhr = createXHR(); + + var count = 0; + xhr.onloadend = function () { + ok(xhr.response instanceof Blob, "We have a blob!"); + ok(!(xhr.response instanceof File), "Our blob is not a file!"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(xhr.response).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + is(xhr.response.size, data.length, "Data length matches"); + + var fr = new FileReader(); + fr.readAsText(xhr.response); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + if (++count > 2) { + next(); + return; + } + + xhr.open("POST", "temporaryFileBlob.sjs"); + xhr.responseType = "blob"; + xhr.send({ + toString() { + return data; + }, + }); + }; + }; +} + +function test_worker_generic(test) { + var w = new Worker("worker_temporaryFileBlob.js"); + w.onmessage = function (e) { + if (e.data.type == "info") { + info(e.data.msg); + } else if (e.data.type == "check") { + ok(e.data.what, e.data.msg); + } else if (e.data.type == "finish") { + next(); + } else { + ok(false, "Something wrong happened"); + } + }; + + w.postMessage(test); +} + +function test_worker() { + info("XHR in workers"); + test_worker_generic("simple"); +} + +function test_worker_abort() { + info("XHR in workers"); + test_worker_generic("abort"); +} + +function test_worker_reuse() { + info("XHR in workers"); + test_worker_generic("reuse"); +} diff --git a/dom/xhr/tests/crashtests/1546185.html b/dom/xhr/tests/crashtests/1546185.html new file mode 100644 index 0000000000..98eb673592 --- /dev/null +++ b/dom/xhr/tests/crashtests/1546185.html @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/dom/xhr/tests/crashtests/crashtests.list b/dom/xhr/tests/crashtests/crashtests.list new file mode 100644 index 0000000000..bd78bc54dc --- /dev/null +++ b/dom/xhr/tests/crashtests/crashtests.list @@ -0,0 +1 @@ +load 1546185.html diff --git a/dom/xhr/tests/echo.sjs b/dom/xhr/tests/echo.sjs new file mode 100644 index 0000000000..e2a1553cb4 --- /dev/null +++ b/dom/xhr/tests/echo.sjs @@ -0,0 +1,26 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain"); + if (request.method == "GET") { + response.write(request.queryString); + return; + } + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var body = ""; + var bodyAvail; + while ((bodyAvail = bodyStream.available()) > 0) { + body += String.fromCharCode.apply( + null, + bodyStream.readByteArray(bodyAvail) + ); + } + + response.write(body); +} diff --git a/dom/xhr/tests/empty.html b/dom/xhr/tests/empty.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dom/xhr/tests/empty_parent.html b/dom/xhr/tests/empty_parent.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dom/xhr/tests/file_XHRDocURI.html b/dom/xhr/tests/file_XHRDocURI.html new file mode 100644 index 0000000000..8dbbb05491 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.html @@ -0,0 +1,9 @@ + + + + XMLHttpRequest return document URIs + + +
data
+ + diff --git a/dom/xhr/tests/file_XHRDocURI.html^headers^ b/dom/xhr/tests/file_XHRDocURI.html^headers^ new file mode 100644 index 0000000000..ddfd8c0a9e --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.html^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Access-Control-Allow-Origin: * +Content-Type: text/html diff --git a/dom/xhr/tests/file_XHRDocURI.sjs b/dom/xhr/tests/file_XHRDocURI.sjs new file mode 100644 index 0000000000..452cb77f94 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.sjs @@ -0,0 +1,12 @@ +function handleRequest(aRequest, aResponse) { + var url = aRequest.queryString.match(/\burl=([^#&]*)/); + if (!url) { + aResponse.setStatusLine(aRequest.httpVersion, 404, "Not Found"); + return; + } + + url = decodeURIComponent(url[1]); + aResponse.setStatusLine(aRequest.httpVersion, 302, "Found"); + aResponse.setHeader("Cache-Control", "no-cache", false); + aResponse.setHeader("Location", url, false); +} diff --git a/dom/xhr/tests/file_XHRDocURI.text b/dom/xhr/tests/file_XHRDocURI.text new file mode 100644 index 0000000000..c1dead8d7a --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.text @@ -0,0 +1 @@ +data diff --git a/dom/xhr/tests/file_XHRDocURI.text^headers^ b/dom/xhr/tests/file_XHRDocURI.text^headers^ new file mode 100644 index 0000000000..b2fe7cdeb4 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.text^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Access-Control-Allow-Origin: * +Content-Type: text/plain diff --git a/dom/xhr/tests/file_XHRDocURI.xml b/dom/xhr/tests/file_XHRDocURI.xml new file mode 100644 index 0000000000..c1a9a52566 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.xml @@ -0,0 +1 @@ +data diff --git a/dom/xhr/tests/file_XHRDocURI.xml^headers^ b/dom/xhr/tests/file_XHRDocURI.xml^headers^ new file mode 100644 index 0000000000..f47804337a --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.xml^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Access-Control-Allow-Origin: * +Content-Type: application/xml diff --git a/dom/xhr/tests/file_XHRResponseURL.js b/dom/xhr/tests/file_XHRResponseURL.js new file mode 100644 index 0000000000..1ab1694bfa --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL.js @@ -0,0 +1,388 @@ +"use strict"; + +// utility functions for worker/window communication + +function isInWorker() { + try { + return !(self instanceof Window); + } catch (e) { + return true; + } +} + +function message(aData) { + if (isInWorker()) { + self.postMessage(aData); + } else { + self.postMessage(aData, "*"); + } +} +message.ping = 0; +message.pong = 0; + +function is(aActual, aExpected, aMessage) { + var obj = { + type: "is", + actual: aActual, + expected: aExpected, + message: aMessage, + }; + ++message.ping; + message(obj); +} + +function ok(aBool, aMessage) { + var obj = { + type: "ok", + bool: aBool, + message: aMessage, + }; + ++message.ping; + message(obj); +} + +function info(aMessage) { + var obj = { + type: "info", + message: aMessage, + }; + ++message.ping; + message(obj); +} + +function request(aURL) { + return new Promise(function (aResolve, aReject) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", aURL); + xhr.addEventListener("load", function () { + xhr.succeeded = true; + aResolve(xhr); + }); + xhr.addEventListener("error", function () { + xhr.succeeded = false; + aResolve(xhr); + }); + xhr.send(); + }); +} + +function createSequentialRequest(aParameters, aTest) { + var sequence = aParameters.reduce(function (aPromise, aParam) { + return aPromise + .then(function () { + return request(aParam.requestURL); + }) + .then(function (aXHR) { + return aTest(aXHR, aParam); + }); + }, Promise.resolve()); + + return sequence; +} + +function testSuccessResponse() { + var blob = new Blob(["data"], { type: "text/plain" }); + var blobURL = URL.createObjectURL(blob); + + var parameters = [ + // tests that start with same-origin request + { + message: "request to same-origin without redirect", + requestURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to same-origin redirect to same-origin URL", + requestURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: + "request to same-origin redirects several times and finally go to same-origin URL", + requestURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + + encodeURIComponent( + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + ), + responseURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to same-origin redirect to cross-origin URL", + requestURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: + "request to same-origin redirects several times and finally go to cross-origin URL", + requestURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + + encodeURIComponent( + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text" + ), + responseURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + + // tests that start with cross-origin request + { + message: "request to cross-origin without redirect", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to cross-origin redirect back to same-origin URL", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to cross-origin redirect to the same cross-origin URL", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to cross-origin redirect to another cross-origin URL", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: + "http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: + "request to cross-origin redirects several times and finally go to same-origin URL", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + + encodeURIComponent( + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + ), + responseURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: + "request to cross-origin redirects several times and finally go to the same cross-origin URL", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + + encodeURIComponent( + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text" + ), + responseURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: + "request to cross-origin redirects several times and finally go to another cross-origin URL", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + + encodeURIComponent( + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.text" + ), + responseURL: + "http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: + "request to cross-origin redirects to another cross-origin and finally go to the other cross-origin URL", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + + encodeURIComponent( + "http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://test1.example.com/tests/dom/xhr/tests/file_XHRResponseURL.text" + ), + responseURL: + "http://test1.example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request URL has fragment", + requestURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text#fragment", + responseURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + + // tests for non-http(s) URL + { + message: "request to data: URL", + requestURL: "data:text/plain,data", + responseURL: "data:text/plain,data", + }, + { + message: "request to blob: URL", + requestURL: blobURL, + responseURL: blobURL, + }, + ]; + + var sequence = createSequentialRequest(parameters, function (aXHR, aParam) { + ok(aXHR.succeeded, "assert request succeeded"); + is(aXHR.responseURL, aParam.responseURL, aParam.message); + }); + + sequence.then(function () { + URL.revokeObjectURL(blobURL); + }); + + return sequence; +} + +function testFailedResponse() { + info("test not to leak responseURL for denied cross-origin request"); + var parameters = [ + { + message: + "should be empty for denied cross-origin request without redirect", + requestURL: + "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL_nocors.text", + }, + { + message: "should be empty for denied cross-origin request with redirect", + requestURL: + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL_nocors.text", + }, + ]; + + var sequence = createSequentialRequest(parameters, function (aXHR, aParam) { + ok(!aXHR.succeeded, "assert request failed"); + is(aXHR.responseURL, "", aParam.message); + }); + + return sequence; +} + +function testNotToLeakResponseURLWhileDoingRedirects() { + info("test not to leak responeseURL while doing redirects"); + + if (isInWorker()) { + return testNotToLeakResponseURLWhileDoingRedirectsInWorker(); + } + return testNotToLeakResponseURLWhileDoingRedirectsInWindow(); +} + +function testNotToLeakResponseURLWhileDoingRedirectsInWindow() { + var xhr = new XMLHttpRequest(); + var requestObserver = { + observe(aSubject, aTopic, aData) { + is(xhr.readyState, XMLHttpRequest.OPENED, "assert for XHR state"); + is( + xhr.responseURL, + "", + "responseURL should return empty string before HEADERS_RECEIVED" + ); + }, + }; + SpecialPowers.addObserver( + requestObserver, + "specialpowers-http-notify-request" + ); + + return new Promise(function (aResolve, aReject) { + xhr.open( + "GET", + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + ); + xhr.addEventListener("load", function () { + SpecialPowers.removeObserver( + requestObserver, + "specialpowers-http-notify-request" + ); + aResolve(); + }); + xhr.addEventListener("error", function () { + ok(false, "unexpected request falilure"); + SpecialPowers.removeObserver( + requestObserver, + "specialpowers-http-notify-request" + ); + aResolve(); + }); + xhr.send(); + }); +} + +function testNotToLeakResponseURLWhileDoingRedirectsInWorker() { + var xhr = new XMLHttpRequest(); + var testRedirect = function (e) { + if (e.data === "request" && xhr.readyState === XMLHttpRequest.OPENED) { + is( + xhr.responseURL, + "", + "responseURL should return empty string before HEADERS_RECEIVED" + ); + } + }; + + return new Promise(function (aResolve, aReject) { + self.addEventListener("message", testRedirect); + message({ type: "redirect_test", status: "start" }); + xhr.open( + "GET", + "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + ); + xhr.addEventListener("load", function () { + self.removeEventListener("message", testRedirect); + message({ type: "redirect_test", status: "end" }); + aResolve(); + }); + xhr.addEventListener("error", function (e) { + ok(false, "unexpected request falilure"); + self.removeEventListener("message", testRedirect); + message({ type: "redirect_test", status: "end" }); + aResolve(); + }); + xhr.send(); + }); +} + +function waitForAllMessagesProcessed() { + return new Promise(function (aResolve, aReject) { + var id = setInterval(function () { + if (message.ping === message.pong) { + clearInterval(id); + aResolve(); + } + }, 100); + }); +} + +self.addEventListener("message", function (aEvent) { + if (aEvent.data === "start") { + ok( + "responseURL" in new XMLHttpRequest(), + "XMLHttpRequest should have responseURL attribute" + ); + is( + new XMLHttpRequest().responseURL, + "", + "responseURL should be empty string if response's url is null" + ); + + var promise = testSuccessResponse(); + promise + .then(function () { + return testFailedResponse(); + }) + .then(function () { + return testNotToLeakResponseURLWhileDoingRedirects(); + }) + .then(function () { + return waitForAllMessagesProcessed(); + }) + .then(function () { + message("done"); + }); + } + if (aEvent.data === "pong") { + ++message.pong; + } +}); diff --git a/dom/xhr/tests/file_XHRResponseURL.sjs b/dom/xhr/tests/file_XHRResponseURL.sjs new file mode 100644 index 0000000000..792680dfce --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL.sjs @@ -0,0 +1,13 @@ +function handleRequest(aRequest, aResponse) { + var url = aRequest.queryString.match(/\burl=([^#&]*)/); + if (!url) { + aResponse.setStatusLine(aRequest.httpVersion, 404, "Not Found"); + return; + } + url = decodeURIComponent(url[1]); + + aResponse.setStatusLine(aRequest.httpVersion, 302, "Found"); + aResponse.setHeader("Access-Control-Allow-Origin", "*", false); + aResponse.setHeader("Cache-Control", "no-cache", false); + aResponse.setHeader("Location", url, false); +} diff --git a/dom/xhr/tests/file_XHRResponseURL.text b/dom/xhr/tests/file_XHRResponseURL.text new file mode 100644 index 0000000000..1269488f7f --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL.text @@ -0,0 +1 @@ +data diff --git a/dom/xhr/tests/file_XHRResponseURL.text^headers^ b/dom/xhr/tests/file_XHRResponseURL.text^headers^ new file mode 100644 index 0000000000..b2fe7cdeb4 --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL.text^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Access-Control-Allow-Origin: * +Content-Type: text/plain diff --git a/dom/xhr/tests/file_XHRResponseURL_nocors.text b/dom/xhr/tests/file_XHRResponseURL_nocors.text new file mode 100644 index 0000000000..1269488f7f --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL_nocors.text @@ -0,0 +1 @@ +data diff --git a/dom/xhr/tests/file_XHRSendData.sjs b/dom/xhr/tests/file_XHRSendData.sjs new file mode 100644 index 0000000000..a39d888fdc --- /dev/null +++ b/dom/xhr/tests/file_XHRSendData.sjs @@ -0,0 +1,34 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + if (request.hasHeader("Content-Type")) { + response.setHeader( + "Result-Content-Type", + request.getHeader("Content-Type") + ); + } + + response.setHeader("Content-Type", "text/plain; charset=ISO-8859-1"); + + var body = new BinaryInputStream(request.bodyInputStream); + var avail; + var bytes = []; + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + var data = String.fromCharCode.apply(null, bytes); + response.setHeader("Result-Content-Length", "" + data.length); + if (data.includes("TEST_REDIRECT_STR")) { + var newURL = "http://" + data.split("&url=")[1]; + response.setStatusLine(null, 307, "redirect"); + response.setHeader("Location", newURL, false); + } else { + response.write(data); + } +} diff --git a/dom/xhr/tests/file_XHRSendData_doc.xml b/dom/xhr/tests/file_XHRSendData_doc.xml new file mode 100644 index 0000000000..2e385258cb --- /dev/null +++ b/dom/xhr/tests/file_XHRSendData_doc.xml @@ -0,0 +1,2 @@ + +hi diff --git a/dom/xhr/tests/file_XHRSendData_doc.xml^headers^ b/dom/xhr/tests/file_XHRSendData_doc.xml^headers^ new file mode 100644 index 0000000000..b0f1ebfbf3 --- /dev/null +++ b/dom/xhr/tests/file_XHRSendData_doc.xml^headers^ @@ -0,0 +1 @@ +Content-Type: application/xml; charset=ISO-8859-1 diff --git a/dom/xhr/tests/file_XHR_anon.sjs b/dom/xhr/tests/file_XHR_anon.sjs new file mode 100644 index 0000000000..8440e9f0c2 --- /dev/null +++ b/dom/xhr/tests/file_XHR_anon.sjs @@ -0,0 +1,24 @@ +function handleRequest(request, response) { + let invalidHeaders = ["Cookie"]; + let headers = {}; + + if (request.queryString == "expectAuth=true") { + if (request.hasHeader("Authorization")) { + headers.authorization = request.getHeader("Authorization"); + } else { + response.setStatusLine(null, 401, "Authentication required"); + response.setHeader("WWW-Authenticate", 'basic realm="testrealm"', true); + } + } else { + invalidHeaders.push("Authorization"); + } + + for (let header of invalidHeaders) { + if (request.hasHeader(header)) { + response.setStatusLine(null, 500, "Server Error"); + headers[header.toLowerCase()] = request.getHeader(header); + } + } + + response.write(JSON.stringify(headers)); +} diff --git a/dom/xhr/tests/file_XHR_binary1.bin b/dom/xhr/tests/file_XHR_binary1.bin new file mode 100644 index 0000000000..39e527bfc3 Binary files /dev/null and b/dom/xhr/tests/file_XHR_binary1.bin differ diff --git a/dom/xhr/tests/file_XHR_binary1.bin^headers^ b/dom/xhr/tests/file_XHR_binary1.bin^headers^ new file mode 100644 index 0000000000..8e8c8d859e --- /dev/null +++ b/dom/xhr/tests/file_XHR_binary1.bin^headers^ @@ -0,0 +1 @@ +Content-Type: application/binary diff --git a/dom/xhr/tests/file_XHR_binary2.bin b/dom/xhr/tests/file_XHR_binary2.bin new file mode 100644 index 0000000000..9f442b092d Binary files /dev/null and b/dom/xhr/tests/file_XHR_binary2.bin differ diff --git a/dom/xhr/tests/file_XHR_fail1.txt b/dom/xhr/tests/file_XHR_fail1.txt new file mode 100644 index 0000000000..462209d8da --- /dev/null +++ b/dom/xhr/tests/file_XHR_fail1.txt @@ -0,0 +1 @@ +redirect file diff --git a/dom/xhr/tests/file_XHR_fail1.txt^headers^ b/dom/xhr/tests/file_XHR_fail1.txt^headers^ new file mode 100644 index 0000000000..41c2359329 --- /dev/null +++ b/dom/xhr/tests/file_XHR_fail1.txt^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: http://example.com/tests/dom/xhr/tests/file_XHR_pass2.txt diff --git a/dom/xhr/tests/file_XHR_fail1b.txt b/dom/xhr/tests/file_XHR_fail1b.txt new file mode 100644 index 0000000000..8944657af1 --- /dev/null +++ b/dom/xhr/tests/file_XHR_fail1b.txt @@ -0,0 +1 @@ +hello pass diff --git a/dom/xhr/tests/file_XHR_header.sjs b/dom/xhr/tests/file_XHR_header.sjs new file mode 100644 index 0000000000..d520d2befe --- /dev/null +++ b/dom/xhr/tests/file_XHR_header.sjs @@ -0,0 +1,8 @@ +// SJS file for getAllResponseRequests vs getResponseRequest +function handleRequest(request, response) { + // Header strings are interpreted by truncating the characters in them to + // bytes, so U+2026 HORIZONTAL ELLIPSIS here must be encoded manually: using + // "…" as the string would write a \x26 byte. + response.setHeader("X-Custom-Header-Bytes", "\xE2\x80\xA6", false); + response.write("42"); +} diff --git a/dom/xhr/tests/file_XHR_pass1.xml b/dom/xhr/tests/file_XHR_pass1.xml new file mode 100644 index 0000000000..06826d6c67 --- /dev/null +++ b/dom/xhr/tests/file_XHR_pass1.xml @@ -0,0 +1 @@ +hello diff --git a/dom/xhr/tests/file_XHR_pass2.txt b/dom/xhr/tests/file_XHR_pass2.txt new file mode 100644 index 0000000000..0d7f879f95 --- /dev/null +++ b/dom/xhr/tests/file_XHR_pass2.txt @@ -0,0 +1 @@ +hello pass diff --git a/dom/xhr/tests/file_XHR_pass3.txt b/dom/xhr/tests/file_XHR_pass3.txt new file mode 100644 index 0000000000..462209d8da --- /dev/null +++ b/dom/xhr/tests/file_XHR_pass3.txt @@ -0,0 +1 @@ +redirect file diff --git a/dom/xhr/tests/file_XHR_pass3.txt^headers^ b/dom/xhr/tests/file_XHR_pass3.txt^headers^ new file mode 100644 index 0000000000..fb5056c382 --- /dev/null +++ b/dom/xhr/tests/file_XHR_pass3.txt^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: file_XHR_pass2.txt diff --git a/dom/xhr/tests/file_XHR_system_redirect.html b/dom/xhr/tests/file_XHR_system_redirect.html new file mode 100644 index 0000000000..eaca3f49fd --- /dev/null +++ b/dom/xhr/tests/file_XHR_system_redirect.html @@ -0,0 +1,5 @@ + + + + + diff --git a/dom/xhr/tests/file_XHR_system_redirect.html^headers^ b/dom/xhr/tests/file_XHR_system_redirect.html^headers^ new file mode 100644 index 0000000000..cefb165b02 --- /dev/null +++ b/dom/xhr/tests/file_XHR_system_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: file:///etc/passwd diff --git a/dom/xhr/tests/file_XHR_timeout.sjs b/dom/xhr/tests/file_XHR_timeout.sjs new file mode 100644 index 0000000000..ebee7f8e1c --- /dev/null +++ b/dom/xhr/tests/file_XHR_timeout.sjs @@ -0,0 +1,16 @@ +var timer = null; + +function handleRequest(request, response) { + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + function () { + response.setStatusLine(null, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.write("hello"); + response.finish(); + }, + 3000 /* milliseconds */, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/dom/xhr/tests/file_html_in_xhr.html b/dom/xhr/tests/file_html_in_xhr.html new file mode 100644 index 0000000000..d77aeb4402 --- /dev/null +++ b/dom/xhr/tests/file_html_in_xhr.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/dom/xhr/tests/file_html_in_xhr.sjs b/dom/xhr/tests/file_html_in_xhr.sjs new file mode 100644 index 0000000000..6e7c57cae4 --- /dev/null +++ b/dom/xhr/tests/file_html_in_xhr.sjs @@ -0,0 +1,19 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/javascript", false); + if (request.queryString.includes("report")) { + if (getState("loaded") == "loaded") { + response.write( + "ok(false, 'This script was not supposed to get fetched.'); continueAfterReport();" + ); + } else { + response.write( + "ok(true, 'This script was not supposed to get fetched.'); continueAfterReport();" + ); + } + } else { + setState("loaded", "loaded"); + response.write( + 'document.documentElement.setAttribute("data-fail", "FAIL");' + ); + } +} diff --git a/dom/xhr/tests/file_html_in_xhr2.html b/dom/xhr/tests/file_html_in_xhr2.html new file mode 100644 index 0000000000..046052c753 --- /dev/null +++ b/dom/xhr/tests/file_html_in_xhr2.html @@ -0,0 +1 @@ +Þ diff --git a/dom/xhr/tests/file_html_in_xhr3.html b/dom/xhr/tests/file_html_in_xhr3.html new file mode 100644 index 0000000000..ff43ca4091 --- /dev/null +++ b/dom/xhr/tests/file_html_in_xhr3.html @@ -0,0 +1 @@ +SUCCESS diff --git a/dom/xhr/tests/file_sync_xhr_document_write_with_iframe.html b/dom/xhr/tests/file_sync_xhr_document_write_with_iframe.html new file mode 100644 index 0000000000..2135011d9c --- /dev/null +++ b/dom/xhr/tests/file_sync_xhr_document_write_with_iframe.html @@ -0,0 +1,22 @@ + + + + diff --git a/dom/xhr/tests/file_sync_xhr_event_handling_helper.html b/dom/xhr/tests/file_sync_xhr_event_handling_helper.html new file mode 100644 index 0000000000..d1235328f6 --- /dev/null +++ b/dom/xhr/tests/file_sync_xhr_event_handling_helper.html @@ -0,0 +1,37 @@ + + + + + + + diff --git a/dom/xhr/tests/file_sync_xhr_nested_helper.html b/dom/xhr/tests/file_sync_xhr_nested_helper.html new file mode 100644 index 0000000000..9b25a4453b --- /dev/null +++ b/dom/xhr/tests/file_sync_xhr_nested_helper.html @@ -0,0 +1,30 @@ + + + + + + + diff --git a/dom/xhr/tests/iframe_sync_xhr_unload.html b/dom/xhr/tests/iframe_sync_xhr_unload.html new file mode 100644 index 0000000000..134fad76d0 --- /dev/null +++ b/dom/xhr/tests/iframe_sync_xhr_unload.html @@ -0,0 +1,17 @@ + + + + + + diff --git a/dom/xhr/tests/mochitest.ini b/dom/xhr/tests/mochitest.ini new file mode 100644 index 0000000000..64280f0ddc --- /dev/null +++ b/dom/xhr/tests/mochitest.ini @@ -0,0 +1,161 @@ +[DEFAULT] +support-files = + echo.sjs + temporaryFileBlob.sjs + file_html_in_xhr.html + file_html_in_xhr.sjs + file_html_in_xhr2.html + file_html_in_xhr3.html + file_XHRDocURI.text + file_XHRDocURI.text^headers^ + file_XHRDocURI.xml + file_XHRDocURI.xml^headers^ + file_XHRDocURI.html + file_XHRDocURI.html^headers^ + file_XHRDocURI.sjs + file_XHRResponseURL.js + file_XHRResponseURL.sjs + file_XHRResponseURL.text + file_XHRResponseURL.text^headers^ + file_XHRResponseURL_nocors.text + file_XHRSendData.sjs + file_XHRSendData_doc.xml + file_XHRSendData_doc.xml^headers^ + file_XHR_anon.sjs + file_XHR_binary1.bin + file_XHR_binary1.bin^headers^ + file_XHR_binary2.bin + file_XHR_fail1.txt + file_XHR_fail1.txt^headers^ + file_XHR_header.sjs + file_XHR_pass1.xml + file_XHR_pass2.txt + file_XHR_pass3.txt + file_XHR_pass3.txt^headers^ + file_XHR_system_redirect.html + file_XHR_system_redirect.html^headers^ + file_XHR_timeout.sjs + progressserver.sjs + worker_terminateSyncXHR_frame.html + terminateSyncXHR_worker.js + worker_testXHR.txt + xhr_worker.js + xhr2_worker.js + xhrAbort_worker.js + test_worker_xhr_parameters.js + test_worker_xhr_system.js + worker_xhr_cors_redirect.js + worker_xhr_cors_redirect.sjs + worker_xhr_headers_server.sjs + worker_xhr_headers_worker.js + worker_file_getcookie.sjs + xhr_implicit_cancel_worker.js + relativeLoad_import.js + relativeLoad_worker.js + relativeLoad_worker2.js + responseIdentical.sjs + subdir/relativeLoad_sub_worker.js + subdir/relativeLoad_sub_worker2.js + subdir/relativeLoad_sub_import.js + common_temporaryFileBlob.js + worker_temporaryFileBlob.js + worker_bug1300552.js + sync_xhr_unload.sjs + iframe_sync_xhr_unload.html + empty.html + file_sync_xhr_document_write_with_iframe.html + slow.sjs + !/dom/events/test/event_leak_utils.js + worker_bug1697539.js + +[test_bug1070763.html] +skip-if = + http3 +[test_bug1300552.html] +skip-if = + http3 +[test_bug1788125.html] +[test_html_in_xhr.html] +[test_relativeLoad.html] +[test_sync_xhr_timer.xhtml] +[test_sync_xhr_unload.html] +[test_temporaryFileBlob.html] +skip-if = + http3 +[test_worker_terminateSyncXHR.html] +[test_worker_xhr.html] +[test_worker_xhr2.html] +[test_worker_xhr_3rdparty.html] +support-files = window_worker_xhr_3rdparty.html +# Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +skip-if = xorigin +[test_worker_xhr_cors_redirect.html] +skip-if = + http3 +[test_worker_xhr_headers.html] +skip-if = + http3 +[test_worker_xhr_implicit_cancel.html] +[test_worker_xhr_parameters.html] +[test_worker_xhr_responseURL.html] +skip-if = + http3 +[test_worker_xhr_system.html] +skip-if = + http3 +[test_worker_xhr_timeout.html] +[test_worker_xhrAbort.html] +skip-if = (os == "win") || (os == "mac") +[test_XHR.html] +[test_xhr_abort_after_load.html] +[test_XHR_anon.html] +skip-if = + http3 +[test_xhr_forbidden_headers.html] +[test_XHR_header.html] +skip-if = + http3 +[test_XHR_onuploadprogress.html] +[test_xhr_overridemimetype_throws_on_invalid_state.html] +[test_XHR_parameters.html] +[test_xhr_progressevents.html] +skip-if = + http3 +[test_xhr_send.html] +[test_xhr_send_readystate.html] +[test_XHR_system.html] +skip-if = + http3 +[test_XHR_timeout.html] +support-files = test_XHR_timeout.js +[test_xhr_withCredentials.html] +[test_XHRDocURI.html] +skip-if = + http3 +[test_XHRResponseURL.html] +skip-if = + http3 +[test_XHRSendData.html] +[test_sync_xhr_document_write_with_iframe.html] +skip-if = toolkit == "android" && debug +[test_sync_xhr_event_handling.html] +support-files = + file_sync_xhr_event_handling_helper.html +[test_sync_xhr_nested.html] +support-files = + file_sync_xhr_nested_helper.html +skip-if = + release_or_beta # Input event will be discarded during sync XHR, thus timeout +[test_nestedSyncXHR.html] +[test_event_listener_leaks.html] +skip-if = (os == "win" && processor == "aarch64") #bug 1535784 +[test_worker_xhr_doubleSend.html] +support-files = xhr_worker_doubleSend.js +[test_sharedworker_xhr.html] +support-files = xhr_sharedworker.js +[test_bug1697539.html] +support-files = worker_bug1697539.js +[test_bug1752863.html] +support-files = test_bug1752863_worker.js +skip-if = + os == 'linux' && bits == 64 && !debug # Bug 1755010 diff --git a/dom/xhr/tests/progressserver.sjs b/dom/xhr/tests/progressserver.sjs new file mode 100644 index 0000000000..3aae7e91db --- /dev/null +++ b/dom/xhr/tests/progressserver.sjs @@ -0,0 +1,56 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function setReq(req) { + setObjectState("dom/xhr/tests/progressserver", req); +} + +function getReq() { + var req; + getObjectState("dom/xhr/tests/progressserver", function (v) { + req = v; + }); + return req; +} + +function handleRequest(request, response) { + var pairs = request.queryString.split("&"); + var command = pairs.shift(); + dump("received '" + command + "' command\n"); + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var body = ""; + var bodyAvail; + while ((bodyAvail = bodyStream.available()) > 0) { + body += String.fromCharCode.apply( + null, + bodyStream.readByteArray(bodyAvail) + ); + } + + if (command == "open") { + response.processAsync(); + setReq(response); + + response.setHeader("Cache-Control", "no-cache", false); + pairs.forEach(function (val) { + var [name, value] = val.split("="); + response.setHeader(name, unescape(value), false); + }); + response.write(body); + return; + } + + if (command == "send") { + getReq().write(body); + } else if (command == "close") { + getReq().finish(); + setReq(null); + } + response.setHeader("Content-Type", "text/plain"); + response.write("ok"); +} diff --git a/dom/xhr/tests/relativeLoad_import.js b/dom/xhr/tests/relativeLoad_import.js new file mode 100644 index 0000000000..114d1883cd --- /dev/null +++ b/dom/xhr/tests/relativeLoad_import.js @@ -0,0 +1,5 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const workerURL = "relativeLoad_worker.js"; diff --git a/dom/xhr/tests/relativeLoad_worker.js b/dom/xhr/tests/relativeLoad_worker.js new file mode 100644 index 0000000000..b600b592be --- /dev/null +++ b/dom/xhr/tests/relativeLoad_worker.js @@ -0,0 +1,31 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-env worker */ +/* global workerURL */ +const importURL = "relativeLoad_import.js"; + +onmessage = function (event) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "worker_testXHR.txt", false); + xhr.send(null); + if ( + xhr.status != 200 || + xhr.responseText != "A noisy noise annoys an oyster." + ) { + throw new Error("Couldn't get xhr text from where we wanted it!"); + } + + importScripts(importURL); + var worker = new Worker("relativeLoad_worker2.js"); + worker.onerror = function (e) { + throw e.message; + }; + worker.onmessage = function (e) { + if (e.data != workerURL) { + throw new Error("Bad data!"); + } + postMessage(workerURL); + }; +}; diff --git a/dom/xhr/tests/relativeLoad_worker2.js b/dom/xhr/tests/relativeLoad_worker2.js new file mode 100644 index 0000000000..680f80a76a --- /dev/null +++ b/dom/xhr/tests/relativeLoad_worker2.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-env worker */ +/* global workerURL */ +const importURL = "relativeLoad_import.js"; + +importScripts(importURL); + +postMessage(workerURL); diff --git a/dom/xhr/tests/responseIdentical.sjs b/dom/xhr/tests/responseIdentical.sjs new file mode 100644 index 0000000000..0e7b796e0f --- /dev/null +++ b/dom/xhr/tests/responseIdentical.sjs @@ -0,0 +1,19 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +// Simply sending back the same data that is received +function handleRequest(request, response) { + var body = ""; + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var avail = 0; + while ((avail = bodyStream.available()) > 0) { + body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail)); + } + + response.setHeader("Content-Type", "application/octet-stream", false); + response.write(body); +} diff --git a/dom/xhr/tests/slow.sjs b/dom/xhr/tests/slow.sjs new file mode 100644 index 0000000000..0d0308f6e7 --- /dev/null +++ b/dom/xhr/tests/slow.sjs @@ -0,0 +1,13 @@ +function handleRequest(request, response) { + response.processAsync(); + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + function () { + response.write("Here the content. But slowly."); + response.finish(); + }, + 5000, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/dom/xhr/tests/subdir/relativeLoad_sub_import.js b/dom/xhr/tests/subdir/relativeLoad_sub_import.js new file mode 100644 index 0000000000..cac2a6f041 --- /dev/null +++ b/dom/xhr/tests/subdir/relativeLoad_sub_import.js @@ -0,0 +1,5 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const workerSubURL = "subdir/relativeLoad_sub_worker.js"; diff --git a/dom/xhr/tests/subdir/relativeLoad_sub_worker.js b/dom/xhr/tests/subdir/relativeLoad_sub_worker.js new file mode 100644 index 0000000000..5c1a4d59fb --- /dev/null +++ b/dom/xhr/tests/subdir/relativeLoad_sub_worker.js @@ -0,0 +1,28 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-env worker */ +/* global workerSubURL */ +const importSubURL = "relativeLoad_sub_import.js"; + +onmessage = function (_) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "testXHR.txt", false); + xhr.send(null); + if (xhr.status != 404) { + throw new Error("Loaded an xhr from the wrong location!"); + } + + importScripts(importSubURL); + var worker = new Worker("relativeLoad_sub_worker2.js"); + worker.onerror = function (event) { + throw event.data; + }; + worker.onmessage = function (event) { + if (event.data != workerSubURL) { + throw new Error("Bad data!"); + } + postMessage(workerSubURL); + }; +}; diff --git a/dom/xhr/tests/subdir/relativeLoad_sub_worker2.js b/dom/xhr/tests/subdir/relativeLoad_sub_worker2.js new file mode 100644 index 0000000000..b3e2c14d1a --- /dev/null +++ b/dom/xhr/tests/subdir/relativeLoad_sub_worker2.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-env worker */ +/* global workerSubURL */ +const importSubURL = "relativeLoad_sub_import.js"; + +importScripts(importSubURL); + +postMessage(workerSubURL); diff --git a/dom/xhr/tests/sync_xhr_unload.sjs b/dom/xhr/tests/sync_xhr_unload.sjs new file mode 100644 index 0000000000..f4f92649a8 --- /dev/null +++ b/dom/xhr/tests/sync_xhr_unload.sjs @@ -0,0 +1,16 @@ +var timer = null; + +function handleRequest(request, response) { + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + function () { + response.setStatusLine(null, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.write("hello"); + response.finish(); + }, + 30000 /* milliseconds */, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/dom/xhr/tests/temporaryFileBlob.sjs b/dom/xhr/tests/temporaryFileBlob.sjs new file mode 100644 index 0000000000..d952b325ce --- /dev/null +++ b/dom/xhr/tests/temporaryFileBlob.sjs @@ -0,0 +1,45 @@ +const CC = Components.Constructor; + +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +const BinaryOutputStream = CC( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" +); +const Timer = CC("@mozilla.org/timer;1", "nsITimer", "initWithCallback"); + +function handleRequest(request, response) { + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bodyBytes = []; + let bodyAvail; + while ((bodyAvail = bodyStream.available()) > 0) { + Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail)); + } + + var bos = new BinaryOutputStream(response.bodyOutputStream); + + response.processAsync(); + + var part = bodyBytes.splice(0, 256); + bos.writeByteArray(part); + + response.timer1 = new Timer( + function (timer) { + bos.writeByteArray(bodyBytes); + }, + 1000, + Ci.nsITimer.TYPE_ONE_SHOT + ); + + response.timer2 = new Timer( + function (timer) { + response.finish(); + }, + 2000, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/dom/xhr/tests/terminateSyncXHR_worker.js b/dom/xhr/tests/terminateSyncXHR_worker.js new file mode 100644 index 0000000000..7a2509af3d --- /dev/null +++ b/dom/xhr/tests/terminateSyncXHR_worker.js @@ -0,0 +1,20 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function (event) { + throw new Error("No messages should reach me!"); +}; + +var xhr = new XMLHttpRequest(); +xhr.open("GET", "worker_testXHR.txt", false); +xhr.addEventListener("loadstart", function () { + // Tell the parent to terminate us. + postMessage("TERMINATE"); + // And wait for it to do so. + while (1) { + true; + } +}); +xhr.send(null); diff --git a/dom/xhr/tests/test_XHR.html b/dom/xhr/tests/test_XHR.html new file mode 100644 index 0000000000..161efe455e --- /dev/null +++ b/dom/xhr/tests/test_XHR.html @@ -0,0 +1,383 @@ + + + + Test for XMLHttpRequest + + + + +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_XHRDocURI.html b/dom/xhr/tests/test_XHRDocURI.html new file mode 100644 index 0000000000..1062be13a6 --- /dev/null +++ b/dom/xhr/tests/test_XHRDocURI.html @@ -0,0 +1,487 @@ + + + + + XMLHttpRequest return document URIs + + + + + +Mozilla Bug 459470
+Mozilla Bug 859095 + +

+ +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_XHRResponseURL.html b/dom/xhr/tests/test_XHRResponseURL.html new file mode 100644 index 0000000000..f6bd3f3964 --- /dev/null +++ b/dom/xhr/tests/test_XHRResponseURL.html @@ -0,0 +1,69 @@ + + + + + + Test for Bug 998076 + + + + + + +Mozilla Bug 998076 +

+ +
+
+ + diff --git a/dom/xhr/tests/test_XHRSendData.html b/dom/xhr/tests/test_XHRSendData.html new file mode 100644 index 0000000000..5f801e827d --- /dev/null +++ b/dom/xhr/tests/test_XHRSendData.html @@ -0,0 +1,273 @@ + + + + + XMLHttpRequest send data and headers + + + + +Mozilla Bug 464848 +

+

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_XHR_anon.html b/dom/xhr/tests/test_XHR_anon.html new file mode 100644 index 0000000000..6747b6a918 --- /dev/null +++ b/dom/xhr/tests/test_XHR_anon.html @@ -0,0 +1,180 @@ + + + + + Test for XMLHttpRequest with system privileges + + + + +

+ +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_XHR_header.html b/dom/xhr/tests/test_XHR_header.html new file mode 100644 index 0000000000..0fd5829cac --- /dev/null +++ b/dom/xhr/tests/test_XHR_header.html @@ -0,0 +1,32 @@ + + + + Test for XMLHttpRequest.GetResponseHeader(foo) byte-inflates the output + + + + + +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_XHR_onuploadprogress.html b/dom/xhr/tests/test_XHR_onuploadprogress.html new file mode 100644 index 0000000000..89426c034a --- /dev/null +++ b/dom/xhr/tests/test_XHR_onuploadprogress.html @@ -0,0 +1,40 @@ + + + + + + Test for Bug 743666 + + + + +Mozilla Bug 743666 +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_XHR_parameters.html b/dom/xhr/tests/test_XHR_parameters.html new file mode 100644 index 0000000000..4ba1c5562a --- /dev/null +++ b/dom/xhr/tests/test_XHR_parameters.html @@ -0,0 +1,97 @@ + + + + + + + Test for XMLHttpRequest with system privileges + + + + +

+

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_XHR_system.html b/dom/xhr/tests/test_XHR_system.html new file mode 100644 index 0000000000..917a034ada --- /dev/null +++ b/dom/xhr/tests/test_XHR_system.html @@ -0,0 +1,99 @@ + + + + + Test for XMLHttpRequest with system privileges + + + + +

+

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_XHR_timeout.html b/dom/xhr/tests/test_XHR_timeout.html new file mode 100644 index 0000000000..e66ef1d30b --- /dev/null +++ b/dom/xhr/tests/test_XHR_timeout.html @@ -0,0 +1,59 @@ + + + + + Test for Bug 525816 + + + + + + +Mozilla Bug 525816 (XMLHttpRequest timeout) +

+
+ This test takes over 1 minute to run, probably over 2 minutes. +
+
+
+
+
+ + diff --git a/dom/xhr/tests/test_XHR_timeout.js b/dom/xhr/tests/test_XHR_timeout.js new file mode 100644 index 0000000000..e5ab627efc --- /dev/null +++ b/dom/xhr/tests/test_XHR_timeout.js @@ -0,0 +1,398 @@ +/* Notes: + - All times are expressed in milliseconds in this test suite. + - Test harness code is at the end of this file. + - We generate only one request at a time, to avoid overloading the HTTP + request handlers. + */ + +var inWorker = false; +try { + inWorker = !(self instanceof Window); +} catch (e) { + inWorker = true; +} + +function message(data) { + if (inWorker) { + self.postMessage(data); + } else { + self.postMessage(data, "*"); + } +} + +function is(got, expected, msg) { + var obj = {}; + obj.type = "is"; + obj.got = got; + obj.expected = expected; + obj.msg = msg; + + message(obj); +} + +function ok(bool, msg) { + var obj = {}; + obj.type = "ok"; + obj.bool = bool; + obj.msg = msg; + + message(obj); +} + +/** + * Generate and track results from a XMLHttpRequest with regards to timeouts. + * + * @param {String} id The test description. + * @param {Number} timeLimit The initial setting for the request timeout. + * @param {Number} resetAfter (Optional) The time after sending the request, to + * reset the timeout. + * @param {Number} resetTo (Optional) The delay to reset the timeout to. + * + * @note The actual testing takes place in handleEvent(event). + * The requests are generated in startXHR(). + * + * @note If resetAfter and resetTo are omitted, only the initial timeout setting + * applies. + * + * @constructor + * @implements DOMEventListener + */ +function RequestTracker(async, id, timeLimit /*[, resetAfter, resetTo]*/) { + this.async = async; + this.id = id; + this.timeLimit = timeLimit; + + if (arguments.length > 3) { + this.mustReset = true; + this.resetAfter = arguments[3]; + this.resetTo = arguments[4]; + } + + this.hasFired = false; +} +RequestTracker.prototype = { + /** + * Start the XMLHttpRequest! + */ + startXHR() { + var req = new XMLHttpRequest(); + this.request = req; + req.open("GET", "file_XHR_timeout.sjs", this.async); + var me = this; + function handleEvent(e) { + return me.handleEvent(e); + } + req.onerror = handleEvent; + req.onload = handleEvent; + req.onabort = handleEvent; + req.ontimeout = handleEvent; + + req.timeout = this.timeLimit; + + if (this.mustReset) { + var resetTo = this.resetTo; + self.setTimeout(function () { + req.timeout = resetTo; + }, this.resetAfter); + } + + req.send(null); + }, + + /** + * Get a message describing this test. + * + * @returns {String} The test description. + */ + getMessage() { + var rv = this.id + ", "; + if (this.mustReset) { + rv += "original timeout at " + this.timeLimit + ", "; + rv += "reset at " + this.resetAfter + " to " + this.resetTo; + } else { + rv += "timeout scheduled at " + this.timeLimit; + } + return rv; + }, + + /** + * Check the event received, and if it's the right (and only) one we get. + * + * @param {DOMProgressEvent} evt An event of type "load" or "timeout". + */ + handleEvent(evt) { + if (this.hasFired) { + ok(false, "Only one event should fire: " + this.getMessage()); + return; + } + this.hasFired = true; + + var type = evt.type, + expectedType; + // The XHR responds after 3000 milliseconds with a load event. + var timeLimit = + this.mustReset && this.resetAfter < Math.min(3000, this.timeLimit) + ? this.resetTo + : this.timeLimit; + if (timeLimit == 0 || timeLimit >= 3000) { + expectedType = "load"; + } else { + expectedType = "timeout"; + } + is(type, expectedType, this.getMessage()); + TestCounter.testComplete(); + }, +}; + +/** + * Generate and track XMLHttpRequests which will have abort() called on. + * + * @param shouldAbort {Boolean} True if we should call abort at all. + * @param abortDelay {Number} The time in ms to wait before calling abort(). + */ +function AbortedRequest(shouldAbort, abortDelay) { + this.shouldAbort = shouldAbort; + this.abortDelay = abortDelay; + this.hasFired = false; +} +AbortedRequest.prototype = { + /** + * Start the XMLHttpRequest! + */ + startXHR() { + var req = new XMLHttpRequest(); + this.request = req; + req.open("GET", "file_XHR_timeout.sjs"); + var me = this; + function handleEvent(e) { + return me.handleEvent(e); + } + req.onerror = handleEvent; + req.onload = handleEvent; + req.onabort = handleEvent; + req.ontimeout = handleEvent; + + req.timeout = 2000; + var _this = this; + + function abortReq() { + req.abort(); + } + + if (!this.shouldAbort) { + self.setTimeout(function () { + try { + _this.noEventsFired(); + } catch (e) { + ok(false, "Unexpected error: " + e); + TestCounter.testComplete(); + } + }, 5000); + } else { + // Abort events can only be triggered on sent requests. + req.send(); + if (this.abortDelay == -1) { + abortReq(); + } else { + self.setTimeout(abortReq, this.abortDelay); + } + } + }, + + /** + * Ensure that no events fired at all, especially not our timeout event. + */ + noEventsFired() { + ok( + !this.hasFired, + "No events should fire for an unsent, unaborted request" + ); + // We're done; if timeout hasn't fired by now, it never will. + TestCounter.testComplete(); + }, + + /** + * Get a message describing this test. + * + * @returns {String} The test description. + */ + getMessage() { + return "time to abort is " + this.abortDelay + ", timeout set at 2000"; + }, + + /** + * Check the event received, and if it's the right (and only) one we get. + * + * @param {DOMProgressEvent} evt An event of type "load" or "timeout". + */ + handleEvent(evt) { + if (this.hasFired) { + ok(false, "Only abort event should fire: " + this.getMessage()); + return; + } + this.hasFired = true; + + var expectedEvent = this.abortDelay >= 2000 ? "timeout" : "abort"; + is(evt.type, expectedEvent, this.getMessage()); + TestCounter.testComplete(); + }, +}; + +var SyncRequestSettingTimeoutAfterOpen = { + startXHR() { + var pass = false; + var req = new XMLHttpRequest(); + req.open("GET", "file_XHR_timeout.sjs", false); + try { + req.timeout = 1000; + } catch (e) { + pass = true; + } + ok(pass, "Synchronous XHR must not allow a timeout to be set"); + TestCounter.testComplete(); + }, +}; + +var SyncRequestSettingTimeoutBeforeOpen = { + startXHR() { + var pass = false; + var req = new XMLHttpRequest(); + req.timeout = 1000; + try { + req.open("GET", "file_XHR_timeout.sjs", false); + } catch (e) { + pass = true; + } + ok(pass, "Synchronous XHR must not allow a timeout to be set"); + TestCounter.testComplete(); + }, +}; + +var TestRequests = [ + // Simple timeouts. + new RequestTracker(true, "no time out scheduled, load fires normally", 0), + new RequestTracker(true, "load fires normally", 5000), + new RequestTracker(true, "timeout hit before load", 2000), + + // Timeouts reset after a certain delay. + new RequestTracker( + true, + "load fires normally with no timeout set, twice", + 0, + 2000, + 0 + ), + new RequestTracker( + true, + "load fires normally with same timeout set twice", + 5000, + 2000, + 5000 + ), + new RequestTracker( + true, + "timeout fires normally with same timeout set twice", + 2000, + 1000, + 2000 + ), + + new RequestTracker( + true, + "timeout disabled after initially set", + 5000, + 2000, + 0 + ), + new RequestTracker( + true, + "timeout overrides load after a delay", + 5000, + 1000, + 2000 + ), + new RequestTracker( + true, + "timeout enabled after initially disabled", + 0, + 2000, + 5000 + ), + + new RequestTracker( + true, + "timeout set to expiring value after load fires", + 5000, + 4000, + 1000 + ), + new RequestTracker( + true, + "timeout set to expired value before load fires", + 5000, + 2000, + 1000 + ), + new RequestTracker( + true, + "timeout set to non-expiring value after timeout fires", + 1000, + 2000, + 5000 + ), + + // Aborted requests. + new AbortedRequest(false), + new AbortedRequest(true, -1), + new AbortedRequest(true, 5000), +]; + +var MainThreadTestRequests = [ + new AbortedRequest(true, 0), + new AbortedRequest(true, 1000), + + // Synchronous requests. + SyncRequestSettingTimeoutAfterOpen, + SyncRequestSettingTimeoutBeforeOpen, +]; + +var WorkerThreadTestRequests = [ + // Simple timeouts. + new RequestTracker(false, "no time out scheduled, load fires normally", 0), + new RequestTracker(false, "load fires normally", 5000), + new RequestTracker(false, "timeout hit before load", 2000), + + // Reset timeouts don't make much sense with a sync request ... +]; + +if (inWorker) { + TestRequests = TestRequests.concat(WorkerThreadTestRequests); +} else { + TestRequests = TestRequests.concat(MainThreadTestRequests); +} + +// This code controls moving from one test to another. +var TestCounter = { + testComplete() { + // Allow for the possibility there are other events coming. + self.setTimeout(function () { + TestCounter.next(); + }, 5000); + }, + + next() { + var test = TestRequests.shift(); + + if (test) { + test.startXHR(); + } else { + message("done"); + } + }, +}; + +self.addEventListener("message", function (event) { + if (event.data == "start") { + TestCounter.next(); + } +}); diff --git a/dom/xhr/tests/test_bug1070763.html b/dom/xhr/tests/test_bug1070763.html new file mode 100644 index 0000000000..baf6ade4c1 --- /dev/null +++ b/dom/xhr/tests/test_bug1070763.html @@ -0,0 +1,58 @@ + + + + + XMLHttpRequest send data and headers + + + + +Mozilla Bug 1405571 +

+

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_bug1300552.html b/dom/xhr/tests/test_bug1300552.html new file mode 100644 index 0000000000..2374eb7c97 --- /dev/null +++ b/dom/xhr/tests/test_bug1300552.html @@ -0,0 +1,29 @@ + + + + + Test for Bug 1300552 + + + + + + + diff --git a/dom/xhr/tests/test_bug1697539.html b/dom/xhr/tests/test_bug1697539.html new file mode 100644 index 0000000000..c776bf9796 --- /dev/null +++ b/dom/xhr/tests/test_bug1697539.html @@ -0,0 +1,27 @@ + + + + + Test for Bug 1697539 + + + + + + + diff --git a/dom/xhr/tests/test_bug1752863.html b/dom/xhr/tests/test_bug1752863.html new file mode 100644 index 0000000000..567fdb017d --- /dev/null +++ b/dom/xhr/tests/test_bug1752863.html @@ -0,0 +1,32 @@ + + + + + Test for Bug 1752863 + + + + + diff --git a/dom/xhr/tests/test_bug1752863_worker.js b/dom/xhr/tests/test_bug1752863_worker.js new file mode 100644 index 0000000000..196b825c0c --- /dev/null +++ b/dom/xhr/tests/test_bug1752863_worker.js @@ -0,0 +1,34 @@ +var xhr; +var myself; + +async function handleLoadstart() { + try { + xhr.open("POST", "FOOBAR", false); + // This will potentially queue another "loadstart" event + // before we can catch (err). But the order should be + // guaranteed, that is the first postMessage arriving at + // our parent is from the first catch (err). + xhr.send(); + myself.postMessage("MissingError"); + } catch (err) { + if (err instanceof DOMException) { + // This is what we expect to happen on the first error + // and the parent will check for this to arrive. + myself.postMessage("DOMException"); + } else { + myself.postMessage("OtherError"); + } + // Let's ensure we still bail out from the processing. + xhr.removeEventListener("loadstart", handleLoadstart, true); + throw err; + } +} + +self.onmessage = async function (ev) { + xhr = new XMLHttpRequest({ mozAnon: false }); + myself = self; + xhr.addEventListener("loadstart", handleLoadstart, true); + xhr.open("POST", "FOOBAR", false); + xhr.send(); + postMessage("TERMINATE"); +}; diff --git a/dom/xhr/tests/test_bug1788125.html b/dom/xhr/tests/test_bug1788125.html new file mode 100644 index 0000000000..44cb79864c --- /dev/null +++ b/dom/xhr/tests/test_bug1788125.html @@ -0,0 +1,59 @@ + + + + + <!-- TODO: insert title here --> + + + + + + +
+

+ +

+
+
diff --git a/dom/xhr/tests/test_event_listener_leaks.html b/dom/xhr/tests/test_event_listener_leaks.html
new file mode 100644
index 0000000000..da0ca75323
--- /dev/null
+++ b/dom/xhr/tests/test_event_listener_leaks.html
@@ -0,0 +1,45 @@
+
+
+
+
+  Bug 1450271 - Test XHR event listener leak conditions
+  
+  
+  
+
+
+

+ + + + diff --git a/dom/xhr/tests/test_html_in_xhr.html b/dom/xhr/tests/test_html_in_xhr.html new file mode 100644 index 0000000000..e09b700295 --- /dev/null +++ b/dom/xhr/tests/test_html_in_xhr.html @@ -0,0 +1,97 @@ + + + + + Test for Bug 651072 + + + + + +Mozilla Bug 651072 +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_nestedSyncXHR.html b/dom/xhr/tests/test_nestedSyncXHR.html new file mode 100644 index 0000000000..a2e10ad9e5 --- /dev/null +++ b/dom/xhr/tests/test_nestedSyncXHR.html @@ -0,0 +1,101 @@ + + + + + Test for sync XHR into sync XHRs + + + + + + + diff --git a/dom/xhr/tests/test_relativeLoad.html b/dom/xhr/tests/test_relativeLoad.html new file mode 100644 index 0000000000..fdfe35ddd8 --- /dev/null +++ b/dom/xhr/tests/test_relativeLoad.html @@ -0,0 +1,51 @@ + + + + + + Test for DOM Worker Threads + + + + +
+
+
+ + diff --git a/dom/xhr/tests/test_sharedworker_xhr.html b/dom/xhr/tests/test_sharedworker_xhr.html new file mode 100644 index 0000000000..c0c43bcb26 --- /dev/null +++ b/dom/xhr/tests/test_sharedworker_xhr.html @@ -0,0 +1,23 @@ + + + + Test for SharedWorker Threads XHR + + + + + + + diff --git a/dom/xhr/tests/test_sync_xhr_document_write_with_iframe.html b/dom/xhr/tests/test_sync_xhr_document_write_with_iframe.html new file mode 100644 index 0000000000..eb03f7cf23 --- /dev/null +++ b/dom/xhr/tests/test_sync_xhr_document_write_with_iframe.html @@ -0,0 +1,28 @@ + + + + Test for Bug + + + + +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_sync_xhr_event_handling.html b/dom/xhr/tests/test_sync_xhr_event_handling.html new file mode 100644 index 0000000000..eaaa1f608a --- /dev/null +++ b/dom/xhr/tests/test_sync_xhr_event_handling.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/dom/xhr/tests/test_sync_xhr_nested.html b/dom/xhr/tests/test_sync_xhr_nested.html new file mode 100644 index 0000000000..450c5f7e54 --- /dev/null +++ b/dom/xhr/tests/test_sync_xhr_nested.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + diff --git a/dom/xhr/tests/test_sync_xhr_timer.xhtml b/dom/xhr/tests/test_sync_xhr_timer.xhtml new file mode 100644 index 0000000000..823488889f --- /dev/null +++ b/dom/xhr/tests/test_sync_xhr_timer.xhtml @@ -0,0 +1,52 @@ + + + + + Test for Bug + + + + +Mozilla Bug +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_sync_xhr_unload.html b/dom/xhr/tests/test_sync_xhr_unload.html new file mode 100644 index 0000000000..069071461b --- /dev/null +++ b/dom/xhr/tests/test_sync_xhr_unload.html @@ -0,0 +1,36 @@ + + + + + Test for Bug 1307122 + + + + + + + + diff --git a/dom/xhr/tests/test_temporaryFileBlob.html b/dom/xhr/tests/test_temporaryFileBlob.html new file mode 100644 index 0000000000..ed0bca28e2 --- /dev/null +++ b/dom/xhr/tests/test_temporaryFileBlob.html @@ -0,0 +1,40 @@ + + + + + Test for Bug 1202006 + + + + + + + + diff --git a/dom/xhr/tests/test_worker_terminateSyncXHR.html b/dom/xhr/tests/test_worker_terminateSyncXHR.html new file mode 100644 index 0000000000..d952e3c6e7 --- /dev/null +++ b/dom/xhr/tests/test_worker_terminateSyncXHR.html @@ -0,0 +1,44 @@ + + + + + + Test for DOM Worker Threads XHR (Bug 450452 ) + + + + +DOM Worker Threads XHR (Bug 450452) +

+
+ +
+
+
+
+ + diff --git a/dom/xhr/tests/test_worker_xhr.html b/dom/xhr/tests/test_worker_xhr.html new file mode 100644 index 0000000000..f95fbe8050 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr.html @@ -0,0 +1,76 @@ + + + + + + Test for DOM Worker Threads XHR (Bug 450452 ) + + + + +DOM Worker Threads XHR (Bug 450452) +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_worker_xhr2.html b/dom/xhr/tests/test_worker_xhr2.html new file mode 100644 index 0000000000..41a7b8ee85 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr2.html @@ -0,0 +1,37 @@ + + + + + + Test for DOM Worker Threads XHR (Bug 450452 ) + + + + +DOM Worker Threads XHR (Bug 450452) +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_worker_xhrAbort.html b/dom/xhr/tests/test_worker_xhrAbort.html new file mode 100644 index 0000000000..35e77077c0 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhrAbort.html @@ -0,0 +1,44 @@ + + + + + + Test for DOM Worker Threads XHR (Bug 450452 ) + + + + +DOM Worker Threads XHR (Bug 450452) +

+ +
+
+
+
+ + diff --git a/dom/xhr/tests/test_worker_xhr_3rdparty.html b/dom/xhr/tests/test_worker_xhr_3rdparty.html new file mode 100644 index 0000000000..ab29b09615 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_3rdparty.html @@ -0,0 +1,51 @@ + + + + + + Test for DOM Worker Threads XHR (Bug 450452 ) + + + + +DOM Worker Threads XHR (Bug 450452) + + +
+
+
+ + diff --git a/dom/xhr/tests/test_worker_xhr_cors_redirect.html b/dom/xhr/tests/test_worker_xhr_cors_redirect.html new file mode 100644 index 0000000000..777c31b1c2 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_cors_redirect.html @@ -0,0 +1,35 @@ + + + + + Test for Bug 1206121 + + + + + + + diff --git a/dom/xhr/tests/test_worker_xhr_doubleSend.html b/dom/xhr/tests/test_worker_xhr_doubleSend.html new file mode 100644 index 0000000000..d5a052bcf9 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_doubleSend.html @@ -0,0 +1,30 @@ + + + + + Test for DOM Worker Threads XHR - double send + + + + + + + + diff --git a/dom/xhr/tests/test_worker_xhr_headers.html b/dom/xhr/tests/test_worker_xhr_headers.html new file mode 100644 index 0000000000..1416ababd7 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_headers.html @@ -0,0 +1,86 @@ + + + + + Test for XHR Headers + + + + +

+ +
+      
+    
+ + diff --git a/dom/xhr/tests/test_worker_xhr_implicit_cancel.html b/dom/xhr/tests/test_worker_xhr_implicit_cancel.html new file mode 100644 index 0000000000..d749b0696f --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_implicit_cancel.html @@ -0,0 +1,43 @@ + + + + + + Test for DOM Worker Threads XHR (Bug 450452 ) + + + + +DOM Worker Threads XHR (Bug 450452) +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_worker_xhr_parameters.html b/dom/xhr/tests/test_worker_xhr_parameters.html new file mode 100644 index 0000000000..dde0c32ed7 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_parameters.html @@ -0,0 +1,66 @@ + + + + + Test for XMLHttpRequest with system privileges + + + + +

+

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_worker_xhr_parameters.js b/dom/xhr/tests/test_worker_xhr_parameters.js new file mode 100644 index 0000000000..688457db96 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_parameters.js @@ -0,0 +1,84 @@ +function ok(what, msg) { + postMessage({ event: msg, test: "ok", a: what }); +} + +function is(a, b, msg) { + postMessage({ event: msg, test: "is", a, b }); +} + +// This is a copy of dom/xhr/tests/test_XHR_parameters.js +var validParameters = [ + undefined, + null, + {}, + { mozSystem: "" }, + { mozSystem: 0 }, + { mozAnon: 1 }, + { mozAnon: [] }, + { + get mozAnon() { + return true; + }, + }, + 0, + 7, + Math.PI, + "string", + true, + false, +]; + +var invalidParameters = [ + { + get mozSystem() { + throw new Error("Bla"); + }, + }, +]; + +function testParameters(havePrivileges) { + function testValidParameter(value) { + var xhr; + try { + xhr = new XMLHttpRequest(value); + } catch (ex) { + ok(false, "Got unexpected exception: " + ex); + return; + } + ok(!!xhr, "passed " + JSON.stringify(value)); + + // If the page doesnt have privileges to create a system or anon XHR, + // these flags will always be false no matter what is passed. + var expectedAnon = false; + var expectedSystem = false; + if (havePrivileges) { + expectedAnon = Boolean(value && value.mozAnon); + expectedSystem = Boolean(value && value.mozSystem); + } + is(xhr.mozAnon, expectedAnon, "testing mozAnon"); + is(xhr.mozSystem, expectedSystem, "testing mozSystem"); + } + + function testInvalidParameter(value) { + try { + new XMLHttpRequest(value); + ok( + false, + "invalid parameter did not cause exception: " + JSON.stringify(value) + ); + } catch (ex) { + ok( + true, + "invalid parameter raised exception as expected: " + JSON.stringify(ex) + ); + } + } + + validParameters.forEach(testValidParameter); + invalidParameters.forEach(testInvalidParameter); +} + +self.onmessage = function onmessage(event) { + testParameters(event.data); + postMessage({ test: "finish" }); +}; diff --git a/dom/xhr/tests/test_worker_xhr_responseURL.html b/dom/xhr/tests/test_worker_xhr_responseURL.html new file mode 100644 index 0000000000..89924e9815 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_responseURL.html @@ -0,0 +1,76 @@ + + + + + + Test for Bug 998076 + + + + + +Mozilla Bug 998076 +

+ +
+
+ + diff --git a/dom/xhr/tests/test_worker_xhr_system.html b/dom/xhr/tests/test_worker_xhr_system.html new file mode 100644 index 0000000000..6d86d110e5 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_system.html @@ -0,0 +1,54 @@ + + + + + Test for XMLHttpRequest with system privileges + + + + +

+

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_worker_xhr_system.js b/dom/xhr/tests/test_worker_xhr_system.js new file mode 100644 index 0000000000..23137801a0 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_system.js @@ -0,0 +1,30 @@ +function ok(what, msg) { + postMessage({ event: msg, test: "ok", a: what }); +} + +function is(a, b, msg) { + postMessage({ event: msg, test: "is", a, b }); +} + +self.onmessage = function onmessage(event) { + // An XHR with system privileges will be able to do cross-site calls. + + const TEST_URL = + "http://example.com/tests/dom/xhr/tests/test_XHR_system.html"; + is(location.hostname, "mochi.test", "hostname should be mochi.test"); + + var xhr = new XMLHttpRequest({ mozSystem: true }); + is(xhr.mozSystem, true, ".mozSystem == true"); + xhr.open("GET", TEST_URL); + xhr.onload = function onload() { + is(xhr.status, 200); + ok(xhr.responseText != null); + ok(xhr.responseText.length); + postMessage({ test: "finish" }); + }; + xhr.onerror = function onerror() { + ok(false, "Got an error event!"); + postMessage({ test: "finish" }); + }; + xhr.send(); +}; diff --git a/dom/xhr/tests/test_worker_xhr_timeout.html b/dom/xhr/tests/test_worker_xhr_timeout.html new file mode 100644 index 0000000000..3cbfcefb5e --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_timeout.html @@ -0,0 +1,57 @@ + + + + + Test for Bug 498998 + + + + + + +Mozilla Bug 498998 (Worker XMLHttpRequest timeout) +

+
+ This test takes over 1 minute to run, probably over 2 minutes. +
+
+
+
+ + diff --git a/dom/xhr/tests/test_xhr_abort_after_load.html b/dom/xhr/tests/test_xhr_abort_after_load.html new file mode 100644 index 0000000000..f3d6f4d2f2 --- /dev/null +++ b/dom/xhr/tests/test_xhr_abort_after_load.html @@ -0,0 +1,96 @@ + + + + Test bug 482935 + + + + + + + diff --git a/dom/xhr/tests/test_xhr_forbidden_headers.html b/dom/xhr/tests/test_xhr_forbidden_headers.html new file mode 100644 index 0000000000..d393544b79 --- /dev/null +++ b/dom/xhr/tests/test_xhr_forbidden_headers.html @@ -0,0 +1,96 @@ + + + + + Test for Bug 308484 + + + + +Mozilla Bug 308484 +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_xhr_overridemimetype_throws_on_invalid_state.html b/dom/xhr/tests/test_xhr_overridemimetype_throws_on_invalid_state.html new file mode 100644 index 0000000000..99e6bde004 --- /dev/null +++ b/dom/xhr/tests/test_xhr_overridemimetype_throws_on_invalid_state.html @@ -0,0 +1,62 @@ + + + + Test bug 482935 + + + + + + + diff --git a/dom/xhr/tests/test_xhr_progressevents.html b/dom/xhr/tests/test_xhr_progressevents.html new file mode 100644 index 0000000000..ebfc06fd5b --- /dev/null +++ b/dom/xhr/tests/test_xhr_progressevents.html @@ -0,0 +1,307 @@ + + + + Test for XMLHttpRequest Progress Events + + + + +

+
+
+
+
diff --git a/dom/xhr/tests/test_xhr_send.html b/dom/xhr/tests/test_xhr_send.html
new file mode 100644
index 0000000000..cec347a88b
--- /dev/null
+++ b/dom/xhr/tests/test_xhr_send.html
@@ -0,0 +1,83 @@
+
+
+
+
+  
+  Test for Bug 1096263
+  
+  
+  
+
+
+Mozilla Bug 1096263
+

+
+ +
+
+
+ + diff --git a/dom/xhr/tests/test_xhr_send_readystate.html b/dom/xhr/tests/test_xhr_send_readystate.html new file mode 100644 index 0000000000..a810055a64 --- /dev/null +++ b/dom/xhr/tests/test_xhr_send_readystate.html @@ -0,0 +1,39 @@ + + + + + + Test for Bug 814064 + + + + +Mozilla Bug 814064 +

+ +
+
+
+ + diff --git a/dom/xhr/tests/test_xhr_withCredentials.html b/dom/xhr/tests/test_xhr_withCredentials.html new file mode 100644 index 0000000000..320e303e68 --- /dev/null +++ b/dom/xhr/tests/test_xhr_withCredentials.html @@ -0,0 +1,35 @@ + + + + + + Test for Bug 814050 + + + + +Mozilla Bug 814050 +

+ +
+
+
+ + diff --git a/dom/xhr/tests/window_worker_xhr_3rdparty.html b/dom/xhr/tests/window_worker_xhr_3rdparty.html new file mode 100644 index 0000000000..1c22a3aad0 --- /dev/null +++ b/dom/xhr/tests/window_worker_xhr_3rdparty.html @@ -0,0 +1,75 @@ + + + + + + + + + diff --git a/dom/xhr/tests/worker_bug1300552.js b/dom/xhr/tests/worker_bug1300552.js new file mode 100644 index 0000000000..d874f32c8d --- /dev/null +++ b/dom/xhr/tests/worker_bug1300552.js @@ -0,0 +1,37 @@ +function info(msg) { + postMessage({ type: "info", msg }); +} + +function ok(a, msg) { + postMessage({ type: "check", what: !!a, msg }); +} + +function finish() { + postMessage({ type: "finish" }); +} + +info("Creating XHR..."); +var xhr = new XMLHttpRequest(); +xhr.open("POST", "echo.sjs"); +xhr.responseType = "arraybuffer"; + +info("Sending some data..."); +var data = new Array(256).join("1234567890ABCDEF"); +xhr.send({ + toString() { + return data; + }, +}); + +var aborted = false; + +xhr.onprogress = function () { + info("Onprogress, we abort!"); + aborted = true; + xhr.abort(); +}; + +xhr.onloadend = function () { + ok(aborted, "We are still alive after an abort()!"); + finish(); +}; diff --git a/dom/xhr/tests/worker_bug1697539.js b/dom/xhr/tests/worker_bug1697539.js new file mode 100644 index 0000000000..faef1f1614 --- /dev/null +++ b/dom/xhr/tests/worker_bug1697539.js @@ -0,0 +1,19 @@ +onmessage = function (e) { + let xhr = new XMLHttpRequest(); + let already_sent = false; + xhr.addEventListener("readystatechange", event => { + try { + event.originalTarget.send("test"); + } catch (error) { + if (error.name == "InvalidStateError") { + if (!already_sent) { + postMessage(error.name); + already_sent = true; + } + } + } + }); + + xhr.open("POST", e.data, false); + xhr.send(); +}; diff --git a/dom/xhr/tests/worker_file_getcookie.sjs b/dom/xhr/tests/worker_file_getcookie.sjs new file mode 100644 index 0000000000..b5204bdd71 --- /dev/null +++ b/dom/xhr/tests/worker_file_getcookie.sjs @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +function handleRequest(request, response) { + try { + var cookie = request.getHeader("Cookie"); + } catch (e) { + cookie = "EMPTY_COOKIE"; + } + + // avoid confusing cache behaviors. + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-type", "text/plain", false); + response.setStatusLine(request.httpVersion, "200", "OK"); + response.write(cookie); +} diff --git a/dom/xhr/tests/worker_temporaryFileBlob.js b/dom/xhr/tests/worker_temporaryFileBlob.js new file mode 100644 index 0000000000..50f071bab7 --- /dev/null +++ b/dom/xhr/tests/worker_temporaryFileBlob.js @@ -0,0 +1,30 @@ +/* eslint-env worker */ +importScripts("common_temporaryFileBlob.js"); + +function info(msg) { + postMessage({ type: "info", msg }); +} + +function ok(a, msg) { + postMessage({ type: "check", what: !!a, msg }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function next() { + postMessage({ type: "finish" }); +} + +onmessage = function (e) { + if (e.data == "simple") { + test_simple(); + } else if (e.data == "abort") { + test_abort(); + } else if (e.data == "reuse") { + test_reuse(); + } else { + ok(false, "Something wrong happened"); + } +}; diff --git a/dom/xhr/tests/worker_terminateSyncXHR_frame.html b/dom/xhr/tests/worker_terminateSyncXHR_frame.html new file mode 100644 index 0000000000..04bd53ff1d --- /dev/null +++ b/dom/xhr/tests/worker_terminateSyncXHR_frame.html @@ -0,0 +1,25 @@ + + + + + Test for SharedWorker + + + + + diff --git a/dom/xhr/tests/worker_testXHR.txt b/dom/xhr/tests/worker_testXHR.txt new file mode 100644 index 0000000000..2beab22c66 --- /dev/null +++ b/dom/xhr/tests/worker_testXHR.txt @@ -0,0 +1 @@ +A noisy noise annoys an oyster. \ No newline at end of file diff --git a/dom/xhr/tests/worker_xhr_cors_redirect.js b/dom/xhr/tests/worker_xhr_cors_redirect.js new file mode 100644 index 0000000000..0d86e75eaa --- /dev/null +++ b/dom/xhr/tests/worker_xhr_cors_redirect.js @@ -0,0 +1,10 @@ +onmessage = function (e) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", e.data, true); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + postMessage(xhr.status); + } + }; + xhr.send(); +}; diff --git a/dom/xhr/tests/worker_xhr_cors_redirect.sjs b/dom/xhr/tests/worker_xhr_cors_redirect.sjs new file mode 100644 index 0000000000..aac9c8ffe6 --- /dev/null +++ b/dom/xhr/tests/worker_xhr_cors_redirect.sjs @@ -0,0 +1,10 @@ +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + + if (request.queryString == "redirect") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "worker_xhr_cors_redirect.sjs"); + } else { + response.write("'hello world'"); + } +} diff --git a/dom/xhr/tests/worker_xhr_headers_server.sjs b/dom/xhr/tests/worker_xhr_headers_server.sjs new file mode 100644 index 0000000000..c2a944f6ef --- /dev/null +++ b/dom/xhr/tests/worker_xhr_headers_server.sjs @@ -0,0 +1,69 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +function handleRequest(request, response) { + switch (request.method) { + case "POST": + try { + var optionsHost = request.getHeader("options-host"); + } catch (e) {} + + var headerFound = false; + if (optionsHost) { + setState("postHost", request.host); + setState("optionsHost", optionsHost); + headerFound = true; + } + + try { + var emptyHeader = "nada" + request.getHeader("empty"); + } catch (e) {} + + if (emptyHeader && emptyHeader == "nada") { + setState("emptyHeader", "nada"); + headerFound = true; + } + if (headerFound) { + return; + } + break; + + case "OPTIONS": + if (getState("optionsHost") == request.host) { + try { + var optionsHeader = request.getHeader( + "Access-Control-Request-Headers" + ); + } catch (e) {} + setState("optionsHeader", "'" + optionsHeader + "'"); + } + break; + + case "GET": + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + + if ( + getState("postHost") == request.host && + getState("emptyHeader") == "nada" + ) { + var result = getState("optionsHeader"); + if (result) { + response.write( + "Success: expected OPTIONS request with " + result + " header" + ); + } else if (getState("badGet") == 1) { + response.write("Error: unexpected GET request"); + } + } else { + setState("badGet", "1"); + response.write("Error: this response should never be seen"); + } + return; + } + + response.setStatusLine(request.httpVersion, 501, "Not Implemented"); +} diff --git a/dom/xhr/tests/worker_xhr_headers_worker.js b/dom/xhr/tests/worker_xhr_headers_worker.js new file mode 100644 index 0000000000..e4f4341cef --- /dev/null +++ b/dom/xhr/tests/worker_xhr_headers_worker.js @@ -0,0 +1,16 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +var customHeader = "custom-key"; +var customHeaderValue = "custom-key-value"; + +self.onmessage = function (event) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", event.data, false); + xhr.setRequestHeader(customHeader, customHeaderValue); + xhr.send(); + postMessage({ response: xhr.responseText, header: customHeader }); +}; diff --git a/dom/xhr/tests/xhr2_worker.js b/dom/xhr/tests/xhr2_worker.js new file mode 100644 index 0000000000..f9bfc88ed9 --- /dev/null +++ b/dom/xhr/tests/xhr2_worker.js @@ -0,0 +1,102 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-env worker */ +onmessage = function (event) { + const url = event.data; + + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.send(); + + const refText = xhr.responseText; + + function getResponse(type) { + xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + if (type !== undefined) { + xhr.responseType = type; + } + xhr.send(); + return xhr.response; + } + + if (getResponse() != refText) { + throw new Error("unset responseType failed"); + } + + if (getResponse("") != refText) { + throw new Error("'' responseType failed"); + } + + if (getResponse("text") != refText) { + throw new Error("'text' responseType failed"); + } + + var array = new Uint8Array(getResponse("arraybuffer")); + if (String.fromCharCode.apply(String, array) != refText) { + throw new Error("'arraybuffer' responseType failed"); + } + + var blob = getResponse("blob"); + if (new FileReaderSync().readAsText(blob) != refText) { + throw new Error("'blob' responseType failed"); + } + + // Make sure that we get invalid state exceptions when getting the wrong + // property. + + function testResponseTextException(type) { + xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.responseType = type; + xhr.send(); + + var exception; + + try { + xhr.responseText; + } catch (e) { + exception = e; + } + + if (!exception) { + throw new Error( + "Failed to throw when getting responseText on '" + type + "' type" + ); + } + + if (exception.name != "InvalidStateError") { + throw new Error( + "Unexpected error when getting responseText on '" + type + "' type" + ); + } + + if (exception.code != DOMException.INVALID_STATE_ERR) { + throw new Error( + "Unexpected error code when getting responseText on '" + type + "' type" + ); + } + } + + testResponseTextException("arraybuffer"); + testResponseTextException("blob"); + + // Make sure "document" works, but returns text. + xhr = new XMLHttpRequest(); + + if (xhr.responseType != "") { + throw new Error("Default value for responseType is wrong!"); + } + + xhr.open("GET", url, false); + xhr.responseType = "document"; + xhr.send(); + + if (xhr.responseText != refText) { + throw new Error("'document' type not working correctly"); + } + + postMessage("done"); +}; diff --git a/dom/xhr/tests/xhrAbort_worker.js b/dom/xhr/tests/xhrAbort_worker.js new file mode 100644 index 0000000000..6b82241d68 --- /dev/null +++ b/dom/xhr/tests/xhrAbort_worker.js @@ -0,0 +1,101 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-env worker */ +/* global WorkerProgressEvent */ +function runTest() { + var xhr = new XMLHttpRequest(); + + var events = []; + function pushEvent(event) { + var readyState, responseText, status, statusText; + + try { + readyState = xhr.readyState; + } catch (e) { + readyState = "[exception]"; + } + + try { + responseText = xhr.responseText; + } catch (e) { + responseText = "[exception]"; + } + + try { + status = xhr.status; + } catch (e) { + status = "[exception]"; + } + + try { + statusText = xhr.statusText; + } catch (e) { + statusText = "[exception]"; + } + + var str = + event.type + + "(" + + readyState + + ", '" + + responseText + + "', " + + status + + ", '" + + statusText + + "'"; + if ( + ("ProgressEvent" in this && event instanceof ProgressEvent) || + ("WorkerProgressEvent" in this && event instanceof WorkerProgressEvent) + ) { + str += ", progressEvent"; + } + str += ")"; + + events.push(str); + } + + xhr.onerror = function (event) { + throw new Error("Error: " + xhr.statusText); + }; + + xhr.onload = function (event) { + throw new Error("Shouldn't have gotten load event!"); + }; + + var seenAbort; + xhr.onabort = function (event) { + if (seenAbort) { + throw new Error("Already seen the abort event!"); + } + seenAbort = true; + + pushEvent(event); + postMessage(events); + }; + + xhr.onreadystatechange = function (event) { + pushEvent(event); + if (xhr.readyState == xhr.HEADERS_RECEIVED) { + xhr.abort(); + } + }; + + xhr.open("GET", "worker_testXHR.txt"); + xhr.overrideMimeType("text/plain"); + xhr.send(null); +} + +function messageListener(event) { + switch (event.data) { + case "start": + runTest(); + break; + default: + throw new Error("Bad message!"); + } +} + +addEventListener("message", messageListener, false); diff --git a/dom/xhr/tests/xhr_implicit_cancel_worker.js b/dom/xhr/tests/xhr_implicit_cancel_worker.js new file mode 100644 index 0000000000..2045452caa --- /dev/null +++ b/dom/xhr/tests/xhr_implicit_cancel_worker.js @@ -0,0 +1,10 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var xhr = new XMLHttpRequest(); +xhr.open("GET", "worker_testXHR.txt"); +xhr.send(null); +xhr.open("GET", "worker_testXHR.txt"); +xhr.send(null); +postMessage("done"); diff --git a/dom/xhr/tests/xhr_sharedworker.js b/dom/xhr/tests/xhr_sharedworker.js new file mode 100644 index 0000000000..c8e1c10094 --- /dev/null +++ b/dom/xhr/tests/xhr_sharedworker.js @@ -0,0 +1,105 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* eslint-env worker */ +onconnect = e => { + e.ports[0].onmessage = event => { + const url = event.data; + + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.send(); + + const refText = xhr.responseText; + + function getResponse(type) { + xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + if (type !== undefined) { + xhr.responseType = type; + } + xhr.send(); + return xhr.response; + } + + if (getResponse() != refText) { + throw new Error("unset responseType failed"); + } + + if (getResponse("") != refText) { + throw new Error("'' responseType failed"); + } + + if (getResponse("text") != refText) { + throw new Error("'text' responseType failed"); + } + + var array = new Uint8Array(getResponse("arraybuffer")); + if (String.fromCharCode.apply(String, array) != refText) { + throw new Error("'arraybuffer' responseType failed"); + } + + var blob = getResponse("blob"); + if (new FileReaderSync().readAsText(blob) != refText) { + throw new Error("'blob' responseType failed"); + } + + // Make sure that we get invalid state exceptions when getting the wrong + // property. + + function testResponseTextException(type) { + xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.responseType = type; + xhr.send(); + + var exception; + + try { + xhr.responseText; + } catch (ex) { + exception = ex; + } + + if (!exception) { + throw new Error( + `Failed to throw when getting responseText on ${type} type` + ); + } + + if (exception.name != "InvalidStateError") { + throw new Error( + `Unexpected error when getting responseText on ${type} type` + ); + } + + if (exception.code != DOMException.INVALID_STATE_ERR) { + throw new Error( + `Unexpected error code when getting responseText on ${type} type` + ); + } + } + + testResponseTextException("arraybuffer"); + testResponseTextException("blob"); + + // Make sure "document" works, but returns text. + xhr = new XMLHttpRequest(); + + if (xhr.responseType != "") { + throw new Error("Default value for responseType is wrong!"); + } + + xhr.open("GET", url, false); + xhr.responseType = "document"; + xhr.send(); + + if (xhr.responseText != refText) { + throw new Error("'document' type not working correctly"); + } + + e.ports[0].postMessage("done"); + }; +}; diff --git a/dom/xhr/tests/xhr_worker.js b/dom/xhr/tests/xhr_worker.js new file mode 100644 index 0000000000..46edd700db --- /dev/null +++ b/dom/xhr/tests/xhr_worker.js @@ -0,0 +1,84 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var xhr = new XMLHttpRequest(); + +function onload(event) { + if (event.target != xhr) { + throw new Error("onload event.target != xhr"); + } + + if (event.target.status != 200) { + const message = { type: "error", error: event.target.status }; + postMessage(message); + } + + const message = { type: "load", data: xhr.responseText }; + postMessage(message); +} + +xhr.onload = onload; +xhr.addEventListener("load", onload); +xhr.removeEventListener("load", onload); +if (!xhr.onload) { + const message = { type: "error", error: "Lost message listener!" }; + postMessage(message); +} + +xhr.onerror = function (event) { + if (event.target != xhr) { + throw new Error("onerror event.target != xhr"); + } + var message = { type: "error", error: event.target.status }; + postMessage(message); +}; +// eslint-disable-next-line no-self-assign +xhr.onerror = xhr.onerror; +// eslint-disable-next-line no-self-compare +if (!xhr.onerror || xhr.onerror != xhr.onerror) { + throw new Error("onerror wasn't set properly"); +} + +function onprogress(event) { + if (event.target != xhr) { + throw new Error("onprogress event.target != xhr"); + } + const message = { + type: "progress", + current: event.loaded, + total: event.total, + }; + postMessage(message); +} +xhr.addEventListener("progress", onprogress); + +xhr.addEventListener("foopety", function (event) {}); +xhr.removeEventListener("doopety", function (event) {}); + +xhr.onloadend = function (event) { + const message = { type: "loadend" }; + postMessage(message); +}; + +var upload = xhr.upload; +upload.onprogress = function (event) {}; +upload.addEventListener("foo", function (event) {}); +upload.removeEventListener("foo", function (event) {}); +upload.addEventListener("load", function (event) {}); +upload.removeEventListener("foo", function (event) {}); +upload.onload = function (event) { + const message = { type: "upload.load" }; + postMessage(message); +}; + +onmessage = function (event) { + if (xhr.DONE != 4 || XMLHttpRequest.DONE != 4) { + throw new Error("xhr constants not correct!"); + } + if (xhr.readystate > xhr.UNSENT) { + throw new Error("XHR already running!"); + } + xhr.open("POST", event.data); + xhr.send("Data to send"); +}; diff --git a/dom/xhr/tests/xhr_worker_doubleSend.js b/dom/xhr/tests/xhr_worker_doubleSend.js new file mode 100644 index 0000000000..1ecf28e57c --- /dev/null +++ b/dom/xhr/tests/xhr_worker_doubleSend.js @@ -0,0 +1,11 @@ +var xhr = new XMLHttpRequest(); +xhr.open("POST", "worker_testXHR.txt"); +xhr.send(); +try { + xhr.send(); + postMessage("KO double send should fail"); +} catch (e) { + postMessage( + e.name === "InvalidStateError" ? "OK" : "KO InvalidStateError expected" + ); +} -- cgit v1.2.3