// Copyright 2014 The Chromium Authors
// 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/response.h"

#include <memory>
#include <optional>

#include "base/memory/scoped_refptr.h"
#include "services/network/public/cpp/header_util.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_response.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/dictionary.h"
#include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
#include "third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_blob.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_form_data.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_readable_stream.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_response_init.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_response_type.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_url_search_params.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fetch/blob_bytes_consumer.h"
#include "third_party/blink/renderer/core/fetch/body_stream_buffer.h"
#include "third_party/blink/renderer/core/fetch/form_data_bytes_consumer.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/streams/readable_stream.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_view.h"
#include "third_party/blink/renderer/core/url/url_search_params.h"
#include "third_party/blink/renderer/platform/bindings/exception_messages.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/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/loader/cors/cors.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_utils.h"
#include "third_party/blink/renderer/platform/network/encoded_form_data.h"
#include "third_party/blink/renderer/platform/network/http_header_map.h"
#include "third_party/blink/renderer/platform/network/http_names.h"
#include "third_party/blink/renderer/platform/network/network_utils.h"

namespace blink {

namespace {

template <typename CorsHeadersContainer>
FetchResponseData* FilterResponseDataInternal(
    network::mojom::FetchResponseType response_type,
    FetchResponseData* response,
    CorsHeadersContainer& headers) {
  switch (response_type) {
    case network::mojom::FetchResponseType::kBasic:
      return response->CreateBasicFilteredResponse();
      break;
    case network::mojom::FetchResponseType::kCors: {
      HTTPHeaderSet header_names;
      for (const auto& header : headers)
        header_names.insert(header.Ascii());
      return response->CreateCorsFilteredResponse(header_names);
      break;
    }
    case network::mojom::FetchResponseType::kOpaque:
      return response->CreateOpaqueFilteredResponse();
      break;
    case network::mojom::FetchResponseType::kOpaqueRedirect:
      return response->CreateOpaqueRedirectFilteredResponse();
      break;
    case network::mojom::FetchResponseType::kDefault:
      return response;
      break;
    case network::mojom::FetchResponseType::kError:
      DCHECK_EQ(response->GetType(), network::mojom::FetchResponseType::kError);
      return response;
      break;
  }
  return response;
}

FetchResponseData* CreateFetchResponseDataFromFetchAPIResponse(
    ScriptState* script_state,
    mojom::blink::FetchAPIResponse& fetch_api_response) {
  FetchResponseData* response =
      Response::CreateUnfilteredFetchResponseDataWithoutBody(
          script_state, fetch_api_response);

  if (fetch_api_response.blob) {
    response->ReplaceBodyStreamBuffer(BodyStreamBuffer::Create(
        script_state,
        MakeGarbageCollected<BlobBytesConsumer>(
            ExecutionContext::From(script_state), fetch_api_response.blob),
        nullptr /* AbortSignal */, /*cached_metadata_handler=*/nullptr,
        fetch_api_response.side_data_blob));
  }

  // Filter the response according to |fetch_api_response|'s ResponseType.
  response =
      FilterResponseDataInternal(fetch_api_response.response_type, response,
                                 fetch_api_response.cors_exposed_header_names);

  return response;
}

// Checks whether |status| is a null body status.
// Spec: https://fetch.spec.whatwg.org/#null-body-status
bool IsNullBodyStatus(uint16_t status) {
  if (status == 101 || status == 204 || status == 205 || status == 304)
    return true;

  return false;
}

// Check whether |statusText| is a ByteString and
// matches the Reason-Phrase token production.
// RFC 2616: https://tools.ietf.org/html/rfc2616
// RFC 7230: https://tools.ietf.org/html/rfc7230
// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
bool IsValidReasonPhrase(const String& status_text) {
  for (unsigned i = 0; i < status_text.length(); ++i) {
    UChar c = status_text[i];
    if (!(c == 0x09                      // HTAB
          || (0x20 <= c && c <= 0x7E)    // SP / VCHAR
          || (0x80 <= c && c <= 0xFF)))  // obs-text
      return false;
  }
  return true;
}

}  // namespace

Response* Response::Create(ScriptState* script_state,
                           ExceptionState& exception_state) {
  return Create(script_state, nullptr, String(), ResponseInit::Create(),
                exception_state);
}

Response* Response::Create(ScriptState* script_state,
                           ScriptValue body_value,
                           const ResponseInit* init,
                           ExceptionState& exception_state) {
  v8::Local<v8::Value> body = body_value.V8Value();
  v8::Isolate* isolate = script_state->GetIsolate();
  ExecutionContext* execution_context = ExecutionContext::From(script_state);

  BodyStreamBuffer* body_buffer = nullptr;
  String content_type;
  if (body_value.IsUndefined() || body_value.IsNull()) {
    // Note: The IDL processor cannot handle this situation. See
    // https://crbug.com/335871.
  } else if (Blob* blob = V8Blob::ToWrappable(isolate, body)) {
    body_buffer = BodyStreamBuffer::Create(
        script_state,
        MakeGarbageCollected<BlobBytesConsumer>(execution_context,
                                                blob->GetBlobDataHandle()),
        nullptr /* AbortSignal */, /*cached_metadata_handler=*/nullptr);
    content_type = blob->type();
  } else if (body->IsArrayBuffer()) {
    // Avoid calling into V8 from the following constructor parameters, which
    // is potentially unsafe.
    DOMArrayBuffer* array_buffer =
        NativeValueTraits<DOMArrayBuffer>::NativeValue(isolate, body,
                                                       exception_state);
    if (exception_state.HadException())
      return nullptr;
    if (!base::CheckedNumeric<wtf_size_t>(array_buffer->ByteLength())
             .IsValid()) {
      exception_state.ThrowRangeError(
          "The provided ArrayBuffer exceeds the maximum supported size");
      return nullptr;
    } else {
      body_buffer = BodyStreamBuffer::Create(
          script_state,
          MakeGarbageCollected<FormDataBytesConsumer>(array_buffer),
          nullptr /* AbortSignal */, /*cached_metadata_handler=*/nullptr);
    }
  } else if (body->IsArrayBufferView()) {
    // Avoid calling into V8 from the following constructor parameters, which
    // is potentially unsafe.
    DOMArrayBufferView* array_buffer_view =
        NativeValueTraits<MaybeShared<DOMArrayBufferView>>::NativeValue(
            isolate, body, exception_state)
            .Get();
    if (exception_state.HadException())
      return nullptr;
    if (!base::CheckedNumeric<wtf_size_t>(array_buffer_view->byteLength())
             .IsValid()) {
      exception_state.ThrowRangeError(
          "The provided ArrayBufferView exceeds the maximum supported size");
      return nullptr;
    } else {
      body_buffer = BodyStreamBuffer::Create(
          script_state,
          MakeGarbageCollected<FormDataBytesConsumer>(array_buffer_view),
          nullptr /* AbortSignal */, /*cached_metadata_handler=*/nullptr);
    }
  } else if (FormData* form = V8FormData::ToWrappable(isolate, body)) {
    scoped_refptr<EncodedFormData> form_data = form->EncodeMultiPartFormData();
    content_type = form_data->FormatContentTypeWithBoundary();
    body_buffer = BodyStreamBuffer::Create(
        script_state,
        MakeGarbageCollected<FormDataBytesConsumer>(execution_context,
                                                    std::move(form_data)),
        nullptr /* AbortSignal */, /*cached_metadata_handler=*/nullptr);
  } else if (URLSearchParams* url_search_params =
                 V8URLSearchParams::ToWrappable(isolate, body)) {
    scoped_refptr<EncodedFormData> form_data =
        url_search_params->ToEncodedFormData();
    body_buffer = BodyStreamBuffer::Create(
        script_state,
        MakeGarbageCollected<FormDataBytesConsumer>(execution_context,
                                                    std::move(form_data)),
        nullptr /* AbortSignal */, /*cached_metadata_handler=*/nullptr);
    content_type = "application/x-www-form-urlencoded;charset=UTF-8";
  } else if (ReadableStream* stream =
                 V8ReadableStream::ToWrappable(isolate, body)) {
    UseCounter::Count(execution_context,
                      WebFeature::kFetchResponseConstructionWithStream);
    body_buffer = MakeGarbageCollected<BodyStreamBuffer>(
        script_state, stream, /*cached_metadata_handler=*/nullptr);
  } else {
    String string = NativeValueTraits<IDLUSVString>::NativeValue(
        isolate, body, exception_state);
    if (exception_state.HadException())
      return nullptr;
    body_buffer = BodyStreamBuffer::Create(
        script_state, MakeGarbageCollected<FormDataBytesConsumer>(string),
        nullptr /* AbortSignal */, /*cached_metadata_handler=*/nullptr);
    content_type = "text/plain;charset=UTF-8";
  }
  return Create(script_state, body_buffer, content_type, init, exception_state);
}

Response* Response::Create(ScriptState* script_state,
                           BodyStreamBuffer* body,
                           const String& content_type,
                           const ResponseInit* init,
                           ExceptionState& exception_state) {
  uint16_t status = init->status();

  // "1. If |init|'s status member is not in the range 200 to 599, inclusive,
  // throw a RangeError."
  if (status < 200 || 599 < status) {
    exception_state.ThrowRangeError(
        ExceptionMessages::IndexOutsideRange<unsigned>(
            "status", status, 200, ExceptionMessages::kInclusiveBound, 599,
            ExceptionMessages::kInclusiveBound));
    return nullptr;
  }

  // "2. If |init|'s statusText member does not match the Reason-Phrase
  // token production, throw a TypeError."
  if (!IsValidReasonPhrase(init->statusText())) {
    exception_state.ThrowTypeError("Invalid statusText");
    return nullptr;
  }

  // "3. Let |r| be a new Response object, associated with a new response.
  // "4. Set |r|'s headers to a new Headers object whose list is
  // |r|'s response's header list, and guard is "response" "
  Response* r =
      MakeGarbageCollected<Response>(ExecutionContext::From(script_state));
  // "5. Set |r|'s response's status to |init|'s status member."
  r->response_->SetStatus(init->status());

  // "6. Set |r|'s response's status message to |init|'s statusText member."
  r->response_->SetStatusMessage(AtomicString(init->statusText()));

  // "7. If |init|'s headers exists, then fill |r|’s headers with
  // |init|'s headers"
  if (init->hasHeaders()) {
    // "1. Empty |r|'s response's header list."
    r->response_->HeaderList()->ClearList();
    // "2. Fill |r|'s Headers object with |init|'s headers member. Rethrow
    // any exceptions."
    r->headers_->FillWith(script_state, init->headers(), exception_state);
    if (exception_state.HadException())
      return nullptr;
  }
  // "8. If body is non-null, then:"
  if (body) {
    // "1. If |init|'s status is a null body status, then throw a TypeError."
    if (IsNullBodyStatus(status)) {
      exception_state.ThrowTypeError(
          "Response with null body status cannot have body");
      return nullptr;
    }
    // "2. Let |Content-Type| be null."
    // "3. Set |r|'s response's body and |Content-Type|
    // to the result of extracting body."
    // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
    // Step 5, Blob:
    // "If object's type attribute is not the empty byte sequence, set
    // Content-Type to its value."
    r->response_->ReplaceBodyStreamBuffer(body);

    // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
    // Step 5, ReadableStream:
    // "If object is disturbed or locked, then throw a TypeError."
    // If the BodyStreamBuffer was not constructed from a ReadableStream
    // then IsStreamLocked and IsStreamDisturbed will always be false.
    // So we don't have to check BodyStreamBuffer is a ReadableStream
    // or not.
    if (body->IsStreamLocked() || body->IsStreamDisturbed()) {
      exception_state.ThrowTypeError(
          "Response body object should not be disturbed or locked");
      return nullptr;
    }

    // "4. If |Content-Type| is non-null and |r|'s response's header list
    // contains no header named `Content-Type`, append `Content-Type`/
    // |Content-Type| to |r|'s response's header list."
    if (!content_type.empty() &&
        !r->response_->HeaderList()->Has("Content-Type"))
      r->response_->HeaderList()->Append("Content-Type", content_type);
  }

  // "9. Set |r|'s MIME type to the result of extracting a MIME type
  // from |r|'s response's header list."
  r->response_->SetMimeType(r->response_->HeaderList()->ExtractMIMEType());

  // "10. Set |r|'s response’s HTTPS state to current settings object's"
  // HTTPS state."
  // "11. Resolve |r|'s trailer promise with a new Headers object whose
  // guard is "immutable"."
  // "12. Return |r|."
  return r;
}

Response* Response::Create(ExecutionContext* context,
                           FetchResponseData* response) {
  return MakeGarbageCollected<Response>(context, response);
}

Response* Response::Create(ScriptState* script_state,
                           mojom::blink::FetchAPIResponse& response) {
  auto* fetch_response_data =
      CreateFetchResponseDataFromFetchAPIResponse(script_state, response);
  return MakeGarbageCollected<Response>(ExecutionContext::From(script_state),
                                        fetch_response_data);
}

Response* Response::error(ScriptState* script_state) {
  FetchResponseData* response_data =
      FetchResponseData::CreateNetworkErrorResponse();
  Response* r = MakeGarbageCollected<Response>(
      ExecutionContext::From(script_state), response_data);
  r->headers_->SetGuard(Headers::kImmutableGuard);
  return r;
}

Response* Response::redirect(ScriptState* script_state,
                             const String& url,
                             uint16_t status,
                             ExceptionState& exception_state) {
  KURL parsed_url = ExecutionContext::From(script_state)->CompleteURL(url);
  if (!parsed_url.IsValid()) {
    exception_state.ThrowTypeError(StrCat({"Failed to parse URL from ", url}));
    return nullptr;
  }

  if (!network_utils::IsRedirectResponseCode(status)) {
    exception_state.ThrowRangeError("Invalid status code");
    return nullptr;
  }

  Response* r =
      MakeGarbageCollected<Response>(ExecutionContext::From(script_state));
  r->headers_->SetGuard(Headers::kImmutableGuard);
  r->response_->SetStatus(status);
  r->response_->HeaderList()->Set("Location", parsed_url);

  return r;
}

Response* Response::staticJson(ScriptState* script_state,
                               ScriptValue data,
                               const ResponseInit* init,
                               ExceptionState& exception_state) {
  // "1. Let bytes the result of running serialize a JavaScript value to JSON
  // bytes on data."
  v8::Local<v8::String> v8_string;
  TryRethrowScope rethrow_scope(script_state->GetIsolate(), exception_state);
  if (!v8::JSON::Stringify(script_state->GetContext(), data.V8Value())
           .ToLocal(&v8_string)) {
    return nullptr;
  }

  String string = ToBlinkString<String>(script_state->GetIsolate(), v8_string,
                                        kDoNotExternalize);

  // JSON.stringify can fail to produce a string value in one of two ways: it
  // can throw an exception (as with unserializable objects), or it can return
  // `undefined` (as with e.g. passing a function). If JSON.stringify returns
  // `undefined`, the v8 API then coerces it to the string value "undefined".
  // Check for this, and consider it a failure.
  if (string == "undefined") {
    exception_state.ThrowTypeError("The data is not JSON serializable");
    return nullptr;
  }

  BodyStreamBuffer* body_buffer = BodyStreamBuffer::Create(
      script_state, MakeGarbageCollected<FormDataBytesConsumer>(string),
      /*abort_signal=*/nullptr, /*cached_metadata_handler=*/nullptr);
  String content_type = "application/json";

  return Create(script_state, body_buffer, content_type, init, exception_state);
}

FetchResponseData* Response::CreateUnfilteredFetchResponseDataWithoutBody(
    ScriptState* script_state,
    mojom::blink::FetchAPIResponse& fetch_api_response) {
  FetchResponseData* response = nullptr;
  if (fetch_api_response.status_code > 0)
    response = FetchResponseData::Create();
  else
    response = FetchResponseData::CreateNetworkErrorResponse();

  response->SetPadding(fetch_api_response.padding);
  response->SetResponseSource(fetch_api_response.response_source);
  response->SetURLList(fetch_api_response.url_list);
  response->SetStatus(fetch_api_response.status_code);
  response->SetStatusMessage(AtomicString(fetch_api_response.status_text));
  response->SetRequestMethod(AtomicString(fetch_api_response.request_method));
  response->SetResponseTime(fetch_api_response.response_time);
  response->SetCacheStorageCacheName(
      fetch_api_response.cache_storage_cache_name);
  response->SetConnectionInfo(fetch_api_response.connection_info);
  response->SetAlpnNegotiatedProtocol(
      AtomicString(fetch_api_response.alpn_negotiated_protocol));
  response->SetWasFetchedViaSpdy(fetch_api_response.was_fetched_via_spdy);
  response->SetHasRangeRequested(fetch_api_response.has_range_requested);
  response->SetRequestIncludeCredentials(
      fetch_api_response.request_include_credentials);

  for (const auto& header : fetch_api_response.headers)
    response->HeaderList()->Append(header.key, header.value);

  // Use the |mime_type| provided by the FetchAPIResponse if its set.
  // Otherwise fall back to extracting the mime type from the headers.  This
  // can happen when the response is loaded from an older cache_storage
  // instance that did not yet store the mime_type value.
  if (!fetch_api_response.mime_type.IsNull())
    response->SetMimeType(fetch_api_response.mime_type);
  else
    response->SetMimeType(response->HeaderList()->ExtractMIMEType());

  return response;
}

FetchResponseData* Response::FilterResponseData(
    network::mojom::FetchResponseType response_type,
    FetchResponseData* response,
    Vector<String>& headers) {
  return FilterResponseDataInternal(response_type, response, headers);
}

V8ResponseType Response::type() const {
  // "The type attribute's getter must return response's type."
  switch (response_->GetType()) {
    case network::mojom::FetchResponseType::kBasic:
      return V8ResponseType(V8ResponseType::Enum::kBasic);
    case network::mojom::FetchResponseType::kCors:
      return V8ResponseType(V8ResponseType::Enum::kCors);
    case network::mojom::FetchResponseType::kDefault:
      return V8ResponseType(V8ResponseType::Enum::kDefault);
    case network::mojom::FetchResponseType::kError:
      return V8ResponseType(V8ResponseType::Enum::kError);
    case network::mojom::FetchResponseType::kOpaque:
      return V8ResponseType(V8ResponseType::Enum::kOpaque);
    case network::mojom::FetchResponseType::kOpaqueRedirect:
      return V8ResponseType(V8ResponseType::Enum::kOpaqueredirect);
  }
  NOTREACHED();
}

String Response::url() const {
  // "The url attribute's getter must return the empty string if response's
  // url is null and response's url, serialized with the exclude fragment
  // flag set, otherwise."
  const KURL* response_url = response_->Url();
  if (!response_url)
    return g_empty_string;
  if (!response_url->HasFragmentIdentifier())
    return *response_url;
  KURL url(*response_url);
  url.RemoveFragmentIdentifier();
  return url;
}

bool Response::redirected() const {
  return response_->UrlList().size() > 1;
}

uint16_t Response::status() const {
  // "The status attribute's getter must return response's status."
  return response_->Status();
}

bool Response::ok() const {
  // "The ok attribute's getter must return true
  // if response's status is in the range 200 to 299, and false otherwise."
  return network::IsSuccessfulStatus(status());
}

String Response::statusText() const {
  // "The statusText attribute's getter must return response's status message."
  return response_->StatusMessage();
}

Headers* Response::headers() const {
  // "The headers attribute's getter must return the associated Headers object."
  return headers_.Get();
}

Response* Response::clone(ScriptState* script_state,
                          ExceptionState& exception_state) {
  if (IsBodyLocked() || IsBodyUsed()) {
    exception_state.ThrowTypeError("Response body is already used");
    return nullptr;
  }

  FetchResponseData* response = response_->Clone(script_state, exception_state);
  if (exception_state.HadException())
    return nullptr;
  Headers* headers = Headers::Create(response->HeaderList());
  headers->SetGuard(headers_->GetGuard());
  return MakeGarbageCollected<Response>(GetExecutionContext(), response,
                                        headers);
}

mojom::blink::FetchAPIResponsePtr Response::PopulateFetchAPIResponse(
    const KURL& request_url) {
  return response_->PopulateFetchAPIResponse(request_url);
}

Response::Response(ExecutionContext* context)
    : Response(context, FetchResponseData::Create()) {}

Response::Response(ExecutionContext* context, FetchResponseData* response)
    : Response(context, response, Headers::Create(response->HeaderList())) {
  headers_->SetGuard(Headers::kResponseGuard);
}

Response::Response(ExecutionContext* context,
                   FetchResponseData* response,
                   Headers* headers)
    : Body(context), response_(response), headers_(headers) {}

bool Response::HasBody() const {
  return response_->InternalBuffer();
}

bool Response::IsBodyUsed() const {
  auto* body_buffer = InternalBodyBuffer();
  return body_buffer && body_buffer->IsStreamDisturbed();
}

String Response::MimeType() const {
  return response_->HeaderList()->ExtractMIMEType();
}

String Response::ContentType() const {
  String result;
  response_->HeaderList()->Get(http_names::kContentType, result);
  return result;
}

String Response::InternalMIMEType() const {
  return response_->InternalMIMEType();
}

const Vector<KURL>& Response::InternalURLList() const {
  return response_->InternalURLList();
}

FetchHeaderList* Response::InternalHeaderList() const {
  return response_->InternalHeaderList();
}

void Response::Trace(Visitor* visitor) const {
  ScriptWrappable::Trace(visitor);
  Body::Trace(visitor);
  visitor->Trace(response_);
  visitor->Trace(headers_);
}

}  // namespace blink
