| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/fetch/body.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "base/optional.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_array_buffer.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context.h" |
| #include "third_party/blink/renderer/core/fetch/body_stream_buffer.h" |
| #include "third_party/blink/renderer/core/fetch/fetch_data_loader.h" |
| #include "third_party/blink/renderer/core/fileapi/blob.h" |
| #include "third_party/blink/renderer/core/frame/web_feature.h" |
| #include "third_party/blink/renderer/core/html/forms/form_data.h" |
| #include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h" |
| #include "third_party/blink/renderer/core/typed_arrays/dom_typed_array.h" |
| #include "third_party/blink/renderer/core/url/url_search_params.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/bindings/script_state.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h" |
| #include "third_party/blink/renderer/platform/heap/disallow_new_wrapper.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/text_resource_decoder_options.h" |
| #include "third_party/blink/renderer/platform/network/parsed_content_type.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| class BodyConsumerBase : public GarbageCollected<BodyConsumerBase>, |
| public FetchDataLoader::Client { |
| public: |
| explicit BodyConsumerBase(ScriptPromiseResolver* resolver) |
| : resolver_(resolver), |
| task_runner_(ExecutionContext::From(resolver_->GetScriptState()) |
| ->GetTaskRunner(TaskType::kNetworking)) {} |
| ScriptPromiseResolver* Resolver() { return resolver_; } |
| void DidFetchDataLoadFailed() override { |
| ScriptState::Scope scope(Resolver()->GetScriptState()); |
| resolver_->Reject(V8ThrowException::CreateTypeError( |
| Resolver()->GetScriptState()->GetIsolate(), "Failed to fetch")); |
| } |
| |
| void Abort() override { |
| resolver_->Reject( |
| MakeGarbageCollected<DOMException>(DOMExceptionCode::kAbortError)); |
| } |
| |
| // Resource Timing event is not yet added, so delay the resolution timing |
| // a bit. See https://crbug.com/507169. |
| // TODO(yhirano): Fix this problem in a more sophisticated way. |
| template <typename T> |
| void ResolveLater(const T& object) { |
| task_runner_->PostTask(FROM_HERE, |
| WTF::Bind(&BodyConsumerBase::ResolveNow<T>, |
| WrapPersistent(this), object)); |
| } |
| |
| void Trace(Visitor* visitor) const override { |
| visitor->Trace(resolver_); |
| FetchDataLoader::Client::Trace(visitor); |
| } |
| |
| private: |
| template <typename T> |
| void ResolveNow(const T& object) { |
| resolver_->Resolve(object); |
| } |
| |
| const Member<ScriptPromiseResolver> resolver_; |
| const scoped_refptr<base::SingleThreadTaskRunner> task_runner_; |
| DISALLOW_COPY_AND_ASSIGN(BodyConsumerBase); |
| }; |
| |
| class BodyBlobConsumer final : public BodyConsumerBase { |
| public: |
| explicit BodyBlobConsumer(ScriptPromiseResolver* resolver) |
| : BodyConsumerBase(resolver) {} |
| |
| void DidFetchDataLoadedBlobHandle( |
| scoped_refptr<BlobDataHandle> blob_data_handle) override { |
| ResolveLater(WrapPersistent( |
| MakeGarbageCollected<Blob>(std::move(blob_data_handle)))); |
| } |
| DISALLOW_COPY_AND_ASSIGN(BodyBlobConsumer); |
| }; |
| |
| class BodyArrayBufferConsumer final : public BodyConsumerBase { |
| public: |
| explicit BodyArrayBufferConsumer(ScriptPromiseResolver* resolver) |
| : BodyConsumerBase(resolver) {} |
| |
| void DidFetchDataLoadedArrayBuffer(DOMArrayBuffer* array_buffer) override { |
| ResolveLater(WrapPersistent(array_buffer)); |
| } |
| DISALLOW_COPY_AND_ASSIGN(BodyArrayBufferConsumer); |
| }; |
| |
| class BodyFormDataConsumer final : public BodyConsumerBase { |
| public: |
| explicit BodyFormDataConsumer(ScriptPromiseResolver* resolver) |
| : BodyConsumerBase(resolver) {} |
| |
| void DidFetchDataLoadedFormData(FormData* formData) override { |
| ResolveLater(WrapPersistent(formData)); |
| } |
| |
| void DidFetchDataLoadedString(const String& string) override { |
| auto* formData = MakeGarbageCollected<FormData>(); |
| for (const auto& pair : URLSearchParams::Create(string)->Params()) |
| formData->append(pair.first, pair.second); |
| DidFetchDataLoadedFormData(formData); |
| } |
| DISALLOW_COPY_AND_ASSIGN(BodyFormDataConsumer); |
| }; |
| |
| class BodyTextConsumer final : public BodyConsumerBase { |
| public: |
| explicit BodyTextConsumer(ScriptPromiseResolver* resolver) |
| : BodyConsumerBase(resolver) {} |
| |
| void DidFetchDataLoadedString(const String& string) override { |
| ResolveLater(string); |
| } |
| DISALLOW_COPY_AND_ASSIGN(BodyTextConsumer); |
| }; |
| |
| class BodyJsonConsumer final : public BodyConsumerBase { |
| public: |
| explicit BodyJsonConsumer(ScriptPromiseResolver* resolver) |
| : BodyConsumerBase(resolver) {} |
| |
| void DidFetchDataLoadedString(const String& string) override { |
| if (!Resolver()->GetExecutionContext() || |
| Resolver()->GetExecutionContext()->IsContextDestroyed()) |
| return; |
| ScriptState::Scope scope(Resolver()->GetScriptState()); |
| v8::Isolate* isolate = Resolver()->GetScriptState()->GetIsolate(); |
| v8::Local<v8::String> input_string = V8String(isolate, string); |
| v8::TryCatch trycatch(isolate); |
| v8::Local<v8::Value> parsed; |
| if (v8::JSON::Parse(Resolver()->GetScriptState()->GetContext(), |
| input_string) |
| .ToLocal(&parsed)) { |
| ResolveLater(WrapPersistent(WrapDisallowNew( |
| ScriptValue(Resolver()->GetScriptState()->GetIsolate(), parsed)))); |
| } else |
| Resolver()->Reject(trycatch.Exception()); |
| } |
| DISALLOW_COPY_AND_ASSIGN(BodyJsonConsumer); |
| }; |
| |
| } // namespace |
| |
| ScriptPromise Body::arrayBuffer(ScriptState* script_state, |
| ExceptionState& exception_state) { |
| RejectInvalidConsumption(exception_state); |
| if (exception_state.HadException()) |
| return ScriptPromise(); |
| |
| // When the main thread sends a V8::TerminateExecution() signal to a worker |
| // thread, any V8 API on the worker thread starts returning an empty |
| // handle. This can happen in this function. To avoid the situation, we |
| // first check the ExecutionContext and return immediately if it's already |
| // gone (which means that the V8::TerminateExecution() signal has been sent |
| // to this worker thread). |
| if (!ExecutionContext::From(script_state)) |
| return ScriptPromise(); |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| if (BodyBuffer()) { |
| BodyBuffer()->StartLoading( |
| FetchDataLoader::CreateLoaderAsArrayBuffer(), |
| MakeGarbageCollected<BodyArrayBufferConsumer>(resolver), |
| exception_state); |
| if (exception_state.HadException()) { |
| // Need to resolve the ScriptPromiseResolver to avoid a DCHECK(). |
| resolver->Resolve(); |
| return ScriptPromise(); |
| } |
| } else { |
| resolver->Resolve(DOMArrayBuffer::Create(size_t{0}, size_t{0})); |
| } |
| return promise; |
| } |
| |
| ScriptPromise Body::blob(ScriptState* script_state, |
| ExceptionState& exception_state) { |
| RejectInvalidConsumption(exception_state); |
| if (exception_state.HadException()) |
| return ScriptPromise(); |
| |
| // See above comment. |
| if (!ExecutionContext::From(script_state)) |
| return ScriptPromise(); |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| if (BodyBuffer()) { |
| ExecutionContext* context = ExecutionContext::From(script_state); |
| BodyBuffer()->StartLoading( |
| FetchDataLoader::CreateLoaderAsBlobHandle( |
| MimeType(), context->GetTaskRunner(TaskType::kNetworking)), |
| MakeGarbageCollected<BodyBlobConsumer>(resolver), exception_state); |
| if (exception_state.HadException()) { |
| // Need to resolve the ScriptPromiseResolver to avoid a DCHECK(). |
| resolver->Resolve(); |
| return ScriptPromise(); |
| } |
| } else { |
| auto blob_data = std::make_unique<BlobData>(); |
| blob_data->SetContentType(MimeType()); |
| resolver->Resolve(MakeGarbageCollected<Blob>( |
| BlobDataHandle::Create(std::move(blob_data), 0))); |
| } |
| return promise; |
| } |
| |
| ScriptPromise Body::formData(ScriptState* script_state, |
| ExceptionState& exception_state) { |
| RejectInvalidConsumption(exception_state); |
| if (exception_state.HadException()) |
| return ScriptPromise(); |
| |
| // See above comment. |
| if (!ExecutionContext::From(script_state)) |
| return ScriptPromise(); |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| const ParsedContentType parsedTypeWithParameters(ContentType()); |
| const String parsedType = parsedTypeWithParameters.MimeType().LowerASCII(); |
| ScriptPromise promise = resolver->Promise(); |
| if (parsedType == "multipart/form-data") { |
| const String boundary = |
| parsedTypeWithParameters.ParameterValueForName("boundary"); |
| auto* body_buffer = BodyBuffer(); |
| if (body_buffer && !boundary.IsEmpty()) { |
| body_buffer->StartLoading( |
| FetchDataLoader::CreateLoaderAsFormData(boundary), |
| MakeGarbageCollected<BodyFormDataConsumer>(resolver), |
| exception_state); |
| if (exception_state.HadException()) { |
| // Need to resolve the ScriptPromiseResolver to avoid a DCHECK(). |
| resolver->Resolve(); |
| return ScriptPromise(); |
| } |
| return promise; |
| } |
| } else if (parsedType == "application/x-www-form-urlencoded") { |
| if (BodyBuffer()) { |
| // According to https://fetch.spec.whatwg.org/#concept-body-package-data |
| // application/x-www-form-urlencoded FormData bytes are parsed using |
| // https://url.spec.whatwg.org/#concept-urlencoded-parser |
| // which does not decode BOM. |
| BodyBuffer()->StartLoading( |
| FetchDataLoader::CreateLoaderAsString( |
| TextResourceDecoderOptions::CreateUTF8DecodeWithoutBOM()), |
| MakeGarbageCollected<BodyFormDataConsumer>(resolver), |
| exception_state); |
| if (exception_state.HadException()) { |
| // Need to resolve the ScriptPromiseResolver to avoid a DCHECK(). |
| resolver->Resolve(); |
| return ScriptPromise(); |
| } |
| } else { |
| resolver->Resolve(MakeGarbageCollected<FormData>()); |
| } |
| return promise; |
| } else { |
| if (BodyBuffer()) { |
| BodyBuffer()->StartLoading( |
| FetchDataLoader::CreateLoaderAsFailure(), |
| MakeGarbageCollected<BodyFormDataConsumer>(resolver), |
| exception_state); |
| if (exception_state.HadException()) { |
| // Need to resolve the ScriptPromiseResolver to avoid a DCHECK(). |
| resolver->Resolve(); |
| return ScriptPromise(); |
| } |
| return promise; |
| } |
| } |
| |
| resolver->Reject(V8ThrowException::CreateTypeError(script_state->GetIsolate(), |
| "Invalid MIME type")); |
| return promise; |
| } |
| |
| ScriptPromise Body::json(ScriptState* script_state, |
| ExceptionState& exception_state) { |
| RejectInvalidConsumption(exception_state); |
| if (exception_state.HadException()) |
| return ScriptPromise(); |
| |
| // See above comment. |
| if (!ExecutionContext::From(script_state)) |
| return ScriptPromise(); |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| if (BodyBuffer()) { |
| BodyBuffer()->StartLoading( |
| FetchDataLoader::CreateLoaderAsString( |
| TextResourceDecoderOptions::CreateUTF8Decode()), |
| MakeGarbageCollected<BodyJsonConsumer>(resolver), exception_state); |
| if (exception_state.HadException()) { |
| // Need to resolve the ScriptPromiseResolver to avoid a DCHECK(). |
| resolver->Resolve(); |
| return ScriptPromise(); |
| } |
| } else { |
| resolver->Reject(V8ThrowException::CreateSyntaxError( |
| script_state->GetIsolate(), "Unexpected end of input")); |
| } |
| return promise; |
| } |
| |
| ScriptPromise Body::text(ScriptState* script_state, |
| ExceptionState& exception_state) { |
| RejectInvalidConsumption(exception_state); |
| if (exception_state.HadException()) |
| return ScriptPromise(); |
| |
| // See above comment. |
| if (!ExecutionContext::From(script_state)) |
| return ScriptPromise(); |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| if (BodyBuffer()) { |
| BodyBuffer()->StartLoading( |
| FetchDataLoader::CreateLoaderAsString( |
| TextResourceDecoderOptions::CreateUTF8Decode()), |
| MakeGarbageCollected<BodyTextConsumer>(resolver), exception_state); |
| if (exception_state.HadException()) { |
| // Need to resolve the ScriptPromiseResolver to avoid a DCHECK(). |
| resolver->Resolve(); |
| return ScriptPromise(); |
| } |
| } else { |
| resolver->Resolve(String()); |
| } |
| return promise; |
| } |
| |
| ReadableStream* Body::body() { |
| if (auto* execution_context = GetExecutionContext()) { |
| if (execution_context->IsServiceWorkerGlobalScope()) { |
| execution_context->CountUse(WebFeature::kFetchBodyStreamInServiceWorker); |
| } else { |
| execution_context->CountUse( |
| WebFeature::kFetchBodyStreamOutsideServiceWorker); |
| } |
| } |
| |
| if (!BodyBuffer()) |
| return nullptr; |
| return BodyBuffer()->Stream(); |
| } |
| |
| bool Body::IsBodyUsed() const { |
| auto* body_buffer = BodyBuffer(); |
| return body_buffer && body_buffer->IsStreamDisturbed(); |
| } |
| |
| bool Body::IsBodyLocked() const { |
| auto* body_buffer = BodyBuffer(); |
| return body_buffer && body_buffer->IsStreamLocked(); |
| } |
| |
| bool Body::HasPendingActivity() const { |
| if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) |
| return false; |
| auto* body_buffer = BodyBuffer(); |
| if (!body_buffer) |
| return false; |
| return body_buffer->HasPendingActivity(); |
| } |
| |
| Body::Body(ExecutionContext* context) : ExecutionContextClient(context) {} |
| |
| void Body::RejectInvalidConsumption(ExceptionState& exception_state) const { |
| if (IsBodyLocked()) { |
| exception_state.ThrowTypeError("body stream is locked"); |
| } |
| |
| if (IsBodyUsed()) { |
| exception_state.ThrowTypeError("body stream already read"); |
| } |
| } |
| |
| } // namespace blink |