diff options
Diffstat (limited to 'dom/xhr')
120 files changed, 14599 insertions, 0 deletions
diff --git a/dom/xhr/XMLHttpRequest.cpp b/dom/xhr/XMLHttpRequest.cpp new file mode 100644 index 000000000..36abb3835 --- /dev/null +++ b/dom/xhr/XMLHttpRequest.cpp @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequest.h" +#include "XMLHttpRequestMainThread.h" +#include "XMLHttpRequestWorker.h" + +namespace mozilla { +namespace dom { + +/* static */ already_AddRefed<XMLHttpRequest> +XMLHttpRequest::Constructor(const GlobalObject& aGlobal, + const MozXMLHttpRequestParameters& aParams, + ErrorResult& aRv) +{ + if (NS_IsMainThread()) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + nsCOMPtr<nsIScriptObjectPrincipal> principal = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!global || ! principal) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<XMLHttpRequestMainThread> req = new XMLHttpRequestMainThread(); + req->Construct(principal->GetPrincipal(), global); + req->InitParameters(aParams.mMozAnon, aParams.mMozSystem); + return req.forget(); + } + + return XMLHttpRequestWorker::Construct(aGlobal, aParams, aRv); +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/xhr/XMLHttpRequest.h b/dom/xhr/XMLHttpRequest.h new file mode 100644 index 000000000..a0dd0dbf6 --- /dev/null +++ b/dom/xhr/XMLHttpRequest.h @@ -0,0 +1,187 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequest_h +#define mozilla_dom_XMLHttpRequest_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/XMLHttpRequestEventTarget.h" +#include "mozilla/dom/XMLHttpRequestBinding.h" +#include "nsIXMLHttpRequest.h" + +class nsIJSID; + +namespace mozilla { +namespace dom { + +class Blob; +class DOMString; +class FormData; +class URLSearchParams; +class XMLHttpRequestUpload; + +class XMLHttpRequest : public XMLHttpRequestEventTarget +{ +public: + static already_AddRefed<XMLHttpRequest> + Constructor(const GlobalObject& aGlobal, + const MozXMLHttpRequestParameters& aParams, + ErrorResult& aRv); + + static already_AddRefed<XMLHttpRequest> + Constructor(const GlobalObject& aGlobal, const nsAString& ignored, + ErrorResult& aRv) + { + // Pretend like someone passed null, so we can pick up the default values + MozXMLHttpRequestParameters params; + if (!params.Init(aGlobal.Context(), JS::NullHandleValue)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + return Constructor(aGlobal, params, aRv); + } + + IMPL_EVENT_HANDLER(readystatechange) + + virtual uint16_t + ReadyState() const = 0; + + virtual void + Open(const nsACString& aMethod, const nsAString& aUrl, ErrorResult& aRv) = 0; + + virtual void + Open(const nsACString& aMethod, const nsAString& aUrl, bool aAsync, + const nsAString& aUser, const nsAString& aPassword, ErrorResult& aRv) = 0; + + virtual void + SetRequestHeader(const nsACString& aHeader, const nsACString& aValue, + ErrorResult& aRv) = 0; + + virtual uint32_t + Timeout() const = 0; + + virtual void + SetTimeout(uint32_t aTimeout, ErrorResult& aRv) = 0; + + virtual bool + WithCredentials() const = 0; + + virtual void + SetWithCredentials(bool aWithCredentials, ErrorResult& aRv) = 0; + + virtual XMLHttpRequestUpload* + GetUpload(ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, const ArrayBuffer& aArrayBuffer, ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, const ArrayBufferView& aArrayBufferView, + ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, Blob& aBlob, ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, URLSearchParams& aURLSearchParams, ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, nsIDocument& aDoc, ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, const nsAString& aString, ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, FormData& aFormData, ErrorResult& aRv) = 0; + + virtual void + Send(JSContext* aCx, nsIInputStream* aStream, ErrorResult& aRv) = 0; + + virtual void + Abort(ErrorResult& aRv) = 0; + + virtual void + GetResponseURL(nsAString& aUrl) = 0; + + virtual uint32_t + GetStatus(ErrorResult& aRv) = 0; + + virtual void + GetStatusText(nsACString& aStatusText, ErrorResult& aRv) = 0; + + virtual void + GetResponseHeader(const nsACString& aHeader, nsACString& aResult, + ErrorResult& aRv) = 0; + + virtual void + GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) = 0; + + virtual void + OverrideMimeType(const nsAString& aMimeType, ErrorResult& aRv) = 0; + + virtual XMLHttpRequestResponseType + ResponseType() const = 0; + + virtual void + SetResponseType(XMLHttpRequestResponseType aType, + ErrorResult& aRv) = 0; + + virtual void + GetResponse(JSContext* aCx, JS::MutableHandle<JS::Value> aResponse, + ErrorResult& aRv) = 0; + + virtual void + GetResponseText(DOMString& aResponseText, ErrorResult& aRv) = 0; + + virtual nsIDocument* + GetResponseXML(ErrorResult& aRv) = 0; + + virtual bool + MozBackgroundRequest() const = 0; + + virtual void + SetMozBackgroundRequest(bool aMozBackgroundRequest, ErrorResult& aRv) = 0; + + virtual nsIChannel* + GetChannel() const = 0; + + virtual void + GetNetworkInterfaceId(nsACString& aId) const = 0; + + virtual void + SetNetworkInterfaceId(const nsACString& aId) = 0; + + // We need a GetInterface callable from JS for chrome JS + virtual void + GetInterface(JSContext* aCx, nsIJSID* aIID, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv) = 0; + + virtual void + SetOriginAttributes(const mozilla::dom::OriginAttributesDictionary& aAttrs) = 0; + + virtual bool + MozAnon() const = 0; + + virtual bool + MozSystem() const = 0; + + virtual JSObject* + WrapObject(JSContext *aCx, JS::Handle<JSObject*> aGivenProto) override + { + return mozilla::dom::XMLHttpRequestBinding::Wrap(aCx, this, aGivenProto); + } +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_XMLHttpRequest_h diff --git a/dom/xhr/XMLHttpRequestEventTarget.cpp b/dom/xhr/XMLHttpRequestEventTarget.cpp new file mode 100644 index 000000000..b6e65001a --- /dev/null +++ b/dom/xhr/XMLHttpRequestEventTarget.cpp @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestEventTarget.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(XMLHttpRequestEventTarget) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XMLHttpRequestEventTarget, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XMLHttpRequestEventTarget, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(XMLHttpRequestEventTarget) + NS_INTERFACE_MAP_ENTRY(nsIXMLHttpRequestEventTarget) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(XMLHttpRequestEventTarget, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(XMLHttpRequestEventTarget, DOMEventTargetHelper) + +void +XMLHttpRequestEventTarget::DisconnectFromOwner() +{ + DOMEventTargetHelper::DisconnectFromOwner(); +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/xhr/XMLHttpRequestEventTarget.h b/dom/xhr/XMLHttpRequestEventTarget.h new file mode 100644 index 000000000..6f48d5758 --- /dev/null +++ b/dom/xhr/XMLHttpRequestEventTarget.h @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestEventTarget_h +#define mozilla_dom_XMLHttpRequestEventTarget_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "nsIXMLHttpRequest.h" + +namespace mozilla { +namespace dom { + +class XMLHttpRequestEventTarget : public DOMEventTargetHelper, + public nsIXMLHttpRequestEventTarget +{ +protected: + explicit XMLHttpRequestEventTarget(DOMEventTargetHelper* aOwner) + : DOMEventTargetHelper(aOwner) + {} + + XMLHttpRequestEventTarget() + {} + + virtual ~XMLHttpRequestEventTarget() {} + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XMLHttpRequestEventTarget, + DOMEventTargetHelper) + NS_DECL_NSIXMLHTTPREQUESTEVENTTARGET + NS_REALLY_FORWARD_NSIDOMEVENTTARGET(DOMEventTargetHelper) + + IMPL_EVENT_HANDLER(loadstart) + IMPL_EVENT_HANDLER(progress) + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(error) + IMPL_EVENT_HANDLER(load) + IMPL_EVENT_HANDLER(timeout) + IMPL_EVENT_HANDLER(loadend) + + nsISupports* GetParentObject() const + { + return GetOwner(); + } + + virtual void DisconnectFromOwner() override; +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_XMLHttpRequestEventTarget_h diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp new file mode 100644 index 000000000..a47a01aa0 --- /dev/null +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -0,0 +1,4168 @@ +/* -*- 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 <algorithm> +#ifndef XP_WIN +#include <unistd.h> +#endif +#include "mozilla/ArrayUtils.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/BlobSet.h" +#include "mozilla/dom/DOMString.h" +#include "mozilla/dom/File.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/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/LoadContext.h" +#include "mozilla/MemoryReporting.h" +#include "nsIDOMDocument.h" +#include "mozilla/dom/ProgressEvent.h" +#include "nsIJARChannel.h" +#include "nsIJARURI.h" +#include "nsLayoutCID.h" +#include "nsReadableUtils.h" + +#include "nsIURI.h" +#include "nsILoadGroup.h" +#include "nsNetUtil.h" +#include "nsStringStream.h" +#include "nsIAuthPrompt.h" +#include "nsIAuthPrompt2.h" +#include "nsIOutputStream.h" +#include "nsISupportsPrimitives.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsIUploadChannel.h" +#include "nsIUploadChannel2.h" +#include "nsIDOMSerializer.h" +#include "nsXPCOM.h" +#include "nsIDOMEventListener.h" +#include "nsIScriptSecurityManager.h" +#include "nsIVariant.h" +#include "nsVariant.h" +#include "nsIScriptError.h" +#include "nsIStreamConverterService.h" +#include "nsICachingChannel.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsError.h" +#include "nsIHTMLDocument.h" +#include "nsIStorageStream.h" +#include "nsIPromptFactory.h" +#include "nsIWindowWatcher.h" +#include "nsIConsoleService.h" +#include "nsIContentSecurityPolicy.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "nsStringBuffer.h" +#include "nsIFileChannel.h" +#include "mozilla/Telemetry.h" +#include "jsfriendapi.h" +#include "GeckoProfiler.h" +#include "mozilla/dom/EncodingUtils.h" +#include "nsIUnicodeDecoder.h" +#include "mozilla/dom/XMLHttpRequestBinding.h" +#include "mozilla/Attributes.h" +#include "MultipartBlobImpl.h" +#include "nsIPermissionManager.h" +#include "nsMimeTypes.h" +#include "nsIHttpChannelInternal.h" +#include "nsIClassOfService.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsStreamListenerWrapper.h" +#include "xpcjsid.h" +#include "nsITimedChannel.h" +#include "nsWrapperCacheInlines.h" +#include "nsZipArchive.h" +#include "mozilla/Preferences.h" +#include "private/pprio.h" +#include "XMLHttpRequestUpload.h" + +using namespace mozilla::net; + +namespace mozilla { +namespace 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[] = { + NS_LITERAL_STRING("loadstart"), + NS_LITERAL_STRING("progress"), + NS_LITERAL_STRING("error"), + NS_LITERAL_STRING("abort"), + NS_LITERAL_STRING("timeout"), + NS_LITERAL_STRING("load"), + NS_LITERAL_STRING("loadend") + }; + static_assert(MOZ_ARRAY_LENGTH(ProgressEventTypeStrings) == + size_t(XMLHttpRequestMainThread::ProgressEventType::ENUM_MAX), + "Mismatched lengths for ProgressEventTypeStrings and ProgressEventType enums"); + + const nsString kLiteralString_readystatechange = NS_LITERAL_STRING("readystatechange"); + const nsString kLiteralString_xmlhttprequest = NS_LITERAL_STRING("xmlhttprequest"); + const nsString kLiteralString_DOMContentLoaded = NS_LITERAL_STRING("DOMContentLoaded"); +} + +// CIDs +#define NS_BADCERTHANDLER_CONTRACTID \ + "@mozilla.org/content/xmlhttprequest-bad-cert-handler;1" + +#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) : mWindow(aWindow) {} + + NS_IMETHOD Run() override + { + mWindow->Resume(); + return NS_OK; + } + +private: + nsCOMPtr<nsPIDOMWindowInner> 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); +} + +///////////////////////////////////////////// +// +// +///////////////////////////////////////////// + +bool +XMLHttpRequestMainThread::sDontWarnAboutSyncXHR = false; + +XMLHttpRequestMainThread::XMLHttpRequestMainThread() + : mResponseBodyDecodedPos(0), + mResponseType(XMLHttpRequestResponseType::_empty), + mRequestObserver(nullptr), + mState(State::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(false), mErrorParsingXML(false), + mWaitingForOnStopRequest(false), + mProgressTimerIsActive(false), + mIsHtml(false), + mWarnAboutSyncHtml(false), + mLoadTotal(-1), + mIsSystem(false), + mIsAnon(false), + mFirstStartRequestSeen(false), + mInLoadProgressEvent(false), + mResultJSON(JS::UndefinedValue()), + mResultArrayBuffer(nullptr), + mIsMappedArrayBuffer(false), + mXPCOMifier(nullptr) +{ + mozilla::HoldJSObjects(this); +} + +XMLHttpRequestMainThread::~XMLHttpRequestMainThread() +{ + mFlagDeleted = true; + + if ((mState == State::opened && mFlagSend) || + mState == State::loading) { + Abort(); + } + + MOZ_ASSERT(!mFlagSyncLooping, "we rather crash than hang"); + mFlagSyncLooping = false; + + mResultJSON.setUndefined(); + mResultArrayBuffer = nullptr; + mozilla::DropJSObjects(this); +} + +/** + * This Init method is called from the factory constructor. + */ +nsresult +XMLHttpRequestMainThread::Init() +{ + nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); + nsCOMPtr<nsIPrincipal> subjectPrincipal; + if (secMan) { + secMan->GetSystemPrincipal(getter_AddRefs(subjectPrincipal)); + } + NS_ENSURE_STATE(subjectPrincipal); + + // Instead of grabbing some random global from the context stack, + // let's use the default one (junk scope) for now. + // We should move away from this Init... + Construct(subjectPrincipal, xpc::NativeGlobal(xpc::PrivilegedJunkScope())); + return NS_OK; +} + +/** + * This Init method should only be called by C++ consumers. + */ +NS_IMETHODIMP +XMLHttpRequestMainThread::Init(nsIPrincipal* aPrincipal, + nsIGlobalObject* aGlobalObject, + nsIURI* aBaseURI, + nsILoadGroup* aLoadGroup) +{ + NS_ENSURE_ARG_POINTER(aPrincipal); + Construct(aPrincipal, aGlobalObject, aBaseURI, aLoadGroup); + return NS_OK; +} + +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<nsIPermissionManager> permMgr = + services::GetPermissionManager(); + if (NS_WARN_IF(!permMgr)) { + SetParameters(aAnon, false); + return; + } + + uint32_t permission; + nsresult rv = + permMgr->TestPermissionFromPrincipal(principal, "systemXHR", &permission); + if (NS_FAILED(rv) || permission != nsIPermissionManager::ALLOW_ACTION) { + SetParameters(aAnon, false); + return; + } + } + + SetParameters(aAnon, aSystem); +} + +void +XMLHttpRequestMainThread::ResetResponse() +{ + mResponseXML = nullptr; + mResponseBody.Truncate(); + TruncateResponseText(); + mResponseBlob = nullptr; + mDOMBlob = nullptr; + mBlobStorage = nullptr; + mBlobSet = nullptr; + mResultArrayBuffer = nullptr; + mArrayBufferBuilder.reset(); + mResultJSON.setUndefined(); + mDataAvailable = 0; + mLoadTransferred = 0; + mResponseBodyDecodedPos = 0; +} + +void +XMLHttpRequestMainThread::SetRequestObserver(nsIRequestObserver* aObserver) +{ + mRequestObserver = aObserver; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(XMLHttpRequestMainThread) + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(XMLHttpRequestMainThread) + bool isBlack = tmp->IsBlack(); + if (isBlack || tmp->mWaitingForOnStopRequest) { + if (tmp->mListenerManager) { + tmp->mListenerManager->MarkForCC(); + } + if (!isBlack && tmp->PreservingWrapper()) { + // This marks the wrapper black. + tmp->GetWrapper(); + } + return true; + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(XMLHttpRequestMainThread) + return tmp-> + IsBlackAndDoesNotNeedTracing(static_cast<DOMEventTargetHelper*>(tmp)); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(XMLHttpRequestMainThread) + return tmp->IsBlack(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END + +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(mDOMBlob) + 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.reset(); + tmp->mResultJSON.setUndefined(); + 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(mDOMBlob) + 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 + +// QueryInterface implementation for XMLHttpRequestMainThread +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(XMLHttpRequestMainThread) + NS_INTERFACE_MAP_ENTRY(nsIXMLHttpRequest) + NS_INTERFACE_MAP_ENTRY(nsIJSXMLHttpRequest) + 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(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsISizeOfEventTarget) +NS_INTERFACE_MAP_END_INHERITING(XMLHttpRequestEventTarget) + +NS_IMPL_ADDREF_INHERITED(XMLHttpRequestMainThread, XMLHttpRequestEventTarget) +NS_IMPL_RELEASE_INHERITED(XMLHttpRequestMainThread, XMLHttpRequestEventTarget) + +NS_IMPL_EVENT_HANDLER(XMLHttpRequestMainThread, readystatechange) + +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 +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetChannel(nsIChannel **aChannel) +{ + NS_ENSURE_ARG_POINTER(aChannel); + NS_IF_ADDREF(*aChannel = mChannel); + + return NS_OK; +} + +static void LogMessage(const char* aWarning, nsPIDOMWindowInner* aWindow, + const char16_t** aParams=nullptr, uint32_t aParamCount=0) +{ + nsCOMPtr<nsIDocument> doc; + if (aWindow) { + doc = aWindow->GetExtantDoc(); + } + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("DOM"), doc, + nsContentUtils::eDOM_PROPERTIES, + aWarning, aParams, aParamCount); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetResponseXML(nsIDOMDocument **aResponseXML) +{ + ErrorResult rv; + nsIDocument* responseXML = GetResponseXML(rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + if (!responseXML) { + *aResponseXML = nullptr; + return NS_OK; + } + + return CallQueryInterface(responseXML, aResponseXML); +} + +nsIDocument* +XMLHttpRequestMainThread::GetResponseXML(ErrorResult& aRv) +{ + if (mResponseType != XMLHttpRequestResponseType::_empty && + mResponseType != XMLHttpRequestResponseType::Document) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_HAS_WRONG_RESPONSETYPE_FOR_RESPONSEXML); + return nullptr; + } + if (mWarnAboutSyncHtml) { + mWarnAboutSyncHtml = false; + LogMessage("HTMLSyncXHRWarning", GetOwner()); + } + if (mState != State::done) { + return nullptr; + } + return mResponseXML; +} + +/* + * This piece copied from XMLDocument, we try to get the charset + * from HTTP headers. + */ +nsresult +XMLHttpRequestMainThread::DetectCharset() +{ + mResponseCharset.Truncate(); + mDecoder = nullptr; + + if (mResponseType != XMLHttpRequestResponseType::_empty && + mResponseType != XMLHttpRequestResponseType::Text && + mResponseType != XMLHttpRequestResponseType::Json && + mResponseType != XMLHttpRequestResponseType::Moz_chunked_text) { + return NS_OK; + } + + nsAutoCString charsetVal; + bool ok = mChannel && + NS_SUCCEEDED(mChannel->GetContentCharset(charsetVal)) && + EncodingUtils::FindEncodingForLabel(charsetVal, mResponseCharset); + if (!ok || mResponseCharset.IsEmpty()) { + // MS documentation states UTF-8 is default for responseText + mResponseCharset.AssignLiteral("UTF-8"); + } + + if (mResponseType == XMLHttpRequestResponseType::Json && + !mResponseCharset.EqualsLiteral("UTF-8")) { + // The XHR spec says only UTF-8 is supported for responseType == "json" + LogMessage("JSONCharsetWarning", GetOwner()); + mResponseCharset.AssignLiteral("UTF-8"); + } + + mDecoder = EncodingUtils::DecoderForEncoding(mResponseCharset); + + return NS_OK; +} + +nsresult +XMLHttpRequestMainThread::AppendToResponseText(const char * aSrcBuffer, + uint32_t aSrcBufferLen) +{ + NS_ENSURE_STATE(mDecoder); + + int32_t destBufferLen; + nsresult rv = mDecoder->GetMaxLength(aSrcBuffer, aSrcBufferLen, + &destBufferLen); + NS_ENSURE_SUCCESS(rv, rv); + + CheckedInt32 size = mResponseText.Length(); + size += destBufferLen; + if (!size.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + XMLHttpRequestStringWriterHelper helper(mResponseText); + + if (!helper.AddCapacity(destBufferLen)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // This code here is basically a copy of a similar thing in + // nsScanner::Append(const char* aBuffer, uint32_t aLen). + int32_t srclen = (int32_t)aSrcBufferLen; + int32_t destlen = (int32_t)destBufferLen; + rv = mDecoder->Convert(aSrcBuffer, + &srclen, + helper.EndOfExistingData(), + &destlen); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(destlen <= destBufferLen); + + helper.AddLength(destlen); + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetResponseText(nsAString& aResponseText) +{ + ErrorResult rv; + DOMString str; + GetResponseText(str, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + str.ToString(aResponseText); + return NS_OK; +} + +void +XMLHttpRequestMainThread::GetResponseText(DOMString& aResponseText, + ErrorResult& aRv) +{ + 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 && + mResponseType != XMLHttpRequestResponseType::Moz_chunked_text) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_HAS_WRONG_RESPONSETYPE_FOR_RESPONSETEXT); + return; + } + + if (mResponseType == XMLHttpRequestResponseType::Moz_chunked_text && + !mInLoadProgressEvent) { + aSnapshot.SetVoid(); + return; + } + + if (mState != State::loading && mState != State::done) { + 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()) { + mResponseText.CreateSnapshot(aSnapshot); + return; + } + + MatchCharsetAndDecoderToResponseDocument(); + + NS_ASSERTION(mResponseBodyDecodedPos < mResponseBody.Length(), + "Unexpected mResponseBodyDecodedPos"); + aRv = AppendToResponseText(mResponseBody.get() + mResponseBodyDecodedPos, + mResponseBody.Length() - mResponseBodyDecodedPos); + if (aRv.Failed()) { + return; + } + + mResponseBodyDecodedPos = mResponseBody.Length(); + + if (mState == State::done) { + // 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; + if (!mResponseText.GetAsString(string)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // The Unicode converter has already zapped the BOM if there was one + JS::Rooted<JS::Value> value(aCx); + if (!JS_ParseJSON(aCx, string.BeginReading(), string.Length(), &value)) { + return NS_ERROR_FAILURE; + } + + mResultJSON = value; + return NS_OK; +} + +void +XMLHttpRequestMainThread::CreatePartialBlob(ErrorResult& aRv) +{ + if (mDOMBlob) { + // Use progress info to determine whether load is complete, but use + // mDataAvailable to ensure a slice is created based on the uncompressed + // data count. + if (mState == State::done) { + mResponseBlob = mDOMBlob; + } else { + mResponseBlob = mDOMBlob->CreateSlice(0, mDataAvailable, + EmptyString(), aRv); + } + return; + } + + // mBlobSet can be null if the request has been canceled + if (!mBlobSet) { + return; + } + + nsAutoCString contentType; + if (mState == State::done) { + mChannel->GetContentType(contentType); + } + + nsTArray<RefPtr<BlobImpl>> subImpls(mBlobSet->GetBlobImpls()); + RefPtr<BlobImpl> blobImpl = + MultipartBlobImpl::Create(Move(subImpls), + NS_ConvertASCIItoUTF16(contentType), + aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mResponseBlob = Blob::Create(GetOwner(), blobImpl); +} + +NS_IMETHODIMP XMLHttpRequestMainThread::GetResponseType(nsAString& aResponseType) +{ + MOZ_ASSERT(mResponseType < XMLHttpRequestResponseType::EndGuard_); + const EnumEntry& entry = + XMLHttpRequestResponseTypeValues::strings[static_cast<uint32_t>(mResponseType)]; + aResponseType.AssignASCII(entry.value, entry.length); + return NS_OK; +} + +NS_IMETHODIMP XMLHttpRequestMainThread::SetResponseType(const nsAString& aResponseType) +{ + uint32_t i = 0; + for (const EnumEntry* entry = XMLHttpRequestResponseTypeValues::strings; + entry->value; ++entry, ++i) { + if (aResponseType.EqualsASCII(entry->value, entry->length)) { + ErrorResult rv; + SetResponseType(static_cast<XMLHttpRequestResponseType>(i), rv); + return rv.StealNSResult(); + } + } + + return NS_OK; +} + +void +XMLHttpRequestMainThread::SetResponseType(XMLHttpRequestResponseType aResponseType, + ErrorResult& aRv) +{ + if (mState == State::loading || mState == State::done) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_LOADING_OR_DONE); + return; + } + + // sync request is not allowed setting responseType in window context + if (HasOrHasHadOwner() && mState != State::unsent && mFlagSynchronous) { + LogMessage("ResponseTypeSyncXHRWarning", GetOwner()); + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_XHR_TIMEOUT_AND_RESPONSETYPE_UNSUPPORTED_FOR_SYNC); + return; + } + + if (mFlagSynchronous && + (aResponseType == XMLHttpRequestResponseType::Moz_chunked_text || + aResponseType == XMLHttpRequestResponseType::Moz_chunked_arraybuffer)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_CHUNKED_RESPONSETYPES_UNSUPPORTED_FOR_SYNC); + return; + } + + // We want to get rid of this moz-only types. Bug 1335365. + if (aResponseType == XMLHttpRequestResponseType::Moz_blob) { + Telemetry::Accumulate(Telemetry::MOZ_BLOB_IN_XHR, 1); + } else if (aResponseType == XMLHttpRequestResponseType::Moz_chunked_text) { + Telemetry::Accumulate(Telemetry::MOZ_CHUNKED_TEXT_IN_XHR, 1); + } else if (aResponseType == XMLHttpRequestResponseType::Moz_chunked_arraybuffer) { + Telemetry::Accumulate(Telemetry::MOZ_CHUNKED_ARRAYBUFFER_IN_XHR, 1); + } + + // Set the responseType attribute's value to the given value. + mResponseType = aResponseType; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetResponse(JSContext *aCx, JS::MutableHandle<JS::Value> aResult) +{ + ErrorResult rv; + GetResponse(aCx, aResult, rv); + return rv.StealNSResult(); +} + +void +XMLHttpRequestMainThread::GetResponse(JSContext* aCx, + JS::MutableHandle<JS::Value> aResponse, + ErrorResult& aRv) +{ + switch (mResponseType) { + case XMLHttpRequestResponseType::_empty: + case XMLHttpRequestResponseType::Text: + case XMLHttpRequestResponseType::Moz_chunked_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: + case XMLHttpRequestResponseType::Moz_chunked_arraybuffer: + { + if (!(mResponseType == XMLHttpRequestResponseType::Arraybuffer && + mState == State::done) && + !(mResponseType == XMLHttpRequestResponseType::Moz_chunked_arraybuffer && + mInLoadProgressEvent)) { + aResponse.setNull(); + return; + } + + if (!mResultArrayBuffer) { + mResultArrayBuffer = mArrayBufferBuilder.getArrayBuffer(aCx); + if (!mResultArrayBuffer) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + aResponse.setObject(*mResultArrayBuffer); + return; + } + case XMLHttpRequestResponseType::Blob: + case XMLHttpRequestResponseType::Moz_blob: + { + if (mState != State::done) { + if (mResponseType != XMLHttpRequestResponseType::Moz_blob) { + aResponse.setNull(); + return; + } + + if (!mResponseBlob) { + CreatePartialBlob(aRv); + } + } + + if (!mResponseBlob) { + aResponse.setNull(); + return; + } + + GetOrCreateDOMReflector(aCx, mResponseBlob, aResponse); + return; + } + case XMLHttpRequestResponseType::Document: + { + if (!mResponseXML || mState != State::done) { + aResponse.setNull(); + return; + } + + aRv = nsContentUtils::WrapNative(aCx, mResponseXML, aResponse); + return; + } + case XMLHttpRequestResponseType::Json: + { + if (mState != State::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(); +} + +bool +XMLHttpRequestMainThread::IsCrossSiteCORSRequest() const +{ + if (!mChannel) { + return false; + } + + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo(); + MOZ_ASSERT(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(); + + uint16_t readyState = ReadyState(); + if ((readyState == UNSENT || readyState == OPENED) || !mChannel) { + return; + } + + // Make sure we don't leak responseURL information from denied cross-site + // requests. + if (IsDeniedCrossSiteCORSRequest()) { + return; + } + + nsCOMPtr<nsIURI> responseUrl; + mChannel->GetURI(getter_AddRefs(responseUrl)); + + if (!responseUrl) { + return; + } + + nsAutoCString temp; + responseUrl->GetSpecIgnoringRef(temp); + CopyUTF8toUTF16(temp, aUrl); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetStatus(uint32_t *aStatus) +{ + ErrorResult rv; + *aStatus = GetStatus(rv); + return rv.StealNSResult(); +} + +uint32_t +XMLHttpRequestMainThread::GetStatus(ErrorResult& aRv) +{ + // Make sure we don't leak status information from denied cross-site + // requests. + if (IsDeniedCrossSiteCORSRequest()) { + return 0; + } + + uint16_t readyState = ReadyState(); + if (readyState == UNSENT || readyState == OPENED) { + return 0; + } + + if (mErrorLoad) { + // Let's simulate the http protocol for jar/app requests: + nsCOMPtr<nsIJARChannel> 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<nsIHttpChannel> 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; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetStatusText(nsACString& aOut) +{ + ErrorResult rv; + GetStatusText(aOut, rv); + return rv.StealNSResult(); +} + +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. + uint16_t readyState = ReadyState(); + if (readyState == UNSENT || readyState == OPENED) { + return; + } + + if (mErrorLoad) { + return; + } + + nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel(); + if (httpChannel) { + httpChannel->GetResponseStatusText(aStatusText); + } else { + aStatusText.AssignLiteral("OK"); + } +} + +void +XMLHttpRequestMainThread::CloseRequest() +{ + mWaitingForOnStopRequest = false; + if (mChannel) { + mChannel->Cancel(NS_BINDING_ABORTED); + } + 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 != State::unsent && + !(mState == State::opened && !mFlagSend) && + mState != State::done) { + ChangeState(State::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(State::unsent, false); // IE seems to do it + } + + mFlagSyncLooping = false; +} + +void +XMLHttpRequestMainThread::Abort(ErrorResult& arv) +{ + mFlagAborted = true; + CloseRequestWithError(ProgressEventType::abort); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::SlowAbort() +{ + Abort(); + return NS_OK; +} + +/*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<nsIHttpChannel*> 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", "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. + aHttpChannel-> + GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Expose-Headers"), + headerVal); + nsCCharSeparatedTokenizer exposeTokens(headerVal, ','); + bool isSafe = false; + while (exposeTokens.hasMoreTokens()) { + const nsDependentCSubstring& token = exposeTokens.nextToken(); + if (token.IsEmpty()) { + continue; + } + if (!NS_IsValidHTTPToken(token)) { + return false; + } + if (aHeader.Equals(token, nsCaseInsensitiveCStringComparator())) { + isSafe = true; + } + } + return isSafe; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetAllResponseHeaders(nsACString& aOut) +{ + ErrorResult rv; + GetAllResponseHeaders(aOut, rv); + return rv.StealNSResult(); +} + +void +XMLHttpRequestMainThread::GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) +{ + aResponseHeaders.Truncate(); + + // If the state is UNSENT or OPENED, + // return the empty string and terminate these steps. + if (mState == State::unsent || mState == State::opened) { + return; + } + + if (mErrorLoad) { + return; + } + + if (nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel()) { + RefPtr<nsHeaderVisitor> 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<nsIURI> uri; + bool isDataURI; + if (NS_FAILED(mChannel->GetURI(getter_AddRefs(uri))) || + NS_FAILED(uri->SchemeIs("data", &isDataURI)) || + !isDataURI) { + int64_t length; + if (NS_SUCCEEDED(mChannel->GetContentLength(&length))) { + aResponseHeaders.AppendLiteral("Content-Length: "); + aResponseHeaders.AppendInt(length); + aResponseHeaders.AppendLiteral("\r\n"); + } + } +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetResponseHeader(const nsACString& aHeader, + nsACString& aResult) +{ + ErrorResult rv; + GetResponseHeader(aHeader, aResult, rv); + return rv.StealNSResult(); +} + +void +XMLHttpRequestMainThread::GetResponseHeader(const nsACString& header, + nsACString& _retval, ErrorResult& aRv) +{ + _retval.SetIsVoid(true); + + nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel(); + + if (!httpChannel) { + // If the state is UNSENT or OPENED, + // return null and terminate these steps. + if (mState == State::unsent || mState == State::opened) { + return; + } + + // Even non-http channels supply content type and content length. + // Remember we don't leak header information from denied cross-site + // requests. + nsresult status; + if (!mChannel || + NS_FAILED(mChannel->GetStatus(&status)) || + NS_FAILED(status)) { + 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<nsILoadGroup> +XMLHttpRequestMainThread::GetLoadGroup() const +{ + if (mFlagBackgroundRequest) { + return nullptr; + } + + if (mLoadGroup) { + nsCOMPtr<nsILoadGroup> ref = mLoadGroup; + return ref.forget(); + } + + nsIDocument* doc = GetDocumentIfCurrent(); + if (doc) { + return doc->GetDocumentLoadGroup(); + } + + return nullptr; +} + +nsresult +XMLHttpRequestMainThread::FireReadystatechangeEvent() +{ + RefPtr<Event> 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); + DispatchDOMEvent(nullptr, event, nullptr, nullptr); + 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(CheckInnerWindowCorrectness()) || + (!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; + } + + if (aType == ProgressEventType::progress) { + mInLoadProgressEvent = true; + } + + 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<ProgressEvent> event = + ProgressEvent::Constructor(aTarget, typeString, init); + event->SetTrusted(true); + + aTarget->DispatchDOMEvent(nullptr, event, nullptr, nullptr); + + if (aType == ProgressEventType::progress) { + mInLoadProgressEvent = false; + + // clear chunked responses after every progress event + if (mResponseType == XMLHttpRequestResponseType::Moz_chunked_text || + mResponseType == XMLHttpRequestResponseType::Moz_chunked_arraybuffer) { + mResponseBody.Truncate(); + TruncateResponseText(); + mResultArrayBuffer = nullptr; + mArrayBufferBuilder.reset(); + } + } + + // 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); + } +} + +already_AddRefed<nsIHttpChannel> +XMLHttpRequestMainThread::GetCurrentHttpChannel() +{ + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + return httpChannel.forget(); +} + +already_AddRefed<nsIJARChannel> +XMLHttpRequestMainThread::GetCurrentJARChannel() +{ + nsCOMPtr<nsIJARChannel> appChannel = do_QueryInterface(mChannel); + return appChannel.forget(); +} + +bool +XMLHttpRequestMainThread::IsSystemXHR() const +{ + return mIsSystem || nsContentUtils::IsSystemPrincipal(mPrincipal); +} + +bool +XMLHttpRequestMainThread::InUploadPhase() const +{ + // We're in the upload phase while our state is State::opened. + return mState == State::opened; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::Open(const nsACString& aMethod, const nsACString& aUrl, + bool aAsync, const nsAString& aUsername, + const nsAString& aPassword, uint8_t optional_argc) +{ + return Open(aMethod, aUrl, optional_argc > 0 ? aAsync : true, + aUsername, aPassword); +} + +// 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, NullString(), NullString(), 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) +{ + nsresult rv = Open(aMethod, NS_ConvertUTF16toUTF8(aUrl), aAsync, aUsername, aPassword); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +nsresult +XMLHttpRequestMainThread::Open(const nsACString& aMethod, + const nsACString& aUrl, + bool aAsync, + const nsAString& aUsername, + const nsAString& aPassword) { + // Gecko-specific + if (!aAsync && !DontWarnAboutSyncXHR() && GetOwner() && + GetOwner()->GetExtantDoc()) { + GetOwner()->GetExtantDoc()->WarnOnceAbout(nsIDocument::eSyncXMLHttpRequest); + } + + Telemetry::Accumulate(Telemetry::XMLHTTPREQUEST_ASYNC_OR_SYNC, aAsync ? 0 : 1); + + // Step 1 + nsCOMPtr<nsIDocument> responsibleDocument = GetDocumentIfCurrent(); + if (!responsibleDocument) { + // This could be because we're no longer current or because we're in some + // non-window context... + nsresult rv = CheckInnerWindowCorrectness(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT; + } + } + NS_ENSURE_TRUE(mPrincipal, NS_ERROR_NOT_INITIALIZED); + + // Steps 2-4 + nsAutoCString method; + nsresult rv = FetchUtil::GetValidRequestMethod(aMethod, method); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Steps 5-6 + nsCOMPtr<nsIURI> baseURI; + if (mBaseURI) { + baseURI = mBaseURI; + } else if (responsibleDocument) { + baseURI = responsibleDocument->GetBaseURI(); + } + nsCOMPtr<nsIURI> parsedURL; + rv = NS_NewURI(getter_AddRefs(parsedURL), aUrl, nullptr, baseURI); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_MALFORMED_URI) { + return NS_ERROR_DOM_MALFORMED_URI; + } + return rv; + } + if (NS_WARN_IF(NS_FAILED(CheckInnerWindowCorrectness()))) { + return NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT; + } + + // 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()) { + nsAutoCString userpass; + if (!aUsername.IsVoid()) { + CopyUTF16toUTF8(aUsername, userpass); + } + userpass.AppendLiteral(":"); + if (!aPassword.IsVoid()) { + AppendUTF16toUTF8(aPassword, userpass); + } + parsedURL->SetUserPass(userpass); + } + + // Step 9 + if (!aAsync && HasOrHasHadOwner() && (mTimeoutMilliseconds || + mResponseType != XMLHttpRequestResponseType::_empty)) { + if (mTimeoutMilliseconds) { + LogMessage("TimeoutSyncXHRWarning", GetOwner()); + } + if (mResponseType != XMLHttpRequestResponseType::_empty) { + LogMessage("ResponseTypeSyncXHRWarning", GetOwner()); + } + return NS_ERROR_DOM_INVALID_ACCESS_XHR_TIMEOUT_AND_RESPONSETYPE_UNSUPPORTED_FOR_SYNC; + } + + // Step 10 + CloseRequest(); + + // Step 11 + // timeouts are handled without a flag + mFlagSend = false; + mRequestMethod.Assign(method); + mRequestURL = parsedURL; + mFlagSynchronous = !aAsync; + mAuthorRequestHeaders.Clear(); + ResetResponse(); + + // Gecko-specific + mFlagHadUploadListenersOnSend = false; + mFlagAborted = false; + mFlagTimedOut = false; + + // 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 != State::opened) { + mState = State::opened; + FireReadystatechangeEvent(); + } + + return NS_OK; +} + +void +XMLHttpRequestMainThread::SetOriginAttributes(const OriginAttributesDictionary& aAttrs) +{ + MOZ_ASSERT((mState == State::opened) && !mFlagSend); + + GenericOriginAttributes attrs(aAttrs); + NeckoOriginAttributes neckoAttrs; + neckoAttrs.SetFromGenericAttributes(attrs); + + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo(); + MOZ_ASSERT(loadInfo); + loadInfo->SetOriginAttributes(neckoAttrs); +} + +void +XMLHttpRequestMainThread::PopulateNetworkInterfaceId() +{ + if (mNetworkInterfaceId.IsEmpty()) { + return; + } + nsCOMPtr<nsIHttpChannelInternal> channel(do_QueryInterface(mChannel)); + if (!channel) { + return; + } + channel->SetNetworkInterfaceId(mNetworkInterfaceId); +} + +/* + * "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<XMLHttpRequestMainThread*>(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) { + if (!xmlHttpRequest->mDOMBlob) { + xmlHttpRequest->MaybeCreateBlobStorage(); + rv = xmlHttpRequest->mBlobStorage->Append(fromRawSegment, count); + } + } else if (xmlHttpRequest->mResponseType == XMLHttpRequestResponseType::Moz_blob) { + if (!xmlHttpRequest->mDOMBlob) { + if (!xmlHttpRequest->mBlobSet) { + xmlHttpRequest->mBlobSet = new BlobSet(); + } + rv = xmlHttpRequest->mBlobSet->AppendVoidPtr(fromRawSegment, count); + } + // Clear the cache so that the blob size is updated. + xmlHttpRequest->mResponseBlob = nullptr; + } else if ((xmlHttpRequest->mResponseType == XMLHttpRequestResponseType::Arraybuffer && + !xmlHttpRequest->mIsMappedArrayBuffer) || + xmlHttpRequest->mResponseType == XMLHttpRequestResponseType::Moz_chunked_arraybuffer) { + // 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)); + + xmlHttpRequest->mArrayBufferBuilder.append(reinterpret_cast<const uint8_t*>(fromRawSegment), count, + XML_HTTP_REQUEST_ARRAYBUFFER_MAX_GROWTH); + } 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 || + xmlHttpRequest->mResponseType == XMLHttpRequestResponseType::Moz_chunked_text) { + NS_ASSERTION(!xmlHttpRequest->mResponseXML, + "We shouldn't be parsing a doc here"); + xmlHttpRequest->AppendToResponseText(fromRawSegment, count); + } + + 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<nsIInputStream> copyStream; + rv = NS_NewByteInputStream(getter_AddRefs(copyStream), fromRawSegment, count); + + if (NS_SUCCEEDED(rv) && xmlHttpRequest->mXMLParserStreamListener) { + NS_ASSERTION(copyStream, "NS_NewByteInputStream lied"); + nsresult parsingResult = xmlHttpRequest->mXMLParserStreamListener + ->OnDataAvailable(xmlHttpRequest->mChannel, + xmlHttpRequest->mContext, + 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; +} + +bool XMLHttpRequestMainThread::CreateDOMBlob(nsIRequest *request) +{ + nsCOMPtr<nsIFile> file; + nsCOMPtr<nsIFileChannel> fc = do_QueryInterface(request); + if (fc) { + fc->GetFile(getter_AddRefs(file)); + } + + if (!file) + return false; + + nsAutoCString contentType; + mChannel->GetContentType(contentType); + + mDOMBlob = File::CreateFromFile(GetOwner(), file, EmptyString(), + NS_ConvertASCIItoUTF16(contentType)); + + mBlobStorage = nullptr; + mBlobSet = nullptr; + NS_ASSERTION(mResponseBody.IsEmpty(), "mResponseBody should be empty"); + return true; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::OnDataAvailable(nsIRequest *request, + nsISupports *ctxt, + nsIInputStream *inStr, + uint64_t sourceOffset, + uint32_t count) +{ + NS_ENSURE_ARG_POINTER(inStr); + + MOZ_ASSERT(mContext.get() == ctxt,"start context different from OnDataAvailable context"); + + mProgressSinceLastProgressEvent = true; + XMLHttpRequestBinding::ClearCachedResponseTextValue(this); + + bool cancelable = false; + if ((mResponseType == XMLHttpRequestResponseType::Blob || + mResponseType == XMLHttpRequestResponseType::Moz_blob) && !mDOMBlob) { + cancelable = CreateDOMBlob(request); + // The nsIStreamListener contract mandates us + // to read from the stream before returning. + } + + uint32_t totalRead; + nsresult rv = inStr->ReadSegments(XMLHttpRequestMainThread::StreamReaderFunc, + (void*)this, count, &totalRead); + NS_ENSURE_SUCCESS(rv, rv); + + if (cancelable) { + // We don't have to read from the local file for the blob response + ErrorResult error; + mDataAvailable = mDOMBlob->GetSize(error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + ChangeState(State::loading); + return request->Cancel(NS_OK); + } + + mDataAvailable += totalRead; + + // Fire the first progress event/loading state change + if (mState != State::loading) { + ChangeState(State::loading); + if (!mFlagSynchronous) { + DispatchProgressEvent(this, ProgressEventType::progress, + mLoadTransferred, mLoadTotal); + } + mProgressSinceLastProgressEvent = false; + } + + if (!mFlagSynchronous && !mProgressTimerIsActive) { + StartProgressEventTimer(); + } + + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::OnStartRequest(nsIRequest *request, nsISupports *ctxt) +{ + PROFILER_LABEL("XMLHttpRequestMainThread", "OnStartRequest", + js::ProfileEntry::Category::NETWORK); + + nsresult rv = NS_OK; + if (!mFirstStartRequestSeen && mRequestObserver) { + mFirstStartRequestSeen = true; + mRequestObserver->OnStartRequest(request, ctxt); + } + + if (request != mChannel) { + // Can this still happen? + return NS_OK; + } + + // Don't do anything if we have been aborted + if (mState == State::unsent) { + return NS_OK; + } + + /* Apparently, Abort() should set State::unsent. See bug 361773. + XHR2 spec says this is correct. */ + if (mFlagAborted) { + NS_ERROR("Ugh, still getting data on an aborted XMLHttpRequest!"); + + return NS_ERROR_UNEXPECTED; + } + + // Don't do anything if we have timed out. + if (mFlagTimedOut) { + return NS_OK; + } + + nsCOMPtr<nsIChannel> channel(do_QueryInterface(request)); + NS_ENSURE_TRUE(channel, NS_ERROR_UNEXPECTED); + + nsresult status; + request->GetStatus(&status); + mErrorLoad = mErrorLoad || NS_FAILED(status); + + // Upload phase is now over. If we were uploading anything, + // stop the timer and fire any final progress events. + if (mUpload && !mUploadComplete && !mErrorLoad && !mFlagSynchronous) { + StopProgressEventTimer(); + + mUploadTransferred = mUploadTotal; + + if (mProgressSinceLastProgressEvent) { + DispatchProgressEvent(mUpload, ProgressEventType::progress, + mUploadTransferred, mUploadTotal); + mProgressSinceLastProgressEvent = false; + } + + mUploadComplete = true; + DispatchProgressEvent(mUpload, ProgressEventType::load, + mUploadTotal, mUploadTotal); + } + + mContext = ctxt; + mFlagParseBody = true; + ChangeState(State::headers_received); + + ResetResponse(); + + if (!mOverrideMimeType.IsEmpty()) { + channel->SetContentType(NS_ConvertUTF16toUTF8(mOverrideMimeType)); + } + + DetectCharset(); + + // Set up arraybuffer + if (mResponseType == XMLHttpRequestResponseType::Arraybuffer && + NS_SUCCEEDED(status)) { + if (mIsMappedArrayBuffer) { + nsCOMPtr<nsIJARChannel> jarChannel = do_QueryInterface(channel); + if (jarChannel) { + nsCOMPtr<nsIURI> uri; + rv = channel->GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString file; + nsAutoCString scheme; + uri->GetScheme(scheme); + if (scheme.LowerCaseEqualsLiteral("jar")) { + nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri); + if (jarURI) { + jarURI->GetJAREntry(file); + } + } + nsCOMPtr<nsIFile> jarFile; + jarChannel->GetJarFile(getter_AddRefs(jarFile)); + if (!jarFile) { + mIsMappedArrayBuffer = false; + } else { + rv = mArrayBufferBuilder.mapToFileInPackage(file, jarFile); + if (NS_WARN_IF(NS_FAILED(rv))) { + mIsMappedArrayBuffer = false; + } else { + channel->SetContentType(NS_LITERAL_CSTRING("application/mem-mapped")); + } + } + } + } + } + // 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<int32_t>(contentLength)); + } + } + } + + // Set up responseXML + bool parseBody = mResponseType == XMLHttpRequestResponseType::_empty || + mResponseType == XMLHttpRequestResponseType::Document; + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel)); + if (parseBody && httpChannel) { + nsAutoCString method; + httpChannel->GetRequestMethod(method); + parseBody = !method.EqualsLiteral("HEAD"); + } + + 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") || + type.RFind("+xml", true, -1, 4) != kNotFound)) { + // 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<nsIURI> baseURI, docURI; + rv = mChannel->GetURI(getter_AddRefs(docURI)); + NS_ENSURE_SUCCESS(rv, rv); + baseURI = docURI; + + nsCOMPtr<nsIDocument> doc = GetDocumentIfCurrent(); + nsCOMPtr<nsIURI> 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(CheckInnerWindowCorrectness()))) { + return NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT; + } + } + + // Create an empty document from it. + const nsAString& emptyStr = EmptyString(); + nsCOMPtr<nsIDOMDocument> responseDoc; + nsIGlobalObject* global = DOMEventTargetHelper::GetParentObject(); + + nsCOMPtr<nsIPrincipal> requestingPrincipal; + rv = nsContentUtils::GetSecurityManager()-> + GetChannelResultPrincipal(channel, getter_AddRefs(requestingPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewDOMDocument(getter_AddRefs(responseDoc), + emptyStr, emptyStr, nullptr, docURI, + baseURI, requestingPrincipal, true, global, + mIsHtml ? DocumentFlavorHTML : + DocumentFlavorLegacyGuess); + NS_ENSURE_SUCCESS(rv, rv); + mResponseXML = do_QueryInterface(responseDoc); + mResponseXML->SetChromeXHRDocURI(chromeXHRDocURI); + mResponseXML->SetChromeXHRDocBaseURI(chromeXHRDocBaseURI); + + if (nsContentUtils::IsSystemPrincipal(mPrincipal)) { + mResponseXML->ForceEnableXULXBL(); + } + + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo(); + MOZ_ASSERT(loadInfo); + bool isCrossSite = loadInfo->GetTainting() != LoadTainting::Basic; + + if (isCrossSite) { + nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(mResponseXML); + if (htmlDoc) { + htmlDoc->DisableCookieAccess(); + } + } + + nsCOMPtr<nsIStreamListener> listener; + nsCOMPtr<nsILoadGroup> loadGroup; + channel->GetLoadGroup(getter_AddRefs(loadGroup)); + + // suppress <parsererror> 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 + mResponseXML->SetReferrer(NS_LITERAL_CSTRING("")); + + mXMLParserStreamListener = listener; + rv = mXMLParserStreamListener->OnStartRequest(request, ctxt); + 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, nsISupports *ctxt, nsresult status) +{ + PROFILER_LABEL("XMLHttpRequestMainThread", "OnStopRequest", + js::ProfileEntry::Category::NETWORK); + + if (request != mChannel) { + // Can this still happen? + return NS_OK; + } + + mWaitingForOnStopRequest = false; + + if (mRequestObserver) { + NS_ASSERTION(mFirstStartRequestSeen, "Inconsistent state!"); + mFirstStartRequestSeen = false; + mRequestObserver->OnStopRequest(request, ctxt, status); + } + + // suppress parsing failure messages to console for status 204/304 (see bug 884693). + if (mResponseXML) { + uint32_t responseStatus; + if (NS_SUCCEEDED(GetStatus(&responseStatus)) && + (responseStatus == 204 || responseStatus == 304)) { + mResponseXML->SetSuppressParserErrorConsoleMessages(true); + } + } + + // make sure to notify the listener if we were aborted + // XXX in fact, why don't we do the cleanup below in this case?? + // State::unsent is for abort calls. See OnStartRequest above. + if (mState == State::unsent || mFlagTimedOut) { + if (mXMLParserStreamListener) + (void) mXMLParserStreamListener->OnStopRequest(request, ctxt, status); + return NS_OK; + } + + // Is this good enough here? + if (mXMLParserStreamListener && mFlagParseBody) { + mXMLParserStreamListener->OnStopRequest(request, ctxt, status); + } + + mXMLParserStreamListener = nullptr; + mContext = nullptr; + + bool waitingForBlobCreation = false; + + if (NS_SUCCEEDED(status) && + (mResponseType == XMLHttpRequestResponseType::Blob || + mResponseType == XMLHttpRequestResponseType::Moz_blob)) { + ErrorResult rv; + if (!mDOMBlob) { + CreateDOMBlob(request); + } + if (mDOMBlob) { + mResponseBlob = mDOMBlob; + mDOMBlob = nullptr; + } else { + // Smaller files may be written in cache map instead of separate files. + // Also, no-store response cannot be written in persistent cache. + nsAutoCString contentType; + mChannel->GetContentType(contentType); + + if (mResponseType == XMLHttpRequestResponseType::Blob) { + // mBlobStorage can be null if the channel is non-file non-cacheable + // and if the response length is zero. + MaybeCreateBlobStorage(); + mBlobStorage->GetBlobWhenReady(GetOwner(), contentType, this); + waitingForBlobCreation = true; + } else { + // mBlobSet can be null if the channel is non-file non-cacheable + // and if the response length is zero. + if (!mBlobSet) { + mBlobSet = new BlobSet(); + } + + nsTArray<RefPtr<BlobImpl>> subImpls(mBlobSet->GetBlobImpls()); + RefPtr<BlobImpl> blobImpl = + MultipartBlobImpl::Create(Move(subImpls), + NS_ConvertASCIItoUTF16(contentType), + rv); + mBlobSet = nullptr; + + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + mResponseBlob = Blob::Create(GetOwner(), blobImpl); + } + } + + NS_ASSERTION(mResponseBody.IsEmpty(), "mResponseBody should be empty"); + NS_ASSERTION(mResponseText.IsEmpty(), "mResponseText should be empty"); + } else if (NS_SUCCEEDED(status) && + ((mResponseType == XMLHttpRequestResponseType::Arraybuffer && + !mIsMappedArrayBuffer) || + mResponseType == XMLHttpRequestResponseType::Moz_chunked_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<nsIChannel> channel(do_QueryInterface(request)); + NS_ENSURE_TRUE(channel, NS_ERROR_UNEXPECTED); + + channel->SetNotificationCallbacks(nullptr); + mNotificationCallbacks = nullptr; + mChannelEventSink = nullptr; + mProgressEventSink = nullptr; + + mFlagSyncLooping = false; + + // 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 = true; + 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 == State::unsent || mState == State::done) { + return NS_OK; + } + + if (!mResponseXML) { + mFlagParseBody = false; + + //We postpone the 'done' until the creation of the Blob is completed. + if (!waitingForBlobCreation) { + ChangeStateToDone(); + } + + return NS_OK; + } + + if (mIsHtml) { + NS_ASSERTION(!mFlagSyncLooping, + "We weren't supposed to support HTML parsing with XHR!"); + nsCOMPtr<EventTarget> eventTarget = do_QueryInterface(mResponseXML); + EventListenerManager* manager = + eventTarget->GetOrCreateListenerManager(); + manager->AddEventListenerByType(new nsXHRParseEndListener(this), + 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(); + return NS_OK; +} + +void +XMLHttpRequestMainThread::OnBodyParseEnd() +{ + mFlagParseBody = false; + ChangeStateToDone(); +} + +void +XMLHttpRequestMainThread::MatchCharsetAndDecoderToResponseDocument() +{ + if (mResponseXML && mResponseCharset != mResponseXML->GetDocumentCharacterSet()) { + mResponseCharset = mResponseXML->GetDocumentCharacterSet(); + TruncateResponseText(); + mResponseBodyDecodedPos = 0; + mDecoder = EncodingUtils::DecoderForEncoding(mResponseCharset); + } +} + +void +XMLHttpRequestMainThread::ChangeStateToDone() +{ + 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; + } + + // Per spec, fire readystatechange=4/done before final error events. + ChangeState(State::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). + DispatchProgressEvent(this, + mErrorLoad ? ProgressEventType::error : + ProgressEventType::load, + mErrorLoad ? 0 : mLoadTransferred, + mErrorLoad ? -1 : mLoadTotal); + + if (mErrorLoad) { + // 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; + } +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<nsIDocument>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + nsCOMPtr<nsIDOMDocument> domdoc(do_QueryInterface(mBody)); + NS_ENSURE_STATE(domdoc); + aCharset.AssignLiteral("UTF-8"); + + nsresult rv; + nsCOMPtr<nsIStorageStream> storStream; + rv = NS_NewStorageStream(4096, UINT32_MAX, getter_AddRefs(storStream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIOutputStream> output; + rv = storStream->GetOutputStream(0, getter_AddRefs(output)); + NS_ENSURE_SUCCESS(rv, rv); + + if (mBody->IsHTMLDocument()) { + aContentType.AssignLiteral("text/html"); + + nsString serialized; + if (!nsContentUtils::SerializeNodeToMarkup(mBody, true, serialized)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsAutoCString utf8Serialized; + if (!AppendUTF16toUTF8(serialized, utf8Serialized, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t written; + rv = output->Write(utf8Serialized.get(), utf8Serialized.Length(), &written); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(written == utf8Serialized.Length()); + } else { + aContentType.AssignLiteral("application/xml"); + + nsCOMPtr<nsIDOMSerializer> serializer = + do_CreateInstance(NS_XMLSERIALIZER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Make sure to use the encoding we'll send + rv = serializer->SerializeToStream(domdoc, output, aCharset); + NS_ENSURE_SUCCESS(rv, rv); + } + + output->Close(); + + uint32_t length; + rv = storStream->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + *aContentLength = length; + + rv = storStream->NewInputStream(0, aResult); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<const nsAString>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + aContentType.AssignLiteral("text/plain"); + aCharset.AssignLiteral("UTF-8"); + + nsAutoCString converted; + if (!AppendUTF16toUTF8(*mBody, converted, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + *aContentLength = converted.Length(); + nsresult rv = NS_NewCStringInputStream(aResult, converted); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<nsIInputStream>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + aContentType.AssignLiteral("text/plain"); + aCharset.Truncate(); + + nsresult rv = mBody->Available(aContentLength); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIInputStream> stream(mBody); + stream.forget(aResult); + return NS_OK; +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<Blob>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + return mBody->GetSendInfo(aResult, aContentLength, aContentType, aCharset); +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<FormData>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + return mBody->GetSendInfo(aResult, aContentLength, aContentType, aCharset); +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<URLSearchParams>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + return mBody->GetSendInfo(aResult, aContentLength, aContentType, aCharset); +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<nsIXHRSendable>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + return mBody->GetSendInfo(aResult, aContentLength, aContentType, aCharset); +} + +static nsresult +GetBufferDataAsStream(const uint8_t* aData, uint32_t aDataLength, + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) +{ + aContentType.SetIsVoid(true); + aCharset.Truncate(); + + *aContentLength = aDataLength; + const char* data = reinterpret_cast<const char*>(aData); + + nsCOMPtr<nsIInputStream> stream; + nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream), data, aDataLength, + NS_ASSIGNMENT_COPY); + NS_ENSURE_SUCCESS(rv, rv); + + stream.forget(aResult); + + return NS_OK; +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<const ArrayBuffer>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + mBody->ComputeLengthAndData(); + return GetBufferDataAsStream(mBody->Data(), mBody->Length(), + aResult, aContentLength, aContentType, aCharset); +} + +template<> nsresult +XMLHttpRequestMainThread::RequestBody<const ArrayBufferView>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) const +{ + mBody->ComputeLengthAndData(); + return GetBufferDataAsStream(mBody->Data(), mBody->Length(), + aResult, aContentLength, aContentType, aCharset); +} + + +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<nsILoadGroup> loadGroup = GetLoadGroup(); + + nsSecurityFlags secFlags; + nsLoadFlags loadFlags = nsIRequest::LOAD_BACKGROUND | + nsIChannel::LOAD_CLASSIFY_URI; + if (nsContentUtils::IsSystemPrincipal(mPrincipal)) { + // 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_DATA_IS_NULL | + nsILoadInfo::SEC_SANDBOXED; + } 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_DATA_INHERITS | + 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_DATA_INHERITS | + 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<nsIDocument> responsibleDocument = GetDocumentIfCurrent(); + if (responsibleDocument && responsibleDocument->NodePrincipal() == mPrincipal) { + rv = NS_NewChannel(getter_AddRefs(mChannel), + mRequestURL, + responsibleDocument, + secFlags, + nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST, + loadGroup, + nullptr, // aCallbacks + loadFlags); + } else { + // Otherwise use the principal. + rv = NS_NewChannel(getter_AddRefs(mChannel), + mRequestURL, + mPrincipal, + secFlags, + nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST, + loadGroup, + nullptr, // aCallbacks + loadFlags); + } + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel)); + if (httpChannel) { + rv = httpChannel->SetRequestMethod(mRequestMethod); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the initiator type + nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChannel)); + if (timedChannel) { + timedChannel->SetInitiatorType(NS_LITERAL_STRING("xmlhttprequest")); + } + } + + // Using the provided principal as the triggeringPrincipal is fine, since we + // want to be able to access any of the origins that the principal has access + // to during the security checks, but we don't want a document to inherit an + // expanded principal, so in that case we need to select the principal in the + // expanded principal's whitelist that can load our URL as principalToInherit. + nsCOMPtr<nsIPrincipal> resultingDocumentPrincipal(mPrincipal); + nsCOMPtr<nsIExpandedPrincipal> ep = do_QueryInterface(mPrincipal); + if (ep) { + nsTArray<nsCOMPtr<nsIPrincipal>>* whitelist = nullptr; + ep->GetWhiteList(&whitelist); + if (!whitelist) { + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(!(secFlags & nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS)); + bool dataInherits = (secFlags & + (nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS | + nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS)) != 0; + for (const auto& principal : *whitelist) { + if (NS_SUCCEEDED(principal->CheckMayLoad(mRequestURL, false, dataInherits))) { + resultingDocumentPrincipal = principal; + break; + } + } + } + + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo(); + rv = loadInfo->SetPrincipalToInherit(resultingDocumentPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +XMLHttpRequestMainThread::InitiateFetch(nsIInputStream* aUploadStream, + int64_t aUploadLength, + nsACString& aUploadContentType) +{ + nsresult rv; + + // 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<nsIHttpChannel> 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_LITERAL_CSTRING("*/*")); + } + + mAuthorRequestHeaders.ApplyToChannel(httpChannel); + + if (!IsSystemXHR()) { + nsCOMPtr<nsPIDOMWindowInner> owner = GetOwner(); + nsCOMPtr<nsIDocument> doc = owner ? owner->GetExtantDoc() : nullptr; + nsContentUtils::SetFetchReferrerURIWithPolicy(mPrincipal, doc, + httpChannel, + mozilla::net::RP_Default); + } + + // 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<nsIUploadChannel2> uploadChannel2 = do_QueryInterface(httpChannel); + if (!uploadChannel2) { + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService) { + consoleService->LogStringMessage(NS_LITERAL_STRING( + "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." + ).get()); + } + } + + if (aUploadStream) { + // If necessary, wrap the stream in a buffered stream so as to guarantee + // support for our upload when calling ExplicitSetUploadStream. + nsCOMPtr<nsIInputStream> bufferedStream; + if (!NS_InputStreamIsBuffered(aUploadStream)) { + rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), + aUploadStream, 4096); + NS_ENSURE_SUCCESS(rv, rv); + + aUploadStream = 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<nsIUploadChannel2> uploadChannel2(do_QueryInterface(httpChannel)); + // This assertion will fire if buggy extensions are installed + NS_ASSERTION(uploadChannel2, "http must support nsIUploadChannel2"); + if (uploadChannel2) { + uploadChannel2->ExplicitSetUploadStream(aUploadStream, + 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<nsIUploadChannel> uploadChannel = + do_QueryInterface(httpChannel); + uploadChannel->SetUploadStream(aUploadStream, aUploadContentType, + mUploadTotal); + // Reset the method to its original value + httpChannel->SetRequestMethod(mRequestMethod); + } + } + } + + // 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<nsILoadInfo> loadInfo = mChannel->GetLoadInfo(); + static_cast<net::LoadInfo*>(loadInfo.get())->SetIncludeCookiesSecFlag(); + } + + // Blocking gets are common enough out of XHR that we should mark + // the channel slow by default for pipeline purposes + AddLoadFlags(mChannel, nsIRequest::INHIBIT_PIPELINE); + + // 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<nsIClassOfService> cos(do_QueryInterface(mChannel)); + if (cos) { + cos->AddClassFlags(nsIClassOfService::Unblocked); + } + + // Disable Necko-internal response timeouts. + nsCOMPtr<nsIHttpChannelInternal> + internalHttpChannel(do_QueryInterface(mChannel)); + if (internalHttpChannel) { + internalHttpChannel->SetResponseTimeoutEnabled(false); + } + + 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); + } + + // 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 + nsAutoCString contentType; + if (NS_FAILED(mChannel->GetContentType(contentType)) || + contentType.IsEmpty() || + contentType.Equals(UNKNOWN_CONTENT_TYPE)) { + mChannel->SetContentType(NS_LITERAL_CSTRING("application/xml")); + } + + // Set up the preflight if needed + if (!IsSystemXHR()) { + nsTArray<nsCString> CORSUnsafeHeaders; + mAuthorRequestHeaders.GetCORSUnsafeHeaders(CORSUnsafeHeaders); + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo(); + 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<nsIStreamListener> listener = new net::nsStreamListenerWrapper(this); + + // Start reading from the channel + rv = mChannel->AsyncOpen2(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 = true; + + // Per spec, we throw on sync errors, but not async. + if (mFlagSynchronous) { + return NS_ERROR_DOM_NETWORK_ERR; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::Send(nsIVariant* aVariant) +{ + if (!aVariant) { + return SendInternal(nullptr); + } + + uint16_t dataType; + nsresult rv = aVariant->GetDataType(&dataType); + NS_ENSURE_SUCCESS(rv, rv); + + if (dataType == nsIDataType::VTYPE_INTERFACE || + dataType == nsIDataType::VTYPE_INTERFACE_IS) { + nsCOMPtr<nsISupports> supports; + nsID *iid; + rv = aVariant->GetAsInterface(&iid, getter_AddRefs(supports)); + NS_ENSURE_SUCCESS(rv, rv); + + free(iid); + + // document? + nsCOMPtr<nsIDocument> doc = do_QueryInterface(supports); + if (doc) { + RequestBody<nsIDocument> body(doc); + return SendInternal(&body); + } + + // nsISupportsString? + nsCOMPtr<nsISupportsString> wstr = do_QueryInterface(supports); + if (wstr) { + nsAutoString string; + wstr->GetData(string); + RequestBody<const nsAString> body(&string); + return SendInternal(&body); + } + + // nsIInputStream? + nsCOMPtr<nsIInputStream> stream = do_QueryInterface(supports); + if (stream) { + RequestBody<nsIInputStream> body(stream); + return SendInternal(&body); + } + + // nsIXHRSendable? + nsCOMPtr<nsIXHRSendable> sendable = do_QueryInterface(supports); + if (sendable) { + RequestBody<nsIXHRSendable> body(sendable); + return SendInternal(&body); + } + + // ArrayBuffer? + JS::RootingContext* rootingCx = RootingCx(); + JS::Rooted<JS::Value> realVal(rootingCx); + + nsresult rv = aVariant->GetAsJSVal(&realVal); + if (NS_SUCCEEDED(rv) && !realVal.isPrimitive()) { + JS::Rooted<JSObject*> obj(rootingCx, realVal.toObjectOrNull()); + RootedTypedArray<ArrayBuffer> buf(rootingCx); + if (buf.Init(obj)) { + RequestBody<const ArrayBuffer> body(&buf); + return SendInternal(&body); + } + } + } else if (dataType == nsIDataType::VTYPE_VOID || + dataType == nsIDataType::VTYPE_EMPTY) { + return SendInternal(nullptr); + } + + char16_t* data = nullptr; + uint32_t len = 0; + rv = aVariant->GetAsWStringWithSize(&len, &data); + NS_ENSURE_SUCCESS(rv, rv); + + nsString string; + string.Adopt(data, len); + + RequestBody<const nsAString> body(&string); + return SendInternal(&body); +} + +nsresult +XMLHttpRequestMainThread::SendInternal(const RequestBodyBase* aBody) +{ + NS_ENSURE_TRUE(mPrincipal, NS_ERROR_NOT_INITIALIZED); + + // Step 1 + if (mState != State::opened) { + return NS_ERROR_DOM_INVALID_STATE_XHR_MUST_BE_OPENED; + } + + // Step 2 + if (mFlagSend) { + return NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_SENDING; + } + + nsresult rv = CheckInnerWindowCorrectness(); + if (NS_FAILED(rv)) { + return NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT; + } + + // 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) { + return NS_ERROR_DOM_NETWORK_ERR; + } + + PopulateNetworkInterfaceId(); + + // 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 = false; + mLoadTotal = -1; + nsCOMPtr<nsIInputStream> uploadStream; + nsAutoCString uploadContentType; + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel)); + if (aBody && httpChannel && + !mRequestMethod.EqualsLiteral("GET") && + !mRequestMethod.EqualsLiteral("HEAD")) { + + nsAutoCString charset; + nsAutoCString defaultContentType; + uint64_t size_u64; + rv = aBody->GetAsStream(getter_AddRefs(uploadStream), + &size_u64, defaultContentType, charset); + NS_ENSURE_SUCCESS(rv, rv); + + // make sure it fits within js MAX_SAFE_INTEGER + mUploadTotal = + net::InScriptableRange(size_u64) ? static_cast<int64_t>(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; + + if (!charset.IsEmpty()) { + // If we are providing the default content type, then we also need to + // provide a charset declaration. + uploadContentType.Append(NS_LITERAL_CSTRING(";charset=")); + uploadContentType.Append(charset); + } + } + + // We don't want to set a charset for streams. + if (!charset.IsEmpty()) { + // Replace all case-insensitive matches of the charset in the + // content-type with the correct case. + RequestHeaders::CharsetIterator iter(uploadContentType); + const nsCaseInsensitiveCStringComparator cmp; + while (iter.Next()) { + if (!iter.Equals(charset, cmp)) { + 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 && + Preferences::GetBool("dom.mapped_arraybuffer.enabled", true)) { + nsCOMPtr<nsIURI> uri; + nsAutoCString scheme; + + rv = mChannel->GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv)) { + uri->GetScheme(scheme); + if (scheme.LowerCaseEqualsLiteral("jar")) { + mIsMappedArrayBuffer = true; + } + } + } + + rv = InitiateFetch(uploadStream, mUploadTotal, uploadContentType); + NS_ENSURE_SUCCESS(rv, rv); + + // Start our timeout + mRequestSentTime = PR_Now(); + StartTimeoutTimer(); + + mWaitingForOnStopRequest = true; + + // Step 8 + mFlagSend = true; + + // If we're synchronous, spin an event loop here and wait + if (mFlagSynchronous) { + mFlagSyncLooping = true; + + nsCOMPtr<nsIDocument> suspendedDoc; + nsCOMPtr<nsIRunnable> resumeTimeoutRunnable; + if (GetOwner()) { + if (nsCOMPtr<nsPIDOMWindowOuter> topWindow = GetOwner()->GetOuterWindow()->GetTop()) { + if (nsCOMPtr<nsPIDOMWindowInner> topInner = topWindow->GetCurrentInnerWindow()) { + suspendedDoc = topWindow->GetExtantDoc(); + if (suspendedDoc) { + suspendedDoc->SuppressEventHandling(nsIDocument::eEvents); + } + topInner->Suspend(); + resumeTimeoutRunnable = new nsResumeTimeoutsEvent(topInner); + } + } + } + + StopProgressEventTimer(); + + SyncTimeoutType syncTimeoutType = MaybeStartSyncTimeoutTimer(); + if (syncTimeoutType == eErrorOrExpired) { + Abort(); + rv = NS_ERROR_DOM_NETWORK_ERR; + } + + if (NS_SUCCEEDED(rv)) { + nsAutoSyncOperation sync(suspendedDoc); + nsIThread *thread = NS_GetCurrentThread(); + while (mFlagSyncLooping) { + if (!NS_ProcessNextEvent(thread)) { + rv = NS_ERROR_UNEXPECTED; + break; + } + } + + // Time expired... We should throw. + if (syncTimeoutType == eTimerStarted && !mSyncTimeoutTimer) { + rv = NS_ERROR_DOM_NETWORK_ERR; + } + + CancelSyncTimeoutTimer(); + } + + if (suspendedDoc) { + suspendedDoc->UnsuppressEventHandlingAndFireEvents(nsIDocument::eEvents, + true); + } + + if (resumeTimeoutRunnable) { + NS_DispatchToCurrentThread(resumeTimeoutRunnable); + } + } 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) { + // Per spec, silently fail on async request failures; throw for sync. + if (mFlagSynchronous) { + return NS_ERROR_DOM_NETWORK_ERR; + } else { + // Defer the actual sending of async events just in case listeners + // are attached after the send() method is called. + NS_DispatchToCurrentThread( + NewRunnableMethod<ProgressEventType>(this, + &XMLHttpRequestMainThread::CloseRequestWithError, + ProgressEventType::error)); + return NS_OK; + } + } + + return rv; +} + +// http://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#dom-xmlhttprequest-setrequestheader +NS_IMETHODIMP +XMLHttpRequestMainThread::SetRequestHeader(const nsACString& aName, + const nsACString& aValue) +{ + // Step 1 + if (mState != State::opened) { + return NS_ERROR_DOM_INVALID_STATE_XHR_MUST_BE_OPENED; + } + + // Step 2 + if (mFlagSend) { + return NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_SENDING; + } + + // Step 3 + nsAutoCString value(aValue); + static const char kHTTPWhitespace[] = "\n\t\r "; + value.Trim(kHTTPWhitespace); + + // Step 4 + if (!NS_IsValidHTTPToken(aName) || !NS_IsReasonableHTTPHeaderValue(value)) { + return NS_ERROR_DOM_INVALID_HEADER_NAME; + } + + // Step 5 + bool isPrivilegedCaller = IsSystemXHR(); + bool isForbiddenHeader = nsContentUtils::IsForbiddenRequestHeader(aName); + if (!isPrivilegedCaller && isForbiddenHeader) { + NS_ConvertUTF8toUTF16 name(aName); + const char16_t* params[] = { name.get() }; + LogMessage("ForbiddenHeaderWarning", GetOwner(), params, ArrayLength(params)); + return NS_OK; + } + + // 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); + } + + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetTimeout(uint32_t *aTimeout) +{ + *aTimeout = Timeout(); + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::SetTimeout(uint32_t aTimeout) +{ + ErrorResult rv; + SetTimeout(aTimeout, rv); + return rv.StealNSResult(); +} + +void +XMLHttpRequestMainThread::SetTimeout(uint32_t aTimeout, ErrorResult& aRv) +{ + if (mFlagSynchronous && mState != State::unsent && HasOrHasHadOwner()) { + /* Timeout is not supported for synchronous requests with an owning window, + per XHR2 spec. */ + LogMessage("TimeoutSyncXHRWarning", GetOwner()); + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_XHR_TIMEOUT_AND_RESPONSETYPE_UNSUPPORTED_FOR_SYNC); + return; + } + + mTimeoutMilliseconds = aTimeout; + if (mRequestSentTime) { + StartTimeoutTimer(); + } +} + +void +XMLHttpRequestMainThread::StartTimeoutTimer() +{ + MOZ_ASSERT(mRequestSentTime, + "StartTimeoutTimer mustn't be called before the request was sent!"); + if (mState == State::done) { + // do nothing! + return; + } + + if (mTimeoutTimer) { + mTimeoutTimer->Cancel(); + } + + if (!mTimeoutMilliseconds) { + return; + } + + if (!mTimeoutTimer) { + mTimeoutTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + } + uint32_t elapsed = + (uint32_t)((PR_Now() - mRequestSentTime) / PR_USEC_PER_MSEC); + mTimeoutTimer->InitWithCallback( + this, + mTimeoutMilliseconds > elapsed ? mTimeoutMilliseconds - elapsed : 0, + nsITimer::TYPE_ONE_SHOT + ); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetReadyState(uint16_t *aState) +{ + *aState = ReadyState(); + return NS_OK; +} + +uint16_t +XMLHttpRequestMainThread::ReadyState() const +{ + // Translate some of our internal states for external consumers + switch(mState) { + case State::unsent: + return UNSENT; + case State::opened: + return OPENED; + case State::headers_received: + return HEADERS_RECEIVED; + case State::loading: + return LOADING; + case State::done: + return DONE; + default: + MOZ_CRASH("Unknown state"); + } + return 0; +} + +void XMLHttpRequestMainThread::OverrideMimeType(const nsAString& aMimeType, ErrorResult& aRv) +{ + if (mState == State::loading || mState == State::done) { + ResetResponse(); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_LOADING_OR_DONE); + return; + } + + mOverrideMimeType = aMimeType; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::SlowOverrideMimeType(const nsAString& aMimeType) +{ + ErrorResult aRv; + OverrideMimeType(aMimeType, aRv); + return aRv.StealNSResult(); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetMozBackgroundRequest(bool *_retval) +{ + *_retval = MozBackgroundRequest(); + return NS_OK; +} + +bool +XMLHttpRequestMainThread::MozBackgroundRequest() const +{ + return mFlagBackgroundRequest; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::SetMozBackgroundRequest(bool aMozBackgroundRequest) +{ + if (!IsSystemXHR()) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + if (mState != State::unsent) { + // Can't change this while we're in the middle of something. + return NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_SENDING; + } + + mFlagBackgroundRequest = aMozBackgroundRequest; + + return NS_OK; +} + +void +XMLHttpRequestMainThread::SetMozBackgroundRequest(bool aMozBackgroundRequest, + ErrorResult& aRv) +{ + // No errors for this webIDL method on main-thread. + SetMozBackgroundRequest(aMozBackgroundRequest); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetWithCredentials(bool *_retval) +{ + *_retval = WithCredentials(); + return NS_OK; +} + +bool +XMLHttpRequestMainThread::WithCredentials() const +{ + return mFlagACwithCredentials; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::SetWithCredentials(bool aWithCredentials) +{ + ErrorResult rv; + SetWithCredentials(aWithCredentials, rv); + return rv.StealNSResult(); +} + +void +XMLHttpRequestMainThread::SetWithCredentials(bool aWithCredentials, ErrorResult& aRv) +{ + // 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 != State::unsent && mState != State::opened) || + mFlagSend || mIsAnon) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_SENDING); + return; + } + + mFlagACwithCredentials = aWithCredentials; +} + +nsresult +XMLHttpRequestMainThread::ChangeState(State aState, bool aBroadcast) +{ + mState = aState; + nsresult rv = NS_OK; + + if (aState != State::headers_received && aState != State::loading) { + StopProgressEventTimer(); + } + + + if (aBroadcast && (!mFlagSynchronous || + aState == State::opened || + aState == State::done)) { + rv = FireReadystatechangeEvent(); + } + + return rv; +} + +///////////////////////////////////////////////////// +// nsIChannelEventSink methods: +// +NS_IMETHODIMP +XMLHttpRequestMainThread::AsyncOnChannelRedirect(nsIChannel *aOldChannel, + nsIChannel *aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback *callback) +{ + NS_PRECONDITION(aNewChannel, "Redirect without a channel?"); + + // Prepare to receive callback + mRedirectCallback = callback; + mNewRedirectChannel = aNewChannel; + + if (mChannelEventSink) { + nsCOMPtr<nsIAsyncVerifyRedirectCallback> fwd = + EnsureXPCOMifier(); + + nsresult rv = mChannelEventSink->AsyncOnChannelRedirect(aOldChannel, + aNewChannel, + aFlags, fwd); + if (NS_FAILED(rv)) { + mRedirectCallback = nullptr; + mNewRedirectChannel = nullptr; + } + return rv; + } + OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +nsresult +XMLHttpRequestMainThread::OnRedirectVerifyCallback(nsresult result) +{ + NS_ASSERTION(mRedirectCallback, "mRedirectCallback not set in callback"); + NS_ASSERTION(mNewRedirectChannel, "mNewRedirectChannel not set in callback"); + + if (NS_SUCCEEDED(result)) { + mChannel = mNewRedirectChannel; + + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel)); + if (httpChannel) { + // Ensure all original headers are duplicated for the new channel (bug #553888) + mAuthorRequestHeaders.ApplyToChannel(httpChannel); + } + } else { + mErrorLoad = true; + } + + 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, nsISupports *aContext, 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, aContext, aProgress, + aProgressMax); + } + + return NS_OK; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::OnStatus(nsIRequest *aRequest, nsISupports *aContext, nsresult aStatus, const char16_t *aStatusArg) +{ + if (mProgressEventSink) { + mProgressEventSink->OnStatus(aRequest, aContext, 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<nsIChannelEventSink*>(EnsureXPCOMifier().take()); + return NS_OK; + } else if (aIID.Equals(NS_GET_IID(nsIProgressEventSink))) { + mProgressEventSink = do_GetInterface(mNotificationCallbacks); + *aResult = static_cast<nsIProgressEventSink*>(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) { + nsCOMPtr<nsIInterfaceRequestor> badCertHandler(do_CreateInstance(NS_BADCERTHANDLER_CONTRACTID, &rv)); + + // Ignore failure to get component, we may not have all its dependencies + // available + if (NS_SUCCEEDED(rv)) { + rv = badCertHandler->GetInterface(aIID, aResult); + if (NS_SUCCEEDED(rv)) + return rv; + } + } + else if (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) || + aIID.Equals(NS_GET_IID(nsIAuthPrompt2))) { + nsCOMPtr<nsIPromptFactory> 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<nsPIDOMWindowOuter> window; + if (GetOwner()) { + window = GetOwner()->GetOuterWindow(); + } + + return wwatch->GetPrompt(window, aIID, + reinterpret_cast<void**>(aResult)); + } + // Now check for the various XHR non-DOM interfaces, except + // nsIProgressEventSink and nsIChannelEventSink which we already + // handled above. + else if (aIID.Equals(NS_GET_IID(nsIStreamListener))) { + *aResult = static_cast<nsIStreamListener*>(EnsureXPCOMifier().take()); + return NS_OK; + } + else if (aIID.Equals(NS_GET_IID(nsIRequestObserver))) { + *aResult = static_cast<nsIRequestObserver*>(EnsureXPCOMifier().take()); + return NS_OK; + } + else if (aIID.Equals(NS_GET_IID(nsITimerCallback))) { + *aResult = static_cast<nsITimerCallback*>(EnsureXPCOMifier().take()); + return NS_OK; + } + + return QueryInterface(aIID, aResult); +} + +void +XMLHttpRequestMainThread::GetInterface(JSContext* aCx, nsIJSID* aIID, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv) +{ + dom::GetInterface(aCx, this, aIID, aRetval, aRv); +} + +XMLHttpRequestUpload* +XMLHttpRequestMainThread::GetUpload(ErrorResult& aRv) +{ + if (!mUpload) { + mUpload = new XMLHttpRequestUpload(this); + } + return mUpload; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetUpload(nsIXMLHttpRequestUpload** aUpload) +{ + ErrorResult rv; + RefPtr<XMLHttpRequestUpload> upload = GetUpload(rv); + upload.forget(aUpload); + return rv.StealNSResult(); +} + +bool +XMLHttpRequestMainThread::MozAnon() const +{ + return mIsAnon; +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetMozAnon(bool* aAnon) +{ + *aAnon = MozAnon(); + return NS_OK; +} + +bool +XMLHttpRequestMainThread::MozSystem() const +{ + return IsSystemXHR(); +} + +NS_IMETHODIMP +XMLHttpRequestMainThread::GetMozSystem(bool* aSystem) +{ + *aSystem = MozSystem(); + return NS_OK; +} + +void +XMLHttpRequestMainThread::HandleTimeoutCallback() +{ + if (mState == State::done) { + NS_NOTREACHED("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) { + return; + } + + if (InUploadPhase()) { + if (mUpload && !mUploadComplete) { + 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 = do_CreateInstance(NS_TIMER_CONTRACTID); + } + if (mProgressNotifier) { + mProgressTimerIsActive = true; + mProgressNotifier->Cancel(); + mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL, + nsITimer::TYPE_ONE_SHOT); + } +} + +XMLHttpRequestMainThread::SyncTimeoutType +XMLHttpRequestMainThread::MaybeStartSyncTimeoutTimer() +{ + MOZ_ASSERT(mFlagSynchronous); + + nsIDocument* 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 = do_CreateInstance(NS_TIMER_CONTRACTID); + 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<nsXMLHttpRequestXPCOMifier> +XMLHttpRequestMainThread::EnsureXPCOMifier() +{ + if (!mXPCOMifier) { + mXPCOMifier = new nsXMLHttpRequestXPCOMifier(this); + } + RefPtr<nsXMLHttpRequestXPCOMifier> 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<nsIURI> 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(); + XMLHttpRequestBinding::ClearCachedResponseTextValue(this); +} + +NS_IMPL_ISUPPORTS(XMLHttpRequestMainThread::nsHeaderVisitor, nsIHttpHeaderVisitor) + +NS_IMETHODIMP XMLHttpRequestMainThread:: +nsHeaderVisitor::VisitHeader(const nsACString &header, const nsACString &value) +{ + if (mXHR.IsSafeHeader(header, mHttpChannel)) { + mHeaders.Append(header); + mHeaders.AppendLiteral(": "); + mHeaders.Append(value); + mHeaders.AppendLiteral("\r\n"); + } + return NS_OK; +} + +void +XMLHttpRequestMainThread::MaybeCreateBlobStorage() +{ + MOZ_ASSERT(mResponseType == XMLHttpRequestResponseType::Blob); + + if (mBlobStorage) { + return; + } + + MutableBlobStorage::MutableBlobStorageType storageType = + BasePrincipal::Cast(mPrincipal)->PrivateBrowsingId() == 0 + ? MutableBlobStorage::eCouldBeInTemporaryFile + : MutableBlobStorage::eOnlyInMemory; + + mBlobStorage = new MutableBlobStorage(storageType); +} + +void +XMLHttpRequestMainThread::BlobStoreCompleted(MutableBlobStorage* aBlobStorage, + Blob* aBlob, nsresult aRv) +{ + // Ok, the state is changed... + if (mBlobStorage != aBlobStorage || NS_FAILED(aRv)) { + return; + } + + MOZ_ASSERT(mState != State::done); + + mResponseBlob = aBlob; + mBlobStorage = nullptr; + + ChangeStateToDone(); +} + +// 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_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() + : mDataPtr(nullptr), + mCapacity(0), + mLength(0), + mMapPtr(nullptr) +{ +} + +ArrayBufferBuilder::~ArrayBufferBuilder() +{ + reset(); +} + +void +ArrayBufferBuilder::reset() +{ + 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) +{ + MOZ_ASSERT(!mMapPtr); + + // 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) +{ + MOZ_ASSERT(!mMapPtr); + + 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 (!setCapacity(newcap.value())) { + 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; +} + +JSObject* +ArrayBufferBuilder::getArrayBuffer(JSContext* aCx) +{ + if (mMapPtr) { + JSObject* obj = JS_NewMappedArrayBufferWithContents(aCx, mLength, mMapPtr); + if (!obj) { + JS_ReleaseMappedArrayBufferContents(mMapPtr, mLength); + } + mMapPtr = nullptr; + + // 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 (!setCapacity(mLength)) { + return nullptr; + } + } + + JSObject* obj = JS_NewArrayBufferWithContents(aCx, mLength, mDataPtr); + mLength = mCapacity = 0; + if (!obj) { + js_free(mDataPtr); + } + mDataPtr = nullptr; + return obj; +} + +nsresult +ArrayBufferBuilder::mapToFileInPackage(const nsCString& aFile, + nsIFile* aJarFile) +{ + nsresult rv; + + // Open Jar file to get related attributes of target file. + RefPtr<nsZipArchive> zip = new nsZipArchive(); + rv = zip->OpenArchive(aJarFile); + if (NS_FAILED(rv)) { + return rv; + } + nsZipItem* zipItem = zip->GetItem(aFile.get()); + if (!zipItem) { + return NS_ERROR_FILE_TARGET_DOES_NOT_EXIST; + } + + // 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) +{ + const nsCaseInsensitiveCStringComparator ignoreCase; + for (RequestHeaders::RequestHeader& header : mHeaders) { + if (header.mName.Equals(aName, ignoreCase)) { + return &header; + } + } + return nullptr; +} + +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* aHttpChannel) const +{ + for (const RequestHeader& header : mHeaders) { + if (header.mValue.IsEmpty()) { + aHttpChannel->SetEmptyRequestHeader(header.mName); + } else { + aHttpChannel->SetRequestHeader(header.mName, header.mValue, false); + } + } +} + +void +RequestHeaders::GetCORSUnsafeHeaders(nsTArray<nsCString>& aArray) const +{ + static const char *kCrossOriginSafeHeaders[] = { + "accept", "accept-language", "content-language", "content-type", + "last-event-id" + }; + const uint32_t kCrossOriginSafeHeadersLength = + ArrayLength(kCrossOriginSafeHeaders); + for (const RequestHeader& header : mHeaders) { + bool safe = false; + for (uint32_t i = 0; i < kCrossOriginSafeHeadersLength; ++i) { + if (header.mName.LowerCaseEqualsASCII(kCrossOriginSafeHeaders[i])) { + safe = true; + break; + } + } + if (!safe) { + 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; +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/xhr/XMLHttpRequestMainThread.h b/dom/xhr/XMLHttpRequestMainThread.h new file mode 100644 index 000000000..c9bcddf99 --- /dev/null +++ b/dom/xhr/XMLHttpRequestMainThread.h @@ -0,0 +1,872 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestMainThread_h +#define mozilla_dom_XMLHttpRequestMainThread_h + +#include <bitset> +#include "nsAutoPtr.h" +#include "nsIXMLHttpRequest.h" +#include "nsISupportsUtils.h" +#include "nsIURI.h" +#include "nsIHttpChannel.h" +#include "nsIDocument.h" +#include "nsIStreamListener.h" +#include "nsWeakReference.h" +#include "nsIChannelEventSink.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIInterfaceRequestor.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIProgressEventSink.h" +#include "nsJSUtils.h" +#include "nsTArray.h" +#include "nsITimer.h" +#include "nsIPrincipal.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsISizeOfEventTarget.h" +#include "nsIXPConnect.h" +#include "nsIInputStream.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/NotNull.h" +#include "mozilla/dom/MutableBlobStorage.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/XMLHttpRequest.h" +#include "mozilla/dom/XMLHttpRequestBinding.h" +#include "mozilla/dom/XMLHttpRequestEventTarget.h" +#include "mozilla/dom/XMLHttpRequestString.h" + +#ifdef Status +/* Xlib headers insist on this for some reason... Nuke it because + it'll override our member name */ +#undef Status +#endif + +class nsIJARChannel; +class nsILoadGroup; +class nsIUnicodeDecoder; +class nsIJSID; + +namespace mozilla { +namespace dom { + +class Blob; +class BlobSet; +class DOMString; +class FormData; +class URLSearchParams; +class XMLHttpRequestUpload; +struct OriginAttributesDictionary; + +// A helper for building up an ArrayBuffer object's data +// before creating the ArrayBuffer itself. Will do doubling +// based reallocation, up to an optional maximum growth given. +// +// When all the data has been appended, call getArrayBuffer, +// passing in the JSContext* for which the ArrayBuffer object +// is to be created. This also implicitly resets the builder, +// or it can be reset explicitly at any point by calling reset(). +class ArrayBufferBuilder +{ + uint8_t* mDataPtr; + uint32_t mCapacity; + uint32_t mLength; + void* mMapPtr; +public: + ArrayBufferBuilder(); + ~ArrayBufferBuilder(); + + void reset(); + + // Will truncate if aNewCap is < length(). + bool setCapacity(uint32_t aNewCap); + + // Append aDataLen bytes from data to the current buffer. If we + // need to grow the buffer, grow by doubling the size up to a + // maximum of aMaxGrowth (if given). If aDataLen is greater than + // what the new capacity would end up as, then grow by aDataLen. + // + // The data parameter must not overlap with anything beyond the + // builder's current valid contents [0..length) + bool append(const uint8_t* aNewData, uint32_t aDataLen, + uint32_t aMaxGrowth = 0); + + uint32_t length() { return mLength; } + uint32_t capacity() { return mCapacity; } + + JSObject* getArrayBuffer(JSContext* aCx); + + // Memory mapping to starting position of file(aFile) in the zip + // package(aJarFile). + // + // The file in the zip package has to be uncompressed and the starting + // position of the file must be aligned according to array buffer settings + // in JS engine. + nsresult mapToFileInPackage(const nsCString& aFile, nsIFile* aJarFile); + +protected: + static bool areOverlappingRegions(const uint8_t* aStart1, uint32_t aLength1, + const uint8_t* aStart2, uint32_t aLength2); +}; + +class nsXMLHttpRequestXPCOMifier; + +class RequestHeaders +{ + struct RequestHeader + { + nsCString mName; + nsCString mValue; + }; + nsTArray<RequestHeader> mHeaders; + RequestHeader* Find(const nsACString& aName); + +public: + class CharsetIterator + { + bool mValid; + int32_t mCurPos, mCurLen, mCutoff; + nsACString& mSource; + + public: + explicit CharsetIterator(nsACString& aSource); + bool Equals(const nsACString& aOther, const nsCStringComparator& aCmp) const; + void Replace(const nsACString& aReplacement); + bool Next(); + }; + + bool Has(const char* aName); + bool Has(const nsACString& aName); + void Get(const char* aName, nsACString& aValue); + void Get(const nsACString& aName, nsACString& aValue); + void Set(const char* aName, const nsACString& aValue); + void Set(const nsACString& aName, const nsACString& aValue); + void MergeOrSet(const char* aName, const nsACString& aValue); + void MergeOrSet(const nsACString& aName, const nsACString& aValue); + void Clear(); + void ApplyToChannel(nsIHttpChannel* aChannel) const; + void GetCORSUnsafeHeaders(nsTArray<nsCString>& aArray) const; +}; + +// Make sure that any non-DOM interfaces added here are also added to +// nsXMLHttpRequestXPCOMifier. +class XMLHttpRequestMainThread final : public XMLHttpRequest, + public nsIXMLHttpRequest, + public nsIJSXMLHttpRequest, + public nsIStreamListener, + public nsIChannelEventSink, + public nsIProgressEventSink, + public nsIInterfaceRequestor, + public nsSupportsWeakReference, + public nsITimerCallback, + public nsISizeOfEventTarget, + public MutableBlobStorageCallback +{ + friend class nsXHRParseEndListener; + friend class nsXMLHttpRequestXPCOMifier; + +public: + enum class ProgressEventType : uint8_t { + loadstart, + progress, + error, + abort, + timeout, + load, + loadend, + ENUM_MAX + }; + + XMLHttpRequestMainThread(); + + void Construct(nsIPrincipal* aPrincipal, + nsIGlobalObject* aGlobalObject, + nsIURI* aBaseURI = nullptr, + nsILoadGroup* aLoadGroup = nullptr) + { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT_IF(nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface( + aGlobalObject), win->IsInnerWindow()); + mPrincipal = aPrincipal; + BindToOwner(aGlobalObject); + mBaseURI = aBaseURI; + mLoadGroup = aLoadGroup; + } + + void InitParameters(bool aAnon, bool aSystem); + + void SetParameters(bool aAnon, bool aSystem) + { + mIsAnon = aAnon || aSystem; + mIsSystem = aSystem; + } + + NS_DECL_ISUPPORTS_INHERITED + + // nsIXMLHttpRequest + NS_DECL_NSIXMLHTTPREQUEST + + NS_FORWARD_NSIXMLHTTPREQUESTEVENTTARGET(XMLHttpRequestEventTarget::) + + // nsIStreamListener + NS_DECL_NSISTREAMLISTENER + + // nsIRequestObserver + NS_DECL_NSIREQUESTOBSERVER + + // nsIChannelEventSink + NS_DECL_NSICHANNELEVENTSINK + + // nsIProgressEventSink + NS_DECL_NSIPROGRESSEVENTSINK + + // nsIInterfaceRequestor + NS_DECL_NSIINTERFACEREQUESTOR + + // nsITimerCallback + NS_DECL_NSITIMERCALLBACK + + // nsISizeOfEventTarget + virtual size_t + SizeOfEventTargetIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + NS_REALLY_FORWARD_NSIDOMEVENTTARGET(XMLHttpRequestEventTarget) + + // states + virtual uint16_t ReadyState() const override; + + // request + nsresult CreateChannel(); + nsresult InitiateFetch(nsIInputStream* aUploadStream, + int64_t aUploadLength, + nsACString& aUploadContentType); + + virtual void + Open(const nsACString& aMethod, const nsAString& aUrl, + ErrorResult& aRv) override; + + virtual void + Open(const nsACString& aMethod, const nsAString& aUrl, bool aAsync, + const nsAString& aUsername, const nsAString& aPassword, + ErrorResult& aRv) override; + + nsresult + Open(const nsACString& aMethod, + const nsACString& aUrl, + bool aAsync, + const nsAString& aUsername, + const nsAString& aPassword); + + virtual void + SetRequestHeader(const nsACString& aName, const nsACString& aValue, + ErrorResult& aRv) override + { + aRv = SetRequestHeader(aName, aValue); + } + + virtual uint32_t + Timeout() const override + { + return mTimeoutMilliseconds; + } + + virtual void + SetTimeout(uint32_t aTimeout, ErrorResult& aRv) override; + + virtual bool WithCredentials() const override; + + virtual void + SetWithCredentials(bool aWithCredentials, ErrorResult& aRv) override; + + virtual XMLHttpRequestUpload* + GetUpload(ErrorResult& aRv) override; + +private: + virtual ~XMLHttpRequestMainThread(); + + class RequestBodyBase + { + public: + virtual nsresult GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentType, + nsACString& aCharset) const + { + NS_ASSERTION(false, "RequestBodyBase should not be used directly."); + return NS_ERROR_FAILURE; + } + }; + + template<typename Type> + class RequestBody final : public RequestBodyBase + { + Type* mBody; + public: + explicit RequestBody(Type* aBody) : mBody(aBody) + { + } + nsresult GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentType, + nsACString& aCharset) const override; + }; + + nsresult SendInternal(const RequestBodyBase* aBody); + + bool IsCrossSiteCORSRequest() const; + bool IsDeniedCrossSiteCORSRequest(); + + // Tell our channel what network interface ID we were told to use. + // If it's an HTTP channel and we were told to use a non-default + // interface ID. + void PopulateNetworkInterfaceId(); + +public: + virtual void + Send(JSContext* /*aCx*/, ErrorResult& aRv) override + { + aRv = SendInternal(nullptr); + } + + virtual void + Send(JSContext* /*aCx*/, const ArrayBuffer& aArrayBuffer, + ErrorResult& aRv) override + { + RequestBody<const ArrayBuffer> body(&aArrayBuffer); + aRv = SendInternal(&body); + } + + virtual void + Send(JSContext* /*aCx*/, const ArrayBufferView& aArrayBufferView, + ErrorResult& aRv) override + { + RequestBody<const ArrayBufferView> body(&aArrayBufferView); + aRv = SendInternal(&body); + } + + virtual void + Send(JSContext* /*aCx*/, Blob& aBlob, ErrorResult& aRv) override + { + RequestBody<Blob> body(&aBlob); + aRv = SendInternal(&body); + } + + virtual void Send(JSContext* /*aCx*/, URLSearchParams& aURLSearchParams, + ErrorResult& aRv) override + { + RequestBody<URLSearchParams> body(&aURLSearchParams); + aRv = SendInternal(&body); + } + + virtual void + Send(JSContext* /*aCx*/, nsIDocument& aDoc, ErrorResult& aRv) override + { + RequestBody<nsIDocument> body(&aDoc); + aRv = SendInternal(&body); + } + + virtual void + Send(JSContext* aCx, const nsAString& aString, ErrorResult& aRv) override + { + if (DOMStringIsNull(aString)) { + Send(aCx, aRv); + } else { + RequestBody<const nsAString> body(&aString); + aRv = SendInternal(&body); + } + } + + virtual void + Send(JSContext* /*aCx*/, FormData& aFormData, ErrorResult& aRv) override + { + RequestBody<FormData> body(&aFormData); + aRv = SendInternal(&body); + } + + virtual void + Send(JSContext* aCx, nsIInputStream* aStream, ErrorResult& aRv) override + { + NS_ASSERTION(aStream, "Null should go to string version"); + RequestBody<nsIInputStream> body(aStream); + aRv = SendInternal(&body); + } + + void + Abort() { + ErrorResult rv; + Abort(rv); + MOZ_ASSERT(!rv.Failed()); + } + + virtual void + Abort(ErrorResult& aRv) override; + + // response + virtual void + GetResponseURL(nsAString& aUrl) override; + + virtual uint32_t + GetStatus(ErrorResult& aRv) override; + + virtual void + GetStatusText(nsACString& aStatusText, ErrorResult& aRv) override; + + virtual void + GetResponseHeader(const nsACString& aHeader, nsACString& aResult, + ErrorResult& aRv) override; + + void + GetResponseHeader(const nsAString& aHeader, nsAString& aResult, + ErrorResult& aRv) + { + nsAutoCString result; + GetResponseHeader(NS_ConvertUTF16toUTF8(aHeader), result, aRv); + if (result.IsVoid()) { + aResult.SetIsVoid(true); + } + else { + // The result value should be inflated: + CopyASCIItoUTF16(result, aResult); + } + } + + virtual void + GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) override; + + bool IsSafeHeader(const nsACString& aHeaderName, + NotNull<nsIHttpChannel*> aHttpChannel) const; + + virtual void + OverrideMimeType(const nsAString& aMimeType, ErrorResult& aRv) override; + + virtual XMLHttpRequestResponseType + ResponseType() const override + { + return XMLHttpRequestResponseType(mResponseType); + } + + virtual void + SetResponseType(XMLHttpRequestResponseType aType, + ErrorResult& aRv) override; + + virtual void + GetResponse(JSContext* aCx, JS::MutableHandle<JS::Value> aResponse, + ErrorResult& aRv) override; + + virtual void + GetResponseText(DOMString& aResponseText, ErrorResult& aRv) override; + + void + GetResponseText(XMLHttpRequestStringSnapshot& aSnapshot, + ErrorResult& aRv); + + virtual nsIDocument* + GetResponseXML(ErrorResult& aRv) override; + + virtual bool + MozBackgroundRequest() const override; + + virtual void + SetMozBackgroundRequest(bool aMozBackgroundRequest, ErrorResult& aRv) override; + + virtual bool + MozAnon() const override; + + virtual bool + MozSystem() const override; + + virtual nsIChannel* + GetChannel() const override + { + return mChannel; + } + + virtual void + GetNetworkInterfaceId(nsACString& aId) const override + { + aId = mNetworkInterfaceId; + } + + virtual void + SetNetworkInterfaceId(const nsACString& aId) override + { + mNetworkInterfaceId = aId; + } + + // We need a GetInterface callable from JS for chrome JS + virtual void + GetInterface(JSContext* aCx, nsIJSID* aIID, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv) override; + + // This fires a trusted readystatechange event, which is not cancelable and + // doesn't bubble. + nsresult FireReadystatechangeEvent(); + void DispatchProgressEvent(DOMEventTargetHelper* aTarget, + const ProgressEventType aType, + int64_t aLoaded, int64_t aTotal); + + // This is called by the factory constructor. + nsresult Init(); + + nsresult init(nsIPrincipal* principal, + nsPIDOMWindowInner* globalObject, + nsIURI* baseURI); + + void SetRequestObserver(nsIRequestObserver* aObserver); + + NS_DECL_CYCLE_COLLECTION_SKIPPABLE_SCRIPT_HOLDER_CLASS_INHERITED(XMLHttpRequestMainThread, + XMLHttpRequest) + bool AllowUploadProgress(); + + virtual void DisconnectFromOwner() override; + + static void SetDontWarnAboutSyncXHR(bool aVal) + { + sDontWarnAboutSyncXHR = aVal; + } + static bool DontWarnAboutSyncXHR() + { + return sDontWarnAboutSyncXHR; + } + + virtual void + SetOriginAttributes(const mozilla::dom::OriginAttributesDictionary& aAttrs) override; + + void BlobStoreCompleted(MutableBlobStorage* aBlobStorage, + Blob* aBlob, + nsresult aResult) override; + +protected: + // XHR states are meant to mirror the XHR2 spec: + // https://xhr.spec.whatwg.org/#states + enum class State : uint8_t { + unsent, // object has been constructed. + opened, // open() has been successfully invoked. + headers_received, // redirects followed and response headers received. + loading, // response body is being received. + done, // data transfer concluded, whether success or error. + }; + + nsresult DetectCharset(); + nsresult AppendToResponseText(const char * aBuffer, uint32_t aBufferLen); + static nsresult StreamReaderFunc(nsIInputStream* in, + void* closure, + const char* fromRawSegment, + uint32_t toOffset, + uint32_t count, + uint32_t *writeCount); + nsresult CreateResponseParsedJSON(JSContext* aCx); + void CreatePartialBlob(ErrorResult& aRv); + bool CreateDOMBlob(nsIRequest *request); + // Change the state of the object with this. The broadcast argument + // determines if the onreadystatechange listener should be called. + nsresult ChangeState(State aState, bool aBroadcast = true); + already_AddRefed<nsILoadGroup> GetLoadGroup() const; + nsIURI *GetBaseURI(); + + already_AddRefed<nsIHttpChannel> GetCurrentHttpChannel(); + already_AddRefed<nsIJARChannel> GetCurrentJARChannel(); + + void TruncateResponseText(); + + bool IsSystemXHR() const; + bool InUploadPhase() const; + + void OnBodyParseEnd(); + void ChangeStateToDone(); + + void StartProgressEventTimer(); + void StopProgressEventTimer(); + + void MaybeCreateBlobStorage(); + + nsresult OnRedirectVerifyCallback(nsresult result); + + already_AddRefed<nsXMLHttpRequestXPCOMifier> EnsureXPCOMifier(); + + nsCOMPtr<nsISupports> mContext; + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsIChannel> mChannel; + nsCString mRequestMethod; + nsCOMPtr<nsIURI> mRequestURL; + nsCOMPtr<nsIDocument> mResponseXML; + + nsCOMPtr<nsIStreamListener> mXMLParserStreamListener; + + // used to implement getAllResponseHeaders() + class nsHeaderVisitor : public nsIHttpHeaderVisitor + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPHEADERVISITOR + nsHeaderVisitor(const XMLHttpRequestMainThread& aXMLHttpRequest, + NotNull<nsIHttpChannel*> aHttpChannel) + : mXHR(aXMLHttpRequest), mHttpChannel(aHttpChannel) {} + const nsACString &Headers() { return mHeaders; } + private: + virtual ~nsHeaderVisitor() {} + + nsCString mHeaders; + const XMLHttpRequestMainThread& mXHR; + NotNull<nsCOMPtr<nsIHttpChannel>> mHttpChannel; + }; + + // The bytes of our response body. Only used for DEFAULT, ARRAYBUFFER and + // BLOB responseTypes + nsCString mResponseBody; + + // The text version of our response body. This is incrementally decoded into + // as we receive network data. However for the DEFAULT responseType we + // lazily decode into this from mResponseBody only when .responseText is + // accessed. + // Only used for DEFAULT and TEXT responseTypes. + XMLHttpRequestString mResponseText; + + // For DEFAULT responseType we use this to keep track of how far we've + // lazily decoded from mResponseBody to mResponseText + uint32_t mResponseBodyDecodedPos; + + // Decoder used for decoding into mResponseText + // Only used for DEFAULT, TEXT and JSON responseTypes. + // In cases where we've only received half a surrogate, the decoder itself + // carries the state to remember this. Next time we receive more data we + // simply feed the new data into the decoder which will handle the second + // part of the surrogate. + nsCOMPtr<nsIUnicodeDecoder> mDecoder; + + nsCString mResponseCharset; + + void MatchCharsetAndDecoderToResponseDocument(); + + XMLHttpRequestResponseType mResponseType; + + // It is either a cached blob-response from the last call to GetResponse, + // but is also explicitly set in OnStopRequest. + RefPtr<Blob> mResponseBlob; + // Non-null only when we are able to get a os-file representation of the + // response, i.e. when loading from a file. + RefPtr<Blob> mDOMBlob; + // We stream data to mBlobStorage when response type is "blob" and mDOMBlob is + // null. + RefPtr<MutableBlobStorage> mBlobStorage; + // We stream data to mBlobStorage when response type is "moz-blob" and + // mDOMBlob is null. + nsAutoPtr<BlobSet> mBlobSet; + + nsString mOverrideMimeType; + + /** + * The notification callbacks the channel had when Send() was + * called. We want to forward things here as needed. + */ + nsCOMPtr<nsIInterfaceRequestor> mNotificationCallbacks; + /** + * Sink interfaces that we implement that mNotificationCallbacks may + * want to also be notified for. These are inited lazily if we're + * asked for the relevant interface. + */ + nsCOMPtr<nsIChannelEventSink> mChannelEventSink; + nsCOMPtr<nsIProgressEventSink> mProgressEventSink; + + nsIRequestObserver* mRequestObserver; + + nsCOMPtr<nsIURI> mBaseURI; + nsCOMPtr<nsILoadGroup> mLoadGroup; + + State mState; + + bool mFlagSynchronous; + bool mFlagAborted; + bool mFlagParseBody; + bool mFlagSyncLooping; + bool mFlagBackgroundRequest; + bool mFlagHadUploadListenersOnSend; + bool mFlagACwithCredentials; + bool mFlagTimedOut; + bool mFlagDeleted; + + // The XHR2 spec's send() flag. Set when the XHR begins uploading, until it + // finishes downloading (or an error/abort has occurred during either phase). + // Used to guard against the user trying to alter headers/etc when it's too + // late, and ensure the XHR only handles one in-flight request at once. + bool mFlagSend; + + RefPtr<XMLHttpRequestUpload> mUpload; + int64_t mUploadTransferred; + int64_t mUploadTotal; + bool mUploadComplete; + bool mProgressSinceLastProgressEvent; + + // Timeout support + PRTime mRequestSentTime; + uint32_t mTimeoutMilliseconds; + nsCOMPtr<nsITimer> mTimeoutTimer; + void StartTimeoutTimer(); + void HandleTimeoutCallback(); + + nsCOMPtr<nsITimer> mSyncTimeoutTimer; + + enum SyncTimeoutType { + eErrorOrExpired, + eTimerStarted, + eNoTimerNeeded + }; + + SyncTimeoutType MaybeStartSyncTimeoutTimer(); + void HandleSyncTimeoutTimer(); + void CancelSyncTimeoutTimer(); + + bool mErrorLoad; + bool mErrorParsingXML; + bool mWaitingForOnStopRequest; + bool mProgressTimerIsActive; + bool mIsHtml; + bool mWarnAboutMultipartHtml; + bool mWarnAboutSyncHtml; + int64_t mLoadTotal; // -1 if not known. + // Amount of script-exposed (i.e. after undoing gzip compresion) data + // received. + uint64_t mDataAvailable; + // Number of HTTP message body bytes received so far. This quantity is + // in the same units as Content-Length and mLoadTotal, and hence counts + // compressed bytes when the channel has gzip Content-Encoding. If the + // channel does not have Content-Encoding, this will be the same as + // mDataReceived except between the OnProgress that changes mLoadTransferred + // and the corresponding OnDataAvailable (which changes mDataReceived). + // Ordering of OnProgress and OnDataAvailable is undefined. + int64_t mLoadTransferred; + nsCOMPtr<nsITimer> mProgressNotifier; + void HandleProgressTimerCallback(); + + bool mIsSystem; + bool mIsAnon; + + // A platform-specific identifer to represent the network interface + // that this request is associated with. + nsCString mNetworkInterfaceId; + + /** + * Close the XMLHttpRequest's channels. + */ + void CloseRequest(); + + /** + * Close the XMLHttpRequest's channels and dispatch appropriate progress + * events. + * + * @param aType The progress event type. + */ + void CloseRequestWithError(const ProgressEventType aType); + + bool mFirstStartRequestSeen; + bool mInLoadProgressEvent; + + nsCOMPtr<nsIAsyncVerifyRedirectCallback> mRedirectCallback; + nsCOMPtr<nsIChannel> mNewRedirectChannel; + + JS::Heap<JS::Value> mResultJSON; + + ArrayBufferBuilder mArrayBufferBuilder; + JS::Heap<JSObject*> mResultArrayBuffer; + bool mIsMappedArrayBuffer; + + void ResetResponse(); + + bool ShouldBlockAuthPrompt(); + + RequestHeaders mAuthorRequestHeaders; + + // Helper object to manage our XPCOM scriptability bits + nsXMLHttpRequestXPCOMifier* mXPCOMifier; + + static bool sDontWarnAboutSyncXHR; +}; + +class MOZ_STACK_CLASS AutoDontWarnAboutSyncXHR +{ +public: + AutoDontWarnAboutSyncXHR() : mOldVal(XMLHttpRequestMainThread::DontWarnAboutSyncXHR()) + { + XMLHttpRequestMainThread::SetDontWarnAboutSyncXHR(true); + } + + ~AutoDontWarnAboutSyncXHR() + { + XMLHttpRequestMainThread::SetDontWarnAboutSyncXHR(mOldVal); + } + +private: + bool mOldVal; +}; + +// A shim class designed to expose the non-DOM interfaces of +// XMLHttpRequest via XPCOM stuff. +class nsXMLHttpRequestXPCOMifier final : public nsIStreamListener, + public nsIChannelEventSink, + public nsIAsyncVerifyRedirectCallback, + public nsIProgressEventSink, + public nsIInterfaceRequestor, + public nsITimerCallback +{ + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXMLHttpRequestXPCOMifier, + nsIStreamListener) + + explicit nsXMLHttpRequestXPCOMifier(XMLHttpRequestMainThread* aXHR) : + mXHR(aXHR) + { + } + +private: + ~nsXMLHttpRequestXPCOMifier() { + if (mXHR) { + mXHR->mXPCOMifier = nullptr; + } + } + +public: + NS_FORWARD_NSISTREAMLISTENER(mXHR->) + NS_FORWARD_NSIREQUESTOBSERVER(mXHR->) + NS_FORWARD_NSICHANNELEVENTSINK(mXHR->) + NS_FORWARD_NSIASYNCVERIFYREDIRECTCALLBACK(mXHR->) + NS_FORWARD_NSIPROGRESSEVENTSINK(mXHR->) + NS_FORWARD_NSITIMERCALLBACK(mXHR->) + + NS_DECL_NSIINTERFACEREQUESTOR + +private: + RefPtr<XMLHttpRequestMainThread> mXHR; +}; + +class nsXHRParseEndListener : public nsIDOMEventListener +{ +public: + NS_DECL_ISUPPORTS + NS_IMETHOD HandleEvent(nsIDOMEvent *event) override + { + nsCOMPtr<nsIXMLHttpRequest> xhr = do_QueryReferent(mXHR); + if (xhr) { + static_cast<XMLHttpRequestMainThread*>(xhr.get())->OnBodyParseEnd(); + } + mXHR = nullptr; + return NS_OK; + } + explicit nsXHRParseEndListener(nsIXMLHttpRequest* aXHR) + : mXHR(do_GetWeakReference(aXHR)) {} +private: + virtual ~nsXHRParseEndListener() {} + + nsWeakPtr mXHR; +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_XMLHttpRequestMainThread_h diff --git a/dom/xhr/XMLHttpRequestString.cpp b/dom/xhr/XMLHttpRequestString.cpp new file mode 100644 index 000000000..c668b94f6 --- /dev/null +++ b/dom/xhr/XMLHttpRequestString.cpp @@ -0,0 +1,267 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestString.h" +#include "mozilla/Mutex.h" +#include "nsISupportsImpl.h" +#include "mozilla/dom/DOMString.h" + +namespace mozilla { +namespace dom { + +class XMLHttpRequestStringBuffer final +{ + friend class XMLHttpRequestStringWriterHelper; + friend class XMLHttpRequestStringSnapshotReaderHelper; + +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XMLHttpRequestStringBuffer) + NS_DECL_OWNINGTHREAD + + XMLHttpRequestStringBuffer() + : mMutex("XMLHttpRequestStringBuffer::mMutex") + { + } + + uint32_t + Length() + { + MutexAutoLock lock(mMutex); + return mData.Length(); + } + + uint32_t + UnsafeLength() const + { + return mData.Length(); + } + + void + Append(const nsAString& aString) + { + NS_ASSERT_OWNINGTHREAD(XMLHttpRequestStringBuffer); + + MutexAutoLock lock(mMutex); + mData.Append(aString); + } + + MOZ_MUST_USE bool + GetAsString(nsAString& aString) + { + MutexAutoLock lock(mMutex); + return aString.Assign(mData, mozilla::fallible); + } + + size_t + SizeOfThis(MallocSizeOf aMallocSizeOf) const + { + return mData.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + + MOZ_MUST_USE bool + GetAsString(DOMString& aString, uint32_t aLength) + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(aLength <= mData.Length()); + nsStringBuffer* buf = nsStringBuffer::FromString(mData); + if (buf) { + // We have to use SetEphemeralStringBuffer, because once we release our + // mutex mData can get mutated from some other thread while the DOMString + // is still alive. + aString.SetEphemeralStringBuffer(buf, aLength); + return true; + } + + // We can get here if mData is empty. In that case it won't have an + // nsStringBuffer.... + MOZ_ASSERT(mData.IsEmpty()); + return aString.AsAString().Assign(mData.BeginReading(), aLength, + mozilla::fallible); + } + + void + CreateSnapshot(XMLHttpRequestStringSnapshot& aSnapshot) + { + MutexAutoLock lock(mMutex); + aSnapshot.Set(this, mData.Length()); + } + +private: + ~XMLHttpRequestStringBuffer() + {} + + nsString& UnsafeData() + { + return mData; + } + + Mutex mMutex; + + // The following member variable is protected by mutex. + nsString mData; +}; + +// --------------------------------------------------------------------------- +// XMLHttpRequestString + +XMLHttpRequestString::XMLHttpRequestString() + : mBuffer(new XMLHttpRequestStringBuffer()) +{ +} + +XMLHttpRequestString::~XMLHttpRequestString() +{ +} + +void +XMLHttpRequestString::Truncate() +{ + mBuffer = new XMLHttpRequestStringBuffer(); +} + +uint32_t +XMLHttpRequestString::Length() const +{ + return mBuffer->Length(); +} + +void +XMLHttpRequestString::Append(const nsAString& aString) +{ + mBuffer->Append(aString); +} + +bool +XMLHttpRequestString::GetAsString(nsAString& aString) const +{ + return mBuffer->GetAsString(aString); +} + +size_t +XMLHttpRequestString::SizeOfThis(MallocSizeOf aMallocSizeOf) const +{ + return mBuffer->SizeOfThis(aMallocSizeOf); +} + +bool +XMLHttpRequestString::IsEmpty() const +{ + return !mBuffer->Length(); +} + +void +XMLHttpRequestString::CreateSnapshot(XMLHttpRequestStringSnapshot& aSnapshot) +{ + mBuffer->CreateSnapshot(aSnapshot); +} + +// --------------------------------------------------------------------------- +// XMLHttpRequestStringSnapshot + +XMLHttpRequestStringSnapshot::XMLHttpRequestStringSnapshot() + : mLength(0) + , mVoid(false) +{ +} + +XMLHttpRequestStringSnapshot::~XMLHttpRequestStringSnapshot() +{ +} + +XMLHttpRequestStringSnapshot& +XMLHttpRequestStringSnapshot::operator=(const XMLHttpRequestStringSnapshot& aOther) +{ + mBuffer = aOther.mBuffer; + mLength = aOther.mLength; + mVoid = aOther.mVoid; + return *this; +} + +void +XMLHttpRequestStringSnapshot::ResetInternal(bool aIsVoid) +{ + mBuffer = nullptr; + mLength = 0; + mVoid = aIsVoid; +} + +void +XMLHttpRequestStringSnapshot::Set(XMLHttpRequestStringBuffer* aBuffer, + uint32_t aLength) +{ + MOZ_ASSERT(aBuffer); + MOZ_ASSERT(aLength <= aBuffer->UnsafeLength()); + + mBuffer = aBuffer; + mLength = aLength; + mVoid = false; +} + +bool +XMLHttpRequestStringSnapshot::GetAsString(DOMString& aString) const +{ + if (mBuffer) { + MOZ_ASSERT(!mVoid); + return mBuffer->GetAsString(aString, mLength); + } + + if (mVoid) { + aString.SetNull(); + } + + return true; +} + +// --------------------------------------------------------------------------- +// XMLHttpRequestStringWriterHelper + +XMLHttpRequestStringWriterHelper::XMLHttpRequestStringWriterHelper(XMLHttpRequestString& aString) + : mBuffer(aString.mBuffer) + , mLock(aString.mBuffer->mMutex) +{ +} + +bool +XMLHttpRequestStringWriterHelper::AddCapacity(int32_t aCapacity) +{ + return mBuffer->UnsafeData().SetCapacity(mBuffer->UnsafeLength() + aCapacity, fallible); +} + +char16_t* +XMLHttpRequestStringWriterHelper::EndOfExistingData() +{ + return mBuffer->UnsafeData().BeginWriting() + mBuffer->UnsafeLength(); +} + +void +XMLHttpRequestStringWriterHelper::AddLength(int32_t aLength) +{ + mBuffer->UnsafeData().SetLength(mBuffer->UnsafeLength() + aLength); +} + +// --------------------------------------------------------------------------- +// XMLHttpRequestStringReaderHelper + +XMLHttpRequestStringSnapshotReaderHelper::XMLHttpRequestStringSnapshotReaderHelper(XMLHttpRequestStringSnapshot& aSnapshot) + : mBuffer(aSnapshot.mBuffer) + , mLock(aSnapshot.mBuffer->mMutex) +{ +} + +const char16_t* +XMLHttpRequestStringSnapshotReaderHelper::Buffer() const +{ + return mBuffer->UnsafeData().BeginReading(); +} + +uint32_t +XMLHttpRequestStringSnapshotReaderHelper::Length() const +{ + return mBuffer->UnsafeLength(); +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/xhr/XMLHttpRequestString.h b/dom/xhr/XMLHttpRequestString.h new file mode 100644 index 000000000..b6b12483f --- /dev/null +++ b/dom/xhr/XMLHttpRequestString.h @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestString_h +#define mozilla_dom_XMLHttpRequestString_h + +#include "nsString.h" + +namespace mozilla { + +class Mutex; + +namespace dom { + +class DOMString; +class XMLHttpRequestStringBuffer; +class XMLHttpRequestStringSnapshot; +class XMLHttpRequestStringWriterHelper; +class XMLHttpRequestStringSnapshotReaderHelper; + +// We want to avoid the dup of strings when XHR in workers has access to +// responseText for events dispatched during the loading state. For this reason +// we use this class, able to create snapshots of the current size of itself +// without making extra copies. +class XMLHttpRequestString final +{ + friend class XMLHttpRequestStringWriterHelper; + +public: + XMLHttpRequestString(); + ~XMLHttpRequestString(); + + void Truncate(); + + uint32_t Length() const; + + void Append(const nsAString& aString); + + // This method should be called only when the string is really needed because + // it can cause the duplication of the strings in case the loading of the XHR + // is not completed yet. + MOZ_MUST_USE bool GetAsString(nsAString& aString) const; + + size_t SizeOfThis(MallocSizeOf aMallocSizeOf) const; + + const char16_t* Data() const; + + bool IsEmpty() const; + + void CreateSnapshot(XMLHttpRequestStringSnapshot& aSnapshot); + +private: + XMLHttpRequestString(const XMLHttpRequestString&) = delete; + XMLHttpRequestString& operator=(const XMLHttpRequestString&) = delete; + XMLHttpRequestString& operator=(const XMLHttpRequestString&&) = delete; + + RefPtr<XMLHttpRequestStringBuffer> mBuffer; +}; + +// This class locks the buffer and allows the callee to write data into it. +class MOZ_STACK_CLASS XMLHttpRequestStringWriterHelper final +{ +public: + explicit XMLHttpRequestStringWriterHelper(XMLHttpRequestString& aString); + + bool + AddCapacity(int32_t aCapacity); + + char16_t* + EndOfExistingData(); + + void + AddLength(int32_t aLength); + +private: + XMLHttpRequestStringWriterHelper(const XMLHttpRequestStringWriterHelper&) = delete; + XMLHttpRequestStringWriterHelper& operator=(const XMLHttpRequestStringWriterHelper&) = delete; + XMLHttpRequestStringWriterHelper& operator=(const XMLHttpRequestStringWriterHelper&&) = delete; + + RefPtr<XMLHttpRequestStringBuffer> mBuffer; + MutexAutoLock mLock; +}; + +// This class is the internal XMLHttpRequestStringBuffer of the +// XMLHttpRequestString plus the current length. GetAsString will return the +// string with that particular length also if the XMLHttpRequestStringBuffer is +// grown in the meantime. +class XMLHttpRequestStringSnapshot final +{ + friend class XMLHttpRequestStringBuffer; + friend class XMLHttpRequestStringSnapshotReaderHelper; + +public: + XMLHttpRequestStringSnapshot(); + ~XMLHttpRequestStringSnapshot(); + + XMLHttpRequestStringSnapshot& operator=(const XMLHttpRequestStringSnapshot&); + + void Reset() + { + ResetInternal(false /* isVoid */); + } + + void SetVoid() + { + ResetInternal(true /* isVoid */); + } + + bool IsVoid() const + { + return mVoid; + } + + bool IsEmpty() const + { + return !mLength; + } + + MOZ_MUST_USE bool GetAsString(DOMString& aString) const; + +private: + XMLHttpRequestStringSnapshot(const XMLHttpRequestStringSnapshot&) = delete; + XMLHttpRequestStringSnapshot& operator=(const XMLHttpRequestStringSnapshot&&) = delete; + + void Set(XMLHttpRequestStringBuffer* aBuffer, uint32_t aLength); + + void ResetInternal(bool aIsVoid); + + RefPtr<XMLHttpRequestStringBuffer> mBuffer; + uint32_t mLength; + bool mVoid; +}; + +// This class locks the buffer and allows the callee to read data from it. +class MOZ_STACK_CLASS XMLHttpRequestStringSnapshotReaderHelper final +{ +public: + explicit XMLHttpRequestStringSnapshotReaderHelper(XMLHttpRequestStringSnapshot& aSnapshot); + + const char16_t* + Buffer() const; + + uint32_t + Length() const; + +private: + XMLHttpRequestStringSnapshotReaderHelper(const XMLHttpRequestStringSnapshotReaderHelper&) = delete; + XMLHttpRequestStringSnapshotReaderHelper& operator=(const XMLHttpRequestStringSnapshotReaderHelper&) = delete; + XMLHttpRequestStringSnapshotReaderHelper& operator=(const XMLHttpRequestStringSnapshotReaderHelper&&) = delete; + + RefPtr<XMLHttpRequestStringBuffer> mBuffer; + MutexAutoLock mLock; +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_XMLHttpRequestString_h diff --git a/dom/xhr/XMLHttpRequestUpload.cpp b/dom/xhr/XMLHttpRequestUpload.cpp new file mode 100644 index 000000000..4de85c46c --- /dev/null +++ b/dom/xhr/XMLHttpRequestUpload.cpp @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestUpload.h" +#include "mozilla/dom/XMLHttpRequestUploadBinding.h" + +namespace mozilla { +namespace dom { + +NS_INTERFACE_MAP_BEGIN(XMLHttpRequestUpload) + NS_INTERFACE_MAP_ENTRY(nsIXMLHttpRequestUpload) +NS_INTERFACE_MAP_END_INHERITING(XMLHttpRequestEventTarget) + +NS_IMPL_ADDREF_INHERITED(XMLHttpRequestUpload, XMLHttpRequestEventTarget) +NS_IMPL_RELEASE_INHERITED(XMLHttpRequestUpload, XMLHttpRequestEventTarget) + +/* virtual */ JSObject* +XMLHttpRequestUpload::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return XMLHttpRequestUploadBinding::Wrap(aCx, this, aGivenProto); +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/xhr/XMLHttpRequestUpload.h b/dom/xhr/XMLHttpRequestUpload.h new file mode 100644 index 000000000..fc83f5eac --- /dev/null +++ b/dom/xhr/XMLHttpRequestUpload.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestUpload_h +#define mozilla_dom_XMLHttpRequestUpload_h + +#include "mozilla/dom/XMLHttpRequestEventTarget.h" +#include "nsIXMLHttpRequest.h" + +namespace mozilla { +namespace dom { + +class XMLHttpRequestUpload final : public XMLHttpRequestEventTarget, + public nsIXMLHttpRequestUpload +{ +public: + explicit XMLHttpRequestUpload(DOMEventTargetHelper* aOwner) + : XMLHttpRequestEventTarget(aOwner) + {} + + explicit XMLHttpRequestUpload() + {} + + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIXMLHTTPREQUESTEVENTTARGET(XMLHttpRequestEventTarget::) + NS_REALLY_FORWARD_NSIDOMEVENTTARGET(XMLHttpRequestEventTarget) + NS_DECL_NSIXMLHTTPREQUESTUPLOAD + + virtual JSObject* + WrapObject(JSContext *cx, JS::Handle<JSObject*> aGivenProto) override; + + bool HasListeners() + { + return mListenerManager && mListenerManager->HasListeners(); + } + +private: + virtual ~XMLHttpRequestUpload() + {} +}; + + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_XMLHttpRequestUpload_h + diff --git a/dom/xhr/XMLHttpRequestWorker.cpp b/dom/xhr/XMLHttpRequestWorker.cpp new file mode 100644 index 000000000..f61383baf --- /dev/null +++ b/dom/xhr/XMLHttpRequestWorker.cpp @@ -0,0 +1,2455 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "XMLHttpRequestWorker.h" + +#include "nsIDOMEvent.h" +#include "nsIDOMEventListener.h" +#include "nsIRunnable.h" +#include "nsIXMLHttpRequest.h" +#include "nsIXPConnect.h" + +#include "jsfriendapi.h" +#include "js/TracingAPI.h" +#include "js/GCPolicyAPI.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/ProgressEvent.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/URLSearchParams.h" +#include "mozilla/Telemetry.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsJSUtils.h" +#include "nsThreadUtils.h" +#include "nsVariant.h" + +#include "RuntimeService.h" +#include "WorkerScope.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "XMLHttpRequestUpload.h" + +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace dom { + +using namespace workers; + +/* static */ void +XMLHttpRequestWorker::StateData::trace(JSTracer *aTrc) +{ + JS::TraceEdge(aTrc, &mResponse, "XMLHttpRequestWorker::StateData::mResponse"); +} + +/** + * XMLHttpRequest in workers + * + * XHR in workers is implemented by proxying calls/events/etc between the + * worker thread and an XMLHttpRequest on the main thread. The glue + * object here is the Proxy, which lives on both threads. All other objects + * live on either the main thread (the XMLHttpRequest) or the worker thread + * (the worker and XHR private objects). + * + * The main thread XHR is always operated in async mode, even for sync XHR + * in workers. Calls made on the worker thread are proxied to the main thread + * synchronously (meaning the worker thread is blocked until the call + * returns). Each proxied call spins up a sync queue, which captures any + * synchronously dispatched events and ensures that they run synchronously + * on the worker as well. Asynchronously dispatched events are posted to the + * worker thread to run asynchronously. Some of the XHR state is mirrored on + * the worker thread to avoid needing a cross-thread call on every property + * access. + * + * The XHR private is stored in the private slot of the XHR JSObject on the + * worker thread. It is destroyed when that JSObject is GCd. The private + * roots its JSObject while network activity is in progress. It also + * adds itself as a feature to the worker to give itself a chance to clean up + * if the worker goes away during an XHR call. It is important that the + * rooting and feature registration (collectively called pinning) happens at + * the proper times. If we pin for too long we can cause memory leaks or even + * shutdown hangs. If we don't pin for long enough we introduce a GC hazard. + * + * The XHR is pinned from the time Send is called to roughly the time loadend + * is received. There are some complications involved with Abort and XHR + * reuse. We maintain a counter on the main thread of how many times Send was + * called on this XHR, and we decrement the counter every time we receive a + * loadend event. When the counter reaches zero we dispatch a runnable to the + * worker thread to unpin the XHR. We only decrement the counter if the + * dispatch was successful, because the worker may no longer be accepting + * regular runnables. In the event that we reach Proxy::Teardown and there + * the outstanding Send count is still non-zero, we dispatch a control + * runnable which is guaranteed to run. + * + * NB: Some of this could probably be simplified now that we have the + * inner/outer channel ids. + */ + +class Proxy final : public nsIDOMEventListener +{ +public: + // Read on multiple threads. + WorkerPrivate* mWorkerPrivate; + XMLHttpRequestWorker* mXMLHttpRequestPrivate; + + // XHR Params: + bool mMozAnon; + bool mMozSystem; + + // Only touched on the main thread. + RefPtr<XMLHttpRequestMainThread> mXHR; + nsCOMPtr<nsIXMLHttpRequestUpload> mXHRUpload; + nsCOMPtr<nsIEventTarget> mSyncLoopTarget; + nsCOMPtr<nsIEventTarget> mSyncEventResponseTarget; + uint32_t mInnerEventStreamId; + uint32_t mInnerChannelId; + uint32_t mOutstandingSendCount; + + // Only touched on the worker thread. + uint32_t mOuterEventStreamId; + uint32_t mOuterChannelId; + uint32_t mOpenCount; + uint64_t mLastLoaded; + uint64_t mLastTotal; + uint64_t mLastUploadLoaded; + uint64_t mLastUploadTotal; + bool mIsSyncXHR; + bool mLastLengthComputable; + bool mLastUploadLengthComputable; + bool mSeenLoadStart; + bool mSeenUploadLoadStart; + + // Only touched on the main thread. + bool mUploadEventListenersAttached; + bool mMainThreadSeenLoadStart; + bool mInOpen; + bool mArrayBufferResponseWasTransferred; + +public: + Proxy(XMLHttpRequestWorker* aXHRPrivate, bool aMozAnon, bool aMozSystem) + : mWorkerPrivate(nullptr), mXMLHttpRequestPrivate(aXHRPrivate), + mMozAnon(aMozAnon), mMozSystem(aMozSystem), + mInnerEventStreamId(0), mInnerChannelId(0), mOutstandingSendCount(0), + mOuterEventStreamId(0), mOuterChannelId(0), mOpenCount(0), mLastLoaded(0), + mLastTotal(0), mLastUploadLoaded(0), mLastUploadTotal(0), mIsSyncXHR(false), + mLastLengthComputable(false), mLastUploadLengthComputable(false), + mSeenLoadStart(false), mSeenUploadLoadStart(false), + mUploadEventListenersAttached(false), mMainThreadSeenLoadStart(false), + mInOpen(false), mArrayBufferResponseWasTransferred(false) + { } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + bool + Init(); + + void + Teardown(bool aSendUnpin); + + bool + AddRemoveEventListeners(bool aUpload, bool aAdd); + + void + Reset() + { + AssertIsOnMainThread(); + + if (mUploadEventListenersAttached) { + AddRemoveEventListeners(true, false); + } + } + + already_AddRefed<nsIEventTarget> + GetEventTarget() + { + AssertIsOnMainThread(); + + nsCOMPtr<nsIEventTarget> target = mSyncEventResponseTarget ? + mSyncEventResponseTarget : + mSyncLoopTarget; + return target.forget(); + } + +private: + ~Proxy() + { + MOZ_ASSERT(!mXHR); + MOZ_ASSERT(!mXHRUpload); + MOZ_ASSERT(!mOutstandingSendCount); + } +}; + +class WorkerThreadProxySyncRunnable : public WorkerMainThreadRunnable +{ +protected: + RefPtr<Proxy> mProxy; + +private: + // mErrorCode is set on the main thread by MainThreadRun and it's used to at + // the end of the Dispatch() to return the error code. + nsresult mErrorCode; + +public: + WorkerThreadProxySyncRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy) + : WorkerMainThreadRunnable(aWorkerPrivate, NS_LITERAL_CSTRING("XHR")) + , mProxy(aProxy) + , mErrorCode(NS_OK) + { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aProxy); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + void + Dispatch(ErrorResult& aRv) + { + WorkerMainThreadRunnable::Dispatch(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (NS_FAILED(mErrorCode)) { + aRv.Throw(mErrorCode); + } + } + +protected: + virtual ~WorkerThreadProxySyncRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) = 0; + +private: + virtual bool MainThreadRun() override; +}; + +class SendRunnable final + : public WorkerThreadProxySyncRunnable + , public StructuredCloneHolder +{ + nsString mStringBody; + nsCOMPtr<nsIEventTarget> mSyncLoopTarget; + bool mHasUploadListeners; + +public: + SendRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsAString& aStringBody) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy) + , StructuredCloneHolder(CloningSupported, TransferringNotSupported, + StructuredCloneScope::SameProcessDifferentThread) + , mStringBody(aStringBody) + , mHasUploadListeners(false) + { + } + + void SetHaveUploadListeners(bool aHasUploadListeners) + { + mHasUploadListeners = aHasUploadListeners; + } + + void SetSyncLoopTarget(nsIEventTarget* aSyncLoopTarget) + { + mSyncLoopTarget = aSyncLoopTarget; + } + +private: + ~SendRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override; +}; + +namespace { + +enum +{ + STRING_abort = 0, + STRING_error, + STRING_load, + STRING_loadstart, + STRING_progress, + STRING_timeout, + STRING_readystatechange, + STRING_loadend, + + STRING_COUNT, + + STRING_LAST_XHR = STRING_loadend, + STRING_LAST_EVENTTARGET = STRING_timeout +}; + +static_assert(STRING_LAST_XHR >= STRING_LAST_EVENTTARGET, "Bad string setup!"); +static_assert(STRING_LAST_XHR == STRING_COUNT - 1, "Bad string setup!"); + +const char* const sEventStrings[] = { + // nsIXMLHttpRequestEventTarget event types, supported by both XHR and Upload. + "abort", + "error", + "load", + "loadstart", + "progress", + "timeout", + + // nsIXMLHttpRequest event types, supported only by XHR. + "readystatechange", + "loadend", +}; + +static_assert(MOZ_ARRAY_LENGTH(sEventStrings) == STRING_COUNT, + "Bad string count!"); + +class MainThreadProxyRunnable : public MainThreadWorkerSyncRunnable +{ +protected: + RefPtr<Proxy> mProxy; + + MainThreadProxyRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy) + : MainThreadWorkerSyncRunnable(aWorkerPrivate, aProxy->GetEventTarget()), + mProxy(aProxy) + { + MOZ_ASSERT(aProxy); + } + + virtual ~MainThreadProxyRunnable() + { } +}; + +class XHRUnpinRunnable final : public MainThreadWorkerControlRunnable +{ + XMLHttpRequestWorker* mXMLHttpRequestPrivate; + +public: + XHRUnpinRunnable(WorkerPrivate* aWorkerPrivate, + XMLHttpRequestWorker* aXHRPrivate) + : MainThreadWorkerControlRunnable(aWorkerPrivate), + mXMLHttpRequestPrivate(aXHRPrivate) + { + MOZ_ASSERT(aXHRPrivate); + } + +private: + ~XHRUnpinRunnable() + { } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + if (mXMLHttpRequestPrivate->SendInProgress()) { + mXMLHttpRequestPrivate->Unpin(); + } + + return true; + } +}; + +class AsyncTeardownRunnable final : public Runnable +{ + RefPtr<Proxy> mProxy; + +public: + explicit AsyncTeardownRunnable(Proxy* aProxy) + : mProxy(aProxy) + { + MOZ_ASSERT(aProxy); + } + +private: + ~AsyncTeardownRunnable() + { } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + // This means the XHR was GC'd, so we can't be pinned, and we don't need to + // try to unpin. + mProxy->Teardown(/* aSendUnpin */ false); + mProxy = nullptr; + + return NS_OK; + } +}; + +class LoadStartDetectionRunnable final : public Runnable, + public nsIDOMEventListener +{ + WorkerPrivate* mWorkerPrivate; + RefPtr<Proxy> mProxy; + RefPtr<XMLHttpRequest> mXHR; + XMLHttpRequestWorker* mXMLHttpRequestPrivate; + nsString mEventType; + uint32_t mChannelId; + bool mReceivedLoadStart; + + class ProxyCompleteRunnable final : public MainThreadProxyRunnable + { + XMLHttpRequestWorker* mXMLHttpRequestPrivate; + uint32_t mChannelId; + + public: + ProxyCompleteRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + XMLHttpRequestWorker* aXHRPrivate, uint32_t aChannelId) + : MainThreadProxyRunnable(aWorkerPrivate, aProxy), + mXMLHttpRequestPrivate(aXHRPrivate), mChannelId(aChannelId) + { } + + private: + ~ProxyCompleteRunnable() + { } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + if (mChannelId != mProxy->mOuterChannelId) { + // Threads raced, this event is now obsolete. + return true; + } + + if (mSyncLoopTarget) { + aWorkerPrivate->StopSyncLoop(mSyncLoopTarget, true); + } + + if (mXMLHttpRequestPrivate->SendInProgress()) { + mXMLHttpRequestPrivate->Unpin(); + } + + return true; + } + + nsresult + Cancel() override + { + // This must run! + nsresult rv = MainThreadProxyRunnable::Cancel(); + nsresult rv2 = Run(); + return NS_FAILED(rv) ? rv : rv2; + } + }; + +public: + LoadStartDetectionRunnable(Proxy* aProxy, XMLHttpRequestWorker* aXHRPrivate) + : mWorkerPrivate(aProxy->mWorkerPrivate), mProxy(aProxy), mXHR(aProxy->mXHR), + mXMLHttpRequestPrivate(aXHRPrivate), mChannelId(mProxy->mInnerChannelId), + mReceivedLoadStart(false) + { + AssertIsOnMainThread(); + mEventType.AssignWithConversion(sEventStrings[STRING_loadstart]); + } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSIDOMEVENTLISTENER + + bool + RegisterAndDispatch() + { + AssertIsOnMainThread(); + + if (NS_FAILED(mXHR->AddEventListener(mEventType, this, false, false, 2))) { + NS_WARNING("Failed to add event listener!"); + return false; + } + + return NS_SUCCEEDED(NS_DispatchToCurrentThread(this)); + } + +private: + ~LoadStartDetectionRunnable() + { + AssertIsOnMainThread(); + } +}; + +class EventRunnable final : public MainThreadProxyRunnable + , public StructuredCloneHolder +{ + nsString mType; + nsString mResponseType; + JS::Heap<JS::Value> mResponse; + XMLHttpRequestStringSnapshot mResponseText; + nsString mResponseURL; + nsCString mStatusText; + uint64_t mLoaded; + uint64_t mTotal; + uint32_t mEventStreamId; + uint32_t mStatus; + uint16_t mReadyState; + bool mUploadEvent; + bool mProgressEvent; + bool mLengthComputable; + bool mUseCachedArrayBufferResponse; + nsresult mResponseTextResult; + nsresult mStatusResult; + nsresult mResponseResult; + // mScopeObj is used in PreDispatch only. We init it in our constructor, and + // reset() in PreDispatch, to ensure that it's not still linked into the + // runtime once we go off-thread. + JS::PersistentRooted<JSObject*> mScopeObj; + +public: + EventRunnable(Proxy* aProxy, bool aUploadEvent, const nsString& aType, + bool aLengthComputable, uint64_t aLoaded, uint64_t aTotal, + JS::Handle<JSObject*> aScopeObj) + : MainThreadProxyRunnable(aProxy->mWorkerPrivate, aProxy), + StructuredCloneHolder(CloningSupported, TransferringNotSupported, + StructuredCloneScope::SameProcessDifferentThread), + mType(aType), mResponse(JS::UndefinedValue()), mLoaded(aLoaded), + mTotal(aTotal), mEventStreamId(aProxy->mInnerEventStreamId), mStatus(0), + mReadyState(0), mUploadEvent(aUploadEvent), mProgressEvent(true), + mLengthComputable(aLengthComputable), mUseCachedArrayBufferResponse(false), + mResponseTextResult(NS_OK), mStatusResult(NS_OK), mResponseResult(NS_OK), + mScopeObj(RootingCx(), aScopeObj) + { } + + EventRunnable(Proxy* aProxy, bool aUploadEvent, const nsString& aType, + JS::Handle<JSObject*> aScopeObj) + : MainThreadProxyRunnable(aProxy->mWorkerPrivate, aProxy), + StructuredCloneHolder(CloningSupported, TransferringNotSupported, + StructuredCloneScope::SameProcessDifferentThread), + mType(aType), mResponse(JS::UndefinedValue()), mLoaded(0), mTotal(0), + mEventStreamId(aProxy->mInnerEventStreamId), mStatus(0), mReadyState(0), + mUploadEvent(aUploadEvent), mProgressEvent(false), mLengthComputable(0), + mUseCachedArrayBufferResponse(false), mResponseTextResult(NS_OK), + mStatusResult(NS_OK), mResponseResult(NS_OK), + mScopeObj(RootingCx(), aScopeObj) + { } + +private: + ~EventRunnable() + { } + + virtual bool + PreDispatch(WorkerPrivate* /* unused */) override final; + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; +}; + +class SyncTeardownRunnable final : public WorkerThreadProxySyncRunnable +{ +public: + SyncTeardownRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy) + { } + +private: + ~SyncTeardownRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->Teardown(/* aSendUnpin */ true); + MOZ_ASSERT(!mProxy->mSyncLoopTarget); + } +}; + +class SetBackgroundRequestRunnable final : + public WorkerThreadProxySyncRunnable +{ + bool mValue; + +public: + SetBackgroundRequestRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + bool aValue) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy) + , mValue(aValue) + { } + +private: + ~SetBackgroundRequestRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->mXHR->SetMozBackgroundRequest(mValue, aRv); + } +}; + +class SetWithCredentialsRunnable final : + public WorkerThreadProxySyncRunnable +{ + bool mValue; + +public: + SetWithCredentialsRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + bool aValue) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy) + , mValue(aValue) + { } + +private: + ~SetWithCredentialsRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->mXHR->SetWithCredentials(mValue, aRv); + } +}; + +class SetResponseTypeRunnable final : public WorkerThreadProxySyncRunnable +{ + XMLHttpRequestResponseType mResponseType; + +public: + SetResponseTypeRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + XMLHttpRequestResponseType aResponseType) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mResponseType(aResponseType) + { } + + XMLHttpRequestResponseType + ResponseType() + { + return mResponseType; + } + +private: + ~SetResponseTypeRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->mXHR->SetResponseType(mResponseType, aRv); + if (!aRv.Failed()) { + mResponseType = mProxy->mXHR->ResponseType(); + } + } +}; + +class SetTimeoutRunnable final : public WorkerThreadProxySyncRunnable +{ + uint32_t mTimeout; + +public: + SetTimeoutRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + uint32_t aTimeout) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mTimeout(aTimeout) + { } + +private: + ~SetTimeoutRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->mXHR->SetTimeout(mTimeout, aRv); + } +}; + +class AbortRunnable final : public WorkerThreadProxySyncRunnable +{ +public: + AbortRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy) + { } + +private: + ~AbortRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override; +}; + +class GetAllResponseHeadersRunnable final : + public WorkerThreadProxySyncRunnable +{ + nsCString& mResponseHeaders; + +public: + GetAllResponseHeadersRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + nsCString& aResponseHeaders) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mResponseHeaders(aResponseHeaders) + { } + +private: + ~GetAllResponseHeadersRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->mXHR->GetAllResponseHeaders(mResponseHeaders, aRv); + } +}; + +class GetResponseHeaderRunnable final : public WorkerThreadProxySyncRunnable +{ + const nsCString mHeader; + nsCString& mValue; + +public: + GetResponseHeaderRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsACString& aHeader, nsCString& aValue) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mHeader(aHeader), + mValue(aValue) + { } + +private: + ~GetResponseHeaderRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->mXHR->GetResponseHeader(mHeader, mValue, aRv); + } +}; + +class OpenRunnable final : public WorkerThreadProxySyncRunnable +{ + nsCString mMethod; + nsString mURL; + Optional<nsAString> mUser; + nsString mUserStr; + Optional<nsAString> mPassword; + nsString mPasswordStr; + bool mBackgroundRequest; + bool mWithCredentials; + uint32_t mTimeout; + XMLHttpRequestResponseType mResponseType; + +public: + OpenRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsACString& aMethod, const nsAString& aURL, + const Optional<nsAString>& aUser, + const Optional<nsAString>& aPassword, + bool aBackgroundRequest, bool aWithCredentials, + uint32_t aTimeout, XMLHttpRequestResponseType aResponseType) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mMethod(aMethod), + mURL(aURL), mBackgroundRequest(aBackgroundRequest), + mWithCredentials(aWithCredentials), mTimeout(aTimeout), + mResponseType(aResponseType) + { + if (aUser.WasPassed()) { + mUserStr = aUser.Value(); + mUser = &mUserStr; + } + if (aPassword.WasPassed()) { + mPasswordStr = aPassword.Value(); + mPassword = &mPasswordStr; + } + } + +private: + ~OpenRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + WorkerPrivate* oldWorker = mProxy->mWorkerPrivate; + mProxy->mWorkerPrivate = mWorkerPrivate; + + aRv = MainThreadRunInternal(); + + mProxy->mWorkerPrivate = oldWorker; + } + + nsresult + MainThreadRunInternal(); +}; + +class SetRequestHeaderRunnable final : public WorkerThreadProxySyncRunnable +{ + nsCString mHeader; + nsCString mValue; + +public: + SetRequestHeaderRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsACString& aHeader, const nsACString& aValue) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mHeader(aHeader), + mValue(aValue) + { } + +private: + ~SetRequestHeaderRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->mXHR->SetRequestHeader(mHeader, mValue, aRv); + } +}; + +class OverrideMimeTypeRunnable final : public WorkerThreadProxySyncRunnable +{ + nsString mMimeType; + +public: + OverrideMimeTypeRunnable(WorkerPrivate* aWorkerPrivate, Proxy* aProxy, + const nsAString& aMimeType) + : WorkerThreadProxySyncRunnable(aWorkerPrivate, aProxy), + mMimeType(aMimeType) + { } + +private: + ~OverrideMimeTypeRunnable() + { } + + virtual void + RunOnMainThread(ErrorResult& aRv) override + { + mProxy->mXHR->OverrideMimeType(mMimeType, aRv); + } +}; + +class AutoUnpinXHR +{ + XMLHttpRequestWorker* mXMLHttpRequestPrivate; + +public: + explicit AutoUnpinXHR(XMLHttpRequestWorker* aXMLHttpRequestPrivate) + : mXMLHttpRequestPrivate(aXMLHttpRequestPrivate) + { + MOZ_ASSERT(aXMLHttpRequestPrivate); + } + + ~AutoUnpinXHR() + { + if (mXMLHttpRequestPrivate) { + mXMLHttpRequestPrivate->Unpin(); + } + } + + void Clear() + { + mXMLHttpRequestPrivate = nullptr; + } +}; + +} // namespace + +bool +Proxy::Init() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mWorkerPrivate); + + if (mXHR) { + return true; + } + + nsPIDOMWindowInner* ownerWindow = mWorkerPrivate->GetWindow(); + if (ownerWindow && !ownerWindow->IsCurrentInnerWindow()) { + NS_WARNING("Window has navigated, cannot create XHR here."); + return false; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(ownerWindow); + + mXHR = new XMLHttpRequestMainThread(); + mXHR->Construct(mWorkerPrivate->GetPrincipal(), global, + mWorkerPrivate->GetBaseURI(), + mWorkerPrivate->GetLoadGroup()); + + mXHR->SetParameters(mMozAnon, mMozSystem); + + ErrorResult rv; + mXHRUpload = mXHR->GetUpload(rv); + if (NS_WARN_IF(rv.Failed())) { + mXHR = nullptr; + return false; + } + + if (!AddRemoveEventListeners(false, true)) { + mXHR = nullptr; + mXHRUpload = nullptr; + return false; + } + + return true; +} + +void +Proxy::Teardown(bool aSendUnpin) +{ + AssertIsOnMainThread(); + + if (mXHR) { + Reset(); + + // NB: We are intentionally dropping events coming from xhr.abort on the + // floor. + AddRemoveEventListeners(false, false); + + ErrorResult rv; + mXHR->Abort(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + + if (mOutstandingSendCount) { + if (aSendUnpin) { + RefPtr<XHRUnpinRunnable> runnable = + new XHRUnpinRunnable(mWorkerPrivate, mXMLHttpRequestPrivate); + if (!runnable->Dispatch()) { + NS_RUNTIMEABORT("We're going to hang at shutdown anyways."); + } + } + + if (mSyncLoopTarget) { + // We have an unclosed sync loop. Fix that now. + RefPtr<MainThreadStopSyncLoopRunnable> runnable = + new MainThreadStopSyncLoopRunnable(mWorkerPrivate, + mSyncLoopTarget.forget(), + false); + if (!runnable->Dispatch()) { + NS_RUNTIMEABORT("We're going to hang at shutdown anyways."); + } + } + + mOutstandingSendCount = 0; + } + + mWorkerPrivate = nullptr; + mXHRUpload = nullptr; + mXHR = nullptr; + } + + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(!mSyncLoopTarget); +} + +bool +Proxy::AddRemoveEventListeners(bool aUpload, bool aAdd) +{ + AssertIsOnMainThread(); + + NS_ASSERTION(!aUpload || + (mUploadEventListenersAttached && !aAdd) || + (!mUploadEventListenersAttached && aAdd), + "Messed up logic for upload listeners!"); + + nsCOMPtr<nsIDOMEventTarget> target = + aUpload ? + do_QueryInterface(mXHRUpload) : + do_QueryInterface(static_cast<nsIXMLHttpRequest*>(mXHR.get())); + NS_ASSERTION(target, "This should never fail!"); + + uint32_t lastEventType = aUpload ? STRING_LAST_EVENTTARGET : STRING_LAST_XHR; + + nsAutoString eventType; + for (uint32_t index = 0; index <= lastEventType; index++) { + eventType = NS_ConvertASCIItoUTF16(sEventStrings[index]); + if (aAdd) { + if (NS_FAILED(target->AddEventListener(eventType, this, false))) { + return false; + } + } + else if (NS_FAILED(target->RemoveEventListener(eventType, this, false))) { + return false; + } + } + + if (aUpload) { + mUploadEventListenersAttached = aAdd; + } + + return true; +} + +NS_IMPL_ISUPPORTS(Proxy, nsIDOMEventListener) + +NS_IMETHODIMP +Proxy::HandleEvent(nsIDOMEvent* aEvent) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate || !mXMLHttpRequestPrivate) { + NS_ERROR("Shouldn't get here!"); + return NS_OK; + } + + nsString type; + if (NS_FAILED(aEvent->GetType(type))) { + NS_WARNING("Failed to get event type!"); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMEventTarget> target; + if (NS_FAILED(aEvent->GetTarget(getter_AddRefs(target)))) { + NS_WARNING("Failed to get target!"); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIXMLHttpRequestUpload> uploadTarget = do_QueryInterface(target); + ProgressEvent* progressEvent = aEvent->InternalDOMEvent()->AsProgressEvent(); + + if (mInOpen && type.EqualsASCII(sEventStrings[STRING_readystatechange])) { + uint16_t readyState = 0; + if (NS_SUCCEEDED(mXHR->GetReadyState(&readyState)) && + readyState == nsIXMLHttpRequest::OPENED) { + mInnerEventStreamId++; + } + } + + { + AutoSafeJSContext cx; + JSAutoRequest ar(cx); + + JS::Rooted<JS::Value> value(cx); + if (!GetOrCreateDOMReflectorNoWrap(cx, mXHR, &value)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JSObject*> scope(cx, &value.toObject()); + + RefPtr<EventRunnable> runnable; + if (progressEvent) { + runnable = new EventRunnable(this, !!uploadTarget, type, + progressEvent->LengthComputable(), + progressEvent->Loaded(), + progressEvent->Total(), + scope); + } + else { + runnable = new EventRunnable(this, !!uploadTarget, type, scope); + } + + runnable->Dispatch(); + } + + if (!uploadTarget) { + if (type.EqualsASCII(sEventStrings[STRING_loadstart])) { + mMainThreadSeenLoadStart = true; + } + else if (mMainThreadSeenLoadStart && + type.EqualsASCII(sEventStrings[STRING_loadend])) { + mMainThreadSeenLoadStart = false; + + RefPtr<LoadStartDetectionRunnable> runnable = + new LoadStartDetectionRunnable(this, mXMLHttpRequestPrivate); + if (!runnable->RegisterAndDispatch()) { + NS_WARNING("Failed to dispatch LoadStartDetectionRunnable!"); + } + } + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED(LoadStartDetectionRunnable, Runnable, + nsIDOMEventListener) + +NS_IMETHODIMP +LoadStartDetectionRunnable::Run() +{ + AssertIsOnMainThread(); + + if (NS_FAILED(mXHR->RemoveEventListener(mEventType, this, false))) { + NS_WARNING("Failed to remove event listener!"); + } + + if (!mReceivedLoadStart) { + if (mProxy->mOutstandingSendCount > 1) { + mProxy->mOutstandingSendCount--; + } else if (mProxy->mOutstandingSendCount == 1) { + mProxy->Reset(); + + RefPtr<ProxyCompleteRunnable> runnable = + new ProxyCompleteRunnable(mWorkerPrivate, mProxy, + mXMLHttpRequestPrivate, mChannelId); + if (runnable->Dispatch()) { + mProxy->mWorkerPrivate = nullptr; + mProxy->mSyncLoopTarget = nullptr; + mProxy->mOutstandingSendCount--; + } + } + } + + mProxy = nullptr; + mXHR = nullptr; + mXMLHttpRequestPrivate = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +LoadStartDetectionRunnable::HandleEvent(nsIDOMEvent* aEvent) +{ + AssertIsOnMainThread(); + +#ifdef DEBUG + { + nsString type; + if (NS_SUCCEEDED(aEvent->GetType(type))) { + MOZ_ASSERT(type == mEventType); + } + else { + NS_WARNING("Failed to get event type!"); + } + } +#endif + + mReceivedLoadStart = true; + return NS_OK; +} + +bool +EventRunnable::PreDispatch(WorkerPrivate* /* unused */) +{ + AssertIsOnMainThread(); + + AutoJSAPI jsapi; + DebugOnly<bool> ok = jsapi.Init(xpc::NativeGlobal(mScopeObj)); + MOZ_ASSERT(ok); + JSContext* cx = jsapi.cx(); + // Now keep the mScopeObj alive for the duration + JS::Rooted<JSObject*> scopeObj(cx, mScopeObj); + // And reset mScopeObj now, before we have a chance to run its destructor on + // some background thread. + mScopeObj.reset(); + + RefPtr<XMLHttpRequestMainThread>& xhr = mProxy->mXHR; + MOZ_ASSERT(xhr); + + if (NS_FAILED(xhr->GetResponseType(mResponseType))) { + MOZ_ASSERT(false, "This should never fail!"); + } + + ErrorResult rv; + xhr->GetResponseText(mResponseText, rv); + mResponseTextResult = rv.StealNSResult(); + + if (NS_SUCCEEDED(mResponseTextResult)) { + mResponseResult = mResponseTextResult; + if (mResponseText.IsVoid()) { + mResponse.setNull(); + } + } + else { + JS::Rooted<JS::Value> response(cx); + mResponseResult = xhr->GetResponse(cx, &response); + if (NS_SUCCEEDED(mResponseResult)) { + if (!response.isGCThing()) { + mResponse = response; + } else { + bool doClone = true; + JS::Rooted<JS::Value> transferable(cx); + JS::Rooted<JSObject*> obj(cx, response.isObjectOrNull() ? + response.toObjectOrNull() : nullptr); + if (obj && JS_IsArrayBufferObject(obj)) { + // Use cached response if the arraybuffer has been transfered. + if (mProxy->mArrayBufferResponseWasTransferred) { + MOZ_ASSERT(JS_IsDetachedArrayBufferObject(obj)); + mUseCachedArrayBufferResponse = true; + doClone = false; + } else { + MOZ_ASSERT(!JS_IsDetachedArrayBufferObject(obj)); + JS::AutoValueArray<1> argv(cx); + argv[0].set(response); + obj = JS_NewArrayObject(cx, argv); + if (obj) { + transferable.setObject(*obj); + // Only cache the response when the readyState is DONE. + if (xhr->ReadyState() == nsIXMLHttpRequest::DONE) { + mProxy->mArrayBufferResponseWasTransferred = true; + } + } else { + mResponseResult = NS_ERROR_OUT_OF_MEMORY; + doClone = false; + } + } + } + + if (doClone) { + Write(cx, response, transferable, JS::CloneDataPolicy(), rv); + if (NS_WARN_IF(rv.Failed())) { + NS_WARNING("Failed to clone response!"); + mResponseResult = rv.StealNSResult(); + mProxy->mArrayBufferResponseWasTransferred = false; + } + } + } + } + } + + mStatusResult = xhr->GetStatus(&mStatus); + + xhr->GetStatusText(mStatusText, rv); + MOZ_ASSERT(!rv.Failed()); + + mReadyState = xhr->ReadyState(); + + xhr->GetResponseURL(mResponseURL); + + return true; +} + +bool +EventRunnable::WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) +{ + if (mEventStreamId != mProxy->mOuterEventStreamId) { + // Threads raced, this event is now obsolete. + return true; + } + + if (!mProxy->mXMLHttpRequestPrivate) { + // Object was finalized, bail. + return true; + } + + if (mType.EqualsASCII(sEventStrings[STRING_loadstart])) { + if (mUploadEvent) { + mProxy->mSeenUploadLoadStart = true; + } + else { + mProxy->mSeenLoadStart = true; + } + } + else if (mType.EqualsASCII(sEventStrings[STRING_loadend])) { + if (mUploadEvent) { + mProxy->mSeenUploadLoadStart = false; + } + else { + if (!mProxy->mSeenLoadStart) { + // We've already dispatched premature abort events. + return true; + } + mProxy->mSeenLoadStart = false; + } + } + else if (mType.EqualsASCII(sEventStrings[STRING_abort])) { + if ((mUploadEvent && !mProxy->mSeenUploadLoadStart) || + (!mUploadEvent && !mProxy->mSeenLoadStart)) { + // We've already dispatched premature abort events. + return true; + } + } + + if (mProgressEvent) { + // Cache these for premature abort events. + if (mUploadEvent) { + mProxy->mLastUploadLengthComputable = mLengthComputable; + mProxy->mLastUploadLoaded = mLoaded; + mProxy->mLastUploadTotal = mTotal; + } + else { + mProxy->mLastLengthComputable = mLengthComputable; + mProxy->mLastLoaded = mLoaded; + mProxy->mLastTotal = mTotal; + } + } + + JS::Rooted<UniquePtr<XMLHttpRequestWorker::StateData>> state(aCx, new XMLHttpRequestWorker::StateData()); + + state->mResponseTextResult = mResponseTextResult; + + state->mResponseText = mResponseText; + + if (NS_SUCCEEDED(mResponseTextResult)) { + MOZ_ASSERT(mResponse.isUndefined() || mResponse.isNull()); + state->mResponseResult = mResponseTextResult; + state->mResponse = mResponse; + } + else { + state->mResponseResult = mResponseResult; + + if (NS_SUCCEEDED(mResponseResult)) { + if (HasData()) { + MOZ_ASSERT(mResponse.isUndefined()); + + ErrorResult rv; + JS::Rooted<JS::Value> response(aCx); + + GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper()); + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(globalObj.GetAsSupports()); + + Read(global, aCx, &response, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return false; + } + + state->mResponse = response; + } + else { + state->mResponse = mResponse; + } + } + } + + state->mStatusResult = mStatusResult; + state->mStatus = mStatus; + + state->mStatusText = mStatusText; + + state->mReadyState = mReadyState; + + state->mResponseURL = mResponseURL; + + XMLHttpRequestWorker* xhr = mProxy->mXMLHttpRequestPrivate; + xhr->UpdateState(*state.get(), mUseCachedArrayBufferResponse); + + if (mType.EqualsASCII(sEventStrings[STRING_readystatechange])) { + if (mReadyState == 4 && !mUploadEvent && !mProxy->mSeenLoadStart) { + // We've already dispatched premature abort events. + return true; + } + } + + if (mUploadEvent && !xhr->GetUploadObjectNoCreate()) { + return true; + } + + JS::Rooted<JSString*> type(aCx, + JS_NewUCStringCopyN(aCx, mType.get(), mType.Length())); + if (!type) { + return false; + } + + XMLHttpRequestEventTarget* target; + if (mUploadEvent) { + target = xhr->GetUploadObjectNoCreate(); + } + else { + target = xhr; + } + + MOZ_ASSERT(target); + + RefPtr<Event> event; + if (mProgressEvent) { + ProgressEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mLengthComputable = mLengthComputable; + init.mLoaded = mLoaded; + init.mTotal = mTotal; + + event = ProgressEvent::Constructor(target, mType, init); + } + else { + event = NS_NewDOMEvent(target, nullptr, nullptr); + + if (event) { + event->InitEvent(mType, false, false); + } + } + + if (!event) { + return false; + } + + event->SetTrusted(true); + + target->DispatchDOMEvent(nullptr, event, nullptr, nullptr); + + // After firing the event set mResponse to JSVAL_NULL for chunked response + // types. + if (StringBeginsWith(mResponseType, NS_LITERAL_STRING("moz-chunked-"))) { + xhr->NullResponseText(); + } + + return true; +} + +bool +WorkerThreadProxySyncRunnable::MainThreadRun() +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIEventTarget> tempTarget = mSyncLoopTarget; + + mProxy->mSyncEventResponseTarget.swap(tempTarget); + + ErrorResult rv; + RunOnMainThread(rv); + mErrorCode = rv.StealNSResult(); + + mProxy->mSyncEventResponseTarget.swap(tempTarget); + + return true; +} + +void +AbortRunnable::RunOnMainThread(ErrorResult& aRv) +{ + mProxy->mInnerEventStreamId++; + + WorkerPrivate* oldWorker = mProxy->mWorkerPrivate; + mProxy->mWorkerPrivate = mWorkerPrivate; + + mProxy->mXHR->Abort(aRv); + + mProxy->mWorkerPrivate = oldWorker; + + mProxy->Reset(); +} + +nsresult +OpenRunnable::MainThreadRunInternal() +{ + if (!mProxy->Init()) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsresult rv; + + if (mBackgroundRequest) { + rv = mProxy->mXHR->SetMozBackgroundRequest(mBackgroundRequest); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mWithCredentials) { + rv = mProxy->mXHR->SetWithCredentials(mWithCredentials); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mTimeout) { + rv = mProxy->mXHR->SetTimeout(mTimeout); + NS_ENSURE_SUCCESS(rv, rv); + } + + MOZ_ASSERT(!mProxy->mInOpen); + mProxy->mInOpen = true; + + ErrorResult rv2; + mProxy->mXHR->Open(mMethod, mURL, true, + mUser.WasPassed() ? mUser.Value() : NullString(), + mPassword.WasPassed() ? mPassword.Value() : NullString(), + rv2); + + MOZ_ASSERT(mProxy->mInOpen); + mProxy->mInOpen = false; + + if (rv2.Failed()) { + return rv2.StealNSResult(); + } + + mProxy->mXHR->SetResponseType(mResponseType, rv2); + if (rv2.Failed()) { + return rv2.StealNSResult(); + } + + return NS_OK; +} + +void +SendRunnable::RunOnMainThread(ErrorResult& aRv) +{ + nsCOMPtr<nsIVariant> variant; + + if (HasData()) { + AutoSafeJSContext cx; + JSAutoRequest ar(cx); + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc); + + JS::Rooted<JSObject*> globalObject(cx, JS::CurrentGlobalOrNull(cx)); + if (NS_WARN_IF(!globalObject)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr<nsIGlobalObject> parent = xpc::NativeGlobal(globalObject); + if (NS_WARN_IF(!parent)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + JS::Rooted<JS::Value> body(cx); + Read(parent, cx, &body, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aRv = xpc->JSValToVariant(cx, body, getter_AddRefs(variant)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + else { + RefPtr<nsVariant> wvariant = new nsVariant(); + + if (NS_FAILED(wvariant->SetAsAString(mStringBody))) { + MOZ_ASSERT(false, "This should never fail!"); + } + + variant = wvariant; + } + + // Send() has been already called, reset the proxy. + if (mProxy->mWorkerPrivate) { + mProxy->Reset(); + } + + mProxy->mWorkerPrivate = mWorkerPrivate; + + MOZ_ASSERT(!mProxy->mSyncLoopTarget); + mProxy->mSyncLoopTarget.swap(mSyncLoopTarget); + + if (mHasUploadListeners) { + // Send() can be called more than once before failure, + // so don't attach the upload listeners more than once. + if (!mProxy->mUploadEventListenersAttached && + !mProxy->AddRemoveEventListeners(true, true)) { + MOZ_ASSERT(false, "This should never fail!"); + } + } + + mProxy->mArrayBufferResponseWasTransferred = false; + + mProxy->mInnerChannelId++; + + aRv = mProxy->mXHR->Send(variant); + + if (!aRv.Failed()) { + mProxy->mOutstandingSendCount++; + + if (!mHasUploadListeners) { + // Send() can be called more than once before failure, + // so don't attach the upload listeners more than once. + if (!mProxy->mUploadEventListenersAttached && + !mProxy->AddRemoveEventListeners(true, true)) { + MOZ_ASSERT(false, "This should never fail!"); + } + } + } +} + +XMLHttpRequestWorker::XMLHttpRequestWorker(WorkerPrivate* aWorkerPrivate) +: mWorkerPrivate(aWorkerPrivate), + mResponseType(XMLHttpRequestResponseType::Text), mTimeout(0), + mRooted(false), mBackgroundRequest(false), mWithCredentials(false), + mCanceled(false), mMozAnon(false), mMozSystem(false) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + mozilla::HoldJSObjects(this); +} + +XMLHttpRequestWorker::~XMLHttpRequestWorker() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + ReleaseProxy(XHRIsGoingAway); + + MOZ_ASSERT(!mRooted); + + mozilla::DropJSObjects(this); +} + +NS_IMPL_ADDREF_INHERITED(XMLHttpRequestWorker, XMLHttpRequestEventTarget) +NS_IMPL_RELEASE_INHERITED(XMLHttpRequestWorker, XMLHttpRequestEventTarget) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(XMLHttpRequestWorker) +NS_INTERFACE_MAP_END_INHERITING(XMLHttpRequestEventTarget) + +NS_IMPL_CYCLE_COLLECTION_CLASS(XMLHttpRequestWorker) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XMLHttpRequestWorker, + XMLHttpRequestEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUpload) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XMLHttpRequestWorker, + XMLHttpRequestEventTarget) + tmp->ReleaseProxy(XHRIsGoingAway); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mUpload) + tmp->mStateData.mResponse.setUndefined(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(XMLHttpRequestWorker, + XMLHttpRequestEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mStateData.mResponse) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +/* static */ already_AddRefed<XMLHttpRequest> +XMLHttpRequestWorker::Construct(const GlobalObject& aGlobal, + const MozXMLHttpRequestParameters& aParams, + ErrorResult& aRv) +{ + JSContext* cx = aGlobal.Context(); + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); + MOZ_ASSERT(workerPrivate); + + Telemetry::Accumulate(Telemetry::XHR_IN_WORKER, 1); + + RefPtr<XMLHttpRequestWorker> xhr = new XMLHttpRequestWorker(workerPrivate); + + if (workerPrivate->XHRParamsAllowed()) { + if (aParams.mMozSystem) + xhr->mMozAnon = true; + else + xhr->mMozAnon = aParams.mMozAnon; + xhr->mMozSystem = aParams.mMozSystem; + } + + return xhr.forget(); +} + +void +XMLHttpRequestWorker::ReleaseProxy(ReleaseType aType) +{ + // Can't assert that we're on the worker thread here because mWorkerPrivate + // may be gone. + + if (mProxy) { + if (aType == XHRIsGoingAway) { + // We're in a GC finalizer, so we can't do a sync call here (and we don't + // need to). + RefPtr<AsyncTeardownRunnable> runnable = + new AsyncTeardownRunnable(mProxy); + mProxy = nullptr; + + if (NS_FAILED(mWorkerPrivate->DispatchToMainThread(runnable.forget()))) { + NS_ERROR("Failed to dispatch teardown runnable!"); + } + } else { + // This isn't necessary if the worker is going away or the XHR is going + // away. + if (aType == Default) { + // Don't let any more events run. + mProxy->mOuterEventStreamId++; + } + + // We need to make a sync call here. + RefPtr<SyncTeardownRunnable> runnable = + new SyncTeardownRunnable(mWorkerPrivate, mProxy); + mProxy = nullptr; + + ErrorResult forAssertionsOnly; + runnable->Dispatch(forAssertionsOnly); + if (forAssertionsOnly.Failed()) { + NS_ERROR("Failed to dispatch teardown runnable!"); + } + } + } +} + +void +XMLHttpRequestWorker::MaybePin(ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mRooted) { + return; + } + + if (!HoldWorker(mWorkerPrivate, Canceling)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + NS_ADDREF_THIS(); + + mRooted = true; +} + +void +XMLHttpRequestWorker::MaybeDispatchPrematureAbortEvents(ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mProxy); + + // Only send readystatechange event when state changed. + bool isStateChanged = false; + if (mStateData.mReadyState != 4) { + isStateChanged = true; + mStateData.mReadyState = 4; + } + + if (mProxy->mSeenUploadLoadStart) { + MOZ_ASSERT(mUpload); + + DispatchPrematureAbortEvent(mUpload, NS_LITERAL_STRING("abort"), true, + aRv); + if (aRv.Failed()) { + return; + } + + DispatchPrematureAbortEvent(mUpload, NS_LITERAL_STRING("loadend"), true, + aRv); + if (aRv.Failed()) { + return; + } + + mProxy->mSeenUploadLoadStart = false; + } + + if (mProxy->mSeenLoadStart) { + if (isStateChanged) { + DispatchPrematureAbortEvent(this, NS_LITERAL_STRING("readystatechange"), + false, aRv); + if (aRv.Failed()) { + return; + } + } + + DispatchPrematureAbortEvent(this, NS_LITERAL_STRING("abort"), false, aRv); + if (aRv.Failed()) { + return; + } + + DispatchPrematureAbortEvent(this, NS_LITERAL_STRING("loadend"), false, + aRv); + if (aRv.Failed()) { + return; + } + + mProxy->mSeenLoadStart = false; + } +} + +void +XMLHttpRequestWorker::DispatchPrematureAbortEvent(EventTarget* aTarget, + const nsAString& aEventType, + bool aUploadTarget, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aTarget); + + if (!mProxy) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<Event> event; + if (aEventType.EqualsLiteral("readystatechange")) { + event = NS_NewDOMEvent(aTarget, nullptr, nullptr); + event->InitEvent(aEventType, false, false); + } + else { + ProgressEventInit init; + init.mBubbles = false; + init.mCancelable = false; + if (aUploadTarget) { + init.mLengthComputable = mProxy->mLastUploadLengthComputable; + init.mLoaded = mProxy->mLastUploadLoaded; + init.mTotal = mProxy->mLastUploadTotal; + } + else { + init.mLengthComputable = mProxy->mLastLengthComputable; + init.mLoaded = mProxy->mLastLoaded; + init.mTotal = mProxy->mLastTotal; + } + event = ProgressEvent::Constructor(aTarget, aEventType, init); + } + + if (!event) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + event->SetTrusted(true); + + aTarget->DispatchDOMEvent(nullptr, event, nullptr, nullptr); +} + +void +XMLHttpRequestWorker::Unpin() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_ASSERT(mRooted, "Mismatched calls to Unpin!"); + + ReleaseWorker(); + + mRooted = false; + + NS_RELEASE_THIS(); +} + +void +XMLHttpRequestWorker::SendInternal(SendRunnable* aRunnable, + ErrorResult& aRv) +{ + MOZ_ASSERT(aRunnable); + mWorkerPrivate->AssertIsOnWorkerThread(); + + // No send() calls when open is running. + if (mProxy->mOpenCount) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + bool hasUploadListeners = mUpload ? mUpload->HasListeners() : false; + + MaybePin(aRv); + if (aRv.Failed()) { + return; + } + + AutoUnpinXHR autoUnpin(this); + Maybe<AutoSyncLoopHolder> autoSyncLoop; + + nsCOMPtr<nsIEventTarget> syncLoopTarget; + bool isSyncXHR = mProxy->mIsSyncXHR; + if (isSyncXHR) { + autoSyncLoop.emplace(mWorkerPrivate); + syncLoopTarget = autoSyncLoop->EventTarget(); + } + + mProxy->mOuterChannelId++; + + aRunnable->SetSyncLoopTarget(syncLoopTarget); + aRunnable->SetHaveUploadListeners(hasUploadListeners); + + aRunnable->Dispatch(aRv); + if (aRv.Failed()) { + // Dispatch() may have spun the event loop and we may have already unrooted. + // If so we don't want autoUnpin to try again. + if (!mRooted) { + autoUnpin.Clear(); + } + return; + } + + if (!isSyncXHR) { + autoUnpin.Clear(); + MOZ_ASSERT(!autoSyncLoop); + return; + } + + autoUnpin.Clear(); + + // Don't clobber an existing exception that we may have thrown on aRv + // already... though can there really be one? In any case, it seems to me + // that this autoSyncLoop->Run() can never fail, since the StopSyncLoop call + // for it will come from ProxyCompleteRunnable and that always passes true for + // the second arg. + if (!autoSyncLoop->Run() && !aRv.Failed()) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +bool +XMLHttpRequestWorker::Notify(Status aStatus) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (aStatus >= Canceling && !mCanceled) { + mCanceled = true; + ReleaseProxy(WorkerIsGoingAway); + } + + return true; +} + +void +XMLHttpRequestWorker::Open(const nsACString& aMethod, + const nsAString& aUrl, bool aAsync, + const Optional<nsAString>& aUser, + const Optional<nsAString>& aPassword, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (mProxy) { + MaybeDispatchPrematureAbortEvents(aRv); + if (aRv.Failed()) { + return; + } + } + else { + mProxy = new Proxy(this, mMozAnon, mMozSystem); + } + + mProxy->mOuterEventStreamId++; + + RefPtr<OpenRunnable> runnable = + new OpenRunnable(mWorkerPrivate, mProxy, aMethod, aUrl, aUser, aPassword, + mBackgroundRequest, mWithCredentials, + mTimeout, mResponseType); + + ++mProxy->mOpenCount; + runnable->Dispatch(aRv); + if (aRv.Failed()) { + if (mProxy && !--mProxy->mOpenCount) { + ReleaseProxy(); + } + + return; + } + + // We have been released in one of the nested Open() calls. + if (!mProxy) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + --mProxy->mOpenCount; + mProxy->mIsSyncXHR = !aAsync; +} + +void +XMLHttpRequestWorker::SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<SetRequestHeaderRunnable> runnable = + new SetRequestHeaderRunnable(mWorkerPrivate, mProxy, aHeader, aValue); + runnable->Dispatch(aRv); +} + +void +XMLHttpRequestWorker::SetTimeout(uint32_t aTimeout, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + mTimeout = aTimeout; + + if (!mProxy) { + // Open may not have been called yet, in which case we'll handle the + // timeout in OpenRunnable. + return; + } + + RefPtr<SetTimeoutRunnable> runnable = + new SetTimeoutRunnable(mWorkerPrivate, mProxy, aTimeout); + runnable->Dispatch(aRv); +} + +void +XMLHttpRequestWorker::SetWithCredentials(bool aWithCredentials, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + mWithCredentials = aWithCredentials; + + if (!mProxy) { + // Open may not have been called yet, in which case we'll handle the + // credentials in OpenRunnable. + return; + } + + RefPtr<SetWithCredentialsRunnable> runnable = + new SetWithCredentialsRunnable(mWorkerPrivate, mProxy, aWithCredentials); + runnable->Dispatch(aRv); +} + +void +XMLHttpRequestWorker::SetMozBackgroundRequest(bool aBackgroundRequest, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + mBackgroundRequest = aBackgroundRequest; + + if (!mProxy) { + // Open may not have been called yet, in which case we'll handle the + // background request in OpenRunnable. + return; + } + + RefPtr<SetBackgroundRequestRunnable> runnable = + new SetBackgroundRequestRunnable(mWorkerPrivate, mProxy, + aBackgroundRequest); + runnable->Dispatch(aRv); +} + +XMLHttpRequestUpload* +XMLHttpRequestWorker::GetUpload(ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return nullptr; + } + + if (!mUpload) { + mUpload = new XMLHttpRequestUpload(); + + if (!mUpload) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + } + + return mUpload; +} + +void +XMLHttpRequestWorker::Send(JSContext* aCx, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<SendRunnable> sendRunnable = + new SendRunnable(mWorkerPrivate, mProxy, NullString()); + + // Nothing to clone. + SendInternal(sendRunnable, aRv); +} + +void +XMLHttpRequestWorker::Send(JSContext* aCx, const nsAString& aBody, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<SendRunnable> sendRunnable = + new SendRunnable(mWorkerPrivate, mProxy, aBody); + + // Nothing to clone. + SendInternal(sendRunnable, aRv); +} + +void +XMLHttpRequestWorker::Send(JSContext* aCx, JS::Handle<JSObject*> aBody, + ErrorResult& aRv) +{ + MOZ_ASSERT(aBody); + + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + JS::Rooted<JS::Value> valToClone(aCx); + if (JS_IsArrayBufferObject(aBody) || JS_IsArrayBufferViewObject(aBody)) { + valToClone.setObject(*aBody); + } + else { + JS::Rooted<JS::Value> obj(aCx, JS::ObjectValue(*aBody)); + JSString* bodyStr = JS::ToString(aCx, obj); + if (!bodyStr) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + valToClone.setString(bodyStr); + } + + RefPtr<SendRunnable> sendRunnable = + new SendRunnable(mWorkerPrivate, mProxy, EmptyString()); + + sendRunnable->Write(aCx, valToClone, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + SendInternal(sendRunnable, aRv); +} + +void +XMLHttpRequestWorker::Send(JSContext* aCx, Blob& aBody, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, &aBody, &value)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<BlobImpl> blobImpl = aBody.Impl(); + MOZ_ASSERT(blobImpl); + + aRv = blobImpl->SetMutable(false); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + RefPtr<SendRunnable> sendRunnable = + new SendRunnable(mWorkerPrivate, mProxy, EmptyString()); + + sendRunnable->Write(aCx, value, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + SendInternal(sendRunnable, aRv); +} + +void +XMLHttpRequestWorker::Send(JSContext* aCx, FormData& aBody, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, &aBody, &value)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<SendRunnable> sendRunnable = + new SendRunnable(mWorkerPrivate, mProxy, EmptyString()); + + sendRunnable->Write(aCx, value, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + SendInternal(sendRunnable, aRv); +} + +void +XMLHttpRequestWorker::Send(JSContext* aCx, URLSearchParams& aBody, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, &aBody, &value)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<SendRunnable> sendRunnable = + new SendRunnable(mWorkerPrivate, mProxy, EmptyString()); + + sendRunnable->Write(aCx, value, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + SendInternal(sendRunnable, aRv); +} + +void +XMLHttpRequestWorker::Send(JSContext* aCx, const ArrayBuffer& aBody, + ErrorResult& aRv) +{ + JS::Rooted<JSObject*> obj(mWorkerPrivate->GetJSContext(), aBody.Obj()); + return Send(aCx, obj, aRv); +} + +void +XMLHttpRequestWorker::Send(JSContext* aCx, const ArrayBufferView& aBody, + ErrorResult& aRv) +{ + if (JS_IsTypedArrayObject(aBody.Obj()) && + JS_GetTypedArraySharedness(aBody.Obj())) { + // Throw if the object is mapping shared memory (must opt in). + aRv.ThrowTypeError<MSG_TYPEDARRAY_IS_SHARED>(NS_LITERAL_STRING("Argument of XMLHttpRequest.send")); + return; + } + JS::Rooted<JSObject*> obj(aCx, aBody.Obj()); + return Send(aCx, obj, aRv); +} + +void +XMLHttpRequestWorker::Abort(ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + return; + } + + MaybeDispatchPrematureAbortEvents(aRv); + if (aRv.Failed()) { + return; + } + + if (mStateData.mReadyState == 4) { + // No one did anything to us while we fired abort events, so reset our state + // to "unsent" + mStateData.mReadyState = 0; + } + + mProxy->mOuterEventStreamId++; + + RefPtr<AbortRunnable> runnable = new AbortRunnable(mWorkerPrivate, mProxy); + runnable->Dispatch(aRv); +} + +void +XMLHttpRequestWorker::GetResponseHeader(const nsACString& aHeader, + nsACString& aResponseHeader, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsCString responseHeader; + RefPtr<GetResponseHeaderRunnable> runnable = + new GetResponseHeaderRunnable(mWorkerPrivate, mProxy, aHeader, + responseHeader); + runnable->Dispatch(aRv); + if (aRv.Failed()) { + return; + } + aResponseHeader = responseHeader; +} + +void +XMLHttpRequestWorker::GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + if (!mProxy) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsCString responseHeaders; + RefPtr<GetAllResponseHeadersRunnable> runnable = + new GetAllResponseHeadersRunnable(mWorkerPrivate, mProxy, responseHeaders); + runnable->Dispatch(aRv); + if (aRv.Failed()) { + return; + } + + aResponseHeaders = responseHeaders; +} + +void +XMLHttpRequestWorker::OverrideMimeType(const nsAString& aMimeType, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + // We're supposed to throw if the state is not OPENED or HEADERS_RECEIVED. We + // can detect OPENED really easily but we can't detect HEADERS_RECEIVED in a + // non-racy way until the XHR state machine actually runs on this thread + // (bug 671047). For now we're going to let this work only if the Send() + // method has not been called, unless the send has been aborted. + if (!mProxy || (SendInProgress() && + (mProxy->mSeenLoadStart || + mStateData.mReadyState > nsIXMLHttpRequest::OPENED))) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<OverrideMimeTypeRunnable> runnable = + new OverrideMimeTypeRunnable(mWorkerPrivate, mProxy, aMimeType); + runnable->Dispatch(aRv); +} + +void +XMLHttpRequestWorker::SetResponseType(XMLHttpRequestResponseType aResponseType, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mCanceled) { + aRv.ThrowUncatchableException(); + return; + } + + // "document" is fine for the main thread but not for a worker. Short-circuit + // that here. + if (aResponseType == XMLHttpRequestResponseType::Document) { + return; + } + + if (!mProxy) { + // Open() has not been called yet. We store the responseType and we will use + // it later in Open(). + mResponseType = aResponseType; + return; + } + + if (SendInProgress() && + (mProxy->mSeenLoadStart || + mStateData.mReadyState > nsIXMLHttpRequest::OPENED)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<SetResponseTypeRunnable> runnable = + new SetResponseTypeRunnable(mWorkerPrivate, mProxy, aResponseType); + runnable->Dispatch(aRv); + if (aRv.Failed()) { + return; + } + + mResponseType = runnable->ResponseType(); +} + +void +XMLHttpRequestWorker::GetResponse(JSContext* /* unused */, + JS::MutableHandle<JS::Value> aResponse, + ErrorResult& aRv) +{ + if (NS_SUCCEEDED(mStateData.mResponseTextResult) && + mStateData.mResponse.isUndefined()) { + MOZ_ASSERT(NS_SUCCEEDED(mStateData.mResponseResult)); + + if (mStateData.mResponseText.IsEmpty()) { + mStateData.mResponse = + JS_GetEmptyStringValue(mWorkerPrivate->GetJSContext()); + } else { + XMLHttpRequestStringSnapshotReaderHelper helper(mStateData.mResponseText); + + JSString* str = + JS_NewUCStringCopyN(mWorkerPrivate->GetJSContext(), + helper.Buffer(), helper.Length()); + + if (!str) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + mStateData.mResponse.setString(str); + } + } + + aRv = mStateData.mResponseResult; + aResponse.set(mStateData.mResponse); +} + +void +XMLHttpRequestWorker::GetResponseText(DOMString& aResponseText, ErrorResult& aRv) +{ + aRv = mStateData.mResponseTextResult; + if (aRv.Failed()) { + return; + } + + if (!mStateData.mResponseText.GetAsString(aResponseText)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } +} + +void +XMLHttpRequestWorker::UpdateState(const StateData& aStateData, + bool aUseCachedArrayBufferResponse) +{ + if (aUseCachedArrayBufferResponse) { + MOZ_ASSERT(mStateData.mResponse.isObject() && + JS_IsArrayBufferObject(&mStateData.mResponse.toObject())); + + JS::Rooted<JS::Value> response(mWorkerPrivate->GetJSContext(), + mStateData.mResponse); + mStateData = aStateData; + mStateData.mResponse = response; + } + else { + mStateData = aStateData; + } + + XMLHttpRequestBinding::ClearCachedResponseTextValue(this); +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/xhr/XMLHttpRequestWorker.h b/dom/xhr/XMLHttpRequestWorker.h new file mode 100644 index 000000000..90232fe85 --- /dev/null +++ b/dom/xhr/XMLHttpRequestWorker.h @@ -0,0 +1,334 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_XMLHttpRequestWorker_h +#define mozilla_dom_XMLHttpRequestWorker_h + +#include "WorkerHolder.h" +#include "XMLHttpRequest.h" +#include "XMLHttpRequestString.h" +#include "mozilla/dom/TypedArray.h" + +namespace mozilla { +namespace dom { + +class Proxy; +class SendRunnable; +class DOMString; + +namespace workers { +class WorkerPrivate; +} + +class XMLHttpRequestWorker final : public XMLHttpRequest, + public workers::WorkerHolder +{ +public: + struct StateData + { + XMLHttpRequestStringSnapshot mResponseText; + nsString mResponseURL; + uint32_t mStatus; + nsCString mStatusText; + uint16_t mReadyState; + JS::Heap<JS::Value> mResponse; + nsresult mResponseTextResult; + nsresult mStatusResult; + nsresult mResponseResult; + + StateData() + : mStatus(0), mReadyState(0), mResponse(JS::UndefinedValue()), + mResponseTextResult(NS_OK), mStatusResult(NS_OK), + mResponseResult(NS_OK) + { } + + void trace(JSTracer* trc); + }; + +private: + RefPtr<XMLHttpRequestUpload> mUpload; + workers::WorkerPrivate* mWorkerPrivate; + RefPtr<Proxy> mProxy; + XMLHttpRequestResponseType mResponseType; + StateData mStateData; + + uint32_t mTimeout; + + bool mRooted; + bool mBackgroundRequest; + bool mWithCredentials; + bool mCanceled; + + bool mMozAnon; + bool mMozSystem; + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(XMLHttpRequestWorker, + XMLHttpRequest) + + + static already_AddRefed<XMLHttpRequest> + Construct(const GlobalObject& aGlobal, + const MozXMLHttpRequestParameters& aParams, + ErrorResult& aRv); + + void + Unpin(); + + bool + Notify(workers::Status aStatus) override; + + virtual uint16_t + ReadyState() const override + { + return mStateData.mReadyState; + } + + virtual void + Open(const nsACString& aMethod, const nsAString& aUrl, + ErrorResult& aRv) override + { + Open(aMethod, aUrl, true, Optional<nsAString>(), + Optional<nsAString>(), aRv); + } + + virtual void + Open(const nsACString& aMethod, const nsAString& aUrl, bool aAsync, + const nsAString& aUsername, const nsAString& aPassword, + ErrorResult& aRv) override + { + Optional<nsAString> username; + username = &aUsername; + Optional<nsAString> password; + password = &aPassword; + Open(aMethod, aUrl, aAsync, username, password, aRv); + } + + void + Open(const nsACString& aMethod, const nsAString& aUrl, + bool aAsync, const Optional<nsAString>& aUser, + const Optional<nsAString>& aPassword, ErrorResult& aRv); + + virtual void + SetRequestHeader(const nsACString& aHeader, const nsACString& aValue, + ErrorResult& aRv) override; + + virtual uint32_t + Timeout() const override + { + return mTimeout; + } + + virtual void + SetTimeout(uint32_t aTimeout, ErrorResult& aRv) override; + + virtual bool + WithCredentials() const override + { + return mWithCredentials; + } + + virtual void + SetWithCredentials(bool aWithCredentials, ErrorResult& aRv) override; + + virtual bool + MozBackgroundRequest() const override + { + return mBackgroundRequest; + } + + virtual void + SetMozBackgroundRequest(bool aBackgroundRequest, ErrorResult& aRv) override; + + virtual nsIChannel* + GetChannel() const override + { + MOZ_CRASH("This method cannot be called on workers."); + } + + virtual void + GetNetworkInterfaceId(nsACString& aId) const override + { + MOZ_CRASH("This method cannot be called on workers."); + } + + virtual void + SetNetworkInterfaceId(const nsACString& aId) override + { + MOZ_CRASH("This method cannot be called on workers."); + } + + virtual XMLHttpRequestUpload* + GetUpload(ErrorResult& aRv) override; + + virtual void + Send(JSContext* aCx, ErrorResult& aRv) override; + + virtual void + Send(JSContext* aCx, const nsAString& aBody, ErrorResult& aRv) override; + + virtual void + Send(JSContext* aCx, nsIInputStream* aStream, ErrorResult& aRv) override + { + MOZ_CRASH("This method cannot be called on workers."); + } + + virtual void + Send(JSContext* aCx, Blob& aBody, ErrorResult& aRv) override; + + virtual void + Send(JSContext* aCx, FormData& aBody, ErrorResult& aRv) override; + + virtual void + Send(JSContext* aCx, const ArrayBuffer& aBody, ErrorResult& aRv) override; + + virtual void + Send(JSContext* aCx, const ArrayBufferView& aBody, ErrorResult& aRv) override; + + virtual void + Send(JSContext* aCx, URLSearchParams& aBody, ErrorResult& aRv) override; + + virtual void + Send(JSContext* aCx, nsIDocument& aDoc, ErrorResult& aRv) override + { + MOZ_CRASH("This method cannot be called on workers."); + } + + virtual void + Abort(ErrorResult& aRv) override; + + virtual void + GetResponseURL(nsAString& aUrl) override + { + aUrl = mStateData.mResponseURL; + } + + uint32_t + GetStatus(ErrorResult& aRv) override + { + aRv = mStateData.mStatusResult; + return mStateData.mStatus; + } + + virtual void + GetStatusText(nsACString& aStatusText, ErrorResult& aRv) override + { + aStatusText = mStateData.mStatusText; + } + + virtual void + GetResponseHeader(const nsACString& aHeader, nsACString& aResponseHeader, + ErrorResult& aRv) override; + + virtual void + GetAllResponseHeaders(nsACString& aResponseHeaders, + ErrorResult& aRv) override; + + virtual void + OverrideMimeType(const nsAString& aMimeType, ErrorResult& aRv) override; + + virtual XMLHttpRequestResponseType + ResponseType() const override + { + return mResponseType; + } + + virtual void + SetResponseType(XMLHttpRequestResponseType aResponseType, + ErrorResult& aRv) override; + + virtual void + GetResponse(JSContext* /* unused */, JS::MutableHandle<JS::Value> aResponse, + ErrorResult& aRv) override; + + virtual void + GetResponseText(DOMString& aResponseText, ErrorResult& aRv) override; + + virtual nsIDocument* + GetResponseXML(ErrorResult& aRv) override + { + MOZ_CRASH("This method should not be called."); + } + + virtual void + GetInterface(JSContext* aCx, nsIJSID* aIID, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv) override + { + aRv.Throw(NS_ERROR_FAILURE); + } + + virtual void + SetOriginAttributes(const mozilla::dom::OriginAttributesDictionary& aAttrs) override + { + MOZ_CRASH("This method cannot be called on workers."); + } + + XMLHttpRequestUpload* + GetUploadObjectNoCreate() const + { + return mUpload; + } + + void + UpdateState(const StateData& aStateData, bool aUseCachedArrayBufferResponse); + + void + NullResponseText() + { + mStateData.mResponseText.SetVoid(); + mStateData.mResponse.setNull(); + } + + virtual bool MozAnon() const override + { + return mMozAnon; + } + + virtual bool MozSystem() const override + { + return mMozSystem; + } + + bool + SendInProgress() const + { + return mRooted; + } + +private: + explicit XMLHttpRequestWorker(workers::WorkerPrivate* aWorkerPrivate); + ~XMLHttpRequestWorker(); + + enum ReleaseType { Default, XHRIsGoingAway, WorkerIsGoingAway }; + + void + ReleaseProxy(ReleaseType aType = Default); + + void + MaybePin(ErrorResult& aRv); + + void + MaybeDispatchPrematureAbortEvents(ErrorResult& aRv); + + void + DispatchPrematureAbortEvent(EventTarget* aTarget, + const nsAString& aEventType, bool aUploadTarget, + ErrorResult& aRv); + + void + Send(JSContext* aCx, JS::Handle<JSObject*> aBody, ErrorResult& aRv); + + void + SendInternal(SendRunnable* aRunnable, + ErrorResult& aRv); +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_workers_xmlhttprequest_h__ diff --git a/dom/xhr/moz.build b/dom/xhr/moz.build new file mode 100644 index 000000000..4ad91deea --- /dev/null +++ b/dom/xhr/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPIDL_SOURCES += [ + 'nsIXMLHttpRequest.idl', +] + +XPIDL_MODULE = 'dom_xhr' + +EXPORTS.mozilla.dom += [ + 'XMLHttpRequest.h', + 'XMLHttpRequestEventTarget.h', + 'XMLHttpRequestMainThread.h', + 'XMLHttpRequestString.h', + 'XMLHttpRequestUpload.h', +] + +UNIFIED_SOURCES += [ + 'XMLHttpRequest.cpp', + 'XMLHttpRequestEventTarget.cpp', + 'XMLHttpRequestMainThread.cpp', + 'XMLHttpRequestString.cpp', + 'XMLHttpRequestUpload.cpp', + 'XMLHttpRequestWorker.cpp', +] + +LOCAL_INCLUDES += [ + '/dom/base', + '/dom/workers', + '/netwerk/base', +] + +MOCHITEST_MANIFESTS += [ 'tests/mochitest.ini' ] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' diff --git a/dom/xhr/nsIXMLHttpRequest.idl b/dom/xhr/nsIXMLHttpRequest.idl new file mode 100644 index 000000000..53e80bab7 --- /dev/null +++ b/dom/xhr/nsIXMLHttpRequest.idl @@ -0,0 +1,349 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsIDOMEventTarget.idl" + +interface nsIChannel; +interface nsIDOMDocument; +interface nsIDOMEventListener; +interface nsILoadGroup; +interface nsIPrincipal; +interface nsIScriptContext; +interface nsIURI; +interface nsIVariant; +interface nsIGlobalObject; +interface nsIInputStream; +interface nsIDOMBlob; + +[builtinclass, uuid(88e7d2a0-2e5b-4f65-9624-a61e607a9948)] +interface nsIXMLHttpRequestEventTarget : nsIDOMEventTarget { + // event handler attributes +}; + +[builtinclass, uuid(d74c4dc4-bc8c-4f5d-b7f1-121a48750abe)] +interface nsIXMLHttpRequestUpload : nsIXMLHttpRequestEventTarget { + // for future use +}; + +/** + * Mozilla's XMLHttpRequest is modelled after Microsoft's IXMLHttpRequest + * object. The goal has been to make Mozilla's version match Microsoft's + * version as closely as possible, but there are bound to be some differences. + * + * In general, Microsoft's documentation for IXMLHttpRequest can be used. + * Mozilla's interface definitions provide some additional documentation. The + * web page to look at is http://www.mozilla.org/xmlextras/ + * + * Mozilla's XMLHttpRequest object can be created in JavaScript like this: + * new XMLHttpRequest() + * compare to Internet Explorer: + * new ActiveXObject("Msxml2.XMLHTTP") + * + * From JavaScript, the methods and properties visible in the XMLHttpRequest + * object are a combination of nsIXMLHttpRequest and nsIJSXMLHttpRequest; + * there is no need to differentiate between those interfaces. + * + * From native code, the way to set up onload and onerror handlers is a bit + * different. Here is a comment from Johnny Stenback <jst@netscape.com>: + * + * The mozilla implementation of nsIXMLHttpRequest implements the interface + * nsIDOMEventTarget and that's how you're supported to add event listeners. + * Try something like this: + * + * nsCOMPtr<nsIDOMEventTarget> target(do_QueryInterface(myxmlhttpreq)); + * + * target->AddEventListener(NS_LITERAL_STRING("load"), mylistener, + * PR_FALSE) + * + * where mylistener is your event listener object that implements the + * interface nsIDOMEventListener. + * + * The 'onload', 'onerror', and 'onreadystatechange' attributes moved to + * nsIJSXMLHttpRequest, but if you're coding in C++ you should avoid using + * those. + * + * Conclusion: Do not use event listeners on XMLHttpRequest from C++, unless + * you're aware of all the security implications. And then think twice about + * it. + */ +[scriptable, uuid(6f54214c-7175-498d-9d2d-0429e38c2869)] +interface nsIXMLHttpRequest : nsISupports +{ + /** + * The request uses a channel in order to perform the + * request. This attribute represents the channel used + * for the request. NULL if the channel has not yet been + * created. + * + * Mozilla only. Requires elevated privileges to access. + */ + readonly attribute nsIChannel channel; + + /** + * The response to the request is parsed as if it were a + * text/xml stream. This attributes represents the response as + * a DOM Document object. NULL if the request is unsuccessful or + * has not yet been sent. + */ + readonly attribute nsIDOMDocument responseXML; + + /** + * The response to the request as text. + * NULL if the request is unsuccessful or + * has not yet been sent. + */ + readonly attribute AString responseText; + + /** + * Determine a response format which response attribute returns. + * empty string (initial value) or "text": as text. + * "arraybuffer": as a typed array ArrayBuffer. + * "blob": as a File API Blob. + * "document": as a DOM Document object. + */ + attribute AString responseType; + + /** + * The response to the request as a specified format by responseType. + * NULL if the request is unsuccessful or + * has not yet been sent. + */ + [implicit_jscontext] readonly attribute jsval /* any */ response; + + /** + * The status of the response to the request for HTTP requests. + */ + // XXX spec says unsigned short + readonly attribute unsigned long status; + + /** + * The string representing the status of the response for + * HTTP requests. + */ + readonly attribute ACString statusText; + + /** + * If the request has been sent already, this method will + * abort the request. + */ + [binaryname(SlowAbort)] void abort(); + + /** + * Returns all of the response headers as a string for HTTP + * requests. + * + * @returns A string containing all of the response headers. + * The empty string if the response has not yet been received. + */ + ACString getAllResponseHeaders(); + + /** + * Returns the text of the header with the specified name for + * HTTP requests. + * + * @param header The name of the header to retrieve + * @returns A string containing the text of the header specified. + * NULL if the response has not yet been received or the + * header does not exist in the response. + */ + ACString getResponseHeader(in ACString header); + +%{C++ + // note this is NOT virtual so this won't muck with the vtable! + inline nsresult Open(const nsACString& method, const nsACString& url, + bool async, const nsAString& user, + const nsAString& password) { + return Open(method, url, async, user, password, 3); + } +%} + /** + * Meant to be a script-only method for initializing a request. + * + * If there is an "active" request (that is, if open() has been called + * already), this is equivalent to calling abort() and then open(). + * + * @param method The HTTP method - either "POST" or "GET". Ignored + * if the URL is not a HTTP URL. + * @param url The URL to which to send the request. + * @param async (optional) Whether the request is synchronous or + * asynchronous i.e. whether send returns only after + * the response is received or if it returns immediately after + * sending the request. In the latter case, notification + * of completion is sent through the event listeners. + * The default value is true. + * @param user (optional) A username for authentication if necessary. + * The default value is the empty string + * @param password (optional) A password for authentication if necessary. + * The default value is the empty string + */ + [optional_argc] void open(in ACString method, in AUTF8String url, + [optional] in boolean async, + [optional,Undefined(Empty)] in DOMString user, + [optional,Undefined(Empty)] in DOMString password); + + /** + * Sends the request. If the request is asynchronous, returns + * immediately after sending the request. If it is synchronous + * returns only after the response has been received. + * + * All event listeners must be set before calling send(). + * + * After the initial response, all event listeners will be cleared. + * // XXXbz what does that mean, exactly? + * + * @param body Either an instance of nsIDOMDocument, nsIInputStream + * or a string (nsISupportsString in the native calling + * case). This is used to populate the body of the + * HTTP request if the HTTP request method is "POST". + * If the parameter is a nsIDOMDocument, it is serialized. + * If the parameter is a nsIInputStream, then it must be + * compatible with nsIUploadChannel.setUploadStream, and a + * Content-Length header will be added to the HTTP request + * with a value given by nsIInputStream.available. Any + * headers included at the top of the stream will be + * treated as part of the message body. The MIME type of + * the stream should be specified by setting the Content- + * Type header via the setRequestHeader method before + * calling send. + */ + void send([optional] in nsIVariant body); + + /** + * Sets a HTTP request header for HTTP requests. You must call open + * before setting the request headers. + * + * @param header The name of the header to set in the request. + * @param value The body of the header. + */ + void setRequestHeader(in ACString header, in ACString value); + + /** + * The amount of milliseconds a request can take before being terminated. + * Initially zero. Zero means there is no timeout. + */ + attribute unsigned long timeout; + + /** + * The state of the request. + * + * Possible values: + * 0 UNSENT open() has not been called yet. + * 1 OPENED send() has not been called yet. + * 2 HEADERS_RECEIVED + * send() has been called, headers and status are available. + * 3 LOADING Downloading, responseText holds the partial data. + * 4 DONE Finished with all operations. + */ + const unsigned short UNSENT = 0; + const unsigned short OPENED = 1; + const unsigned short HEADERS_RECEIVED = 2; + const unsigned short LOADING = 3; + const unsigned short DONE = 4; + readonly attribute unsigned short readyState; + + /** + * Override the mime type returned by the server (if any). This may + * be used, for example, to force a stream to be treated and parsed + * as text/xml, even if the server does not report it as such. This + * must be done before the <code>send</code> method is invoked. + * + * @param mimetype The type used to override that returned by the server + * (if any). + */ + [binaryname(SlowOverrideMimeType)] void overrideMimeType(in DOMString mimetype); + + /** + * Set to true if this is a background service request. This will + * prevent a load group being associated with the request, and + * suppress any security dialogs from being shown * to the user. + * In the cases where one of those dialogs would be shown, the request + * will simply fail instead. + */ + attribute boolean mozBackgroundRequest; + + /** + * When set to true attempts to make cross-site Access-Control requests + * with credentials such as cookies and authorization headers. + * + * Never affects same-site requests. + * + * Defaults to false. + */ + attribute boolean withCredentials; + + /** + * Initialize the object for use from C++ code with the principal, script + * context, and owner window that should be used. + * + * @param principal The principal to use for the request. This must not be + * null. + * @param globalObject The associated global for the request. Can be the + * outer window, a sandbox, or a backstage pass. + * May be null, but then the request cannot create a + * document. + * @param baseURI The base URI to use when resolving relative URIs. May be + * null. + * @param loadGroup An optional load group to use when performing the request. + * This will be used even if the global has a window with a + * load group. + */ + [noscript] void init(in nsIPrincipal principal, + in nsIGlobalObject globalObject, + in nsIURI baseURI, + [optional] in nsILoadGroup loadGroup); + + /** + * Upload process can be tracked by adding event listener to |upload|. + */ + readonly attribute nsIXMLHttpRequestUpload upload; + + /** + * Meant to be a script-only mechanism for setting a callback function. + * The attribute is expected to be JavaScript function object. When the + * readyState changes, the callback function will be called. + * This attribute should not be used from native code!! + * + * After the initial response, all event listeners will be cleared. + * // XXXbz what does that mean, exactly? + * + * Call open() before setting an onreadystatechange listener. + */ + [implicit_jscontext] attribute jsval onreadystatechange; + + /** + * If true, the request will be sent without cookie and authentication + * headers. + */ + readonly attribute boolean mozAnon; + + /** + * If true, the same origin policy will not be enforced on the request. + */ + readonly attribute boolean mozSystem; +}; + +[uuid(840d0d00-e83e-4a29-b3c7-67e96e90a499)] +interface nsIXHRSendable : nsISupports { + void getSendInfo(out nsIInputStream body, + out uint64_t contentLength, + out ACString contentType, + out ACString charset); +}; + +/** + * @deprecated + */ +[scriptable, uuid(8ae70a39-edf1-40b4-a992-472d23421c25)] +interface nsIJSXMLHttpRequest : nsISupports { +}; + +%{ C++ +#define NS_XMLHTTPREQUEST_CID \ + { /* d164e770-4157-11d4-9a42-000064657374 */ \ + 0xd164e770, 0x4157, 0x11d4, \ + {0x9a, 0x42, 0x00, 0x00, 0x64, 0x65, 0x73, 0x74} } +#define NS_XMLHTTPREQUEST_CONTRACTID \ +"@mozilla.org/xmlextras/xmlhttprequest;1" +%} diff --git a/dom/xhr/tests/browser.ini b/dom/xhr/tests/browser.ini new file mode 100644 index 000000000..6bf9a3fc8 --- /dev/null +++ b/dom/xhr/tests/browser.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = + browser_xhr_onchange_leak.html +[browser_blobFromFile.js] +[browser_xhr_onchange_leak.js]
\ No newline at end of file diff --git a/dom/xhr/tests/browser_xhr_onchange_leak.html b/dom/xhr/tests/browser_xhr_onchange_leak.html new file mode 100644 index 000000000..56eb45533 --- /dev/null +++ b/dom/xhr/tests/browser_xhr_onchange_leak.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test page for https://bugzilla.mozilla.org/show_bug.cgi?id=1336811 +--> +<head> + <meta charset="utf-8"> + <title>Test page for Bug 1336811</title> +</head> +<body onload="onload();"> +<p><span id="samplepage">sample page</span></p> +<script type="application/javascript"> + function onload() { + var request = new XMLHttpRequest; + request.open("GET", "about:blank"); + request.onreadystatechange = function() { + request.foo = request; + } + request.foo = request; + request.send(); + request.foo = request; + } +</script> +</body> +</html> diff --git a/dom/xhr/tests/browser_xhr_onchange_leak.js b/dom/xhr/tests/browser_xhr_onchange_leak.js new file mode 100644 index 000000000..f4dbb7e3e --- /dev/null +++ b/dom/xhr/tests/browser_xhr_onchange_leak.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// Bug 1336811 - An XHR that has a .onreadystatechange waiting should +// not leak forever once the tab is closed. CC optimizations need to be +// turned off once it is closed. + +add_task(function* test() { + const url = "http://mochi.test:8888/browser/dom/xhr/tests/browser_xhr_onchange_leak.html"; + let newTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let browser = gBrowser.selectedBrowser; + let done = yield ContentTask.spawn(browser,{}, function*(browser){ + let doc = content.document; + let promise = ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", true); + content.location = "about:home"; + yield promise; + return true; + }); + is(done, true, "need to check something"); + yield BrowserTestUtils.removeTab(newTab); +}); diff --git a/dom/xhr/tests/common_temporaryFileBlob.js b/dom/xhr/tests/common_temporaryFileBlob.js new file mode 100644 index 000000000..81a66238d --- /dev/null +++ b/dom/xhr/tests/common_temporaryFileBlob.js @@ -0,0 +1,100 @@ +var data = new Array(256).join("1234567890ABCDEF"); + +function createXHR() { + var xhr = new XMLHttpRequest(); + xhr.open("POST", "temporaryFileBlob.sjs"); + xhr.responseType = 'blob'; + xhr.send({toString: function() { return data; }}); + return xhr; +} + +function test_simple() { + info("Simple test"); + + var xhr = createXHR(); + + xhr.onloadend = function() { + ok(xhr.response instanceof Blob, "We have a blob!"); + is(xhr.response.size, data.length, "Data length matches"); + + var fr = new FileReader(); + fr.readAsText(xhr.response); + fr.onload = function() { + is(fr.result, data, "Data content matches"); + next(); + } + } +} + +function test_abort() { + info("Aborting during onloading"); + + var xhr = createXHR(); + + xhr.onprogress = function() { + xhr.abort(); + } + + xhr.onloadend = function() { + ok(!xhr.response, "We should not have a Blob!"); + next(); + } +} + +function test_reuse() { + info("Reuse test"); + + var xhr = createXHR(); + + var count = 0; + xhr.onloadend = function() { + ok(xhr.response instanceof Blob, "We have a blob!"); + is(xhr.response.size, data.length, "Data length matches"); + + var fr = new FileReader(); + fr.readAsText(xhr.response); + fr.onload = function() { + is(fr.result, data, "Data content matches"); + if (++count > 2) { + next(); + return; + } + + xhr.open("POST", "temporaryFileBlob.sjs"); + xhr.responseType = 'blob'; + xhr.send({toString: function() { return data; }}); + } + } +} + +function test_worker_generic(test) { + var w = new Worker('worker_temporaryFileBlob.js'); + w.onmessage = function(e) { + if (e.data.type == 'info') { + info(e.data.msg); + } else if (e.data.type == 'check') { + ok(e.data.what, e.data.msg); + } else if (e.data.type == 'finish') { + next(); + } else { + ok(false, 'Something wrong happened'); + } + } + + w.postMessage(test); +} + +function test_worker() { + info("XHR in workers"); + test_worker_generic('simple'); +} + +function test_worker_abort() { + info("XHR in workers"); + test_worker_generic('abort'); +} + +function test_worker_reuse() { + info("XHR in workers"); + test_worker_generic('reuse'); +} diff --git a/dom/xhr/tests/echo.sjs b/dom/xhr/tests/echo.sjs new file mode 100644 index 000000000..6e3242c9f --- /dev/null +++ b/dom/xhr/tests/echo.sjs @@ -0,0 +1,21 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + response.setHeader("Content-Type", "text/plain"); + if (request.method == "GET") { + response.write(request.queryString); + return; + } + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var body = ""; + var bodyAvail; + while ((bodyAvail = bodyStream.available()) > 0) + body += String.fromCharCode.apply(null, bodyStream.readByteArray(bodyAvail)); + + response.write(body); +} diff --git a/dom/xhr/tests/empty.html b/dom/xhr/tests/empty.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/xhr/tests/empty.html diff --git a/dom/xhr/tests/file_XHRDocURI.html b/dom/xhr/tests/file_XHRDocURI.html new file mode 100644 index 000000000..8dbbb0549 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>XMLHttpRequest return document URIs</title> + </head> + <body> + <div id="data">data</div> + </body> +</html> diff --git a/dom/xhr/tests/file_XHRDocURI.html^headers^ b/dom/xhr/tests/file_XHRDocURI.html^headers^ new file mode 100644 index 000000000..ddfd8c0a9 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.html^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Access-Control-Allow-Origin: * +Content-Type: text/html diff --git a/dom/xhr/tests/file_XHRDocURI.sjs b/dom/xhr/tests/file_XHRDocURI.sjs new file mode 100644 index 000000000..275b1e96e --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.sjs @@ -0,0 +1,13 @@ +function handleRequest(aRequest, aResponse) +{ + var url = aRequest.queryString.match(/\burl=([^#&]*)/); + if (!url) { + aResponse.setStatusLine(aRequest.httpVersion, 404, "Not Found"); + return; + } + + url = decodeURIComponent(url[1]); + aResponse.setStatusLine(aRequest.httpVersion, 302, "Found"); + aResponse.setHeader("Cache-Control", "no-cache", false); + aResponse.setHeader("Location", url, false); +} diff --git a/dom/xhr/tests/file_XHRDocURI.text b/dom/xhr/tests/file_XHRDocURI.text new file mode 100644 index 000000000..c1dead8d7 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.text @@ -0,0 +1 @@ +<res>data</res> diff --git a/dom/xhr/tests/file_XHRDocURI.text^headers^ b/dom/xhr/tests/file_XHRDocURI.text^headers^ new file mode 100644 index 000000000..b2fe7cdeb --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.text^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Access-Control-Allow-Origin: * +Content-Type: text/plain diff --git a/dom/xhr/tests/file_XHRDocURI.xml b/dom/xhr/tests/file_XHRDocURI.xml new file mode 100644 index 000000000..c1a9a5256 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.xml @@ -0,0 +1 @@ +<res xml:base="http://www.example.com/">data</res> diff --git a/dom/xhr/tests/file_XHRDocURI.xml^headers^ b/dom/xhr/tests/file_XHRDocURI.xml^headers^ new file mode 100644 index 000000000..f47804337 --- /dev/null +++ b/dom/xhr/tests/file_XHRDocURI.xml^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Access-Control-Allow-Origin: * +Content-Type: application/xml diff --git a/dom/xhr/tests/file_XHRResponseURL.js b/dom/xhr/tests/file_XHRResponseURL.js new file mode 100644 index 000000000..7c48201b2 --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL.js @@ -0,0 +1,303 @@ +"use strict"; + +// utility functions for worker/window communication + +function isInWorker() { + try { + return !(self instanceof Window); + } catch (e) { + return true; + } +} + +function message(aData) { + if (isInWorker()) { + self.postMessage(aData); + } else { + self.postMessage(aData, "*"); + } +} +message.ping = 0; +message.pong = 0; + +function is(aActual, aExpected, aMessage) { + var obj = { + type: "is", + actual: aActual, + expected: aExpected, + message: aMessage + }; + ++message.ping; + message(obj); +} + +function ok(aBool, aMessage) { + var obj = { + type: "ok", + bool: aBool, + message: aMessage + }; + ++message.ping; + message(obj); +} + +function info(aMessage) { + var obj = { + type: "info", + message: aMessage + }; + ++message.ping; + message(obj); +} + +function request(aURL) { + return new Promise(function (aResolve, aReject) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", aURL); + xhr.addEventListener("load", function () { + xhr.succeeded = true; + aResolve(xhr); + }); + xhr.addEventListener("error", function () { + xhr.succeeded = false; + aResolve(xhr); + }); + xhr.send(); + }); +} + +function createSequentialRequest(aParameters, aTest) { + var sequence = aParameters.reduce(function (aPromise, aParam) { + return aPromise.then(function () { + return request(aParam.requestURL); + }).then(function (aXHR) { + return aTest(aXHR, aParam); + }); + }, Promise.resolve()); + + return sequence; +} + +function testSuccessResponse() { + var blob = new Blob(["data"], {type: "text/plain"}); + var blobURL = URL.createObjectURL(blob); + + var parameters = [ + // tests that start with same-origin request + { + message: "request to same-origin without redirect", + requestURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + }, + { + message: "request to same-origin redirect to same-origin URL", + requestURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + }, + { + message: "request to same-origin redirects several times and finally go to same-origin URL", + requestURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text"), + responseURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + }, + { + message: "request to same-origin redirect to cross-origin URL", + requestURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to same-origin redirects several times and finally go to cross-origin URL", + requestURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text"), + responseURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + + // tests that start with cross-origin request + { + message: "request to cross-origin without redirect", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text" + }, + { + message: "request to cross-origin redirect back to same-origin URL", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + }, + { + message: "request to cross-origin redirect to the same cross-origin URL", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to cross-origin redirect to another cross-origin URL", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.text", + responseURL: "http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to cross-origin redirects several times and finally go to same-origin URL", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text"), + responseURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to cross-origin redirects several times and finally go to the same cross-origin URL", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text"), + responseURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to cross-origin redirects several times and finally go to another cross-origin URL", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.text"), + responseURL: "http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request to cross-origin redirects to another cross-origin and finally go to the other cross-origin URL", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://example.org/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://test1.example.com/tests/dom/xhr/tests/file_XHRResponseURL.text"), + responseURL: "http://test1.example.com/tests/dom/xhr/tests/file_XHRResponseURL.text", + }, + { + message: "request URL has fragment", + requestURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text#fragment", + responseURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text" + }, + + // tests for non-http(s) URL + { + message: "request to data: URL", + requestURL: "data:text/plain,data", + responseURL: "data:text/plain,data" + }, + { + message: "request to blob: URL", + requestURL: blobURL, + responseURL: blobURL + } + ]; + + var sequence = createSequentialRequest(parameters, function (aXHR, aParam) { + ok(aXHR.succeeded, "assert request succeeded"); + is(aXHR.responseURL, aParam.responseURL, aParam.message); + }); + + sequence.then(function () { + URL.revokeObjectURL(blobURL); + }); + + return sequence; +} + +function testFailedResponse() { + info("test not to leak responseURL for denied cross-origin request"); + var parameters = [ + { + message: "should be empty for denied cross-origin request without redirect", + requestURL: "http://example.com/tests/dom/xhr/tests/file_XHRResponseURL_nocors.text" + }, + { + message: "should be empty for denied cross-origin request with redirect", + requestURL: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRResponseURL_nocors.text" + } + ]; + + var sequence = createSequentialRequest(parameters, function (aXHR, aParam) { + ok(!aXHR.succeeded, "assert request failed"); + is(aXHR.responseURL, "" , aParam.message); + }); + + return sequence; +} + +function testNotToLeakResponseURLWhileDoingRedirects() { + info("test not to leak responeseURL while doing redirects"); + + if (isInWorker()) { + return testNotToLeakResponseURLWhileDoingRedirectsInWorker(); + } else { + return testNotToLeakResponseURLWhileDoingRedirectsInWindow(); + } +} + +function testNotToLeakResponseURLWhileDoingRedirectsInWindow() { + var xhr = new XMLHttpRequest(); + var requestObserver = { + observe: function (aSubject, aTopic, aData) { + is(xhr.readyState, XMLHttpRequest.OPENED, "assert for XHR state"); + is(xhr.responseURL, "", + "responseURL should return empty string before HEADERS_RECEIVED"); + } + }; + SpecialPowers.addObserver(requestObserver, "specialpowers-http-notify-request", false); + + return new Promise(function (aResolve, aReject) { + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text"); + xhr.addEventListener("load", function () { + SpecialPowers.removeObserver(requestObserver, "specialpowers-http-notify-request"); + aResolve(); + }); + xhr.addEventListener("error", function () { + ok(false, "unexpected request falilure"); + SpecialPowers.removeObserver(requestObserver, "specialpowers-http-notify-request"); + aResolve(); + }); + xhr.send(); + }); +} + +function testNotToLeakResponseURLWhileDoingRedirectsInWorker() { + var xhr = new XMLHttpRequest(); + var testRedirect = function (e) { + if (e.data === "request" && xhr.readyState === XMLHttpRequest.OPENED) { + is(xhr.responseURL, "", + "responseURL should return empty string before HEADERS_RECEIVED"); + } + }; + + return new Promise(function (aResolve, aReject) { + self.addEventListener("message", testRedirect); + message({type: "redirect_test", status: "start"}); + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/xhr/tests/file_XHRResponseURL.text"); + xhr.addEventListener("load", function () { + self.removeEventListener("message", testRedirect); + message({type: "redirect_test", status: "end"}); + aResolve(); + }); + xhr.addEventListener("error", function (e) { + ok(false, "unexpected request falilure"); + self.removeEventListener("message", testRedirect); + message({type: "redirect_test", status: "end"}); + aResolve(); + }); + xhr.send(); + }); +} + +function waitForAllMessagesProcessed() { + return new Promise(function (aResolve, aReject) { + var id = setInterval(function () { + if (message.ping === message.pong) { + clearInterval(id); + aResolve(); + } + }, 100); + }); +} + + +self.addEventListener("message", function (aEvent) { + if (aEvent.data === "start") { + ok("responseURL" in (new XMLHttpRequest()), + "XMLHttpRequest should have responseURL attribute"); + is((new XMLHttpRequest()).responseURL, "", + "responseURL should be empty string if response's url is null"); + + var promise = testSuccessResponse(); + promise.then(function () { + return testFailedResponse(); + }).then(function () { + return testNotToLeakResponseURLWhileDoingRedirects(); + }).then(function () { + return waitForAllMessagesProcessed(); + }).then(function () { + message("done"); + }); + } + if (aEvent.data === "pong") { + ++message.pong; + } +}); diff --git a/dom/xhr/tests/file_XHRResponseURL.sjs b/dom/xhr/tests/file_XHRResponseURL.sjs new file mode 100644 index 000000000..5ac0a346c --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL.sjs @@ -0,0 +1,14 @@ +function handleRequest(aRequest, aResponse) +{ + var url = aRequest.queryString.match(/\burl=([^#&]*)/); + if (!url) { + aResponse.setStatusLine(aRequest.httpVersion, 404, "Not Found"); + return; + } + url = decodeURIComponent(url[1]); + + aResponse.setStatusLine(aRequest.httpVersion, 302, "Found"); + aResponse.setHeader("Access-Control-Allow-Origin", "*", false); + aResponse.setHeader("Cache-Control", "no-cache", false); + aResponse.setHeader("Location", url, false); +} diff --git a/dom/xhr/tests/file_XHRResponseURL.text b/dom/xhr/tests/file_XHRResponseURL.text new file mode 100644 index 000000000..1269488f7 --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL.text @@ -0,0 +1 @@ +data diff --git a/dom/xhr/tests/file_XHRResponseURL.text^headers^ b/dom/xhr/tests/file_XHRResponseURL.text^headers^ new file mode 100644 index 000000000..b2fe7cdeb --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL.text^headers^ @@ -0,0 +1,3 @@ +HTTP 200 OK +Access-Control-Allow-Origin: * +Content-Type: text/plain diff --git a/dom/xhr/tests/file_XHRResponseURL_nocors.text b/dom/xhr/tests/file_XHRResponseURL_nocors.text new file mode 100644 index 000000000..1269488f7 --- /dev/null +++ b/dom/xhr/tests/file_XHRResponseURL_nocors.text @@ -0,0 +1 @@ +data diff --git a/dom/xhr/tests/file_XHRSendData.sjs b/dom/xhr/tests/file_XHRSendData.sjs new file mode 100644 index 000000000..0ccbe8a45 --- /dev/null +++ b/dom/xhr/tests/file_XHRSendData.sjs @@ -0,0 +1,30 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + if (request.hasHeader("Content-Type")) + response.setHeader("Result-Content-Type", + request.getHeader("Content-Type")); + + response.setHeader("Content-Type", "text/plain; charset=ISO-8859-1"); + + var body = new BinaryInputStream(request.bodyInputStream); + var avail; + var bytes = []; + while ((avail = body.available()) > 0) + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + + var data = String.fromCharCode.apply(null, bytes); + response.setHeader("Result-Content-Length", "" + data.length); + if (data.indexOf("TEST_REDIRECT_STR") >= 0) { + var newURL = "http://" + data.split("&url=")[1]; + response.setStatusLine(null, 307, "redirect"); + response.setHeader("Location", newURL, false); + } + else { + response.write(data); + } +} diff --git a/dom/xhr/tests/file_XHRSendData_doc.xml b/dom/xhr/tests/file_XHRSendData_doc.xml new file mode 100644 index 000000000..2e385258c --- /dev/null +++ b/dom/xhr/tests/file_XHRSendData_doc.xml @@ -0,0 +1,2 @@ +<!-- comment --> +<out>hi</out> diff --git a/dom/xhr/tests/file_XHRSendData_doc.xml^headers^ b/dom/xhr/tests/file_XHRSendData_doc.xml^headers^ new file mode 100644 index 000000000..b0f1ebfbf --- /dev/null +++ b/dom/xhr/tests/file_XHRSendData_doc.xml^headers^ @@ -0,0 +1 @@ +Content-Type: application/xml; charset=ISO-8859-1 diff --git a/dom/xhr/tests/file_XHR_anon.sjs b/dom/xhr/tests/file_XHR_anon.sjs new file mode 100644 index 000000000..755c4cdf3 --- /dev/null +++ b/dom/xhr/tests/file_XHR_anon.sjs @@ -0,0 +1,24 @@ +function handleRequest(request, response) { + let invalidHeaders = ["Cookie"]; + let headers = {}; + + if (request.queryString == "expectAuth=true") { + if (request.hasHeader("Authorization")) { + headers["authorization"] = request.getHeader("Authorization"); + } else { + response.setStatusLine(null, 401, "Authentication required"); + response.setHeader("WWW-Authenticate", "basic realm=\"testrealm\"", true); + } + } else { + invalidHeaders.push("Authorization"); + } + + for (let header of invalidHeaders) { + if (request.hasHeader(header)) { + response.setStatusLine(null, 500, "Server Error"); + headers[header.toLowerCase()] = request.getHeader(header); + } + } + + response.write(JSON.stringify(headers)); +} diff --git a/dom/xhr/tests/file_XHR_binary1.bin b/dom/xhr/tests/file_XHR_binary1.bin Binary files differnew file mode 100644 index 000000000..39e527bfc --- /dev/null +++ b/dom/xhr/tests/file_XHR_binary1.bin diff --git a/dom/xhr/tests/file_XHR_binary1.bin^headers^ b/dom/xhr/tests/file_XHR_binary1.bin^headers^ new file mode 100644 index 000000000..8e8c8d859 --- /dev/null +++ b/dom/xhr/tests/file_XHR_binary1.bin^headers^ @@ -0,0 +1 @@ +Content-Type: application/binary diff --git a/dom/xhr/tests/file_XHR_binary2.bin b/dom/xhr/tests/file_XHR_binary2.bin Binary files differnew file mode 100644 index 000000000..9f442b092 --- /dev/null +++ b/dom/xhr/tests/file_XHR_binary2.bin diff --git a/dom/xhr/tests/file_XHR_fail1.txt b/dom/xhr/tests/file_XHR_fail1.txt new file mode 100644 index 000000000..462209d8d --- /dev/null +++ b/dom/xhr/tests/file_XHR_fail1.txt @@ -0,0 +1 @@ +redirect file diff --git a/dom/xhr/tests/file_XHR_fail1.txt^headers^ b/dom/xhr/tests/file_XHR_fail1.txt^headers^ new file mode 100644 index 000000000..41c235932 --- /dev/null +++ b/dom/xhr/tests/file_XHR_fail1.txt^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: http://example.com/tests/dom/xhr/tests/file_XHR_pass2.txt diff --git a/dom/xhr/tests/file_XHR_fail1b.txt b/dom/xhr/tests/file_XHR_fail1b.txt new file mode 100644 index 000000000..8944657af --- /dev/null +++ b/dom/xhr/tests/file_XHR_fail1b.txt @@ -0,0 +1 @@ +hello pass
diff --git a/dom/xhr/tests/file_XHR_header.sjs b/dom/xhr/tests/file_XHR_header.sjs new file mode 100644 index 000000000..937e90a02 --- /dev/null +++ b/dom/xhr/tests/file_XHR_header.sjs @@ -0,0 +1,6 @@ +// SJS file for getAllResponseRequests vs getResponseRequest
+function handleRequest(request, response)
+{
+ response.setHeader("X-Custom-Header-Bytes", "…", false);
+ response.write("42");
+}
diff --git a/dom/xhr/tests/file_XHR_pass1.xml b/dom/xhr/tests/file_XHR_pass1.xml new file mode 100644 index 000000000..06826d6c6 --- /dev/null +++ b/dom/xhr/tests/file_XHR_pass1.xml @@ -0,0 +1 @@ +<res>hello</res> diff --git a/dom/xhr/tests/file_XHR_pass2.txt b/dom/xhr/tests/file_XHR_pass2.txt new file mode 100644 index 000000000..0d7f879f9 --- /dev/null +++ b/dom/xhr/tests/file_XHR_pass2.txt @@ -0,0 +1 @@ +hello pass diff --git a/dom/xhr/tests/file_XHR_pass3.txt b/dom/xhr/tests/file_XHR_pass3.txt new file mode 100644 index 000000000..462209d8d --- /dev/null +++ b/dom/xhr/tests/file_XHR_pass3.txt @@ -0,0 +1 @@ +redirect file diff --git a/dom/xhr/tests/file_XHR_pass3.txt^headers^ b/dom/xhr/tests/file_XHR_pass3.txt^headers^ new file mode 100644 index 000000000..fb5056c38 --- /dev/null +++ b/dom/xhr/tests/file_XHR_pass3.txt^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: file_XHR_pass2.txt diff --git a/dom/xhr/tests/file_XHR_system_redirect.html b/dom/xhr/tests/file_XHR_system_redirect.html new file mode 100644 index 000000000..8267b01d0 --- /dev/null +++ b/dom/xhr/tests/file_XHR_system_redirect.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html> +<body> +</body> +</html>
\ No newline at end of file diff --git a/dom/xhr/tests/file_XHR_system_redirect.html^headers^ b/dom/xhr/tests/file_XHR_system_redirect.html^headers^ new file mode 100644 index 000000000..cefb165b0 --- /dev/null +++ b/dom/xhr/tests/file_XHR_system_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: file:///etc/passwd diff --git a/dom/xhr/tests/file_XHR_timeout.sjs b/dom/xhr/tests/file_XHR_timeout.sjs new file mode 100644 index 000000000..b349d122e --- /dev/null +++ b/dom/xhr/tests/file_XHR_timeout.sjs @@ -0,0 +1,15 @@ +var timer = null; + +function handleRequest(request, response) +{ + response.processAsync(); + timer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + timer.initWithCallback(function() + { + response.setStatusLine(null, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.write("hello"); + response.finish(); + }, 3000 /* milliseconds */, Components.interfaces.nsITimer.TYPE_ONE_SHOT); +} diff --git a/dom/xhr/tests/file_html_in_xhr.html b/dom/xhr/tests/file_html_in_xhr.html new file mode 100644 index 000000000..d77aeb440 --- /dev/null +++ b/dom/xhr/tests/file_html_in_xhr.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html><!-- Þ --> +<meta charset="Windows-1251"> +<script> +document.documentElement.setAttribute("data-fail", "FAIL"); +</script> +<script src="file_html_in_xhr.sjs"></script> +<script src="file_html_in_xhr.sjs" defer></script> +<script src="file_html_in_xhr.sjs" async></script> +<link type="stylesheet" href="file_html_in_xhr.sjs"> +<body onload='document.documentElement.setAttribute("data-fail", "FAIL");'> +<img src="file_html_in_xhr.sjs"> +<iframe src="file_html_in_xhr.sjs"></iframe> +<video poster="file_html_in_xhr.sjs" src="file_html_in_xhr.sjs"></video> +<object data="file_html_in_xhr.sjs"></object> +<noscript><div></div></noscript> diff --git a/dom/xhr/tests/file_html_in_xhr.sjs b/dom/xhr/tests/file_html_in_xhr.sjs new file mode 100644 index 000000000..8f4075ae7 --- /dev/null +++ b/dom/xhr/tests/file_html_in_xhr.sjs @@ -0,0 +1,15 @@ +function handleRequest(request, response) +{ + response.setHeader("Content-Type", "text/javascript", false); + if (request.queryString.indexOf("report") != -1) { + if (getState("loaded") == "loaded") { + response.write("ok(false, 'This script was not supposed to get fetched.'); continueAfterReport();"); + } else { + response.write("ok(true, 'This script was not supposed to get fetched.'); continueAfterReport();"); + } + } else { + setState("loaded", "loaded"); + response.write('document.documentElement.setAttribute("data-fail", "FAIL");'); + } +} + diff --git a/dom/xhr/tests/file_html_in_xhr2.html b/dom/xhr/tests/file_html_in_xhr2.html new file mode 100644 index 000000000..046052c75 --- /dev/null +++ b/dom/xhr/tests/file_html_in_xhr2.html @@ -0,0 +1 @@ +<meta charset="windows-1251">Þ diff --git a/dom/xhr/tests/file_html_in_xhr3.html b/dom/xhr/tests/file_html_in_xhr3.html new file mode 100644 index 000000000..ff43ca409 --- /dev/null +++ b/dom/xhr/tests/file_html_in_xhr3.html @@ -0,0 +1 @@ +SUCCESS diff --git a/dom/xhr/tests/file_sync_xhr_document_write_with_iframe.html b/dom/xhr/tests/file_sync_xhr_document_write_with_iframe.html new file mode 100644 index 000000000..060389427 --- /dev/null +++ b/dom/xhr/tests/file_sync_xhr_document_write_with_iframe.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<body> +<script> +function syncXHR() { + let xhr = new XMLHttpRequest(); + xhr.open("GET", window.location, false); + xhr.send(null); +} + +addEventListener('load', evt => { + syncXHR(); + document.open(); + document.write( + '<body>' + + '<iframe src="about:blank"></iframe>' + + '<script>window.opener.postMessage("DONE", "*");</' + 'script>' + + '</body>'); + document.close(); +}, { once: true }); +</script> +</body> diff --git a/dom/xhr/tests/iframe_sync_xhr_unload.html b/dom/xhr/tests/iframe_sync_xhr_unload.html new file mode 100644 index 000000000..d58b1994c --- /dev/null +++ b/dom/xhr/tests/iframe_sync_xhr_unload.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<body> + <script type="application/javascript"> + +function o() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "sync_xhr_unload.sjs", false); + try { xhr.send(); } catch(e) {} +} + +window.addEventListener("beforeunload", o, false); +window.addEventListener("unload", o, false) + + </script> +</body> +</html> + diff --git a/dom/xhr/tests/mochitest.ini b/dom/xhr/tests/mochitest.ini new file mode 100644 index 000000000..35ba1e3db --- /dev/null +++ b/dom/xhr/tests/mochitest.ini @@ -0,0 +1,115 @@ +[DEFAULT] +support-files = + echo.sjs + temporaryFileBlob.sjs + file_html_in_xhr.html + file_html_in_xhr.sjs + file_html_in_xhr2.html + file_html_in_xhr3.html + file_XHRDocURI.text + file_XHRDocURI.text^headers^ + file_XHRDocURI.xml + file_XHRDocURI.xml^headers^ + file_XHRDocURI.html + file_XHRDocURI.html^headers^ + file_XHRDocURI.sjs + file_XHRResponseURL.js + file_XHRResponseURL.sjs + file_XHRResponseURL.text + file_XHRResponseURL.text^headers^ + file_XHRResponseURL_nocors.text + file_XHRSendData.sjs + file_XHRSendData_doc.xml + file_XHRSendData_doc.xml^headers^ + file_XHR_anon.sjs + file_XHR_binary1.bin + file_XHR_binary1.bin^headers^ + file_XHR_binary2.bin + file_XHR_fail1.txt + file_XHR_fail1.txt^headers^ + file_XHR_header.sjs + file_XHR_pass1.xml + file_XHR_pass2.txt + file_XHR_pass3.txt + file_XHR_pass3.txt^headers^ + file_XHR_system_redirect.html + file_XHR_system_redirect.html^headers^ + file_XHR_timeout.sjs + progressserver.sjs + worker_terminateSyncXHR_frame.html + terminateSyncXHR_worker.js + worker_testXHR.txt + xhr_worker.js + xhr2_worker.js + xhrAbort_worker.js + test_worker_xhr_parameters.js + test_worker_xhr_system.js + worker_xhr_cors_redirect.js + worker_xhr_cors_redirect.sjs + worker_xhr_headers_server.sjs + worker_xhr_headers_worker.js + worker_file_getcookie.sjs + xhr_implicit_cancel_worker.js + relativeLoad_import.js + relativeLoad_worker.js + relativeLoad_worker2.js + responseIdentical.sjs + subdir/relativeLoad_sub_worker.js + subdir/relativeLoad_sub_worker2.js + subdir/relativeLoad_sub_import.js + common_temporaryFileBlob.js + worker_temporaryFileBlob.js + worker_bug1300552.js + sync_xhr_unload.sjs + iframe_sync_xhr_unload.html + empty.html + file_sync_xhr_document_write_with_iframe.html + +[test_bug1300552.html] +[test_html_in_xhr.html] +[test_relativeLoad.html] +skip-if = buildapp == 'b2g' # b2g(Failed to load script: relativeLoad_import.js) b2g-debug(Failed to load script: relativeLoad_import.js) b2g-desktop(Failed to load script: relativeLoad_import.js) +[test_sync_xhr_timer.xhtml] +skip-if = toolkit == 'android' +[test_sync_xhr_unload.html] +[test_temporaryFileBlob.html] +[test_worker_terminateSyncXHR.html] +skip-if = buildapp == 'b2g' +[test_worker_xhr.html] +[test_worker_xhr2.html] +[test_worker_xhr_3rdparty.html] +[test_worker_xhr_cors_redirect.html] +[test_worker_xhr_headers.html] +[test_worker_xhr_implicit_cancel.html] +[test_worker_xhr_parameters.html] +skip-if = buildapp == 'b2g' +[test_worker_xhr_responseURL.html] +[test_worker_xhr_system.html] +[test_worker_xhr_timeout.html] +[test_worker_xhrAbort.html] +skip-if = (os == "win") || (os == "mac") || toolkit == 'android' #bug 798220 +[test_XHR.html] +[test_xhr_abort_after_load.html] +skip-if = toolkit == 'android' +[test_XHR_anon.html] +[test_xhr_forbidden_headers.html] +[test_XHR_header.html] +[test_XHR_onuploadprogress.html] +[test_xhr_overridemimetype_throws_on_invalid_state.html] +skip-if = buildapp == 'b2g' # Requires webgl support +[test_XHR_parameters.html] +skip-if = buildapp == 'b2g' # b2g(86 total, 4 failing - testing mozAnon - got false, expected true) b2g-debug(86 total, 4 failing - testing mozAnon - got false, expected true) b2g-desktop(86 total, 4 failing - testing mozAnon - got false, expected true) +[test_xhr_progressevents.html] +skip-if = toolkit == 'android' +[test_xhr_send.html] +[test_xhr_send_readystate.html] +[test_XHR_system.html] +skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug)) # b2g-debug(12 total, 2 failing - .mozSystem == true - got false, expected true + ) b2g-desktop(12 total, 2 failing - .mozSystem == true - got false, expected true + ) +[test_XHR_timeout.html] +skip-if = buildapp == 'b2g' || (android_version == '18' && debug) # b2g(flaky on B2G, bug 960743) b2g-debug(flaky on B2G, bug 960743) b2g-desktop(flaky on B2G, bug 960743) +support-files = test_XHR_timeout.js +[test_xhr_withCredentials.html] +[test_XHRDocURI.html] +[test_XHRResponseURL.html] +[test_XHRSendData.html] +[test_sync_xhr_document_write_with_iframe.html] diff --git a/dom/xhr/tests/progressserver.sjs b/dom/xhr/tests/progressserver.sjs new file mode 100644 index 000000000..945f79557 --- /dev/null +++ b/dom/xhr/tests/progressserver.sjs @@ -0,0 +1,52 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function setReq(req) { + setObjectState("dom/xhr/tests/progressserver", req); +} + +function getReq() { + var req; + getObjectState("dom/xhr/tests/progressserver", function(v) { + req = v; + }); + return req; +} + +function handleRequest(request, response) +{ + var pairs = request.queryString.split('&'); + var command = pairs.shift(); + dump("received '" + command + "' command\n"); + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var body = ""; + var bodyAvail; + while ((bodyAvail = bodyStream.available()) > 0) + body += String.fromCharCode.apply(null, bodyStream.readByteArray(bodyAvail)); + + if (command == "open") { + response.processAsync(); + setReq(response); + + response.setHeader("Cache-Control", "no-cache", false); + pairs.forEach(function (val) { + var [name, value] = val.split('='); + response.setHeader(name, unescape(value), false); + }); + response.write(body); + return; + } + + if (command == "send") { + getReq().write(body); + } + else if (command == "close") { + getReq().finish(); + setReq(null); + } + response.setHeader("Content-Type", "text/plain"); + response.write("ok"); +} diff --git a/dom/xhr/tests/relativeLoad_import.js b/dom/xhr/tests/relativeLoad_import.js new file mode 100644 index 000000000..114d1883c --- /dev/null +++ b/dom/xhr/tests/relativeLoad_import.js @@ -0,0 +1,5 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const workerURL = "relativeLoad_worker.js"; diff --git a/dom/xhr/tests/relativeLoad_worker.js b/dom/xhr/tests/relativeLoad_worker.js new file mode 100644 index 000000000..42959db30 --- /dev/null +++ b/dom/xhr/tests/relativeLoad_worker.js @@ -0,0 +1,27 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const importURL = "relativeLoad_import.js"; + +onmessage = function(event) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "worker_testXHR.txt", false); + xhr.send(null); + if (xhr.status != 200 || + xhr.responseText != "A noisy noise annoys an oyster.") { + throw "Couldn't get xhr text from where we wanted it!"; + } + + importScripts(importURL); + var worker = new Worker("relativeLoad_worker2.js"); + worker.onerror = function(event) { + throw event.message; + }; + worker.onmessage = function(event) { + if (event.data != workerURL) { + throw "Bad data!"; + } + postMessage(workerURL); + } +}; diff --git a/dom/xhr/tests/relativeLoad_worker2.js b/dom/xhr/tests/relativeLoad_worker2.js new file mode 100644 index 000000000..e9fbdf903 --- /dev/null +++ b/dom/xhr/tests/relativeLoad_worker2.js @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const importURL = "relativeLoad_import.js"; + +importScripts(importURL); + +postMessage(workerURL); diff --git a/dom/xhr/tests/responseIdentical.sjs b/dom/xhr/tests/responseIdentical.sjs new file mode 100644 index 000000000..f1986819a --- /dev/null +++ b/dom/xhr/tests/responseIdentical.sjs @@ -0,0 +1,17 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +// Simply sending back the same data that is received +function handleRequest(request, response) +{ + var body = ""; + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bytes = [], avail = 0; + while ((avail = bodyStream.available()) > 0) + body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail)); + + response.setHeader("Content-Type", "application/octet-stream", false); + response.write(body); +} diff --git a/dom/xhr/tests/subdir/relativeLoad_sub_import.js b/dom/xhr/tests/subdir/relativeLoad_sub_import.js new file mode 100644 index 000000000..cac2a6f04 --- /dev/null +++ b/dom/xhr/tests/subdir/relativeLoad_sub_import.js @@ -0,0 +1,5 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const workerSubURL = "subdir/relativeLoad_sub_worker.js"; diff --git a/dom/xhr/tests/subdir/relativeLoad_sub_worker.js b/dom/xhr/tests/subdir/relativeLoad_sub_worker.js new file mode 100644 index 000000000..f725cfb61 --- /dev/null +++ b/dom/xhr/tests/subdir/relativeLoad_sub_worker.js @@ -0,0 +1,26 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const importSubURL = "relativeLoad_sub_import.js"; + +onmessage = function(event) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "testXHR.txt", false); + xhr.send(null); + if (xhr.status != 404) { + throw "Loaded an xhr from the wrong location!"; + } + + importScripts(importSubURL); + var worker = new Worker("relativeLoad_sub_worker2.js"); + worker.onerror = function(event) { + throw event.data; + }; + worker.onmessage = function(event) { + if (event.data != workerSubURL) { + throw "Bad data!"; + } + postMessage(workerSubURL); + }; +}; diff --git a/dom/xhr/tests/subdir/relativeLoad_sub_worker2.js b/dom/xhr/tests/subdir/relativeLoad_sub_worker2.js new file mode 100644 index 000000000..f47a29ee6 --- /dev/null +++ b/dom/xhr/tests/subdir/relativeLoad_sub_worker2.js @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const importSubURL = "relativeLoad_sub_import.js"; + +importScripts(importSubURL); + +postMessage(workerSubURL); diff --git a/dom/xhr/tests/sync_xhr_unload.sjs b/dom/xhr/tests/sync_xhr_unload.sjs new file mode 100644 index 000000000..4185056e3 --- /dev/null +++ b/dom/xhr/tests/sync_xhr_unload.sjs @@ -0,0 +1,15 @@ +var timer = null; + +function handleRequest(request, response) +{ + response.processAsync(); + timer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + timer.initWithCallback(function() + { + response.setStatusLine(null, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.write("hello"); + response.finish(); + }, 30000 /* milliseconds */, Components.interfaces.nsITimer.TYPE_ONE_SHOT); +} diff --git a/dom/xhr/tests/temporaryFileBlob.sjs b/dom/xhr/tests/temporaryFileBlob.sjs new file mode 100644 index 000000000..ff3410c78 --- /dev/null +++ b/dom/xhr/tests/temporaryFileBlob.sjs @@ -0,0 +1,33 @@ +const CC = Components.Constructor; + +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); +const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream"); +const Timer = CC("@mozilla.org/timer;1", + "nsITimer", + "initWithCallback"); + +function handleRequest(request, response) { + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bodyBytes = []; + while ((bodyAvail = bodyStream.available()) > 0) + Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail)); + + var bos = new BinaryOutputStream(response.bodyOutputStream); + + response.processAsync(); + + var part = bodyBytes.splice(0, 256); + bos.writeByteArray(part, part.length); + + response.timer1 = new Timer(function(timer) { + bos.writeByteArray(bodyBytes, bodyBytes.length); + }, 1000, Components.interfaces.nsITimer.TYPE_ONE_SHOT); + + response.timer2 = new Timer(function(timer) { + response.finish(); + }, 2000, Components.interfaces.nsITimer.TYPE_ONE_SHOT); +} diff --git a/dom/xhr/tests/terminateSyncXHR_worker.js b/dom/xhr/tests/terminateSyncXHR_worker.js new file mode 100644 index 000000000..c86baeb42 --- /dev/null +++ b/dom/xhr/tests/terminateSyncXHR_worker.js @@ -0,0 +1,19 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function(event) { + throw "No messages should reach me!"; +} + +var xhr = new XMLHttpRequest(); +xhr.open("GET", "worker_testXHR.txt", false); +xhr.addEventListener("loadstart", function () +{ + // Tell the parent to terminate us. + postMessage("TERMINATE"); + // And wait for it to do so. + while(1) { true; } +}); +xhr.send(null); diff --git a/dom/xhr/tests/test_XHR.html b/dom/xhr/tests/test_XHR.html new file mode 100644 index 000000000..143805a9e --- /dev/null +++ b/dom/xhr/tests/test_XHR.html @@ -0,0 +1,393 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for XMLHttpRequest</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="gen.next();"> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> +"use strict"; +SimpleTest.waitForExplicitFinish(); + +var gen = runTests(); +function continueTest() { gen.next(); } + +function runTests() { + +var path = "/tests/dom/xhr/tests/"; + +var passFiles = [['file_XHR_pass1.xml', 'GET', 200, 'OK', 'text/xml'], + ['file_XHR_pass2.txt', 'GET', 200, 'OK', 'text/plain'], + ['file_XHR_pass3.txt', 'GET', 200, 'OK', 'text/plain'], + ['data:text/xml,%3Cres%3Ehello%3C/res%3E%0A', 'GET', 200, 'OK', 'text/xml'], + ['data:text/plain,hello%20pass%0A', 'GET', 200, 'OK', 'text/plain'], + ['data:,foo', 'GET', 200, 'OK', 'text/plain;charset=US-ASCII', 'foo'], + ['data:text/plain;base64,Zm9v', 'GET', 200, 'OK', 'text/plain', 'foo'], + ['data:text/plain,foo#bar', 'GET', 200, 'OK', 'text/plain', 'foo'], + ['data:text/plain,foo%23bar', 'GET', 200, 'OK', 'text/plain', 'foo#bar'], + ]; + +var blob = new Blob(["foo"], { type: "text/plain" }); +var blobURL = URL.createObjectURL(blob); + +passFiles.push([blobURL, 'GET', 200, 'OK', 'text/plain', 'foo']); + +var failFiles = [['//example.com' + path + 'file_XHR_pass1.xml', 'GET'], + ['ftp://localhost' + path + 'file_XHR_pass1.xml', 'GET'], + ['file_XHR_fail1.txt', 'GET'], + ]; + +for (i = 0; i < passFiles.length; ++i) { + // Function to give our hacked is() a scope + (function(oldIs) { + function is(actual, expected, message) { + oldIs(actual, expected, message + " for " + passFiles[i][0]); + } + xhr = new XMLHttpRequest(); + is(xhr.getResponseHeader("Content-Type"), null, "should be null"); + is(xhr.getAllResponseHeaders(), "", "should be empty string"); + is(xhr.responseType, "", "wrong initial responseType"); + xhr.open(passFiles[i][1], passFiles[i][0], false); + xhr.send(null); + is(xhr.status, passFiles[i][2], "wrong status"); + is(xhr.statusText, passFiles[i][3], "wrong statusText"); + is(xhr.getResponseHeader("Content-Type"), passFiles[i][4], "wrong content type"); + var headers = xhr.getAllResponseHeaders(); + ok(/(?:^|\n)Content-Type:\s*([^\r\n]*)\r\n/i.test(headers) && + RegExp.$1 === passFiles[i][4], "wrong response headers"); + if (xhr.responseXML) { + is((new XMLSerializer()).serializeToString(xhr.responseXML.documentElement), + passFiles[i][5] || "<res>hello</res>", "wrong responseXML"); + is(xhr.response, passFiles[i][5] || "<res>hello</res>\n", "wrong response"); + } + else { + is(xhr.responseText, passFiles[i][5] || "hello pass\n", "wrong responseText"); + is(xhr.response, passFiles[i][5] || "hello pass\n", "wrong response"); + } + })(is); +} + +URL.revokeObjectURL(blobURL); + +for (i = 0; i < failFiles.length; ++i) { + xhr = new XMLHttpRequest(); + var didthrow = false; + try { + xhr.open(failFiles[i][1], failFiles[i][0], false); + xhr.send(null); + } + catch (e) { + didthrow = true; + } + if (!didthrow) { + is(xhr.status, 301, "wrong status"); + is(xhr.responseText, "redirect file\n", "wrong response"); + } + else { + ok(1, "should have thrown or given incorrect result"); + } +} + +function checkResponseTextAccessThrows(xhr) { + var didthrow = false; + try { xhr.responseText } catch (e) { didthrow = true; } + ok(didthrow, "should have thrown when accessing responseText"); +} +function checkResponseXMLAccessThrows(xhr) { + var didthrow = false; + try { xhr.responseXML } catch (e) { didthrow = true; } + ok(didthrow, "should have thrown when accessing responseXML"); +} +function checkSetResponseType(xhr, type) { + var didthrow = false; + try { xhr.responseType = type; } catch (e) { didthrow = true; } + is(xhr.responseType, type, "responseType should be " + type); + ok(!didthrow, "should not have thrown when setting responseType"); +} +function checkSetResponseTypeThrows(xhr, type) { + var didthrow = false; + try { xhr.responseType = type; } catch (e) { didthrow = true; } + ok(didthrow, "should have thrown when setting responseType"); +} +function checkOpenThrows(xhr, method, url, async) { + var didthrow = false; + try { xhr.open(method, url, async); } catch (e) { didthrow = true; } + ok(didthrow, "should have thrown when open is called"); +} + +// test if setting responseType before calling open() works +xhr = new XMLHttpRequest(); +checkSetResponseType(xhr, ""); +checkSetResponseType(xhr, "text"); +checkSetResponseType(xhr, "document"); +checkSetResponseType(xhr, "arraybuffer"); +checkSetResponseType(xhr, "blob"); +checkSetResponseType(xhr, "json"); +checkSetResponseType(xhr, "moz-chunked-text"); +checkSetResponseType(xhr, "moz-chunked-arraybuffer"); +checkOpenThrows(xhr, "GET", "file_XHR_pass2.txt", false); + +// test response (sync, responseType is not changeable) +xhr = new XMLHttpRequest(); +xhr.open("GET", 'file_XHR_pass2.txt', false); +checkSetResponseTypeThrows(xhr, ""); +checkSetResponseTypeThrows(xhr, "text"); +checkSetResponseTypeThrows(xhr, "document"); +checkSetResponseTypeThrows(xhr, "arraybuffer"); +checkSetResponseTypeThrows(xhr, "blob"); +checkSetResponseTypeThrows(xhr, "json"); +checkSetResponseTypeThrows(xhr, "moz-chunked-text"); +checkSetResponseTypeThrows(xhr, "moz-chunked-arraybuffer"); +xhr.send(null); +checkSetResponseTypeThrows(xhr, "document"); +is(xhr.status, 200, "wrong status"); +is(xhr.response, "hello pass\n", "wrong response"); + +// test response (responseType='document') +xhr = new XMLHttpRequest(); +xhr.open("GET", 'file_XHR_pass1.xml'); +xhr.responseType = 'document'; +xhr.onloadend = continueTest; +xhr.send(null); +yield undefined; +checkSetResponseTypeThrows(xhr, "document"); +is(xhr.status, 200, "wrong status"); +checkResponseTextAccessThrows(xhr); +is((new XMLSerializer()).serializeToString(xhr.response.documentElement), + "<res>hello</res>", + "wrong response"); + +// test response (responseType='text') +xhr = new XMLHttpRequest(); +xhr.open("GET", 'file_XHR_pass2.txt'); +xhr.responseType = 'text'; +xhr.onloadend = continueTest; +xhr.send(null); +yield undefined; +is(xhr.status, 200, "wrong status"); +checkResponseXMLAccessThrows(xhr); +is(xhr.response, "hello pass\n", "wrong response"); + +// test response (responseType='arraybuffer') +function arraybuffer_equals_to(ab, s) { + is(ab.byteLength, s.length, "wrong arraybuffer byteLength"); + + var u8v = new Uint8Array(ab); + is(String.fromCharCode.apply(String, u8v), s, "wrong values"); +} + +// with a simple text file +xhr = new XMLHttpRequest(); +xhr.open("GET", 'file_XHR_pass2.txt'); +xhr.responseType = 'arraybuffer'; +xhr.onloadend = continueTest; +xhr.send(null); +yield undefined; +is(xhr.status, 200, "wrong status"); +checkResponseTextAccessThrows(xhr); +checkResponseXMLAccessThrows(xhr); +var ab = xhr.response; +ok(ab != null, "should have a non-null arraybuffer"); +arraybuffer_equals_to(ab, "hello pass\n"); + +// test reusing the same XHR (Bug 680816) +xhr.open("GET", 'file_XHR_binary1.bin'); +xhr.responseType = 'arraybuffer'; +xhr.onloadend = continueTest; +xhr.send(null); +yield undefined; +is(xhr.status, 200, "wrong status"); +var ab2 = xhr.response; +ok(ab2 != null, "should have a non-null arraybuffer"); +ok(ab2 != ab, "arraybuffer on XHR reuse should be distinct"); +arraybuffer_equals_to(ab, "hello pass\n"); +arraybuffer_equals_to(ab2, "\xaa\xee\0\x03\xff\xff\xff\xff\xbb\xbb\xbb\xbb"); + +// with a binary file +xhr = new XMLHttpRequest(); +xhr.open("GET", 'file_XHR_binary1.bin'); +xhr.responseType = 'arraybuffer'; +xhr.onloadend = continueTest; +xhr.send(null); +yield undefined; +is(xhr.status, 200, "wrong status"); +checkResponseTextAccessThrows(xhr); +checkResponseXMLAccessThrows(xhr); +ab = xhr.response; +ok(ab != null, "should have a non-null arraybuffer"); +arraybuffer_equals_to(ab, "\xaa\xee\0\x03\xff\xff\xff\xff\xbb\xbb\xbb\xbb"); +is(xhr.response, xhr.response, "returns the same ArrayBuffer"); + +// test response (responseType='json') +var xhr = new XMLHttpRequest(); +xhr.open("POST", 'responseIdentical.sjs'); +xhr.responseType = 'json'; +var jsonObjStr = JSON.stringify({title: "aBook", author: "john"}); +xhr.onloadend = continueTest; +xhr.send(jsonObjStr); +yield undefined; +is(xhr.status, 200, "wrong status"); +checkResponseTextAccessThrows(xhr); +checkResponseXMLAccessThrows(xhr); +is(JSON.stringify(xhr.response), jsonObjStr, "correct result"); +is(xhr.response, xhr.response, "returning the same object on each access"); + +// with invalid json +var xhr = new XMLHttpRequest(); +xhr.open("POST", 'responseIdentical.sjs'); +xhr.responseType = 'json'; +xhr.onloadend = continueTest; +xhr.send("{"); +yield undefined; +is(xhr.status, 200, "wrong status"); +checkResponseTextAccessThrows(xhr); +checkResponseXMLAccessThrows(xhr); +is(xhr.response, null, "Bad JSON should result in null response."); +is(xhr.response, null, "Bad JSON should result in null response even 2nd time."); + +// Test status/statusText in all readyStates +xhr = new XMLHttpRequest(); +function checkXHRStatus() { + if (xhr.readyState == xhr.UNSENT || xhr.readyState == xhr.OPENED) { + is(xhr.status, 0, "should be 0 before getting data"); + is(xhr.statusText, "", "should be empty before getting data"); + } + else { + is(xhr.status, 200, "should be 200 when we have data"); + is(xhr.statusText, "OK", "should be OK when we have data"); + } +} +checkXHRStatus(); +xhr.open("GET", 'file_XHR_binary1.bin'); +checkXHRStatus(); +xhr.responseType = 'arraybuffer'; +xhr.send(null); +xhr.onreadystatechange = continueTest; +while (xhr.readyState != 4) { + checkXHRStatus(); + yield undefined; +} +checkXHRStatus(); + +// test response (responseType='blob') +var responseTypes = ['blob', 'moz-blob']; +for (var i = 0; i < responseTypes.length; i++) { + var t = responseTypes[i]; + // with a simple text file + xhr = new XMLHttpRequest(); + xhr.open("GET", 'file_XHR_pass2.txt'); + xhr.responseType = t; + xhr.onloadend = continueTest; + xhr.send(null); + yield undefined; + is(xhr.status, 200, "wrong status"); + checkResponseTextAccessThrows(xhr); + checkResponseXMLAccessThrows(xhr); + var b = xhr.response; + ok(b, "should have a non-null blob"); + ok(b instanceof Blob, "should be a Blob"); + ok(!(b instanceof File), "should not be a File"); + is(b.size, "hello pass\n".length, "wrong blob size"); + + var fr = new FileReader(); + fr.onload = continueTest; + fr.readAsBinaryString(b); + yield undefined; + ok(fr.result, "hello pass\n", "wrong values"); + + // with a binary file + xhr = new XMLHttpRequest(); + xhr.open("GET", 'file_XHR_binary1.bin', true); + xhr.send(null); + xhr.onreadystatechange = continueTest; + while(xhr.readyState != 2) + yield undefined; + + is(xhr.status, 200, "wrong status"); + xhr.responseType = t; + + while(xhr.readyState != 4) + yield undefined; + + xhr.onreadystatechange = null; + + b = xhr.response; + ok(b != null, "should have a non-null blob"); + is(b.size, 12, "wrong blob size"); + + fr = new FileReader(); + fr.readAsBinaryString(b); + xhr = null; // kill the XHR object + b = null; + SpecialPowers.gc(); + fr.onload = continueTest; + yield undefined; + is(fr.result, "\xaa\xee\0\x03\xff\xff\xff\xff\xbb\xbb\xbb\xbb", "wrong values"); + + // with a larger binary file + xhr = new XMLHttpRequest(); + xhr.open("GET", 'file_XHR_binary2.bin', true); + xhr.responseType = t; + xhr.send(null); + xhr.onreadystatechange = continueTest; + + while (xhr.readyState != 4) + yield undefined; + + xhr.onreadystatechange = null; + + var b = xhr.response; + ok(b != null, "should have a non-null blob"); + is(b.size, 65536, "wrong blob size"); + + fr = new FileReader(); + fr.readAsArrayBuffer(b); + fr.onload = continueTest; + xhr = null; // kill the XHR object + b = null; + SpecialPowers.gc(); + yield undefined; + + var u8 = new Uint8Array(fr.result); + for (var i = 0; i < 65536; i++) { + if (u8[i] !== (i & 255)) { + break; + } + } + is(i, 65536, "wrong value at offset " + i); +} + +var client = new XMLHttpRequest(); +client.open("GET", "file_XHR_pass1.xml", true); +client.send(); +client.onreadystatechange = function() { + if(client.readyState == 4) { + try { + is(client.responseXML, null, "responseXML should be null."); + is(client.responseText, "", "responseText should be empty string."); + is(client.response, "", "response should be empty string."); + is(client.status, 0, "status should be 0."); + is(client.statusText, "", "statusText should be empty string."); + is(client.getAllResponseHeaders(), "", + "getAllResponseHeaders() should return empty string."); + } catch(ex) { + ok(false, "Shouldn't throw! [" + ex + "]"); + } + } +} +client.abort(); + +SimpleTest.finish(); +yield undefined; + +} /* runTests */ +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHRDocURI.html b/dom/xhr/tests/test_XHRDocURI.html new file mode 100644 index 000000000..e3bd1d311 --- /dev/null +++ b/dom/xhr/tests/test_XHRDocURI.html @@ -0,0 +1,513 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=459470 +--> +<head> + <title>XMLHttpRequest return document URIs</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <base href="http://example.org/"> +</head> +<body onload="startTest();"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=459470">Mozilla Bug 459470</a><br /> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=859095">Mozilla Bug 859095</a> + +<p id="display"> +<iframe id=loader></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> + +SimpleTest.waitForExplicitFinish(); +var gen; + +function startTest() { + // The test uses history API, so don't do anything before load event has been + // handled. + SimpleTest.executeSoon(function() { + gen = runTest(); + gen.next(); + }); +} + +function testXMLDocURI(aDoc, aExpects) { + is(aDoc.documentURI, aExpects.documentURI, "wrong url"); + is(aDoc.baseURI, aExpects.baseURI, "wrong base"); + is(aDoc.documentElement.baseURI, aExpects.elementBaseURI, + "wrong base (xml:base)"); +} + +function testChromeXMLDocURI(aDoc, aExpects) { + is(aDoc.documentURI, aExpects.documentURI, "wrong url"); + is(aDoc.documentURIObject.spec, aExpects.documentURI, + "wrong url (.documentObjectURI)"); + is(aDoc.baseURI, aExpects.baseURI, "wrong base"); + is(aDoc.baseURIObject.spec, aExpects.baseURI, + "wrong base (.baseURIObject)"); + is(aDoc.documentElement.baseURI, aExpects.elementBaseURI, + "wrong base (xml:base)"); + is(aDoc.documentElement.baseURIObject.spec, aExpects.elementBaseURI, + "wrong base (.baseURIObject, xml:base)"); +} + +function testHTMLDocURI(aDoc, aExpects) { + is(aDoc.documentURI, aExpects.documentURI, "wrong url"); + is(aDoc.baseURI, aExpects.baseURI, "wrong base"); + + var base = aDoc.createElement("base"); + var newBaseURI = "http://www.example.com/"; + base.href = newBaseURI; + aDoc.head.appendChild(base); + is(aDoc.baseURI, newBaseURI, "wrong base (after <base> changed)"); +} + +function testChromeHTMLDocURI(aDoc, aNonChromeBaseURI, aExpects) { + is(aDoc.documentURI, aExpects.documentURI, "wrong url"); + is(aDoc.documentURIObject.spec, aExpects.documentURI, + "wrong url (.documentURIObject)"); + is(aDoc.baseURI, aExpects.baseURI, "wrong base"); + is(aDoc.baseURIObject.spec, aExpects.baseURI, + "wrong url (.baseURIObject)"); + + aDoc.body.setAttributeNS("http://www.w3.org/XML/1998/namespace", "base", + aNonChromeBaseURI); + is(aDoc.body.baseURI, aNonChromeBaseURI, + "wrong base (doc base and xml:base are same)"); + is(aDoc.body.baseURIObject.spec, aNonChromeBaseURI, + "wrong base (.baseURIObject, doc base and xml:base are same)") + var attr = aDoc.getElementById("data").getAttributeNode("id"); + is(attr.baseURI, aNonChromeBaseURI, + "wrong attr base (doc base and xml:base are same)") + is(attr.baseURIObject.spec, aNonChromeBaseURI, + "wrong attr base (.baseURIObject, doc base and xml:base are same)") + + var base = aDoc.createElement("base"); + var newBaseURI = "http://www.example.com/"; + base.href = newBaseURI; + aDoc.head.appendChild(base); + is(aDoc.baseURI, newBaseURI, "wrong base (after <base> changed)"); + is(aDoc.baseURIObject.spec, newBaseURI, + "wrong base (.baseURIObject, after <base> changed)"); +} + +function testCloneDocURI(aDoc) { + var clone = aDoc.cloneNode(true); + is(clone.documentURI, aDoc.documentURI, "wrong url (clone)"); + is(clone.baseURI, aDoc.baseURI, "wrong base (clone)"); +} + +function runTest() { + is(document.baseURI, "http://example.org/", "wrong doc baseURI"); + + // use content XHR and access URI properties from content privileged script + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml", + baseURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml", + elementBaseURI: "http://www.example.com/" + }; + testXMLDocURI(xhr.responseXML, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html", + baseURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html" + }; + testHTMLDocURI(xhr.response, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.text"); + xhr.onreadystatechange = function(e) { + is(xhr.responseXML, null, "should not have document"); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml", + baseURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml", + elementBaseURI: "http://www.example.com/" + }; + testXMLDocURI(xhr.responseXML, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html", + baseURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html" + }; + testHTMLDocURI(xhr.response, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml", + baseURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml", + elementBaseURI: "http://www.example.com/" + }; + testXMLDocURI(xhr.responseXML, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html", + baseURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html" + }; + testHTMLDocURI(xhr.response, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.text"); + xhr.onreadystatechange = function(e) { + is(xhr.responseXML, null, "should not have document"); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + + // use content XHR and access URI properties from chrome privileged script + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml", + baseURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml", + elementBaseURI: "http://www.example.com/" + }; + var xml = SpecialPowers.wrap(xhr.responseXML); + testChromeXMLDocURI(xml, expects); + testCloneDocURI(xml); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html", + baseURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html" + }; + var doc = SpecialPowers.wrap(xhr.response); + testChromeHTMLDocURI(doc, "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html", expects); + testCloneDocURI(doc); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: document.documentURI, + baseURI: document.baseURI, + elementBaseURI: "http://www.example.com/" + }; + var xml = SpecialPowers.wrap(xhr.responseXML); + testChromeXMLDocURI(xml, expects); + testCloneDocURI(xml); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: document.documentURI, + baseURI: document.baseURI + }; + var doc = SpecialPowers.wrap(xhr.response); + testChromeHTMLDocURI(doc, "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html", expects); + testCloneDocURI(doc); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: document.documentURI, + baseURI: document.baseURI, + elementBaseURI: "http://www.example.com/" + }; + var xml = SpecialPowers.wrap(xhr.responseXML); + testChromeXMLDocURI(xml, expects); + testCloneDocURI(xml); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest; + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: document.documentURI, + baseURI: document.baseURI + }; + var doc = SpecialPowers.wrap(xhr.response); + testChromeHTMLDocURI(doc, "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html", expects); + testCloneDocURI(doc); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + + // use the systemXHR special privilege + SpecialPowers.addPermission("systemXHR", true, document); + xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true}); + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml", + baseURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.xml", + elementBaseURI: "http://www.example.com/" + }; + testXMLDocURI(xhr.responseXML, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true}); + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html", + baseURI: "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.html" + }; + testHTMLDocURI(xhr.response, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true}); + xhr.open("GET", "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml", + baseURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml", + elementBaseURI: "http://www.example.com/" + }; + testXMLDocURI(xhr.responseXML, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true}); + xhr.open("GET", "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html", + baseURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html" + }; + testHTMLDocURI(xhr.response, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true}); + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml"); + xhr.onreadystatechange = function(e) { + if (!xhr.responseXML) { + return; + } + var expects = { + documentURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml", + baseURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.xml", + elementBaseURI: "http://www.example.com/" + }; + testXMLDocURI(xhr.responseXML, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + xhr = new XMLHttpRequest({mozAnon: false, mozSystem: true}); + xhr.open("GET", "http://mochi.test:8888/tests/dom/xhr/tests/file_XHRDocURI.sjs?url=http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html"); + xhr.responseType = "document"; + xhr.onreadystatechange = function(e) { + if (!xhr.response) { + return; + } + var expects = { + documentURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html", + baseURI: "http://example.com/tests/dom/xhr/tests/file_XHRDocURI.html" + }; + testHTMLDocURI(xhr.response, expects); + if (xhr.readyState == 4) { + gen.next(); + } + }; + xhr.send(); + yield undefined; + + history.pushState({}, "pushStateTest", window.location.href + "/pushStateTest"); + ok(document.documentURI.indexOf("pushStateTest") > -1); + + var chromeDoc = SpecialPowers.wrap(document); + ok(chromeDoc.documentURI.indexOf("pushStateTest") > -1); + + SimpleTest.executeSoon(function() { gen.next(); }); + + yield undefined; + + history.back(); + SimpleTest.executeSoon(function() { gen.next(); }); + + yield undefined; + + SimpleTest.finish(); + SpecialPowers.removePermission("systemXHR", document); + yield undefined; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHRResponseURL.html b/dom/xhr/tests/test_XHRResponseURL.html new file mode 100644 index 000000000..17e8c627a --- /dev/null +++ b/dom/xhr/tests/test_XHRResponseURL.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=998076 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 998076</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="file_XHRResponseURL.js"></script> + <script type="application/javascript"> + +/** Test for Bug 998076 **/ +"use strict"; + +window.addEventListener("message", function (aEvent) { + var data = aEvent.data; + if (data === "done") { + SimpleTest.finish(); + return; + } + if (data === "start") { + return; + } + if (data.type === "is") { + SimpleTest.is(data.actual, data.expected, data.message); + window.postMessage("pong", "*"); + return; + } + if (data.type === "ok") { + SimpleTest.ok(data.bool, data.message); + window.postMessage("pong", "*"); + return; + } + if (data.type === "info") { + SimpleTest.info(data.message); + window.postMessage("pong", "*"); + return; + } + if (data.type === "todo") { + SimpleTest.todo(data.bool, data.message); + window.postMessage("pong", "*"); + return; + } + if (data.type === "todo_is") { + SimpleTest.todo_is(data.actual, data.expected, data.message); + window.postMessage("pong", "*"); + return; + } +}); + +function runTests() { + SimpleTest.waitForExplicitFinish(); + window.postMessage("start", "*"); +} + + </script> +</head> +<body onload="runTests()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998076">Mozilla Bug 998076</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHRSendData.html b/dom/xhr/tests/test_XHRSendData.html new file mode 100644 index 000000000..c210efc9c --- /dev/null +++ b/dom/xhr/tests/test_XHRSendData.html @@ -0,0 +1,267 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=464848 +--> +<head> + <title>XMLHttpRequest send data and headers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="createFiles();"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=464848">Mozilla Bug 464848</a> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +var testData = "blahblahblahblahblahblahblaaaaaaaah. blah."; +var extensions = [".txt",".png",".jpg",".gif",".xml", "noext"]; +var fileTypes = ["text/plain", "image/png", "image/jpeg", "image/gif", "text/xml", null]; +var gen = runTests(); +var testDOMFiles; + +function createFiles() { + var filesToCreate = new Array(); + extensions.forEach(function (extension) { + filesToCreate.push({name: "testfile" + extension, data: testData}); + }); + SpecialPowers.createFiles(filesToCreate, + function (files) { + testDOMFiles = files; + gen.next(); + }, + function (msg) { + testDOMFiles = new Array; + ok(false, "File creation error: " + msg); + gen.next(); + }); +}; + + +function continueTest() { gen.next(); } + +function runTests() { +xhr = new XMLHttpRequest(); +xhr.open("GET", "file_XHRSendData_doc.xml", false); +xhr.send(); +testDoc1 = xhr.responseXML; +is(testDoc1.inputEncoding, "windows-1252", "wrong encoding"); + +testDoc2 = document.implementation.createDocument("", "", null); +testDoc2.appendChild(testDoc2.createComment(" doc 2 ")); +testDoc2.appendChild(testDoc2.createElement("res")); +testDoc2.documentElement.appendChild(testDoc2.createTextNode("text")); +is(testDoc2.inputEncoding, "UTF-8", "wrong encoding"); + +// arraybuffer test objects +var shortArray = new ArrayBuffer(1); +var shortInt8View = new Uint8Array(shortArray); +shortInt8View[0] = 3; + +var longArray = new ArrayBuffer(512); +var longInt8View = new Uint8Array(longArray); +for (var i = 0; i < longInt8View.length; i++) { + longInt8View[i] = i % 255; +} + +// arraybufferview test objects +var longArraySlice = longArray.slice(256, 384); +var longInt32View1 = new Int32Array(longArraySlice) +var longInt32View2 = new Int32Array(longArray, 256, 32) +var longInt16View1 = new Uint16Array(longArraySlice) +var longInt16View2 = new Uint16Array(longArray, 256, 64) +var longInt8View1 = new Int8Array(longArraySlice) +var longInt8View2 = new Int8Array(longArray, 256, 128) + +tests = [{ body: null, + resBody: "", + }, + { body: undefined, + resBody: "", + }, + { body: "hi", + resBody: "hi", + resContentType: "text/plain;charset=UTF-8", + }, + { body: "r\xe4ksm\xf6rg\xe5s", + resBody: "r\xc3\xa4ksm\xc3\xb6rg\xc3\xa5s", + resContentType: "text/plain;charset=UTF-8", + }, + { body: "hi", + contentType: "", + resBody: "hi", + resContentType: "text/plain;charset=UTF-8", + }, + { body: "hi", + contentType: "foo/bar", + resBody: "hi", + resContentType: "foo/bar", + }, + { body: "hi", + contentType: "foo/bar; baz=bin", + resBody: "hi", + resContentType: "foo/bar; baz=bin", + }, + { body: "hi", + contentType: "foo/bar; charset=ascii; baz=bin", + resBody: "hi", + resContentType: "foo/bar; charset=UTF-8; baz=bin", + }, + { body: "hi", + contentType: "foo/bar; charset=uTf-8", + resBody: "hi", + resContentType: "foo/bar; charset=uTf-8", + }, + { body: testDoc1, + resBody: "<!-- comment -->\n<out>hi</out>", + resContentType: "application/xml;charset=UTF-8", + }, + { body: testDoc1, + contentType: "foo/bar", + resBody: "<!-- comment -->\n<out>hi</out>", + resContentType: "foo/bar", + }, + { body: testDoc1, + contentType: "foo/bar; charset=ascii; baz=bin", + resBody: "<!-- comment -->\n<out>hi</out>", + resContentType: "foo/bar; charset=UTF-8; baz=bin", + }, + { body: testDoc1, + contentType: "foo/bar; charset=wIndows-1252", + resBody: "<!-- comment -->\n<out>hi</out>", + resContentType: "foo/bar; charset=UTF-8", + }, + { body: testDoc2, + resBody: "<!-- doc 2 -->\n<res>text</res>", + resContentType: "application/xml;charset=UTF-8", + }, + { body: testDoc2, + contentType: "foo/bar", + resBody: "<!-- doc 2 -->\n<res>text</res>", + resContentType: "foo/bar", + }, + { body: testDoc2, + contentType: "foo/bar; charset=ascii; baz=bin", + resBody: "<!-- doc 2 -->\n<res>text</res>", + resContentType: "foo/bar; charset=UTF-8; baz=bin", + }, + { body: testDoc2, + contentType: "foo/bar; charset=uTf-8", + resBody: "<!-- doc 2 -->\n<res>text</res>", + resContentType: "foo/bar; charset=uTf-8", + }, + { //will trigger a redirect test server-side + body: ("TEST_REDIRECT_STR&url=" + window.location.host + window.location.pathname), + redirect: true, + }, + { body: shortArray, + resBody: shortArray, + resType: "arraybuffer" + }, + { body: longArray, + resBody: longArray, + resType: "arraybuffer" + }, + { body: longInt32View1, + resBody: longArraySlice, + resType: "arraybuffer" + }, + { body: longInt32View2, + resBody: longArraySlice, + resType: "arraybuffer" + }, + { body: longInt16View1, + resBody: longArraySlice, + resType: "arraybuffer" + }, + { body: longInt16View2, + resBody: longArraySlice, + resType: "arraybuffer" + }, + { body: longInt8View1, + resBody: longArraySlice, + resType: "arraybuffer" + }, + { body: longInt8View2, + resBody: longArraySlice, + resType: "arraybuffer" + }, + ]; + +for (var i = 0; i < testDOMFiles.length; i++) { + tests.push({ body: testDOMFiles[i], + resBody: testData, + resContentType: fileTypes[i], + resContentLength: testData.length, + }); +} + +try { + for (test of tests) { + xhr = new XMLHttpRequest; + xhr.open("POST", "file_XHRSendData.sjs", !!test.resType); + if (test.contentType) + xhr.setRequestHeader("Content-Type", test.contentType); + if (test.resType) { + xhr.responseType = test.resType; + xhr.onloadend = continueTest; + } + xhr.send(test.body); + if (test.resType) + yield undefined; + + if (test.resContentType) { + is(xhr.getResponseHeader("Result-Content-Type"), test.resContentType, + "Wrong Content-Type sent"); + } + else { + is(xhr.getResponseHeader("Result-Content-Type"), null); + } + + if (test.resContentLength) { + is(xhr.getResponseHeader("Result-Content-Length"), + String(test.resContentLength), + "Wrong Content-Length sent"); + } + + if (test.resType == "arraybuffer") { + is_identical_arraybuffer(xhr.response, test.resBody); + } + else if (test.body instanceof Document) { + is(xhr.responseText.replace("\r\n", "\n"), test.resBody, "Wrong body"); + } + else if (!test.redirect) { + is(xhr.responseText, test.resBody, "Wrong body"); + } + else { + // If we're testing redirect, determine whether the body is + // this document by looking for the relevant bug url + is(xhr.responseText.indexOf("https://bugzilla.mozilla.org/show_bug.cgi?id=464848") >= 0, true, + "Wrong page for redirect"); + } + } +} catch (e) { +} + +function is_identical_arraybuffer(ab1, ab2) { + is(ab1.byteLength, ab2.byteLength, "arraybuffer byteLengths not equal"); + u8v1 = new Uint8Array(ab1); + u8v2 = new Uint8Array(ab2); + is(String.fromCharCode.apply(String, u8v1), + String.fromCharCode.apply(String, u8v2), "arraybuffer values not equal"); +} + +SimpleTest.finish(); +yield undefined; +} /* runTests */ + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHR_anon.html b/dom/xhr/tests/test_XHR_anon.html new file mode 100644 index 000000000..ed9271a04 --- /dev/null +++ b/dom/xhr/tests/test_XHR_anon.html @@ -0,0 +1,180 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for XMLHttpRequest with system privileges</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setup();"> +<p id="display"> +<iframe id="loader"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> + +// An XHR with the anon flag set will not send cookie and auth information. +const TEST_URL = "http://example.com/tests/dom/xhr/tests/file_XHR_anon.sjs"; +document.cookie = "foo=bar"; + +let am = { + authMgr: null, + + init: function() { + this.authMgr = SpecialPowers.Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(SpecialPowers.Ci.nsIHttpAuthManager) + }, + + addIdentity: function() { + this.authMgr.setAuthIdentity("http", "example.com", -1, "basic", "testrealm", + "", "example.com", "user1", "password1"); + }, + + tearDown: function() { + this.authMgr.clearAll(); + }, +} + +var tests = [ test1, test2, test2a, test3, test3, test3, test4, test4, test4, test5, test5, test5 ]; + +function runTests() { + if (!tests.length) { + am.tearDown(); + + // Resetting the cookie. + document.cookie = "foo=; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +function test1() { + am.addIdentity(); + + let xhr = new XMLHttpRequest({mozAnon: true, mozSystem: true}); + is(xhr.mozAnon, true, "test1: .mozAnon == true"); + xhr.open("GET", TEST_URL); + xhr.onload = function onload() { + is(xhr.status, 200, "test1: " + xhr.responseText); + am.tearDown(); + runTests(); + }; + xhr.onerror = function onerror() { + ok(false, "Got an error event!"); + am.tearDown(); + runTests(); + } + xhr.send(); +} + +function test2() { + am.addIdentity(); + + let xhr = new XMLHttpRequest({mozAnon: true, mozSystem: true}); + is(xhr.mozAnon, true, "test2: .mozAnon == true"); + xhr.open("GET", TEST_URL + "?expectAuth=true", true, + "user2name", "pass2word"); + xhr.onload = function onload() { + is(xhr.status, 200, "test2: " + xhr.responseText); + let response = JSON.parse(xhr.responseText); + is(response.authorization, "Basic dXNlcjJuYW1lOnBhc3Myd29yZA=="); + am.tearDown(); + runTests(); + }; + xhr.onerror = function onerror() { + ok(false, "Got an error event!"); + am.tearDown(); + runTests(); + } + xhr.send(); +} + +function test2a() { + am.addIdentity(); + + let xhr = new XMLHttpRequest({mozAnon: true, mozSystem: true}); + is(xhr.mozAnon, true, "test2: .mozAnon == true"); + xhr.open("GET", TEST_URL + "?expectAuth=true", true, + "user1", "pass2word"); + xhr.onload = function onload() { + is(xhr.status, 200, "test2: " + xhr.responseText); + let response = JSON.parse(xhr.responseText); + is(response.authorization, "Basic dXNlcjE6cGFzczJ3b3Jk"); + am.tearDown(); + runTests(); + }; + xhr.onerror = function onerror() { + ok(false, "Got an error event!"); + am.tearDown(); + runTests(); + } + xhr.send(); +} + +function test3() { + am.addIdentity(); + + let xhr = new XMLHttpRequest({mozAnon: true, mozSystem: true}); + is(xhr.mozAnon, true, "test3: .mozAnon == true"); + xhr.open("GET", TEST_URL + "?expectAuth=true", true); + xhr.onload = function onload() { + is(xhr.status, 401, "test3: " + xhr.responseText); + am.tearDown(); + runTests(); + }; + xhr.onerror = function onerror() { + ok(false, "Got an error event!"); + am.tearDown(); + runTests(); + } + xhr.send(); +} + +function test4() { + let xhr = new XMLHttpRequest({mozAnon: true, mozSystem: true}); + is(xhr.mozAnon, true, "test4: .mozAnon == true"); + xhr.open("GET", TEST_URL + "?expectAuth=true", true); + xhr.onload = function onload() { + is(xhr.status, 401, "test4: " + xhr.responseText); + runTests(); + }; + xhr.onerror = function onerror() { + ok(false, "Got an error event!"); + runTests(); + } + xhr.send(); +} + +function test5() { + let xhr = new XMLHttpRequest({mozAnon: true, mozSystem: true}); + is(xhr.mozAnon, true, "test5: .mozAnon == true"); + xhr.open("GET", TEST_URL + "?expectAuth=true", true, + "user2name", "pass2word"); + xhr.onload = function onload() { + is(xhr.status, 200, "test5: " + xhr.responseText); + let response = JSON.parse(xhr.responseText); + is(response.authorization, "Basic dXNlcjJuYW1lOnBhc3Myd29yZA=="); + runTests(); + }; + xhr.onerror = function onerror() { + ok(false, "Got an error event!"); + runTests(); + } + xhr.send(); +} + +function setup() { + am.init(); + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], runTests); +} +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHR_header.html b/dom/xhr/tests/test_XHR_header.html new file mode 100644 index 000000000..e213b25c7 --- /dev/null +++ b/dom/xhr/tests/test_XHR_header.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for XMLHttpRequest.GetResponseHeader(foo) byte-inflates the output</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta charset="utf-8"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> +"use strict"; +SimpleTest.waitForExplicitFinish(); + +let xhr = new XMLHttpRequest(); +xhr.open('GET', 'file_XHR_header.sjs', true); +xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + ok(xhr.getResponseHeader('X-Custom-Header-Bytes') == "\xE2\x80\xA6", 'getResponseHeader byte-inflates the output'); + SimpleTest.finish(); + } +} +xhr.send(null); +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHR_onuploadprogress.html b/dom/xhr/tests/test_XHR_onuploadprogress.html new file mode 100644 index 000000000..10c4fd20d --- /dev/null +++ b/dom/xhr/tests/test_XHR_onuploadprogress.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=743666 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 743666</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=743666">Mozilla Bug 743666</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 743666 **/ + +var called = false; +function uploadprogress() +{ + called = true; +} + +var xhr = new XMLHttpRequest(); +xhr.upload.onprogress = uploadprogress; +var event = new ProgressEvent("progress"); +xhr.upload.dispatchEvent(event); +ok(called, + "XMLHttpRequest.upload.onprogress sets upload progress event listener"); + + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHR_parameters.html b/dom/xhr/tests/test_XHR_parameters.html new file mode 100644 index 000000000..b99bb9dfe --- /dev/null +++ b/dom/xhr/tests/test_XHR_parameters.html @@ -0,0 +1,97 @@ + + +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for XMLHttpRequest with system privileges</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="runTests();"> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> + +function runTests() { + SimpleTest.waitForExplicitFinish(); + + let validParameters = [ + undefined, + null, + {}, + {mozSystem: ""}, + {mozSystem: 0}, + {mozAnon: 1}, + {mozAnon: []}, + {get mozAnon() { return true; }}, + 0, + 7, + Math.PI, + "string", + true, + false, + ]; + + let invalidParameters = [ + {get mozSystem() { throw "Bla"; } }, + ]; + + let havePrivileges = false; + + function testValidParameter(value) { + let xhr; + try { + xhr = new XMLHttpRequest(value); + } catch (ex) { + ok(false, "Got unexpected exception: " + ex); + return; + } + ok(xhr instanceof XMLHttpRequest, "passed " + JSON.stringify(value)); + + // If the page doesnt have privileges to create a system XHR, + // this flag will always be false no matter what is passed. + let expectedAnon = Boolean(value && value.mozAnon); + let expectedSystem = false; + if (havePrivileges) { + expectedSystem = Boolean(value && value.mozSystem); + } + is(xhr.mozAnon, expectedAnon, "testing mozAnon"); + is(xhr.mozSystem, expectedSystem, "testing mozSystem"); + } + + function testInvalidParameter(value) { + let expectedError; + try { + new XMLHttpRequest(value); + ok(false, "invalid parameter did not cause exception: " + + JSON.stringify(value)); + } catch (ex) { + expectedError = ex; + } + ok(expectedError, "invalid parameter raised exception as expected: " + + JSON.stringify(expectedError)) + } + + // Run the tests once without API privileges... + validParameters.forEach(testValidParameter); + invalidParameters.forEach(testInvalidParameter); + + // ...and once with privileges. + havePrivileges = true; + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], function() { + validParameters.forEach(testValidParameter); + invalidParameters.forEach(testInvalidParameter); + + SimpleTest.finish(); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHR_system.html b/dom/xhr/tests/test_XHR_system.html new file mode 100644 index 000000000..1d61e3f77 --- /dev/null +++ b/dom/xhr/tests/test_XHR_system.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for XMLHttpRequest with system privileges</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="runTests();"> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> + +let tests = []; + +const PROTECTED_URL = "file:///etc/passwd"; +const REDIRECT_URL = window.location.protocol + "//" + window.location.host + "/tests/dom/xhr/tests/file_XHR_system_redirect.html"; +const CROSSSITE_URL = "http://example.com/tests/dom/xhr/tests/test_XHR_system.html"; + +tests.push(function test_cross_origin() { + // System XHR can load cross-origin resources. + + is(window.location.hostname, "mochi.test", "correct origin"); + + let xhr = new XMLHttpRequest({mozSystem: true}); + is(xhr.mozSystem, true, ".mozSystem == true"); + xhr.open("GET", CROSSSITE_URL); + xhr.onload = function onload() { + is(xhr.status, 200, "correct HTTP status"); + ok(xhr.responseText != null, "HTTP response non-null"); + ok(xhr.responseText.length, "HTTP response not empty"); + runNextTest(); + }; + xhr.onerror = function onerror(event) { + ok(false, "Got an error event: " + event); + runNextTest(); + } + xhr.send(); +}); + +tests.push(function test_file_uri() { + // System XHR is not permitted to access file:/// URIs. + + let xhr = new XMLHttpRequest({mozSystem: true}); + is(xhr.mozSystem, true, ".mozSystem == true"); + xhr.open("GET", PROTECTED_URL); + xhr.onload = function() { + ok(false, "Should not have loaded"); + runNextTest(); + } + xhr.onerror = function(event) { + ok(true, "Got an error event: " + event); + is(xhr.status, 0, "HTTP status is 0"); + runNextTest(); + } + xhr.send(); +}); + +tests.push(function test_redirect_to_file_uri() { + // System XHR won't load file:/// URIs even if an HTTP resource redirects there. + + let xhr = new XMLHttpRequest({mozSystem: true}); + is(xhr.mozSystem, true, ".mozSystem == true"); + xhr.open("GET", REDIRECT_URL); + xhr.onload = function onload() { + ok(false, "Should not have loaded"); + runNextTest(); + }; + xhr.onerror = function onerror(event) { + ok(true, "Got an error event: " + event); + is(xhr.status, 0, "HTTP status is 0"); + runNextTest(); + } + xhr.send(); +}); + + +function runNextTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + tests.shift()(); +} + +function runTests() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], runNextTest); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHR_timeout.html b/dom/xhr/tests/test_XHR_timeout.html new file mode 100644 index 000000000..14422d9f4 --- /dev/null +++ b/dom/xhr/tests/test_XHR_timeout.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=525816 +--> +<head> + <title>Test for Bug 525816</title> + <script type="application/javascript" + src="/MochiKit/MochiKit.js"></script> + <script type="application/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" + type="text/css" + href="/tests/SimpleTest/test.css"> +</head> +<body> + +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=525816" + >Mozilla Bug 525816 (XMLHttpRequest timeout)</a> +<p id="display"></p> +<div id="content"> + This test takes over 1 minute to run, probably over 2 minutes. +</div> +<pre id="test"> +<script class="testbody" + type="text/javascript" + src="test_XHR_timeout.js"></script> +<script type="text/javascript"> + window.addEventListener("message", function (event) { + if (event.data == "done") { + SimpleTest.finish(); + return; + } + if (event.data == "start") { + return; + } + if (event.data.type == "is") { + SimpleTest.is(event.data.got, event.data.expected, event.data.msg); + return; + } + if (event.data.type == "ok") { + SimpleTest.ok(event.data.bool, event.data.msg); + return; + } + }); + // Final test harness setup and launch. + (function() { + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestLongerTimeout(TestRequests.length); + SimpleTest.requestFlakyTimeout("This is testing XHR timeouts."); + var msg = "This test will take approximately " + (TestRequests.length * 10) + msg += " seconds to complete, at most."; + document.getElementById("content").firstChild.nodeValue = msg; + window.postMessage("start", "*"); + })(); +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_XHR_timeout.js b/dom/xhr/tests/test_XHR_timeout.js new file mode 100644 index 000000000..203700887 --- /dev/null +++ b/dom/xhr/tests/test_XHR_timeout.js @@ -0,0 +1,342 @@ +/* Notes: + - All times are expressed in milliseconds in this test suite. + - Test harness code is at the end of this file. + - We generate only one request at a time, to avoid overloading the HTTP + request handlers. + */ + +var inWorker = false; +try { + inWorker = !(self instanceof Window); +} catch (e) { + inWorker = true; +} + +function message(data) { + if (inWorker) + self.postMessage(data); + else + self.postMessage(data, "*"); +} + +function is(got, expected, msg) { + var obj = {}; + obj.type = "is"; + obj.got = got; + obj.expected = expected; + obj.msg = msg; + + message(obj); +} + +function ok(bool, msg) { + var obj = {}; + obj.type = "ok"; + obj.bool = bool; + obj.msg = msg; + + message(obj); +} + +/** + * Generate and track results from a XMLHttpRequest with regards to timeouts. + * + * @param {String} id The test description. + * @param {Number} timeLimit The initial setting for the request timeout. + * @param {Number} resetAfter (Optional) The time after sending the request, to + * reset the timeout. + * @param {Number} resetTo (Optional) The delay to reset the timeout to. + * + * @note The actual testing takes place in handleEvent(event). + * The requests are generated in startXHR(). + * + * @note If resetAfter and resetTo are omitted, only the initial timeout setting + * applies. + * + * @constructor + * @implements DOMEventListener + */ +function RequestTracker(async, id, timeLimit /*[, resetAfter, resetTo]*/) { + this.async = async; + this.id = id; + this.timeLimit = timeLimit; + + if (arguments.length > 3) { + this.mustReset = true; + this.resetAfter = arguments[3]; + this.resetTo = arguments[4]; + } + + this.hasFired = false; +} +RequestTracker.prototype = { + /** + * Start the XMLHttpRequest! + */ + startXHR: function() { + var req = new XMLHttpRequest(); + this.request = req; + req.open("GET", "file_XHR_timeout.sjs", this.async); + var me = this; + function handleEvent(e) { return me.handleEvent(e); }; + req.onerror = handleEvent; + req.onload = handleEvent; + req.onabort = handleEvent; + req.ontimeout = handleEvent; + + req.timeout = this.timeLimit; + + if (this.mustReset) { + var resetTo = this.resetTo; + self.setTimeout(function() { + req.timeout = resetTo; + }, this.resetAfter); + } + + req.send(null); + }, + + /** + * Get a message describing this test. + * + * @returns {String} The test description. + */ + getMessage: function() { + var rv = this.id + ", "; + if (this.mustReset) { + rv += "original timeout at " + this.timeLimit + ", "; + rv += "reset at " + this.resetAfter + " to " + this.resetTo; + } + else { + rv += "timeout scheduled at " + this.timeLimit; + } + return rv; + }, + + /** + * Check the event received, and if it's the right (and only) one we get. + * + * @param {DOMProgressEvent} evt An event of type "load" or "timeout". + */ + handleEvent: function(evt) { + if (this.hasFired) { + ok(false, "Only one event should fire: " + this.getMessage()); + return; + } + this.hasFired = true; + + var type = evt.type, expectedType; + // The XHR responds after 3000 milliseconds with a load event. + var timeLimit = this.mustReset && (this.resetAfter < Math.min(3000, this.timeLimit)) ? + this.resetTo : + this.timeLimit; + if ((timeLimit == 0) || (timeLimit >= 3000)) { + expectedType = "load"; + } + else { + expectedType = "timeout"; + } + is(type, expectedType, this.getMessage()); + TestCounter.testComplete(); + } +}; + +/** + * Generate and track XMLHttpRequests which will have abort() called on. + * + * @param shouldAbort {Boolean} True if we should call abort at all. + * @param abortDelay {Number} The time in ms to wait before calling abort(). + */ +function AbortedRequest(shouldAbort, abortDelay) { + this.shouldAbort = shouldAbort; + this.abortDelay = abortDelay; + this.hasFired = false; +} +AbortedRequest.prototype = { + /** + * Start the XMLHttpRequest! + */ + startXHR: function() { + var req = new XMLHttpRequest(); + this.request = req; + req.open("GET", "file_XHR_timeout.sjs"); + var me = this; + function handleEvent(e) { return me.handleEvent(e); }; + req.onerror = handleEvent; + req.onload = handleEvent; + req.onabort = handleEvent; + req.ontimeout = handleEvent; + + req.timeout = 2000; + var _this = this; + + function abortReq() { + req.abort(); + } + + if (!this.shouldAbort) { + self.setTimeout(function() { + try { + _this.noEventsFired(); + } + catch (e) { + ok(false, "Unexpected error: " + e); + TestCounter.testComplete(); + } + }, 5000); + } + else { + // Abort events can only be triggered on sent requests. + req.send(); + if (this.abortDelay == -1) { + abortReq(); + } + else { + self.setTimeout(abortReq, this.abortDelay); + } + } + }, + + /** + * Ensure that no events fired at all, especially not our timeout event. + */ + noEventsFired: function() { + ok(!this.hasFired, "No events should fire for an unsent, unaborted request"); + // We're done; if timeout hasn't fired by now, it never will. + TestCounter.testComplete(); + }, + + /** + * Get a message describing this test. + * + * @returns {String} The test description. + */ + getMessage: function() { + return "time to abort is " + this.abortDelay + ", timeout set at 2000"; + }, + + /** + * Check the event received, and if it's the right (and only) one we get. + * + * @param {DOMProgressEvent} evt An event of type "load" or "timeout". + */ + handleEvent: function(evt) { + if (this.hasFired) { + ok(false, "Only abort event should fire: " + this.getMessage()); + return; + } + this.hasFired = true; + + var expectedEvent = (this.abortDelay >= 2000) ? "timeout" : "abort"; + is(evt.type, expectedEvent, this.getMessage()); + TestCounter.testComplete(); + } +}; + +var SyncRequestSettingTimeoutAfterOpen = { + startXHR: function() { + var pass = false; + var req = new XMLHttpRequest(); + req.open("GET", "file_XHR_timeout.sjs", false); + try { + req.timeout = 1000; + } + catch (e) { + pass = true; + } + ok(pass, "Synchronous XHR must not allow a timeout to be set"); + TestCounter.testComplete(); + } +}; + +var SyncRequestSettingTimeoutBeforeOpen = { + startXHR: function() { + var pass = false; + var req = new XMLHttpRequest(); + req.timeout = 1000; + try { + req.open("GET", "file_XHR_timeout.sjs", false); + } + catch (e) { + pass = true; + } + ok(pass, "Synchronous XHR must not allow a timeout to be set"); + TestCounter.testComplete(); + } +}; + +var TestRequests = [ + // Simple timeouts. + new RequestTracker(true, "no time out scheduled, load fires normally", 0), + new RequestTracker(true, "load fires normally", 5000), + new RequestTracker(true, "timeout hit before load", 2000), + + // Timeouts reset after a certain delay. + new RequestTracker(true, "load fires normally with no timeout set, twice", 0, 2000, 0), + new RequestTracker(true, "load fires normally with same timeout set twice", 5000, 2000, 5000), + new RequestTracker(true, "timeout fires normally with same timeout set twice", 2000, 1000, 2000), + + new RequestTracker(true, "timeout disabled after initially set", 5000, 2000, 0), + new RequestTracker(true, "timeout overrides load after a delay", 5000, 1000, 2000), + new RequestTracker(true, "timeout enabled after initially disabled", 0, 2000, 5000), + + new RequestTracker(true, "timeout set to expiring value after load fires", 5000, 4000, 1000), + new RequestTracker(true, "timeout set to expired value before load fires", 5000, 2000, 1000), + new RequestTracker(true, "timeout set to non-expiring value after timeout fires", 1000, 2000, 5000), + + // Aborted requests. + new AbortedRequest(false), + new AbortedRequest(true, -1), + new AbortedRequest(true, 5000), +]; + +var MainThreadTestRequests = [ + new AbortedRequest(true, 0), + new AbortedRequest(true, 1000), + + // Synchronous requests. + SyncRequestSettingTimeoutAfterOpen, + SyncRequestSettingTimeoutBeforeOpen +]; + +var WorkerThreadTestRequests = [ + // Simple timeouts. + new RequestTracker(false, "no time out scheduled, load fires normally", 0), + new RequestTracker(false, "load fires normally", 5000), + new RequestTracker(false, "timeout hit before load", 2000), + + // Reset timeouts don't make much sense with a sync request ... +]; + +if (inWorker) { + TestRequests = TestRequests.concat(WorkerThreadTestRequests); +} else { + TestRequests = TestRequests.concat(MainThreadTestRequests); +} + +// This code controls moving from one test to another. +var TestCounter = { + testComplete: function() { + // Allow for the possibility there are other events coming. + self.setTimeout(function() { + TestCounter.next(); + }, 5000); + }, + + next: function() { + var test = TestRequests.shift(); + + if (test) { + test.startXHR(); + } + else { + message("done"); + } + } +}; + +self.addEventListener("message", function (event) { + if (event.data == "start") { + TestCounter.next(); + } +}); diff --git a/dom/xhr/tests/test_bug1300552.html b/dom/xhr/tests/test_bug1300552.html new file mode 100644 index 000000000..04f076f9d --- /dev/null +++ b/dom/xhr/tests/test_bug1300552.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1300552</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +var w = new Worker('worker_bug1300552.js'); +w.onmessage = function(e) { + if (e.data.type == 'info') { + info(e.data.msg); + } else if (e.data.type == 'check') { + ok(e.data.what, e.data.msg); + } else if (e.data.type == 'finish') { + SimpleTest.finish(); + } else { + ok(false, 'Something wrong happened'); + } +} + +SimpleTest.waitForExplicitFinish(); + + </script> +</body> +</html> diff --git a/dom/xhr/tests/test_html_in_xhr.html b/dom/xhr/tests/test_html_in_xhr.html new file mode 100644 index 000000000..ccfb71947 --- /dev/null +++ b/dom/xhr/tests/test_html_in_xhr.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=651072 +--> +<head> + <title>Test for Bug 651072</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload=runTest();> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=651072">Mozilla Bug 651072</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 651072 **/ +SimpleTest.waitForExplicitFinish(); + +var xhr = new XMLHttpRequest(); + +function runTest() { + xhr.onreadystatechange = function() { + if (this.readyState == 4) { + ok(this.responseXML, "Should have gotten responseXML"); + is(this.responseXML.characterSet, "windows-1251", "Wrong character encoding"); + is(this.responseXML.documentElement.firstChild.data, " \u042E ", "Decoded using the wrong encoding."); + try { + this.responseText; + ok(false, "responseText access should have thrown."); + } catch (e) { + is(e.name, "InvalidStateError", "Should have thrown InvalidStateError."); + is(e.code, 11, "Should have thrown INVALID_STATE_ERR."); + } + is(this.responseXML.getElementsByTagName("div").length, 1, "There should be one div."); + ok(!this.responseXML.documentElement.hasAttribute("data-fail"), "Should not have a data-fail attribute."); + var scripts = this.responseXML.getElementsByTagName("script"); + is(scripts.length, 4, "Unexpected number of scripts."); + while (scripts.length) { + // These should not run when moved to another doc + document.body.appendChild(scripts[0]); + } + var s = document.createElement("script"); + s.src = "file_html_in_xhr.sjs?report=1"; + document.body.appendChild(s); + } + } + xhr.open("GET", "file_html_in_xhr.html", true); + xhr.responseType = "document"; + xhr.send(); +} + +function continueAfterReport() { + xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (this.readyState == 4) { + is(this.responseText.indexOf("\u042E"), -1, "Honored meta in default mode."); + is(this.responseText.indexOf("\uFFFD"), 29, "Honored meta in default mode 2."); + is(this.responseXML, null, "responseXML should be null for HTML in the default mode"); + testNonParsingText(); + } + } + xhr.open("GET", "file_html_in_xhr2.html"); + xhr.send(); +} + +function testNonParsingText() { + xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (this.readyState == 4) { + is(this.responseText.indexOf("\u042E"), -1, "Honored meta in text mode."); + is(this.responseText.indexOf("\uFFFD"), 29, "Honored meta in text mode 2."); + testChunkedText(); + } + } + xhr.open("GET", "file_html_in_xhr2.html"); + xhr.responseType = "text"; + xhr.send(); +} + +function testChunkedText() { + xhr = new XMLHttpRequest(); + xhr.onprogress = function() { + is(this.responseText.indexOf("\u042E"), -1, "Honored meta in chunked text mode."); + } + xhr.onreadystatechange = function() { + if (this.readyState == 4) { + testSyncXHR(); + } + } + xhr.open("GET", "file_html_in_xhr2.html"); + xhr.responseType = "moz-chunked-text"; + xhr.send(); +} + +function testSyncXHR() { + xhr = new XMLHttpRequest(); + xhr.open("GET", "file_html_in_xhr3.html", false); + xhr.send(); + is(xhr.responseText, "SUCCESS\n", "responseText should be ready by now"); + is(xhr.responseXML, null, "responseXML should be null in the sync case"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/xhr/tests/test_relativeLoad.html b/dom/xhr/tests/test_relativeLoad.html new file mode 100644 index 000000000..fd471b894 --- /dev/null +++ b/dom/xhr/tests/test_relativeLoad.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads relative load +--> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var index = -1; + + var urls = [ + "relativeLoad_worker.js", + "subdir/relativeLoad_sub_worker.js" + ]; + + function messageHandler(event) { + if (index >= 0) { + is(event.data, urls[index], "Bad url!"); + if (index == urls.length - 1) { + SimpleTest.finish(); + return; + } + } + + var worker = new Worker(urls[++index]); + worker.onmessage = messageHandler; + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + }; + worker.postMessage("start"); + } + + messageHandler(); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/xhr/tests/test_sync_xhr_document_write_with_iframe.html b/dom/xhr/tests/test_sync_xhr_document_write_with_iframe.html new file mode 100644 index 000000000..eaddc2feb --- /dev/null +++ b/dom/xhr/tests/test_sync_xhr_document_write_with_iframe.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug </title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script> +function runTest() { + let w = window.open('file_sync_xhr_document_write_with_iframe.html'); + addEventListener('message', evt => { + is(evt.data, 'DONE'); + w.close(); + SimpleTest.finish(); + }, { once: true }); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_sync_xhr_timer.xhtml b/dom/xhr/tests/test_sync_xhr_timer.xhtml new file mode 100644 index 000000000..ce6572362 --- /dev/null +++ b/dom/xhr/tests/test_sync_xhr_timer.xhtml @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug </title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +<![CDATA[ + +var counter = 0; +var xhr; + +function syncXHR() { + xhr = new XMLHttpRequest(); + xhr.open("GET", window.location, false); + xhr.send(null); +} + +function increaseCounter() { + ++counter; + ok(counter <= 2, "Too many increaseCounter() calls!"); + if (counter == 2) { + ok(true, "increaseCounter() should be called twice!"); + SimpleTest.finish(); + } +} + +function runTest() { + setTimeout(syncXHR, 0); + setTimeout(increaseCounter, 0); + setTimeout(increaseCounter, 0); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_sync_xhr_unload.html b/dom/xhr/tests/test_sync_xhr_unload.html new file mode 100644 index 000000000..b11ab1f75 --- /dev/null +++ b/dom/xhr/tests/test_sync_xhr_unload.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1307122</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="common_temporaryFileBlob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +info("Creating the iframe..."); +var ifr = document.createElement('iframe'); + +ifr.addEventListener("load", function ifr_load1() { + info("Iframe loaded"); + + ifr.removeEventListener("load", ifr_load1); + ifr.src = "empty.html"; + + ifr.addEventListener("load", function ifr_load2() { + ok(true, "Test passed"); + SimpleTest.finish(); + }); + +}); + +ifr.src = "iframe_sync_xhr_unload.html"; +document.body.appendChild(ifr); + + </script> +</body> +</html> diff --git a/dom/xhr/tests/test_temporaryFileBlob.html b/dom/xhr/tests/test_temporaryFileBlob.html new file mode 100644 index 000000000..e4db56abd --- /dev/null +++ b/dom/xhr/tests/test_temporaryFileBlob.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1202006</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="common_temporaryFileBlob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +var tests = [ + // from common_temporaryFileBlob.js: + test_simple, + test_reuse, + test_abort, + + test_worker, + test_worker_reuse, + test_worker_abort, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SpecialPowers.pushPrefEnv({ "set" : [[ "dom.blob.memoryToTemporaryFile", 1 ]] }, + next); +SimpleTest.waitForExplicitFinish(); + + </script> +</body> +</html> diff --git a/dom/xhr/tests/test_worker_terminateSyncXHR.html b/dom/xhr/tests/test_worker_terminateSyncXHR.html new file mode 100644 index 000000000..11cea07d9 --- /dev/null +++ b/dom/xhr/tests/test_worker_terminateSyncXHR.html @@ -0,0 +1,45 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads XHR(Bug 450452 ) +--> +<head> + <title>Test for DOM Worker Threads XHR (Bug 450452 )</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450452">DOM Worker Threads XHR (Bug 450452)</a> +<p id="display"></p> +<div id="content"> + <iframe id="iframe" src="worker_terminateSyncXHR_frame.html"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + var ifr = document.getElementById("iframe"); + + window.onmessage = function(event) { + if (event.data == "TERMINATE") { + ok(true, "Got TERMINATE"); + ifr.parentNode.removeChild(ifr); + SimpleTest.finish(); + } else { + ok(false, "Unexpected message: " + event.data); + } + } + + SimpleTest.waitForExplicitFinish(); + + window.onload = function() { + ifr.contentWindow.doStuff(); + } + +</script> +</pre> +</body> +</html> + diff --git a/dom/xhr/tests/test_worker_xhr.html b/dom/xhr/tests/test_worker_xhr.html new file mode 100644 index 000000000..0e875b7f4 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr.html @@ -0,0 +1,73 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads XHR(Bug 450452 ) +--> +<head> + <title>Test for DOM Worker Threads XHR (Bug 450452 )</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450452">DOM Worker Threads XHR (Bug 450452)</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("xhr_worker.js"); + + var gotUploadLoad = false, gotLoadend = false; + + worker.onmessage = function(event) { + is(event.target, worker); + var args = event.data; + switch (args.type) { + case "progress": { + ok(parseInt(args.current) <= parseInt(args.total)); + } break; + case "error": { + ok(false, "XHR error: " + args.error); + } break; + case "upload.load": { + gotUploadLoad = true; + } break; + case "load": { + ok(gotUploadLoad, "Should have gotten upload load event"); + gotLoadend = true; + is(args.data, "A noisy noise annoys an oyster.", "correct data"); + document.getElementById("content").textContent = args.data; + } break; + case "loadend": { + ok(gotLoadend, "Should have gotten load."); + SimpleTest.finish(); + break; + } + default: { + ok(false, "Unexpected message"); + SimpleTest.finish(); + } + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error:" + event.message); + SimpleTest.finish(); + } + + worker.postMessage("worker_testXHR.txt"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/xhr/tests/test_worker_xhr2.html b/dom/xhr/tests/test_worker_xhr2.html new file mode 100644 index 000000000..5f1816e45 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr2.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads XHR(Bug 450452 ) +--> +<head> + <title>Test for DOM Worker Threads XHR (Bug 450452 )</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450452">DOM Worker Threads XHR (Bug 450452)</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("xhr2_worker.js"); + + worker.onmessage = function(event) { + is(event.data, "done", "Got correct result"); + SimpleTest.finish(); + } + worker.postMessage("worker_testXHR.txt"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/xhr/tests/test_worker_xhrAbort.html b/dom/xhr/tests/test_worker_xhrAbort.html new file mode 100644 index 000000000..0f4b93c5a --- /dev/null +++ b/dom/xhr/tests/test_worker_xhrAbort.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads XHR(Bug 450452 ) +--> +<head> + <title>Test for DOM Worker Threads XHR (Bug 450452 )</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450452">DOM Worker Threads XHR (Bug 450452)</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script language="javascript" src="xhrAbort_worker.js"></script> +<script class="testbody" language="javascript"> + + function postMessage(data) { + dump(data.toString() + "\n"); + + var worker = new Worker("xhrAbort_worker.js"); + + worker.onmessage = function(event) { + is (data.toString(), event.data.toString(), "Got different results!"); + SimpleTest.finish(); + }; + + worker.postMessage("start"); + } + + runTest(); + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_worker_xhr_3rdparty.html b/dom/xhr/tests/test_worker_xhr_3rdparty.html new file mode 100644 index 000000000..46951012a --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_3rdparty.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads XHR(Bug 450452 ) +--> +<head> + <title>Test for DOM Worker Threads XHR (Bug 450452 )</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450452">DOM Worker Threads XHR (Bug 450452)</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("xhr_worker.js"); + + var gotUploadLoad = false, gotLoadend = false; + + worker.onmessage = function(event) { + is(event.target, worker); + var args = event.data; + switch (args.type) { + case "progress": { + ok(parseInt(args.current) <= parseInt(args.total)); + } break; + case "error": { + ok(false, "XHR error: " + args.error); + } break; + case "upload.load": { + gotUploadLoad = true; + } break; + case "load": { + ok(gotUploadLoad, "Should have gotten upload load event"); + gotLoadend = true; + is(args.data, "a=cookie_is_set", "correct data"); + document.getElementById("content").textContent = args.data; + } break; + case "loadend": { + ok(gotLoadend, "Should have gotten load."); + SimpleTest.finish(); + break; + } + default: { + ok(false, "Unexpected message"); + SimpleTest.finish(); + } + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error:" + event.message); + SimpleTest.finish(); + } + + document.cookie = "a=cookie_is_set"; + SpecialPowers.pushPrefEnv({ set: [[ "network.cookie.cookieBehavior", 1 ]] }, + () => worker.postMessage("worker_file_getcookie.sjs")); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_worker_xhr_cors_redirect.html b/dom/xhr/tests/test_worker_xhr_cors_redirect.html new file mode 100644 index 000000000..40967d5d8 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_cors_redirect.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1206121</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + +"use strict"; + +var worker = new Worker("worker_xhr_cors_redirect.js"); +worker.onmessage = function(e) { + is(e.data, 200, "We want to read 200 here."); + runTests(); +}; + +var tests = [ 'http://example.com/tests/dom/xhr/tests/worker_xhr_cors_redirect.sjs', + 'http://example.com/tests/dom/xhr/tests/worker_xhr_cors_redirect.sjs?redirect', + 'worker_xhr_cors_redirect.sjs?redirect' ]; +function runTests() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + worker.postMessage(tests.shift()); +} + +SimpleTest.waitForExplicitFinish(); + </script> +</head> +<body onload="runTests()"> +</body> +</html> diff --git a/dom/xhr/tests/test_worker_xhr_headers.html b/dom/xhr/tests/test_worker_xhr_headers.html new file mode 100644 index 000000000..1416ababd --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_headers.html @@ -0,0 +1,86 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for XHR Headers</title> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + </head> + <body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + <script class="testbody"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); + +var path = + location.pathname.substring(0, location.pathname.lastIndexOf("/") + 1); +var filenamePrefix = "worker_xhr_headers_"; +var serverFilename = filenamePrefix + "server.sjs"; +var workerFilename = filenamePrefix + "worker.js"; +var otherHost = "example.com"; + +info("Informing server about the current host"); + +var xhr = new XMLHttpRequest(); +xhr.open("POST", path + serverFilename); +xhr.setRequestHeader("options-host", otherHost); +xhr.setRequestHeader("empty", ""); +xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + info("Launching worker"); + + var worker = new Worker(path + workerFilename); + worker.postMessage("http://" + otherHost + path + serverFilename); + + worker.onmessage = function(event) { + ok(event.data.response === "", "Worker responded, saw no response"); + + var loopCount = 0; + + function checkServer() { + var xhr2 = new XMLHttpRequest(); + xhr2.open("GET", path + serverFilename); + xhr2.onreadystatechange = function() { + if (xhr2.readyState == 4) { + if (xhr2.responseText) { + is(xhr2.responseText, + "Success: expected OPTIONS request with '" + + event.data.header + "' header", + "Server saw expected requests"); + SimpleTest.finish(); + } else if (++loopCount < 30) { + setTimeout(checkServer, 1000); + } else { + ok(false, "Server never saw any requests"); + SimpleTest.finish(); + } + } + }; + + info("Checking server status (" + loopCount + ")"); + xhr2.send(); + } + + checkServer(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: '" + event.message + "'"); + event.preventDefault(); + SimpleTest.finish(); + }; + } +}; +xhr.send(); + + </script> + </pre> + </body> +</html> diff --git a/dom/xhr/tests/test_worker_xhr_implicit_cancel.html b/dom/xhr/tests/test_worker_xhr_implicit_cancel.html new file mode 100644 index 000000000..a9efb14b9 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_implicit_cancel.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads XHR(Bug 450452 ) +--> +<head> + <title>Test for DOM Worker Threads XHR (Bug 450452 )</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450452">DOM Worker Threads XHR (Bug 450452)</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("xhr_implicit_cancel_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "Expected event target for message"); + ok(true, "Worker didn't have an error"); + SimpleTest.finish(); + }; + + worker.onerror = function(event) { + is(event.target, worker, "Expected event target for error"); + ok(false, "Worker had an error:" + event.message); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/xhr/tests/test_worker_xhr_parameters.html b/dom/xhr/tests/test_worker_xhr_parameters.html new file mode 100644 index 000000000..ae880aabb --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_parameters.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for XMLHttpRequest with system privileges</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> + +function message(event) { + if (event.data.test == 'ok') + ok(event.data.a, event.data.event); + else if(event.data.test == 'is') + is(event.data.a, event.data.b, event.data.event); + else if(event.data.test == 'finish') { + run(); + } +}; + +function test1() { + var worker = new Worker("test_worker_xhr_parameters.js"); + worker.onmessage = message; + + // Run the tests once without API privileges... + worker.postMessage(false); +} + +function test2() { + // ...and once with privileges. + SpecialPowers.pushPermissions([{type: "systemXHR", allow: true, context: document}], + function () { + var worker = new Worker("test_worker_xhr_parameters.js"); + worker.onmessage = message; + worker.postMessage(true); + } + ); +} + +var tests = [ test1, test2 ]; +function run() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var func = tests.shift(); + func(); +} + +SimpleTest.waitForExplicitFinish(); +run(); + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_worker_xhr_parameters.js b/dom/xhr/tests/test_worker_xhr_parameters.js new file mode 100644 index 000000000..80db35a43 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_parameters.js @@ -0,0 +1,75 @@ +function ok(what, msg) { + postMessage({ event: msg, test: 'ok', a: what }); +} + +function is(a, b, msg) { + postMessage({ event: msg, test: 'is', a: a, b: b }); +} + +// This is a copy of dom/xhr/tests/test_XHR_parameters.js +var validParameters = [ + undefined, + null, + {}, + {mozSystem: ""}, + {mozSystem: 0}, + {mozAnon: 1}, + {mozAnon: []}, + {get mozAnon() { return true; }}, + 0, + 7, + Math.PI, + "string", + true, + false, +]; + +var invalidParameters = [ + {get mozSystem() { throw "Bla"; } }, +]; + + +function testParameters(havePrivileges) { + + function testValidParameter(value) { + var xhr; + try { + xhr = new XMLHttpRequest(value); + } catch (ex) { + ok(false, "Got unexpected exception: " + ex); + return; + } + ok(!!xhr, "passed " + JSON.stringify(value)); + + // If the page doesnt have privileges to create a system or anon XHR, + // these flags will always be false no matter what is passed. + var expectedAnon = false; + var expectedSystem = false; + if (havePrivileges) { + expectedAnon = Boolean(value && value.mozAnon); + expectedSystem = Boolean(value && value.mozSystem); + } + is(xhr.mozAnon, expectedAnon, "testing mozAnon"); + is(xhr.mozSystem, expectedSystem, "testing mozSystem"); + } + + + function testInvalidParameter(value) { + try { + new XMLHttpRequest(value); + ok(false, "invalid parameter did not cause exception: " + + JSON.stringify(value)); + } catch (ex) { + ok(true, "invalid parameter raised exception as expected: " + + JSON.stringify(ex)); + } + } + + validParameters.forEach(testValidParameter); + invalidParameters.forEach(testInvalidParameter); +} + +self.onmessage = function onmessage(event) { + testParameters(event.data); + postMessage({test: "finish"}); +}; diff --git a/dom/xhr/tests/test_worker_xhr_responseURL.html b/dom/xhr/tests/test_worker_xhr_responseURL.html new file mode 100644 index 000000000..424d0b95d --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_responseURL.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=998076 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 998076</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + +/** Test for Bug 998076 **/ +"use strict"; + +var worker = new Worker("../../../dom/xhr/tests/file_XHRResponseURL.js"); + +var requestObserver = { + observe: function (aSubject, aTopic, aData) { + worker.postMessage("request"); + } +}; + +worker.addEventListener("message", function (aEvent) { + var data = aEvent.data; + if (data == "done") { + SimpleTest.finish(); + return; + } + if (data == "start") { + return; + } + if (data.type == "is") { + SimpleTest.is(data.actual, data.expected, data.message); + worker.postMessage("pong"); + return; + } + if (data.type == "ok") { + SimpleTest.ok(data.bool, data.message); + worker.postMessage("pong"); + return; + } + if (data.type == "info") { + SimpleTest.info(data.message); + worker.postMessage("pong"); + return; + } + if (data.type === "redirect_test") { + if (data.status === "start") { + SpecialPowers.addObserver(requestObserver, "specialpowers-http-notify-request", false); + return; + } + if (data.status === "end") { + SpecialPowers.removeObserver(requestObserver, "specialpowers-http-notify-request"); + return; + } + } +}); + +function runTests() { + SimpleTest.waitForExplicitFinish(); + worker.postMessage("start"); +} + + </script> +</head> +<body onload="runTests()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998076">Mozilla Bug 998076</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_worker_xhr_system.html b/dom/xhr/tests/test_worker_xhr_system.html new file mode 100644 index 000000000..6de0016e2 --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_system.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for XMLHttpRequest with system privileges</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> + +function message(event) { + if (event.data.test == 'ok') + ok(event.data.a, event.data.event); + else if(event.data.test == 'is') + is(event.data.a, event.data.b, event.data.event); + else if(event.data.test == 'finish') { + run(); + } +}; + +function test1() { + var worker = new Worker("test_worker_xhr_system.js"); + worker.onmessage = message; + worker.postMessage(true); +} + +var tests = [ test1 ]; +function run() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var func = tests.shift(); + func(); +} + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], run); + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_worker_xhr_system.js b/dom/xhr/tests/test_worker_xhr_system.js new file mode 100644 index 000000000..5ad63225c --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_system.js @@ -0,0 +1,30 @@ +function ok(what, msg) { + postMessage({ event: msg, test: 'ok', a: what }); +} + +function is(a, b, msg) { + postMessage({ event: msg, test: 'is', a: a, b: b }); +} + +self.onmessage = function onmessage(event) { + + // An XHR with system privileges will be able to do cross-site calls. + + const TEST_URL = "http://example.com/tests/dom/xhr/tests/test_XHR_system.html"; + is(location.hostname, "mochi.test", "hostname should be mochi.test"); + + var xhr = new XMLHttpRequest({mozSystem: true}); + is(xhr.mozSystem, true, ".mozSystem == true"); + xhr.open("GET", TEST_URL); + xhr.onload = function onload() { + is(xhr.status, 200); + ok(xhr.responseText != null); + ok(xhr.responseText.length); + postMessage({test: "finish"}); + }; + xhr.onerror = function onerror() { + ok(false, "Got an error event!"); + postMessage({test: "finish"}); + } + xhr.send(); +}; diff --git a/dom/xhr/tests/test_worker_xhr_timeout.html b/dom/xhr/tests/test_worker_xhr_timeout.html new file mode 100644 index 000000000..d9bb6a7ae --- /dev/null +++ b/dom/xhr/tests/test_worker_xhr_timeout.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=498998 +--> +<head> + <title>Test for Bug 498998</title> + <script type="application/javascript" + src="/MochiKit/MochiKit.js"></script> + <script type="application/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" + type="text/css" + href="/tests/SimpleTest/test.css"> +</head> +<body> + +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=498998" + >Mozilla Bug 498998 (Worker XMLHttpRequest timeout)</a> +<p id="display"></p> +<div id="content"> + This test takes over 1 minute to run, probably over 2 minutes. +</div> +<pre id="test"> +<script type="text/javascript"> + var worker = new Worker("../../../dom/xhr/tests/test_XHR_timeout.js"); + + worker.addEventListener("message", function (event) { + if (event.data == "done") { + SimpleTest.finish(); + return; + } + if (event.data == "start") { + return; + } + if (event.data.type == "is") { + SimpleTest.is(event.data.got, event.data.expected, event.data.msg); + return; + } + if (event.data.type == "ok") { + SimpleTest.ok(event.data.bool, event.data.msg); + return; + } + }); + // Final test harness setup and launch. + (function() { + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestLongerTimeout(20); + var msg = "This test will take approximately " + (20 * 10) + msg += " seconds to complete, at most."; + document.getElementById("content").firstChild.nodeValue = msg; + worker.postMessage("start"); + })(); +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_xhr_abort_after_load.html b/dom/xhr/tests/test_xhr_abort_after_load.html new file mode 100644 index 000000000..780c11253 --- /dev/null +++ b/dom/xhr/tests/test_xhr_abort_after_load.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test bug 482935</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href=" /tests/SimpleTest/test.css" /> +</head> +<body onload="onWindowLoad()"> +<script class="testbody" type="text/javascript">"use strict"; +SimpleTest.waitForExplicitFinish(); + +var url = "file_XHR_pass1.xml"; + +function onWindowLoad() { + runTest(); +} + +function runTest() { + var testFunctions = [ + startTest1, + startTest2, + startTest3, + ]; + + function nextTest() { + if (testFunctions.length == 0) { + SimpleTest.finish(); + return; + } + (testFunctions.shift())(); + } + + nextTest(); + + var xhr; + function startTest1() { + xhr = new XMLHttpRequest(); + xhr.onload = onLoad1; + xhr.open("GET", url); + xhr.send(); + } + + function onLoad1() { + is(xhr.readyState, xhr.DONE, "readyState should be DONE"); + xhr.onabort = onAbort1; + xhr.abort(); + + function onAbort1(e) { + ok(false, e.type + " event should not be fired!"); + } + + is(xhr.readyState, xhr.UNSENT, "readyState should be UNSENT"); + nextTest(); + } + + function startTest2() { + xhr = new XMLHttpRequest(); + xhr.onloadstart = onAfterSend; + xhr.open("GET", url); + xhr.send(); + } + + function startTest3() { + xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.send(); + onAfterSend(); + } + + function onAfterSend() { + is(xhr.readyState, xhr.OPENED, "readyState should be OPENED"); + var sent = false; + try { + xhr.send(); + } catch (e) { + sent = true; + } + ok(sent, "send() flag should be set"); + var aborted = false; + xhr.onabort = onAbort2; + xhr.abort(); + + function onAbort2() { + is(xhr.readyState, xhr.DONE, "readyState should be DONE"); + aborted = true; + } + + ok(aborted, "abort event should be fired"); + is(xhr.readyState, xhr.UNSENT, "readyState should be UNSENT"); + nextTest(); + } +} + +</script> +</body> +</html> diff --git a/dom/xhr/tests/test_xhr_forbidden_headers.html b/dom/xhr/tests/test_xhr_forbidden_headers.html new file mode 100644 index 000000000..fc076731e --- /dev/null +++ b/dom/xhr/tests/test_xhr_forbidden_headers.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=308484 +--> +<head> + <title>Test for Bug 308484</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=308484">Mozilla Bug 308484</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 308484 **/ + +var headers = [ + "aCCept-chaRset", + "acCePt-eNcoDing", + "aCcEsS-cOnTrOl-ReQuEsT-mEtHoD", + "aCcEsS-cOnTrOl-ReQuEsT-hEaDeRs", + "coNnEctIon", + "coNtEnt-LEngth", + "CoOKIe", + "cOOkiE2", + "DATE", + "dNT", + "exPeCt", + "hOSt", + "keep-alive", + "oRiGiN", + "reFERer", + "te", + "trAiLer", + "trANsfEr-eNcoDiNg", + "uPGraDe", + "viA", + "pRoxy-", + "sEc-", + "proxy-fOobar", + "sec-bAZbOx" +]; +var i, request; + +function startTest() { + // Try setting headers in unprivileged context + request = new XMLHttpRequest(); + request.open("GET", window.location.href); + for (i = 0; i < headers.length; i++) + request.setRequestHeader(headers[i], "test" + i); + request.send(); // headers aren't set on the channel until send() + + // Read out headers + var channel = SpecialPowers.wrap(request).channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel); + for (i = 0; i < headers.length; i++) { + // Retrieving Content-Length will throw an exception + var value = null; + try { + value = channel.getRequestHeader(headers[i]); + } + catch(e) {} + + isnot(value, "test" + i, "Setting " + headers[i] + " header in unprivileged context"); + } + + // Try setting headers in privileged context + request = new XMLHttpRequest({mozAnon: true, mozSystem: true}); + request.open("GET", window.location.href); + for (i = 0; i < headers.length; i++) + request.setRequestHeader(headers[i], "test" + i); + request.send(); // headers aren't set on the channel until send() + + // Read out headers + var channel = SpecialPowers.wrap(request).channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel); + for (i = 0; i < headers.length; i++) { + var value = channel.getRequestHeader(headers[i]); + is(value, "test" + i, "Setting " + headers[i] + " header in privileged context"); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], startTest); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_xhr_overridemimetype_throws_on_invalid_state.html b/dom/xhr/tests/test_xhr_overridemimetype_throws_on_invalid_state.html new file mode 100644 index 000000000..bdbbf1591 --- /dev/null +++ b/dom/xhr/tests/test_xhr_overridemimetype_throws_on_invalid_state.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test bug 482935</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href=" /tests/SimpleTest/test.css" /> +</head> +<body onload="onWindowLoad()"> +<script class="testbody" type="text/javascript">"use strict"; +SimpleTest.waitForExplicitFinish(); + +var url = "file_XHR_pass1.xml"; + +function onWindowLoad() { + runTest(); +} + +function runTest() { + var testFunctions = [ + function() { testOverMimeTypeThrowsDuringReadyState(3, "application/xml"); }, + function() { testOverMimeTypeThrowsDuringReadyState(3, "application/xml;charset=Shift-JIS"); }, + function() { testOverMimeTypeThrowsDuringReadyState(4, "application/xml"); }, + function() { testOverMimeTypeThrowsDuringReadyState(4, "application/xml;charset=Shift-JIS"); }, + ]; + + function nextTest() { + if (testFunctions.length == 0) { + SimpleTest.finish(); + return; + } + (testFunctions.shift())(); + } + + nextTest(); + + function testOverMimeTypeThrowsDuringReadyState(readyState, mimeType) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState === readyState) { + try { + xhr.overrideMimeType(mimeType); + ok(false, "No exception thrown, but expected InvalidStateError" + + " for readyState=" + readyState + ", mimeType=" + mimeType); + } catch(exc) { + is(exc.name, "InvalidStateError", "Expected InvalidStateError, got " + exc.name + + " for readyState=" + readyState + ", mimeType=" + mimeType); + } + } + if (xhr.readyState === 4) { + is(xhr.responseXML, null, "responseXML was not null" + + " for readyState=" + readyState + ", mimeType=" + mimeType); + nextTest(); + } + } + xhr.open("GET", url); + xhr.send(); + } +} +</script> +</body> +</html> diff --git a/dom/xhr/tests/test_xhr_progressevents.html b/dom/xhr/tests/test_xhr_progressevents.html new file mode 100644 index 000000000..f22927daf --- /dev/null +++ b/dom/xhr/tests/test_xhr_progressevents.html @@ -0,0 +1,333 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for XMLHttpRequest Progress Events</title> + <script type="text/javascript" src="/MochiKit/packed.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="gen.next();"> +<pre id=l></pre> +<script type="application/javascript;version=1.7"> +SimpleTest.waitForExplicitFinish(); + +var gen = runTests(); + +function log(s) { + // Uncomment these to get debugging information + /* + document.getElementById("l").textContent += s + "\n"; + dump(s + "\n"); + */ +} + +function getEvent(e) { + log("got event: " + e.type + " (" + e.target.readyState + ")"); + gen.send(e); +} + +function startsWith(a, b) { + return a.substr(0, b.length) === b; +} + +function updateProgress(e, data, testName) { + var test = " while running " + testName; + is(e.type, "progress", "event type" + test); + + let response; + if (data.nodata) { + is(e.target.response, null, "response should be null" + test); + response = null; + } + else if (data.text) { + is(typeof e.target.response, "string", "response should be a string" + test); + response = e.target.response; + } + else if (data.blob) { + ok(e.target.response instanceof Blob, "response should be a Blob" + test); + response = e.target.response; + } + else { + ok(e.target.response instanceof ArrayBuffer, "response should be an ArrayBuffer" + test); + response = bufferToString(e.target.response); + } + is(e.target.response, e.target.response, "reflexivity should hold" + test); + + if (!data.nodata && !data.encoded) { + if (data.blob) { + is(e.loaded, response.size, "event.loaded matches response size" + test); + } + else if (!data.chunked) { + is(e.loaded, response.length, "event.loaded matches response size" + test); + } + else { + is(e.loaded - data.receivedBytes, response.length, + "event.loaded grew by response size" + test); + } + } + ok(e.loaded > data.receivedBytes, "event.loaded increased" + test); + ok(e.loaded - data.receivedBytes <= data.pendingBytes, + "event.loaded didn't increase too much" + test); + + if (!data.nodata && !data.blob) { + var newData; + ok(startsWith(response, data.receivedResult), + "response strictly grew" + test); + newData = response.substr(data.receivedResult.length); + + if (!data.encoded) { + ok(newData.length > 0, "sanity check for progress" + test); + } + ok(startsWith(data.pendingResult, newData), "new data matches expected" + test); + } + + is(e.lengthComputable, "total" in data, "lengthComputable" + test); + if ("total" in data) { + is(e.total, data.total, "total" + test); + } + + if (!data.nodata && !data.blob) { + data.pendingResult = data.pendingResult.substr(newData.length); + } + data.pendingBytes -= e.loaded - data.receivedBytes; + data.receivedResult = response; + data.receivedBytes = e.loaded; +} + +function sendData(s) { + var xhr = new XMLHttpRequest(); + xhr.open("POST", "progressserver.sjs?send"); + // The Blob constructor encodes String elements as UTF-8; + // for straight bytes, manually convert to ArrayBuffer first + var buffer = new Uint8Array(s.length); + for (var i = 0; i < s.length; ++i) { + buffer[i] = s.charCodeAt(i) & 0xff; + }; + xhr.send(new Blob([buffer])); +} + +function closeConn() { + log("in closeConn"); + var xhr = new XMLHttpRequest(); + xhr.open("POST", "progressserver.sjs?close"); + xhr.send(); + return xhr; +} + +var longString = "long"; +while(longString.length < 65536) + longString += longString; + +function utf8encode(s) { + return unescape(encodeURIComponent(s)); +} + +function bufferToString(buffer) { + return String.fromCharCode.apply(String, new Uint8Array(buffer)); +} + +function runTests() { + var xhr = new XMLHttpRequest(); + xhr.onprogress = xhr.onload = xhr.onerror = xhr.onreadystatechange = xhr.onloadend = getEvent; + + var responseTypes = [{ type: "text", text: true }, + { type: "arraybuffer", text: false, nodata: true }, + { type: "blob", text: false, nodata: true, blob: true }, + { type: "moz-blob", text: false, nodata: false, blob: true }, + { type: "document", text: true, nodata: true }, + { type: "json", text: true, nodata: true }, + { type: "", text: true }, + { type: "moz-chunked-text", text: true, chunked: true }, + { type: "moz-chunked-arraybuffer", text: false, chunked: true }, + ]; + var responseType; + var fileExpectedResult = ""; + for (var i = 0; i < 65536; i++) { + fileExpectedResult += String.fromCharCode(i & 255); + } + while (responseType = responseTypes.shift()) { + let tests = [{ open: "Content-Type=text/plain", name: "simple test" }, + { data: "hello world" }, + { data: "\u0000\u0001\u0002\u0003" }, + { data: longString }, + { data: "x" }, + { close: true }, + { open: "Content-Type=text/plain&Content-Length=20", name: "with length", total: 20 }, + // 5 bytes from the "ready" in the open step + { data: "abcde" }, + { data: "0123456789" }, + { close: true }, + { open: "Content-Type=application/xml", name: "without length, as xml" }, + { data: "<out>" }, + { data: "text" }, + { data: "</foo>invalid" }, + { close: true }, + { open: "Content-Type=text/plain;charset%3dutf-8", name: "utf8 data", encoded: true }, + { data: utf8encode("räksmörgÃ¥s"), utf16: "räksmörgÃ¥s" }, + { data: utf8encode("Ã…").substr(0,1), utf16: "" }, + { data: utf8encode("Ã…").substr(1), utf16: "Ã…" }, + { data: utf8encode("aöb").substr(0,2), utf16: "a" }, + { data: utf8encode("aöb").substr(2), utf16: "öb" }, + { data: utf8encode("a\u867Eb").substr(0,3), utf16: "a" }, + { data: utf8encode("a\u867Eb").substr(3,1), utf16: "\u867E" }, + { data: utf8encode("a\u867Eb").substr(4), utf16: "b" }, + { close: true }, + ]; + if (responseType.blob) { + tests.push({ file: "file_XHR_binary2.bin", name: "cacheable data", total: 65536 }, + { close: true }, + { file: "file_XHR_binary2.bin", name: "cached data", total: 65536 }, + { close: true }); + } + let testState = { index: 0 }; + + for (let i = 0; i < tests.length; ++i) { + let test = tests[i]; + testState.index++; + if ("open" in test || "file" in test) { + log("opening " + testState.name); + testState = { name: test.name + " for " + responseType.type, + index: 0, + pendingResult: "ready", + pendingBytes: 5, + receivedResult: "", + receivedBytes: 0, + total: test.total, + encoded: test.encoded, + nodata: responseType.nodata, + chunked: responseType.chunked, + text: responseType.text, + blob: responseType.blob, + file: test.file }; + + xhr.onreadystatechange = null; + if (testState.file) + xhr.open("GET", test.file); + else + xhr.open("POST", "progressserver.sjs?open&" + test.open); + xhr.responseType = responseType.type; + xhr.send("ready"); + xhr.onreadystatechange = getEvent; + + let e = yield undefined; + is(e.type, "readystatechange", "should readystate to headers-received starting " + testState.name); + is(xhr.readyState, xhr.HEADERS_RECEIVED, "should be in state HEADERS_RECEIVED starting " + testState.name); + + e = yield undefined; + is(e.type, "readystatechange", "should readystate to loading starting " + testState.name); + is(xhr.readyState, xhr.LOADING, "should be in state LOADING starting " + testState.name); + if (typeof testState.total == "undefined") + delete testState.total; + } + if ("file" in test) { + testState.pendingBytes = testState.total; + testState.pendingResult = fileExpectedResult; + } + if ("close" in test) { + log("closing"); + let xhrClose; + if (!testState.file) + xhrClose = closeConn(); + + e = yield undefined; + is(e.type, "readystatechange", "should readystate to done closing " + testState.name); + is(xhr.readyState, xhr.DONE, "should be in state DONE closing " + testState.name); + log("readystate to 4"); + + if (responseType.chunked) { + xhr.responseType; + is(xhr.response, null, "chunked data has null response for " + testState.name); + } + + e = yield undefined; + is(e.type, "load", "should fire load closing " + testState.name); + is(e.lengthComputable, e.total != 0, "length should " + (e.total == 0 ? "not " : "") + "be computable during load closing " + testState.name); + log("got load"); + + if (responseType.chunked) { + is(xhr.response, null, "chunked data has null response for " + testState.name); + } + + e = yield undefined; + is(e.type, "loadend", "should fire loadend closing " + testState.name); + is(e.lengthComputable, e.total != 0, "length should " + (e.total == 0 ? "not " : "") + "be computable during loadend closing " + testState.name); + log("got loadend"); + + // if we closed the connection using an explicit request, make sure that goes through before + // running the next test in order to avoid reordered requests from closing the wrong + // connection. + if (xhrClose && xhrClose.readyState != xhrClose.DONE) { + log("wait for closeConn to finish"); + xhrClose.onloadend = getEvent; + yield undefined; + is(xhrClose.readyState, xhrClose.DONE, "closeConn finished"); + } + + if (responseType.chunked) { + is(xhr.response, null, "chunked data has null response for " + testState.name); + } + + if (!testState.nodata && !responseType.blob || responseType.chunked) { + // This branch intentionally left blank + // Under these conditions we check the response during updateProgress + } + else if (responseType.type === "arraybuffer") { + is(bufferToString(xhr.response), testState.pendingResult, + "full response for " + testState.name); + } + else if (responseType.blob) { + let reader = new FileReader; + reader.readAsBinaryString(xhr.response); + reader.onloadend = getEvent; + yield undefined; + + is(reader.result, testState.pendingResult, + "full response in blob for " + testState.name); + } + + testState.name = ""; + } + if ("data" in test) { + log("sending"); + if (responseType.text) { + testState.pendingResult += "utf16" in test ? test.utf16 : test.data; + } + else { + testState.pendingResult += test.data; + } + testState.pendingBytes = test.data.length; + sendData(test.data); + } + + while(testState.pendingBytes) { + log("waiting for more bytes: " + testState.pendingBytes); + e = yield undefined; + // Readystate can fire several times between each progress event. + if (e.type === "readystatechange") + continue; + + updateProgress(e, testState, "data for " + testState.name + "[" + testState.index + "]"); + if (responseType.chunked) { + testState.receivedResult = ""; + } + } + + if (!testState.nodata && !testState.blob) { + is(testState.pendingResult, "", + "should have consumed the expected result"); + } + + log("done with this test"); + } + + is(testState.name, "", "forgot to close last test"); + } + + SimpleTest.finish(); + yield undefined; +} + +</script> + +</body> +</html> diff --git a/dom/xhr/tests/test_xhr_send.html b/dom/xhr/tests/test_xhr_send.html new file mode 100644 index 000000000..6633dcb15 --- /dev/null +++ b/dom/xhr/tests/test_xhr_send.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1096263 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1096263</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1096263 **/ + +SimpleTest.waitForExplicitFinish(); + +function simpleGetTest() { + var x = new XMLHttpRequest(); + x.open("GET", "echo.sjs"); + x.onload = function() { + ok(true, "Should have processed GET"); + simplePostTest(); + } + x.send({}); +} + +function simplePostTest() { + var x = new XMLHttpRequest(); + x.open("POST", "echo.sjs"); + x.onload = function() { + is(x.responseText, "somedata", "Should have processed POST"); + undefinedPostTest(); + } + x.send({toString: function() { return "somedata"; }}); +} + +function undefinedPostTest() { + var x = new XMLHttpRequest(); + x.open("POST", "echo.sjs"); + x.onload = function() { + is(x.responseText, "undefined", "Should have processed POST"); + nullPostTest(); + } + x.send({toString: function() { return undefined; }}); +} + +function nullPostTest() { + var x = new XMLHttpRequest(); + x.open("POST", "echo.sjs"); + x.onload = function() { + is(x.responseText, "null", "Should have processed POST"); + testExceptionInToString(); + } + x.send({toString: function() { return null; }}); +} + +function testExceptionInToString() { + var x = new XMLHttpRequest(); + x.open("GET", "echo.sjs"); + x.onload = function() { + ok(false); + SimpleTest.finish(); + } + try { + x.send({toString: function() { throw "dummy"; }}); + } catch(ex) { + is(ex, "dummy"); + SimpleTest.finish(); + } +} + + </script> +</head> +<body onload="simpleGetTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1096263">Mozilla Bug 1096263</a> +<p id="display"></p> +<div id="content"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_xhr_send_readystate.html b/dom/xhr/tests/test_xhr_send_readystate.html new file mode 100644 index 000000000..1c86681de --- /dev/null +++ b/dom/xhr/tests/test_xhr_send_readystate.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=814064 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 814064</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=814064">Mozilla Bug 814064</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 814064 **/ +var xhr = new XMLHttpRequest(); +var firings = 0; +function readyStateHandler() { + is(xhr.readyState, XMLHttpRequest.OPENED, "Should be in OPENED state"); + ++firings; +} +xhr.onreadystatechange = readyStateHandler; +xhr.open("GET", ""); +is(firings, 1, "Should have fired the readystatechange handler"); +xhr.send(); +is(firings, 1, "Should not have fired the readystatechange handler"); +xhr.onreadystatechange = null; +xhr.abort(); +is(firings, 1, "Should not have fired the handler no-longer-registered handler"); +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/test_xhr_withCredentials.html b/dom/xhr/tests/test_xhr_withCredentials.html new file mode 100644 index 000000000..71d7dcccf --- /dev/null +++ b/dom/xhr/tests/test_xhr_withCredentials.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=814050 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 814050</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=814050">Mozilla Bug 814050</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 814050, but modified now that the spec has changed **/ +var xhr = new XMLHttpRequest(); +xhr.open("GET", "", false); +xhr.withCredentials = true; +ok(true, "Should not throw on withCredentials sets for sync XHR"); + +var xhr = new XMLHttpRequest(); +xhr.open("GET", "", true); +xhr.withCredentials = true; +ok(true, "Should not throw on withCredentials sets for async XHR"); + +</script> +</pre> +</body> +</html> diff --git a/dom/xhr/tests/worker_bug1300552.js b/dom/xhr/tests/worker_bug1300552.js new file mode 100644 index 000000000..fbc58b880 --- /dev/null +++ b/dom/xhr/tests/worker_bug1300552.js @@ -0,0 +1,33 @@ +function info(msg) { + postMessage({type: 'info', msg: msg}); +} + +function ok(a, msg) { + postMessage({type: 'check', what: !!a, msg: msg}); +} + +function finish() { + postMessage({type: 'finish'}); +} + +info("Creating XHR..."); +var xhr = new XMLHttpRequest(); +xhr.open("POST", "echo.sjs"); +xhr.responseType = 'arraybuffer'; + +info("Sending some data..."); +var data = new Array(256).join("1234567890ABCDEF"); +xhr.send({toString: function() { return data; }}); + +var aborted = false; + +xhr.onprogress = function() { + info("Onprogress, we abort!"); + aborted = true; + xhr.abort(); +} + +xhr.onloadend = function() { + ok(aborted, "We are still alive after an abort()!"); + finish(); +} diff --git a/dom/xhr/tests/worker_file_getcookie.sjs b/dom/xhr/tests/worker_file_getcookie.sjs new file mode 100644 index 000000000..b5204bdd7 --- /dev/null +++ b/dom/xhr/tests/worker_file_getcookie.sjs @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +function handleRequest(request, response) { + try { + var cookie = request.getHeader("Cookie"); + } catch (e) { + cookie = "EMPTY_COOKIE"; + } + + // avoid confusing cache behaviors. + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-type", "text/plain", false); + response.setStatusLine(request.httpVersion, "200", "OK"); + response.write(cookie); +} diff --git a/dom/xhr/tests/worker_temporaryFileBlob.js b/dom/xhr/tests/worker_temporaryFileBlob.js new file mode 100644 index 000000000..db40f72a8 --- /dev/null +++ b/dom/xhr/tests/worker_temporaryFileBlob.js @@ -0,0 +1,29 @@ +importScripts('common_temporaryFileBlob.js'); + +function info(msg) { + postMessage({type: 'info', msg: msg}); +} + +function ok(a, msg) { + postMessage({type: 'check', what: !!a, msg: msg}); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function next() { + postMessage({type: 'finish'}); +} + +onmessage = function(e) { + if (e.data == 'simple') { + test_simple(); + } else if (e.data == 'abort') { + test_abort(); + } else if (e.data == 'reuse') { + test_reuse(); + } else { + ok(false, 'Something wrong happened'); + } +} diff --git a/dom/xhr/tests/worker_terminateSyncXHR_frame.html b/dom/xhr/tests/worker_terminateSyncXHR_frame.html new file mode 100644 index 000000000..04bd53ff1 --- /dev/null +++ b/dom/xhr/tests/worker_terminateSyncXHR_frame.html @@ -0,0 +1,25 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + </head> + <body> + <script type="text/javascript"> + function doStuff() { + var worker = new Worker("terminateSyncXHR_worker.js"); + + worker.onmessage = function(event) { + parent.postMessage(event.data, "*"); + }; + + worker.onerror = function(event) { + parent.postMessage("ERROR!", "*"); + } + } + </script> + </body> +</html> diff --git a/dom/xhr/tests/worker_testXHR.txt b/dom/xhr/tests/worker_testXHR.txt new file mode 100644 index 000000000..2beab22c6 --- /dev/null +++ b/dom/xhr/tests/worker_testXHR.txt @@ -0,0 +1 @@ +A noisy noise annoys an oyster.
\ No newline at end of file diff --git a/dom/xhr/tests/worker_xhr_cors_redirect.js b/dom/xhr/tests/worker_xhr_cors_redirect.js new file mode 100644 index 000000000..c006b4a2e --- /dev/null +++ b/dom/xhr/tests/worker_xhr_cors_redirect.js @@ -0,0 +1,10 @@ +onmessage = function(e) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', e.data, true); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + postMessage(xhr.status); + } + }; + xhr.send(); +}; diff --git a/dom/xhr/tests/worker_xhr_cors_redirect.sjs b/dom/xhr/tests/worker_xhr_cors_redirect.sjs new file mode 100644 index 000000000..07e8ed5dc --- /dev/null +++ b/dom/xhr/tests/worker_xhr_cors_redirect.sjs @@ -0,0 +1,10 @@ +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + + if (request.queryString == 'redirect') { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "worker_xhr_cors_redirect.sjs"); + } else { + response.write("'hello world'"); + } +} diff --git a/dom/xhr/tests/worker_xhr_headers_server.sjs b/dom/xhr/tests/worker_xhr_headers_server.sjs new file mode 100644 index 000000000..624abb9fc --- /dev/null +++ b/dom/xhr/tests/worker_xhr_headers_server.sjs @@ -0,0 +1,66 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +function handleRequest(request, response) { + switch (request.method) { + case "POST": + try { + var optionsHost = request.getHeader("options-host"); + } catch(e) { } + + var headerFound = false; + if (optionsHost) { + setState("postHost", request.host); + setState("optionsHost", optionsHost); + headerFound = true; + } + + try { + var emptyHeader = "nada" + request.getHeader("empty"); + } catch(e) { } + + if (emptyHeader && emptyHeader == "nada") { + setState("emptyHeader", "nada"); + headerFound = true; + } + if (headerFound) { + return; + } else { + break; + } + + case "OPTIONS": + if (getState("optionsHost") == request.host) { + try { + var optionsHeader = + request.getHeader("Access-Control-Request-Headers"); + } catch(e) { } + setState("optionsHeader", "'" + optionsHeader + "'"); + } + break; + + case "GET": + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + + if (getState("postHost") == request.host && + getState("emptyHeader") == "nada") { + var result = getState("optionsHeader"); + if (result) { + response.write("Success: expected OPTIONS request with " + result + + " header"); + } else if (getState("badGet") == 1) { + response.write("Error: unexpected GET request"); + } + } else { + setState("badGet", "1"); + response.write("Error: this response should never be seen"); + } + return; + } + + response.setStatusLine(request.httpVersion, 501, "Not Implemented"); +} diff --git a/dom/xhr/tests/worker_xhr_headers_worker.js b/dom/xhr/tests/worker_xhr_headers_worker.js new file mode 100644 index 000000000..af88b83ca --- /dev/null +++ b/dom/xhr/tests/worker_xhr_headers_worker.js @@ -0,0 +1,16 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +var customHeader = "custom-key"; +var customHeaderValue = "custom-key-value"; + +self.onmessage = function(event) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", event.data, false); + xhr.setRequestHeader(customHeader, customHeaderValue); + xhr.send(); + postMessage({ response: xhr.responseText, header: customHeader }); +} diff --git a/dom/xhr/tests/xhr2_worker.js b/dom/xhr/tests/xhr2_worker.js new file mode 100644 index 000000000..d355020c9 --- /dev/null +++ b/dom/xhr/tests/xhr2_worker.js @@ -0,0 +1,163 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function(event) { + const url = event.data; + + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.send(); + + const refText = xhr.responseText; + + function getResponse(type) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + if (type !== undefined) { + xhr.responseType = type; + } + xhr.send(); + return xhr.response; + } + + if (getResponse() != refText) { + throw new Error("unset responseType failed"); + } + + if (getResponse("") != refText) { + throw new Error("'' responseType failed"); + } + + if (getResponse("text") != refText) { + throw new Error("'text' responseType failed"); + } + + var array = new Uint8Array(getResponse("arraybuffer")); + if (String.fromCharCode.apply(String, array) != refText) { + throw new Error("'arraybuffer' responseType failed"); + } + + var blob = getResponse("blob"); + if (new FileReaderSync().readAsText(blob) != refText) { + throw new Error("'blob' responseType failed"); + } + + // Make sure that we get invalid state exceptions when getting the wrong + // property. + + function testResponseTextException(type) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.responseType = type; + xhr.send(); + + var exception; + + try { + xhr.responseText; + } + catch(e) { + exception = e; + } + + if (!exception) { + throw new Error("Failed to throw when getting responseText on '" + type + + "' type"); + } + + if (exception.name != "InvalidStateError") { + throw new Error("Unexpected error when getting responseText on '" + type + + "' type"); + } + + if (exception.code != DOMException.INVALID_STATE_ERR) { + throw new Error("Unexpected error code when getting responseText on '" + type + + "' type"); + } + } + + testResponseTextException("arraybuffer"); + testResponseTextException("blob"); + + // Make sure "document" works, but returns text. + xhr = new XMLHttpRequest(); + + if (xhr.responseType != "text") { + throw new Error("Default value for responseType is wrong!"); + } + + xhr.open("GET", url, false); + xhr.responseType = "document"; + xhr.send(); + + if (xhr.responseText != refText) { + throw new Error("'document' type not working correctly"); + } + + // Make sure setting responseType before open or after send fails. + var exception; + + xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.responseType = "text"; + xhr.onload = function(event) { + if (event.target.response != refText) { + throw new Error("Bad response!"); + } + + xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.responseType = "moz-chunked-text"; + + var lastIndex = 0; + xhr.onprogress = function(event) { + if (refText.substr(lastIndex, xhr.response.length) != xhr.response) { + throw new Error("Bad chunk!"); + } + + lastIndex += xhr.response.length; + }; + + xhr.onload = function(event) { + if (lastIndex != refText.length) { + throw new Error("Didn't see all the data!"); + } + + setTimeout(function() { + if (xhr.response !== null) { + throw new Error("Should have gotten null response outside of event!"); + } + postMessage("done"); + }, 0); + } + + xhr.send(null); + }; + xhr.send(); + + exception = null; + + try { + xhr.responseType = "arraybuffer"; + } + catch(e) { + exception = e; + } + + if (!exception) { + throw new Error("Failed to throw when setting responseType after " + + "calling send()"); + } + + if (exception.name != "InvalidStateError") { + throw new Error("Unexpected error when setting responseType after " + + "calling send()"); + } + + if (exception.code != DOMException.INVALID_STATE_ERR) { + throw new Error("Unexpected error code when setting responseType after " + + "calling send()"); + } +} diff --git a/dom/xhr/tests/xhrAbort_worker.js b/dom/xhr/tests/xhrAbort_worker.js new file mode 100644 index 000000000..281781761 --- /dev/null +++ b/dom/xhr/tests/xhrAbort_worker.js @@ -0,0 +1,92 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +function runTest() { + var xhr = new XMLHttpRequest(); + + var events = []; + function pushEvent(event) { + var readyState, responseText, status, statusText; + + try { + readyState = xhr.readyState; + } + catch (e) { + readyState = "[exception]"; + } + + try { + responseText = xhr.responseText; + } + catch (e) { + responseText = "[exception]"; + } + + try { + status = xhr.status; + } + catch (e) { + status = "[exception]"; + } + + try { + statusText = xhr.statusText; + } + catch (e) { + statusText = "[exception]"; + } + + var str = event.type + "(" + readyState + ", '" + responseText + "', " + + status + ", '" + statusText + "'"; + if ((("ProgressEvent" in this) && event instanceof ProgressEvent) || + (("WorkerProgressEvent" in this) && event instanceof WorkerProgressEvent)) { + str += ", progressEvent"; + } + str += ")"; + + events.push(str); + } + + xhr.onerror = function(event) { + throw new Error("Error: " + xhr.statusText); + } + + xhr.onload = function(event) { + throw new Error("Shouldn't have gotten load event!"); + }; + + var seenAbort; + xhr.onabort = function(event) { + if (seenAbort) { + throw new Error("Already seen the abort event!"); + } + seenAbort = true; + + pushEvent(event); + postMessage(events); + }; + + xhr.onreadystatechange = function(event) { + pushEvent(event); + if (xhr.readyState == xhr.HEADERS_RECEIVED) { + xhr.abort(); + } + }; + + xhr.open("GET", "worker_testXHR.txt"); + xhr.overrideMimeType("text/plain"); + xhr.send(null); +} + +function messageListener(event) { + switch (event.data) { + case "start": + runTest(); + break; + default: + throw new Error("Bad message!"); + } +} + +addEventListener("message", messageListener, false); diff --git a/dom/xhr/tests/xhr_implicit_cancel_worker.js b/dom/xhr/tests/xhr_implicit_cancel_worker.js new file mode 100644 index 000000000..2045452ca --- /dev/null +++ b/dom/xhr/tests/xhr_implicit_cancel_worker.js @@ -0,0 +1,10 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var xhr = new XMLHttpRequest(); +xhr.open("GET", "worker_testXHR.txt"); +xhr.send(null); +xhr.open("GET", "worker_testXHR.txt"); +xhr.send(null); +postMessage("done"); diff --git a/dom/xhr/tests/xhr_worker.js b/dom/xhr/tests/xhr_worker.js new file mode 100644 index 000000000..178c391b6 --- /dev/null +++ b/dom/xhr/tests/xhr_worker.js @@ -0,0 +1,84 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var xhr = new XMLHttpRequest(); + +function onload(event) { + if (event.target != xhr) { + throw "onload event.target != xhr"; + } + + if (event.target.status != 200) { + var message = { type: "error", + error: event.target.status }; + postMessage(message); + } + + var message = { type: "load", + data: xhr.responseText }; + postMessage(message); +} + +xhr.onload = onload; +xhr.addEventListener("load", onload, false); +xhr.removeEventListener("load", onload, false); +if (!xhr.onload) { + var message = { type: "error", + error: "Lost message listener!" }; + postMessage(message); +} + +xhr.onerror = function(event) { + if (event.target != xhr) { + throw "onerror event.target != xhr"; + } + var message = { type: "error", + error: event.target.status }; + postMessage(message); +}; +xhr.onerror = xhr.onerror; +if (!xhr.onerror || xhr.onerror != xhr.onerror) { + throw "onerror wasn't set properly"; +} + +function onprogress(event) { + if (event.target != xhr) { + throw "onprogress event.target != xhr"; + } + var message = { type: "progress", + current: event.loaded, + total: event.total }; + postMessage(message); +} +xhr.addEventListener("progress", onprogress, false); + +xhr.addEventListener("foopety", function(event) {}, false); +xhr.removeEventListener("doopety", function(event) {}, false); + +xhr.onloadend = function(event) { + var message = { type: "loadend" }; + postMessage(message); +} + +var upload = xhr.upload; +upload.onprogress = function(event) { }; +upload.addEventListener("foo", function(event) { }, false); +upload.removeEventListener("foo", function(event) { }, false); +upload.addEventListener("load", function(event) { }, false); +upload.removeEventListener("foo", function(event) { }, false); +upload.onload = function(event) { + var message = { type: "upload.load" }; + postMessage(message); +} + +onmessage = function(event) { + if (xhr.DONE != 4 || XMLHttpRequest.DONE != 4) { + throw "xhr constants not correct!"; + } + if (xhr.readystate > xhr.UNSENT) { + throw "XHR already running!"; + } + xhr.open("POST", event.data); + xhr.send("Data to send"); +} |