/* -*- 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