diff --git a/dom/bindings/Errors.msg b/dom/bindings/Errors.msg index e323c427e608..333a435513d4 100644 --- a/dom/bindings/Errors.msg +++ b/dom/bindings/Errors.msg @@ -60,3 +60,5 @@ MSG_DEF(MSG_INVALID_HEADER_VALUE, 1, "{0} is an invalid header value.") MSG_DEF(MSG_INVALID_HEADER_SEQUENCE, 0, "Headers require name/value tuples when being initialized by a sequence.") MSG_DEF(MSG_PERMISSION_DENIED_TO_PASS_ARG, 1, "Permission denied to pass cross-origin object as {0}.") MSG_DEF(MSG_MISSING_REQUIRED_DICTIONARY_MEMBER, 1, "Missing required {0}.") +MSG_DEF(MSG_INVALID_REQUEST_METHOD, 1, "Invalid request method {0}.") +MSG_DEF(MSG_REQUEST_BODY_CONSUMED_ERROR, 0, "Request body has already been consumed.") diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp new file mode 100644 index 000000000000..fbc87aa7515b --- /dev/null +++ b/dom/fetch/Fetch.cpp @@ -0,0 +1,100 @@ +/* -*- 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 "Fetch.h" + +#include "nsIStringStream.h" +#include "nsIUnicodeEncoder.h" + +#include "nsStringStream.h" + +#include "mozilla/dom/EncodingUtils.h" +#include "mozilla/dom/URLSearchParams.h" + +namespace mozilla { +namespace dom { + +nsresult +ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrScalarValueStringOrURLSearchParams& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentType) +{ + MOZ_ASSERT(aStream); + + nsresult rv; + nsCOMPtr byteStream; + if (aBodyInit.IsArrayBuffer()) { + const ArrayBuffer& buf = aBodyInit.GetAsArrayBuffer(); + buf.ComputeLengthAndData(); + //XXXnsm reinterpret_cast<> is used in DOMParser, should be ok. + rv = NS_NewByteInputStream(getter_AddRefs(byteStream), + reinterpret_cast(buf.Data()), + buf.Length(), NS_ASSIGNMENT_COPY); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (aBodyInit.IsArrayBufferView()) { + const ArrayBufferView& buf = aBodyInit.GetAsArrayBufferView(); + buf.ComputeLengthAndData(); + //XXXnsm reinterpret_cast<> is used in DOMParser, should be ok. + rv = NS_NewByteInputStream(getter_AddRefs(byteStream), + reinterpret_cast(buf.Data()), + buf.Length(), NS_ASSIGNMENT_COPY); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (aBodyInit.IsScalarValueString()) { + nsString str = aBodyInit.GetAsScalarValueString(); + + nsCOMPtr encoder = EncodingUtils::EncoderForEncoding("UTF-8"); + if (!encoder) { + return NS_ERROR_OUT_OF_MEMORY; + } + + int32_t destBufferLen; + rv = encoder->GetMaxLength(str.get(), str.Length(), &destBufferLen); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString encoded; + if (!encoded.SetCapacity(destBufferLen, fallible_t())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char* destBuffer = encoded.BeginWriting(); + int32_t srcLen = (int32_t) str.Length(); + int32_t outLen = destBufferLen; + rv = encoder->Convert(str.get(), &srcLen, destBuffer, &outLen); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(outLen <= destBufferLen); + encoded.SetLength(outLen); + rv = NS_NewCStringInputStream(getter_AddRefs(byteStream), encoded); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aContentType = NS_LITERAL_CSTRING("text/plain;charset=UTF-8"); + } else if (aBodyInit.IsURLSearchParams()) { + URLSearchParams& params = aBodyInit.GetAsURLSearchParams(); + nsString serialized; + params.Stringify(serialized); + rv = NS_NewStringInputStream(getter_AddRefs(byteStream), serialized); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + aContentType = NS_LITERAL_CSTRING("application/x-www-form-urlencoded;charset=UTF-8"); + } + + MOZ_ASSERT(byteStream); + byteStream.forget(aStream); + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h new file mode 100644 index 000000000000..86ba71c2b3b6 --- /dev/null +++ b/dom/fetch/Fetch.h @@ -0,0 +1,29 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_Fetch_h +#define mozilla_dom_Fetch_h + +#include "mozilla/dom/UnionTypes.h" + +class nsIInputStream; + +namespace mozilla { +namespace dom { + +/* + * Creates an nsIInputStream based on the fetch specifications 'extract a byte + * stream algorithm' - http://fetch.spec.whatwg.org/#concept-bodyinit-extract. + * Stores content type in out param aContentType. + */ +nsresult +ExtractByteStreamFromBody(const OwningArrayBufferOrArrayBufferViewOrScalarValueStringOrURLSearchParams& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentType); + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Fetch_h diff --git a/dom/fetch/Headers.cpp b/dom/fetch/Headers.cpp index 3b348db72afc..9109bc95d7e8 100644 --- a/dom/fetch/Headers.cpp +++ b/dom/fetch/Headers.cpp @@ -7,7 +7,6 @@ #include "mozilla/dom/Headers.h" #include "mozilla/ErrorResult.h" -#include "mozilla/dom/UnionTypes.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/Preferences.h" @@ -83,6 +82,39 @@ Headers::Constructor(const GlobalObject& aGlobal, return headers.forget(); } +// static +already_AddRefed +Headers::Constructor(const GlobalObject& aGlobal, + const OwningHeadersOrByteStringSequenceSequenceOrByteStringMozMap& aInit, + ErrorResult& aRv) +{ + nsRefPtr headers = new Headers(aGlobal.GetAsSupports()); + + if (aInit.IsHeaders()) { + headers->Fill(aInit.GetAsHeaders(), aRv); + } else if (aInit.IsByteStringSequenceSequence()) { + headers->Fill(aInit.GetAsByteStringSequenceSequence(), aRv); + } else if (aInit.IsByteStringMozMap()) { + headers->Fill(aInit.GetAsByteStringMozMap(), aRv); + } + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return headers.forget(); +} + +Headers::Headers(const Headers& aOther) + : mOwner(aOther.mOwner) + , mGuard(aOther.mGuard) +{ + SetIsDOMBinding(); + ErrorResult result; + Fill(aOther, result); + MOZ_ASSERT(!result.Failed()); +} + void Headers::Append(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv) @@ -202,6 +234,12 @@ Headers::Set(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv } } +void +Headers::Clear() +{ + mList.Clear(); +} + void Headers::SetGuard(HeadersGuardEnum aGuard, ErrorResult& aRv) { @@ -328,6 +366,5 @@ Headers::Fill(const MozMap& aInit, ErrorResult& aRv) Append(NS_ConvertUTF16toUTF8(keys[i]), aInit.Get(keys[i]), aRv); } } - } // namespace dom } // namespace mozilla diff --git a/dom/fetch/Headers.h b/dom/fetch/Headers.h index fab5f5c5311c..612d7198f9aa 100644 --- a/dom/fetch/Headers.h +++ b/dom/fetch/Headers.h @@ -8,6 +8,8 @@ #define mozilla_dom_Headers_h #include "mozilla/dom/HeadersBinding.h" +#include "mozilla/dom/UnionTypes.h" + #include "nsClassHashtable.h" #include "nsWrapperCache.h" @@ -42,7 +44,7 @@ private: nsCString mValue; }; - nsRefPtr mOwner; + nsCOMPtr mOwner; HeadersGuardEnum mGuard; nsTArray mList; @@ -54,6 +56,8 @@ public: SetIsDOMBinding(); } + explicit Headers(const Headers& aOther); + static bool PrefEnabled(JSContext* cx, JSObject* obj); static already_AddRefed @@ -61,6 +65,11 @@ public: const Optional& aInit, ErrorResult& aRv); + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + const OwningHeadersOrByteStringSequenceSequenceOrByteStringMozMap& aInit, + ErrorResult& aRv); + void Append(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv); void Delete(const nsACString& aName, ErrorResult& aRv); @@ -70,6 +79,8 @@ public: bool Has(const nsACString& aName, ErrorResult& aRv) const; void Set(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv); + void Clear(); + // ChromeOnly HeadersGuardEnum Guard() const { return mGuard; } void SetGuard(HeadersGuardEnum aGuard, ErrorResult& aRv); @@ -77,8 +88,13 @@ public: virtual JSObject* WrapObject(JSContext* aCx); nsISupports* GetParentObject() const { return mOwner; } + void Fill(const Headers& aInit, ErrorResult& aRv); private: - Headers(const Headers& aOther) MOZ_DELETE; + // Since Headers is also an nsISupports, the above constructor can + // accidentally be invoked as new Headers(Headers*[, implied None guard]) when + // the intention is to use the copy constructor. Explicitly disallow it. + Headers(Headers* aOther) MOZ_DELETE; + virtual ~Headers(); static bool IsSimpleHeader(const nsACString& aName, @@ -103,7 +119,6 @@ private: IsForbiddenResponseHeader(aName); } - void Fill(const Headers& aInit, ErrorResult& aRv); void Fill(const Sequence>& aInit, ErrorResult& aRv); void Fill(const MozMap& aInit, ErrorResult& aRv); }; diff --git a/dom/fetch/InternalRequest.cpp b/dom/fetch/InternalRequest.cpp new file mode 100644 index 000000000000..207869fa0a5c --- /dev/null +++ b/dom/fetch/InternalRequest.cpp @@ -0,0 +1,60 @@ +/* -*- 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 "InternalRequest.h" + +#include "nsIContentPolicy.h" +#include "nsIDocument.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/workers/Workers.h" + +#include "WorkerPrivate.h" + +namespace mozilla { +namespace dom { + +// The global is used to extract the principal. +already_AddRefed +InternalRequest::GetRequestConstructorCopy(nsIGlobalObject* aGlobal, ErrorResult& aRv) const +{ + nsRefPtr copy = new InternalRequest(); + copy->mURL.Assign(mURL); + copy->SetMethod(mMethod); + copy->mHeaders = new Headers(*mHeaders); + + copy->mBodyStream = mBodyStream; + copy->mPreserveContentCodings = true; + + if (NS_IsMainThread()) { + nsIPrincipal* principal = aGlobal->PrincipalOrNull(); + MOZ_ASSERT(principal); + aRv = nsContentUtils::GetASCIIOrigin(principal, copy->mOrigin); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } else { + workers::WorkerPrivate* worker = workers::GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + workers::WorkerPrivate::LocationInfo& location = worker->GetLocationInfo(); + copy->mOrigin = NS_ConvertUTF16toUTF8(location.mOrigin); + } + + copy->mMode = mMode; + copy->mCredentialsMode = mCredentialsMode; + // FIXME(nsm): Add ContentType fetch to nsIContentPolicy and friends. + // Then set copy's mContext to that. + return copy.forget(); +} + +InternalRequest::~InternalRequest() +{ +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/fetch/InternalRequest.h b/dom/fetch/InternalRequest.h new file mode 100644 index 000000000000..5b3e75d195fc --- /dev/null +++ b/dom/fetch/InternalRequest.h @@ -0,0 +1,264 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_InternalRequest_h +#define mozilla_dom_InternalRequest_h + +#include "mozilla/dom/Headers.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/UnionTypes.h" + +#include "nsIContentPolicy.h" +#include "nsIInputStream.h" +#include "nsISupportsImpl.h" + +class nsIDocument; +class nsPIDOMWindow; + +namespace mozilla { +namespace dom { + +class FetchBodyStream; +class Request; + +class InternalRequest MOZ_FINAL +{ + friend class Request; + +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InternalRequest) + + enum ContextFrameType + { + FRAMETYPE_AUXILIARY = 0, + FRAMETYPE_TOP_LEVEL, + FRAMETYPE_NESTED, + FRAMETYPE_NONE, + }; + + // Since referrer type can be none, client or a URL. + enum ReferrerType + { + REFERRER_NONE = 0, + REFERRER_CLIENT, + REFERRER_URL, + }; + + enum ResponseTainting + { + RESPONSETAINT_BASIC, + RESPONSETAINT_CORS, + RESPONSETAINT_OPAQUE, + }; + + explicit InternalRequest() + : mMethod("GET") + , mHeaders(new Headers(nullptr, HeadersGuardEnum::None)) + , mContextFrameType(FRAMETYPE_NONE) + , mReferrerType(REFERRER_CLIENT) + , mMode(RequestMode::No_cors) + , mCredentialsMode(RequestCredentials::Omit) + , mResponseTainting(RESPONSETAINT_BASIC) + , mRedirectCount(0) + , mAuthenticationFlag(false) + , mForceOriginHeader(false) + , mManualRedirect(false) + , mPreserveContentCodings(false) + , mSameOriginDataURL(false) + , mSkipServiceWorker(false) + , mSynchronous(false) + , mUnsafeRequest(false) + , mUseURLCredentials(false) + { + } + + explicit InternalRequest(const InternalRequest& aOther) + : mMethod(aOther.mMethod) + , mURL(aOther.mURL) + , mHeaders(aOther.mHeaders) + , mBodyStream(aOther.mBodyStream) + , mContext(aOther.mContext) + , mOrigin(aOther.mOrigin) + , mContextFrameType(aOther.mContextFrameType) + , mReferrerType(aOther.mReferrerType) + , mReferrerURL(aOther.mReferrerURL) + , mMode(aOther.mMode) + , mCredentialsMode(aOther.mCredentialsMode) + , mResponseTainting(aOther.mResponseTainting) + , mRedirectCount(aOther.mRedirectCount) + , mAuthenticationFlag(aOther.mAuthenticationFlag) + , mForceOriginHeader(aOther.mForceOriginHeader) + , mManualRedirect(aOther.mManualRedirect) + , mPreserveContentCodings(aOther.mPreserveContentCodings) + , mSameOriginDataURL(aOther.mSameOriginDataURL) + , mSandboxedStorageAreaURLs(aOther.mSandboxedStorageAreaURLs) + , mSkipServiceWorker(aOther.mSkipServiceWorker) + , mSynchronous(aOther.mSynchronous) + , mUnsafeRequest(aOther.mUnsafeRequest) + , mUseURLCredentials(aOther.mUseURLCredentials) + { + } + + void + GetMethod(nsCString& aMethod) const + { + aMethod.Assign(mMethod); + } + + void + SetMethod(const nsACString& aMethod) + { + mMethod.Assign(aMethod); + } + + void + GetURL(nsCString& aURL) const + { + aURL.Assign(mURL); + } + + bool + ReferrerIsNone() const + { + return mReferrerType == REFERRER_NONE; + } + + bool + ReferrerIsURL() const + { + return mReferrerType == REFERRER_URL; + } + + bool + ReferrerIsClient() const + { + return mReferrerType == REFERRER_CLIENT; + } + + nsCString + ReferrerAsURL() const + { + MOZ_ASSERT(ReferrerIsURL()); + return mReferrerURL; + } + + void + SetReferrer(const nsACString& aReferrer) + { + // May be removed later. + MOZ_ASSERT(!ReferrerIsNone()); + mReferrerType = REFERRER_URL; + mReferrerURL.Assign(aReferrer); + } + + bool + IsSynchronous() const + { + return mSynchronous; + } + + void + SetMode(RequestMode aMode) + { + mMode = aMode; + } + + void + SetCredentialsMode(RequestCredentials aCredentialsMode) + { + mCredentialsMode = aCredentialsMode; + } + + nsContentPolicyType + GetContext() const + { + return mContext; + } + + Headers* + Headers_() + { + return mHeaders; + } + + bool + ForceOriginHeader() + { + return mForceOriginHeader; + } + + void + GetOrigin(nsCString& aOrigin) const + { + aOrigin.Assign(mOrigin); + } + + void + SetBody(nsIInputStream* aStream) + { + mBodyStream = aStream; + } + + // Will return the original stream! + // Use a tee or copy if you don't want to erase the original. + void + GetBody(nsIInputStream** aStream) + { + nsCOMPtr s = mBodyStream; + s.forget(aStream); + } + + // The global is used as the client for the new object. + already_AddRefed + GetRequestConstructorCopy(nsIGlobalObject* aGlobal, ErrorResult& aRv) const; + +private: + ~InternalRequest(); + + void + SetURL(const nsACString& aURL) + { + mURL.Assign(aURL); + } + + nsCString mMethod; + nsCString mURL; + nsRefPtr mHeaders; + nsCOMPtr mBodyStream; + + // nsContentPolicyType does not cover the complete set defined in the spec, + // but it is a good start. + nsContentPolicyType mContext; + + nsCString mOrigin; + + ContextFrameType mContextFrameType; + ReferrerType mReferrerType; + + // When mReferrerType is REFERRER_URL. + nsCString mReferrerURL; + + RequestMode mMode; + RequestCredentials mCredentialsMode; + ResponseTainting mResponseTainting; + + uint32_t mRedirectCount; + + bool mAuthenticationFlag; + bool mForceOriginHeader; + bool mManualRedirect; + bool mPreserveContentCodings; + bool mSameOriginDataURL; + bool mSandboxedStorageAreaURLs; + bool mSkipServiceWorker; + bool mSynchronous; + bool mUnsafeRequest; + bool mUseURLCredentials; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_InternalRequest_h diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp index 08a172dc8c46..8bab8bce5e67 100644 --- a/dom/fetch/Request.cpp +++ b/dom/fetch/Request.cpp @@ -5,30 +5,44 @@ #include "Request.h" -#include "nsDOMString.h" -#include "nsISupportsImpl.h" +#include "nsIUnicodeDecoder.h" #include "nsIURI.h" + +#include "nsDOMFile.h" +#include "nsDOMString.h" +#include "nsNetUtil.h" #include "nsPIDOMWindow.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" #include "mozilla/ErrorResult.h" +#include "mozilla/dom/EncodingUtils.h" #include "mozilla/dom/Headers.h" +#include "mozilla/dom/Fetch.h" #include "mozilla/dom/Promise.h" +#include "mozilla/dom/URL.h" +#include "mozilla/dom/workers/bindings/URL.h" + +// dom/workers +#include "File.h" +#include "WorkerPrivate.h" namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTING_ADDREF(Request) NS_IMPL_CYCLE_COLLECTING_RELEASE(Request) -NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Request, mOwner, mHeaders) +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Request, mOwner) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Request) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END -Request::Request(nsISupports* aOwner) +Request::Request(nsIGlobalObject* aOwner, InternalRequest* aRequest) : mOwner(aOwner) - , mHeaders(new Headers(aOwner)) + , mRequest(aRequest) + , mBodyUsed(false) { SetIsDOMBinding(); } @@ -37,82 +51,393 @@ Request::~Request() { } -/*static*/ already_AddRefed -Request::Constructor(const GlobalObject& global, - const RequestOrScalarValueString& aInput, - const RequestInit& aInit, ErrorResult& rv) +already_AddRefed +Request::GetInternalRequest() { - nsRefPtr request = new Request(global.GetAsSupports()); - return request.forget(); + nsRefPtr r = mRequest; + return r.forget(); +} + +/*static*/ already_AddRefed +Request::Constructor(const GlobalObject& aGlobal, + const RequestOrScalarValueString& aInput, + const RequestInit& aInit, ErrorResult& aRv) +{ + nsRefPtr request; + + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + + if (aInput.IsRequest()) { + nsRefPtr inputReq = &aInput.GetAsRequest(); + if (inputReq->BodyUsed()) { + aRv.ThrowTypeError(MSG_REQUEST_BODY_CONSUMED_ERROR); + return nullptr; + } + + inputReq->SetBodyUsed(); + request = inputReq->GetInternalRequest(); + } else { + request = new InternalRequest(); + } + + request = request->GetRequestConstructorCopy(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RequestMode fallbackMode = RequestMode::EndGuard_; + RequestCredentials fallbackCredentials = RequestCredentials::EndGuard_; + if (aInput.IsScalarValueString()) { + nsString input; + input.Assign(aInput.GetAsScalarValueString()); + + nsString requestURL; + if (NS_IsMainThread()) { + nsCOMPtr window = do_QueryInterface(global); + MOZ_ASSERT(window); + nsCOMPtr docURI = window->GetDocumentURI(); + nsCString spec; + aRv = docURI->GetSpec(spec); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsRefPtr url = + dom::URL::Constructor(aGlobal, input, NS_ConvertUTF8toUTF16(spec), aRv); + if (aRv.Failed()) { + return nullptr; + } + + url->Stringify(requestURL, aRv); + if (aRv.Failed()) { + return nullptr; + } + } else { + workers::WorkerPrivate* worker = workers::GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + nsString baseURL = NS_ConvertUTF8toUTF16(worker->GetLocationInfo().mHref); + nsRefPtr url = + workers::URL::Constructor(aGlobal, input, baseURL, aRv); + if (aRv.Failed()) { + return nullptr; + } + + url->Stringify(requestURL, aRv); + if (aRv.Failed()) { + return nullptr; + } + } + request->SetURL(NS_ConvertUTF16toUTF8(requestURL)); + fallbackMode = RequestMode::Cors; + fallbackCredentials = RequestCredentials::Omit; + } + + RequestMode mode = aInit.mMode.WasPassed() ? aInit.mMode.Value() : fallbackMode; + RequestCredentials credentials = + aInit.mCredentials.WasPassed() ? aInit.mCredentials.Value() + : fallbackCredentials; + + if (mode != RequestMode::EndGuard_) { + request->SetMode(mode); + } + + if (credentials != RequestCredentials::EndGuard_) { + request->SetCredentialsMode(credentials); + } + + if (aInit.mMethod.WasPassed()) { + nsCString method = aInit.mMethod.Value(); + ToLowerCase(method); + + if (!method.EqualsASCII("options") && + !method.EqualsASCII("get") && + !method.EqualsASCII("head") && + !method.EqualsASCII("post") && + !method.EqualsASCII("put") && + !method.EqualsASCII("delete")) { + NS_ConvertUTF8toUTF16 label(method); + aRv.ThrowTypeError(MSG_INVALID_REQUEST_METHOD, &label); + return nullptr; + } + + ToUpperCase(method); + request->SetMethod(method); + } + + nsRefPtr domRequest = new Request(global, request); + nsRefPtr domRequestHeaders = domRequest->Headers_(); + + nsRefPtr headers; + if (aInit.mHeaders.WasPassed()) { + headers = Headers::Constructor(aGlobal, aInit.mHeaders.Value(), aRv); + if (aRv.Failed()) { + return nullptr; + } + } else { + headers = new Headers(*domRequestHeaders); + } + + domRequestHeaders->Clear(); + + if (domRequest->Mode() == RequestMode::No_cors) { + nsCString method; + domRequest->GetMethod(method); + ToLowerCase(method); + if (!method.EqualsASCII("get") && + !method.EqualsASCII("head") && + !method.EqualsASCII("post")) { + NS_ConvertUTF8toUTF16 label(method); + aRv.ThrowTypeError(MSG_INVALID_REQUEST_METHOD, &label); + return nullptr; + } + + domRequestHeaders->SetGuard(HeadersGuardEnum::Request_no_cors, aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + domRequestHeaders->Fill(*headers, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (aInit.mBody.WasPassed()) { + const OwningArrayBufferOrArrayBufferViewOrScalarValueStringOrURLSearchParams& bodyInit = aInit.mBody.Value(); + nsCOMPtr stream; + nsCString contentType; + aRv = ExtractByteStreamFromBody(bodyInit, + getter_AddRefs(stream), contentType); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + request->SetBody(stream); + + if (!contentType.IsVoid() && + !domRequestHeaders->Has(NS_LITERAL_CSTRING("Content-Type"), aRv)) { + domRequestHeaders->Append(NS_LITERAL_CSTRING("Content-Type"), + contentType, aRv); + } + + if (aRv.Failed()) { + return nullptr; + } + } + + // Extract mime type. + nsTArray contentTypeValues; + domRequestHeaders->GetAll(NS_LITERAL_CSTRING("Content-Type"), + contentTypeValues, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // HTTP ABNF states Content-Type may have only one value. + // This is from the "parse a header value" of the fetch spec. + if (contentTypeValues.Length() == 1) { + domRequest->mMimeType = contentTypeValues[0]; + ToLowerCase(domRequest->mMimeType); + } + + return domRequest.forget(); } already_AddRefed Request::Clone() const { - nsRefPtr request = new Request(mOwner); + // FIXME(nsm): Bug 1073231. This is incorrect, but the clone method isn't + // well defined yet. + nsRefPtr request = new Request(mOwner, + new InternalRequest(*mRequest)); return request.forget(); } +namespace { +nsresult +DecodeUTF8(const nsCString& aBuffer, nsString& aDecoded) +{ + nsCOMPtr decoder = + EncodingUtils::DecoderForEncoding("UTF-8"); + if (!decoder) { + return NS_ERROR_FAILURE; + } + + int32_t destBufferLen; + nsresult rv = + decoder->GetMaxLength(aBuffer.get(), aBuffer.Length(), &destBufferLen); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aDecoded.SetCapacity(destBufferLen, fallible_t())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char16_t* destBuffer = aDecoded.BeginWriting(); + int32_t srcLen = (int32_t) aBuffer.Length(); + int32_t outLen = destBufferLen; + rv = decoder->Convert(aBuffer.get(), &srcLen, destBuffer, &outLen); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(outLen <= destBufferLen); + aDecoded.SetLength(outLen); + return NS_OK; +} +} + +already_AddRefed +Request::ConsumeBody(ConsumeType aType, ErrorResult& aRv) +{ + nsRefPtr promise = Promise::Create(mOwner, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (BodyUsed()) { + aRv.ThrowTypeError(MSG_REQUEST_BODY_CONSUMED_ERROR); + return nullptr; + } + + SetBodyUsed(); + + // While the spec says to do this asynchronously, all the body constructors + // right now only accept bodies whose streams are backed by an in-memory + // buffer that can be read without blocking. So I think this is fine. + nsCOMPtr stream; + mRequest->GetBody(getter_AddRefs(stream)); + + if (!stream) { + aRv = NS_NewByteInputStream(getter_AddRefs(stream), "", 0, + NS_ASSIGNMENT_COPY); + if (aRv.Failed()) { + return nullptr; + } + } + + AutoJSAPI api; + api.Init(mOwner); + JSContext* cx = api.cx(); + + // We can make this assertion because for now we only support memory backed + // structures for the body argument for a Request. + MOZ_ASSERT(NS_InputStreamIsBuffered(stream)); + nsCString buffer; + uint64_t len; + aRv = stream->Available(&len); + if (aRv.Failed()) { + return nullptr; + } + + aRv = NS_ReadInputStreamToString(stream, buffer, len); + if (aRv.Failed()) { + return nullptr; + } + + buffer.SetLength(len); + + switch (aType) { + case CONSUME_ARRAYBUFFER: { + JS::Rooted arrayBuffer(cx); + arrayBuffer = + ArrayBuffer::Create(cx, buffer.Length(), + reinterpret_cast(buffer.get())); + JS::Rooted val(cx); + val.setObjectOrNull(arrayBuffer); + promise->MaybeResolve(cx, val); + return promise.forget(); + } + case CONSUME_BLOB: { + // XXXnsm it is actually possible to avoid these duplicate allocations + // for the Blob case by having the Blob adopt the stream's memory + // directly, but I've not added a special case for now. + // + // This is similar to nsContentUtils::CreateBlobBuffer, but also deals + // with worker wrapping. + uint32_t blobLen = buffer.Length(); + void* blobData = moz_malloc(blobLen); + nsCOMPtr blob; + if (blobData) { + memcpy(blobData, buffer.BeginReading(), blobLen); + blob = DOMFile::CreateMemoryFile(blobData, blobLen, + NS_ConvertUTF8toUTF16(mMimeType)); + } else { + aRv = NS_ERROR_OUT_OF_MEMORY; + return nullptr; + } + + JS::Rooted jsBlob(cx); + if (NS_IsMainThread()) { + aRv = nsContentUtils::WrapNative(cx, blob, &jsBlob); + if (aRv.Failed()) { + return nullptr; + } + } else { + jsBlob.setObject(*workers::file::CreateBlob(cx, blob)); + } + promise->MaybeResolve(cx, jsBlob); + return promise.forget(); + } + case CONSUME_JSON: { + nsString decoded; + aRv = DecodeUTF8(buffer, decoded); + if (aRv.Failed()) { + return nullptr; + } + + JS::Rooted json(cx); + if (!JS_ParseJSON(cx, decoded.get(), decoded.Length(), &json)) { + JS::Rooted exn(cx); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(cx, exn); + } + } + promise->MaybeResolve(cx, json); + return promise.forget(); + } + case CONSUME_TEXT: { + nsString decoded; + aRv = DecodeUTF8(buffer, decoded); + if (aRv.Failed()) { + return nullptr; + } + + promise->MaybeResolve(decoded); + return promise.forget(); + } + } + + NS_NOTREACHED("Unexpected consume body type"); + // Silence warnings. + return nullptr; +} + already_AddRefed Request::ArrayBuffer(ErrorResult& aRv) { - nsCOMPtr global = do_QueryInterface(GetParentObject()); - MOZ_ASSERT(global); - nsRefPtr promise = Promise::Create(global, aRv); - if (aRv.Failed()) { - return nullptr; - } - - promise->MaybeReject(NS_ERROR_NOT_AVAILABLE); - return promise.forget(); + return ConsumeBody(CONSUME_ARRAYBUFFER, aRv); } already_AddRefed Request::Blob(ErrorResult& aRv) { - nsCOMPtr global = do_QueryInterface(GetParentObject()); - MOZ_ASSERT(global); - nsRefPtr promise = Promise::Create(global, aRv); - if (aRv.Failed()) { - return nullptr; - } - - promise->MaybeReject(NS_ERROR_NOT_AVAILABLE); - return promise.forget(); + return ConsumeBody(CONSUME_BLOB, aRv); } already_AddRefed Request::Json(ErrorResult& aRv) { - nsCOMPtr global = do_QueryInterface(GetParentObject()); - MOZ_ASSERT(global); - nsRefPtr promise = Promise::Create(global, aRv); - if (aRv.Failed()) { - return nullptr; - } - - promise->MaybeReject(NS_ERROR_NOT_AVAILABLE); - return promise.forget(); + return ConsumeBody(CONSUME_JSON, aRv); } already_AddRefed Request::Text(ErrorResult& aRv) { - nsCOMPtr global = do_QueryInterface(GetParentObject()); - MOZ_ASSERT(global); - nsRefPtr promise = Promise::Create(global, aRv); - if (aRv.Failed()) { - return nullptr; - } - - promise->MaybeReject(NS_ERROR_NOT_AVAILABLE); - return promise.forget(); -} - -bool -Request::BodyUsed() -{ - return false; + return ConsumeBody(CONSUME_TEXT, aRv); } } // namespace dom } // namespace mozilla diff --git a/dom/fetch/Request.h b/dom/fetch/Request.h index 06f181a4f061..7d78891b7fa8 100644 --- a/dom/fetch/Request.h +++ b/dom/fetch/Request.h @@ -9,10 +9,12 @@ #include "nsISupportsImpl.h" #include "nsWrapperCache.h" +#include "mozilla/dom/InternalRequest.h" +// Required here due to certain WebIDL enums/classes being declared in both +// files. #include "mozilla/dom/RequestBinding.h" #include "mozilla/dom/UnionTypes.h" - class nsPIDOMWindow; namespace mozilla { @@ -28,7 +30,7 @@ class Request MOZ_FINAL : public nsISupports NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Request) public: - Request(nsISupports* aOwner); + Request(nsIGlobalObject* aOwner, InternalRequest* aRequest); JSObject* WrapObject(JSContext* aCx) @@ -39,34 +41,40 @@ public: void GetUrl(DOMString& aUrl) const { - aUrl.AsAString() = EmptyString(); + aUrl.AsAString() = NS_ConvertUTF8toUTF16(mRequest->mURL); } void GetMethod(nsCString& aMethod) const { - aMethod = EmptyCString(); + aMethod = mRequest->mMethod; } RequestMode Mode() const { - return RequestMode::Same_origin; + return mRequest->mMode; } RequestCredentials Credentials() const { - return RequestCredentials::Omit; + return mRequest->mCredentialsMode; } void GetReferrer(DOMString& aReferrer) const { - aReferrer.AsAString() = EmptyString(); + if (mRequest->ReferrerIsNone()) { + aReferrer.AsAString() = EmptyString(); + return; + } + + // FIXME(nsm): Spec doesn't say what to do if referrer is client. + aReferrer.AsAString() = NS_ConvertUTF8toUTF16(mRequest->mReferrerURL); } - Headers* Headers_() const { return mHeaders; } + Headers* Headers_() const { return mRequest->Headers_(); } static already_AddRefed Constructor(const GlobalObject& aGlobal, const RequestOrScalarValueString& aInput, @@ -93,12 +101,38 @@ public: Text(ErrorResult& aRv); bool - BodyUsed(); + BodyUsed() const + { + return mBodyUsed; + } + + already_AddRefed + GetInternalRequest(); private: + enum ConsumeType + { + CONSUME_ARRAYBUFFER, + CONSUME_BLOB, + // FormData not supported right now, + CONSUME_JSON, + CONSUME_TEXT, + }; + ~Request(); - nsCOMPtr mOwner; - nsRefPtr mHeaders; + already_AddRefed + ConsumeBody(ConsumeType aType, ErrorResult& aRv); + + void + SetBodyUsed() + { + mBodyUsed = true; + } + + nsCOMPtr mOwner; + nsRefPtr mRequest; + bool mBodyUsed; + nsCString mMimeType; }; } // namespace dom diff --git a/dom/fetch/moz.build b/dom/fetch/moz.build index 7853d318053d..c5491304cf5a 100644 --- a/dom/fetch/moz.build +++ b/dom/fetch/moz.build @@ -5,13 +5,17 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXPORTS.mozilla.dom += [ + 'Fetch.h', 'Headers.h', + 'InternalRequest.h', 'Request.h', 'Response.h', ] UNIFIED_SOURCES += [ + 'Fetch.cpp', 'Headers.cpp', + 'InternalRequest.cpp', 'Request.cpp', 'Response.cpp', ] diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h index daa8185fc2bf..3bbd647c5513 100644 --- a/dom/workers/WorkerPrivate.h +++ b/dom/workers/WorkerPrivate.h @@ -22,7 +22,7 @@ #include "nsString.h" #include "nsTArray.h" #include "nsThreadUtils.h" -#include "StructuredCloneTags.h" +#include "mozilla/dom/StructuredCloneTags.h" #include "Queue.h" #include "WorkerFeature.h" diff --git a/dom/workers/test/fetch/mochitest.ini b/dom/workers/test/fetch/mochitest.ini index 0a7a7f844435..ded2d970f35c 100644 --- a/dom/workers/test/fetch/mochitest.ini +++ b/dom/workers/test/fetch/mochitest.ini @@ -1,5 +1,7 @@ [DEFAULT] support-files = worker_interfaces.js + worker_test_request.js [test_interfaces.html] +[test_request.html] diff --git a/dom/workers/test/fetch/test_request.html b/dom/workers/test/fetch/test_request.html new file mode 100644 index 000000000000..73d7b5b9898e --- /dev/null +++ b/dom/workers/test/fetch/test_request.html @@ -0,0 +1,48 @@ + + + + + Bug XXXXXX - Test Request object in worker + + + + +

+ +

+
+
+
+
+
diff --git a/dom/workers/test/fetch/worker_test_request.js b/dom/workers/test/fetch/worker_test_request.js
new file mode 100644
index 000000000000..fde748e4b997
--- /dev/null
+++ b/dom/workers/test/fetch/worker_test_request.js
@@ -0,0 +1,215 @@
+function ok(a, msg) {
+  dump("OK: " + !!a + "  =>  " + a + " " + msg + "\n");
+  postMessage({type: 'status', status: !!a, msg: a + ": " + msg });
+}
+
+function is(a, b, msg) {
+  dump("IS: " + (a===b) + "  =>  " + a + " | " + b + " " + msg + "\n");
+  postMessage({type: 'status', status: a === b, msg: a + " === " + b + ": " + msg });
+}
+
+function testDefaultCtor() {
+  var req = new Request("");
+  is(req.method, "GET", "Default Request method is GET");
+  ok(req.headers instanceof Headers, "Request should have non-null Headers object");
+  is(req.url, self.location.href, "URL should be resolved with entry settings object's API base URL");
+  is(req.referrer, "", "Default referrer is `client` which serializes to empty string.");
+  is(req.mode, "cors", "Request mode for string input is cors");
+  is(req.credentials, "omit", "Default Request credentials is omit");
+
+  var req = new Request(req);
+  is(req.method, "GET", "Default Request method is GET");
+  ok(req.headers instanceof Headers, "Request should have non-null Headers object");
+  is(req.url, self.location.href, "URL should be resolved with entry settings object's API base URL");
+  is(req.referrer, "", "Default referrer is `client` which serializes to empty string.");
+  is(req.mode, "cors", "Request mode string input is cors");
+  is(req.credentials, "omit", "Default Request credentials is omit");
+}
+
+function testClone() {
+  var req = (new Request("./cloned_request.txt", {
+              method: 'POST',
+              headers: { "Content-Length": 5 },
+              body: "Sample body",
+              mode: "same-origin",
+              credentials: "same-origin",
+            })).clone();
+  ok(req.method === "POST", "Request method is POST");
+  ok(req.headers instanceof Headers, "Request should have non-null Headers object");
+  is(req.headers.get('content-length'), "5", "Request content-length should be 5.");
+  ok(req.url === (new URL("./cloned_request.txt", self.location.href)).href,
+       "URL should be resolved with entry settings object's API base URL");
+  ok(req.referrer === "", "Default referrer is `client` which serializes to empty string.");
+  ok(req.mode === "same-origin", "Request mode is same-origin");
+  ok(req.credentials === "same-origin", "Default credentials is same-origin");
+}
+
+function testUsedRequest() {
+  // Passing a used request should fail.
+  var req = new Request("", { body: "This is foo" });
+  var p1 = req.text().then(function(v) {
+    try {
+      var req2 = new Request(req);
+      ok(false, "Used Request cannot be passed to new Request");
+    } catch(e) {
+      ok(true, "Used Request cannot be passed to new Request");
+    }
+  });
+
+  // Passing a request should set the request as used.
+  var reqA = new Request("", { body: "This is foo" });
+  var reqB = new Request(reqA);
+  is(reqA.bodyUsed, true, "Passing a Request to another Request should set the former as used");
+  return p1;
+}
+
+function testSimpleUrlParse() {
+  // Just checks that the URL parser is actually being used.
+  var req = new Request("/file.html");
+  is(req.url, (new URL("/file.html", self.location.href)).href, "URL parser should be used to resolve Request URL");
+}
+
+function testMethod() {
+  var allowed = ["delete", "get", "head", "options", "post", "put"];
+  for (var i = 0; i < allowed.length; ++i) {
+    try {
+      var r = new Request("", { method: allowed[i] });
+      ok(true, "Method " + allowed[i] + " should be allowed");
+    } catch(e) {
+      ok(false, "Method " + allowed[i] + " should be allowed");
+    }
+  }
+
+  var forbidden = ["aardvark", "connect", "trace", "track"];
+  for (var i = 0; i < forbidden.length; ++i) {
+    try {
+      var r = new Request("", { method: forbidden[i] });
+      ok(false, "Method " + forbidden[i] + " should be forbidden");
+    } catch(e) {
+      ok(true, "Method " + forbidden[i] + " should be forbidden");
+    }
+  }
+
+  var allowedNoCors = ["get", "head", "post"];
+  for (var i = 0; i < allowedNoCors.length; ++i) {
+    try {
+      var r = new Request("", { method: allowedNoCors[i], mode: "no-cors" });
+      ok(true, "Method " + allowedNoCors[i] + " should be allowed in no-cors mode");
+    } catch(e) {
+      ok(false, "Method " + allowedNoCors[i] + " should be allowed in no-cors mode");
+    }
+  }
+
+  var forbiddenNoCors = ["aardvark", "delete", "options", "put"];
+  for (var i = 0; i < forbiddenNoCors.length; ++i) {
+    try {
+      var r = new Request("", { method: forbiddenNoCors[i], mode: "no-cors" });
+      ok(false, "Method " + forbiddenNoCors[i] + " should be forbidden in no-cors mode");
+    } catch(e) {
+      ok(true, "Method " + forbiddenNoCors[i] + " should be forbidden in no-cors mode");
+    }
+  }
+}
+
+function testUrlFragment() {
+  var req = new Request("./request#withfragment");
+  ok(req.url, (new URL("./request", self.location.href)).href, "request.url should be serialized with exclude fragment flag set");
+}
+
+function testBodyUsed() {
+  var req = new Request("./bodyused", { body: "Sample body" });
+  is(req.bodyUsed, false, "bodyUsed is initially false.");
+  return req.text().then((v) => {
+    is(v, "Sample body", "Body should match");
+    is(req.bodyUsed, true, "After reading body, bodyUsed should be true.");
+  }).then((v) => {
+    return req.blob().then((v) => {
+      ok(false, "Attempting to read body again should fail.");
+    }, (e) => {
+      ok(true, "Attempting to read body again should fail.");
+    })
+  });
+}
+
+// FIXME(nsm): Bug 1071290: We can't use Blobs as the body yet.
+function testBodyCreation() {
+  var text = "κόσμε";
+  var req1 = new Request("", { body: text });
+  var p1 = req1.text().then(function(v) {
+    ok(typeof v === "string", "Should resolve to string");
+    is(text, v, "Extracted string should match");
+  });
+
+  var req2 = new Request("", { body: new Uint8Array([72, 101, 108, 108, 111]) });
+  var p2 = req2.text().then(function(v) {
+    is("Hello", v, "Extracted string should match");
+  });
+
+  var req2b = new Request("", { body: (new Uint8Array([72, 101, 108, 108, 111])).buffer });
+  var p2b = req2b.text().then(function(v) {
+    is("Hello", v, "Extracted string should match");
+  });
+
+  var params = new URLSearchParams();
+  params.append("item", "Geckos");
+  params.append("feature", "stickyfeet");
+  params.append("quantity", "700");
+  var req3 = new Request("", { body: params });
+  var p3 = req3.text().then(function(v) {
+    var extracted = new URLSearchParams(v);
+    is(extracted.get("item"), "Geckos", "Param should match");
+    is(extracted.get("feature"), "stickyfeet", "Param should match");
+    is(extracted.get("quantity"), "700", "Param should match");
+  });
+
+  return Promise.all([p1, p2, p2b, p3]);
+}
+
+function testBodyExtraction() {
+  var text = "κόσμε";
+  var newReq = function() { return new Request("", { body: text }); }
+  return newReq().text().then(function(v) {
+    ok(typeof v === "string", "Should resolve to string");
+    is(text, v, "Extracted string should match");
+  }).then(function() {
+    return newReq().blob().then(function(v) {
+      ok(v instanceof Blob, "Should resolve to Blob");
+      var fs = new FileReaderSync();
+      is(fs.readAsText(v), text, "Decoded Blob should match original");
+    });
+  }).then(function() {
+    return newReq().json().then(function(v) {
+      ok(false, "Invalid json should reject");
+    }, function(e) {
+      ok(true, "Invalid json should reject");
+    })
+  }).then(function() {
+    return newReq().arrayBuffer().then(function(v) {
+      ok(v instanceof ArrayBuffer, "Should resolve to ArrayBuffer");
+      var dec = new TextDecoder();
+      is(dec.decode(new Uint8Array(v)), text, "UTF-8 decoded ArrayBuffer should match original");
+    });
+  })
+}
+
+onmessage = function() {
+  var done = function() { postMessage({ type: 'finish' }) }
+
+  testDefaultCtor();
+  testClone();
+  testSimpleUrlParse();
+  testUrlFragment();
+  testMethod();
+
+  Promise.resolve()
+    .then(testBodyCreation)
+    .then(testBodyUsed)
+    .then(testBodyExtraction)
+    .then(testUsedRequest)
+    // Put more promise based tests here.
+    .then(done)
+    .catch(function(e) {
+      ok(false, "Some Request tests failed " + e);
+      done();
+    })
+}