// Copyright (c) 2019 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "shell/browser/api/electron_api_web_request.h"

#include <memory>
#include <string>
#include <utility>

#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/memory/raw_ptr.h"
#include "base/stl_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "extensions/browser/api/web_request/web_request_resource_type.h"
#include "gin/converter.h"
#include "gin/dictionary.h"
#include "gin/object_template_builder.h"
#include "net/http/http_content_disposition.h"
#include "shell/browser/api/electron_api_session.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/frame_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/net_converter.h"
#include "shell/common/gin_converters/std_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"

static constexpr auto ResourceTypes =
    base::MakeFixedFlatMapSorted<base::StringPiece,
                                 extensions::WebRequestResourceType>({
        {"cspReport", extensions::WebRequestResourceType::CSP_REPORT},
        {"font", extensions::WebRequestResourceType::FONT},
        {"image", extensions::WebRequestResourceType::IMAGE},
        {"mainFrame", extensions::WebRequestResourceType::MAIN_FRAME},
        {"media", extensions::WebRequestResourceType::MEDIA},
        {"object", extensions::WebRequestResourceType::OBJECT},
        {"ping", extensions::WebRequestResourceType::PING},
        {"script", extensions::WebRequestResourceType::SCRIPT},
        {"stylesheet", extensions::WebRequestResourceType::STYLESHEET},
        {"subFrame", extensions::WebRequestResourceType::SUB_FRAME},
        {"webSocket", extensions::WebRequestResourceType::WEB_SOCKET},
        {"xhr", extensions::WebRequestResourceType::XHR},
    });

namespace gin {

template <>
struct Converter<extensions::WebRequestResourceType> {
  static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
                                   extensions::WebRequestResourceType type) {
    for (const auto& [name, val] : ResourceTypes)
      if (type == val)
        return StringToV8(isolate, name);

    return StringToV8(isolate, "other");
  }
};

}  // namespace gin

namespace electron::api {

namespace {

const char kUserDataKey[] = "WebRequest";

// BrowserContext <=> WebRequest relationship.
struct UserData : public base::SupportsUserData::Data {
  explicit UserData(WebRequest* data) : data(data) {}
  raw_ptr<WebRequest> data;
};

extensions::WebRequestResourceType ParseResourceType(base::StringPiece value) {
  if (const auto* iter = ResourceTypes.find(value); iter != ResourceTypes.end())
    return iter->second;

  return extensions::WebRequestResourceType::OTHER;
}

// Convert HttpResponseHeaders to V8.
//
// Note that while we already have converters for HttpResponseHeaders, we can
// not use it because it lowercases the header keys, while the webRequest has
// to pass the original keys.
v8::Local<v8::Value> HttpResponseHeadersToV8(
    net::HttpResponseHeaders* headers) {
  base::Value::Dict response_headers;
  if (headers) {
    size_t iter = 0;
    std::string key;
    std::string value;
    while (headers->EnumerateHeaderLines(&iter, &key, &value)) {
      // Note that Web servers not developed with nodejs allow non-utf8
      // characters in content-disposition's filename field. Use Chromium's
      // HttpContentDisposition class to decode the correct encoding instead of
      // arbitrarily converting it to UTF8. It should also be noted that if the
      // encoding is not specified, HttpContentDisposition will transcode
      // according to the system's encoding.
      if (base::EqualsCaseInsensitiveASCII("Content-Disposition", key) &&
          !value.empty()) {
        net::HttpContentDisposition header(value, std::string());
        std::string decodedFilename =
            header.is_attachment() ? " attachment" : " inline";
        // The filename must be encased in double quotes for serialization
        // to happen correctly.
        std::string filename = "\"" + header.filename() + "\"";
        value = decodedFilename + "; filename=" + filename;
      }
      base::Value::List* values = response_headers.FindList(key);
      if (!values)
        values = &response_headers.Set(key, base::Value::List())->GetList();
      values->Append(base::Value(value));
    }
  }
  return gin::ConvertToV8(v8::Isolate::GetCurrent(), response_headers);
}

// Overloaded by multiple types to fill the |details| object.
void ToDictionary(gin_helper::Dictionary* details,
                  extensions::WebRequestInfo* info) {
  details->Set("id", info->id);
  details->Set("url", info->url);
  details->Set("method", info->method);
  details->Set("timestamp", base::Time::Now().ToDoubleT() * 1000);
  details->Set("resourceType", info->web_request_type);
  if (!info->response_ip.empty())
    details->Set("ip", info->response_ip);
  if (info->response_headers) {
    details->Set("fromCache", info->response_from_cache);
    details->Set("statusLine", info->response_headers->GetStatusLine());
    details->Set("statusCode", info->response_headers->response_code());
    details->Set("responseHeaders",
                 HttpResponseHeadersToV8(info->response_headers.get()));
  }

  auto* render_frame_host = content::RenderFrameHost::FromID(
      info->render_process_id, info->frame_routing_id);
  if (render_frame_host) {
    details->SetGetter("frame", render_frame_host);
    auto* web_contents =
        content::WebContents::FromRenderFrameHost(render_frame_host);
    auto* api_web_contents = WebContents::From(web_contents);
    if (api_web_contents) {
      details->Set("webContents", api_web_contents);
      details->Set("webContentsId", api_web_contents->ID());
    }
  }
}

void ToDictionary(gin_helper::Dictionary* details,
                  const network::ResourceRequest& request) {
  details->Set("referrer", request.referrer);
  if (request.request_body)
    details->Set("uploadData", *request.request_body);
}

void ToDictionary(gin_helper::Dictionary* details,
                  const net::HttpRequestHeaders& headers) {
  details->Set("requestHeaders", headers);
}

void ToDictionary(gin_helper::Dictionary* details, const GURL& location) {
  details->Set("redirectURL", location);
}

void ToDictionary(gin_helper::Dictionary* details, int net_error) {
  details->Set("error", net::ErrorToString(net_error));
}

// Helper function to fill |details| with arbitrary |args|.
template <typename Arg>
void FillDetails(gin_helper::Dictionary* details, Arg arg) {
  ToDictionary(details, arg);
}

template <typename Arg, typename... Args>
void FillDetails(gin_helper::Dictionary* details, Arg arg, Args... args) {
  ToDictionary(details, arg);
  FillDetails(details, args...);
}

// Fill the native types with the result from the response object.
void ReadFromResponse(v8::Isolate* isolate,
                      gin::Dictionary* response,
                      GURL* new_location) {
  response->Get("redirectURL", new_location);
}

void ReadFromResponse(v8::Isolate* isolate,
                      gin::Dictionary* response,
                      net::HttpRequestHeaders* headers) {
  v8::Local<v8::Value> value;
  if (response->Get("requestHeaders", &value) && value->IsObject()) {
    headers->Clear();
    gin::Converter<net::HttpRequestHeaders>::FromV8(isolate, value, headers);
  }
}

void ReadFromResponse(v8::Isolate* isolate,
                      gin::Dictionary* response,
                      const std::pair<scoped_refptr<net::HttpResponseHeaders>*,
                                      const std::string>& headers) {
  std::string status_line;
  if (!response->Get("statusLine", &status_line))
    status_line = headers.second;
  v8::Local<v8::Value> value;
  if (response->Get("responseHeaders", &value) && value->IsObject()) {
    *headers.first = new net::HttpResponseHeaders("");
    (*headers.first)->ReplaceStatusLine(status_line);
    gin::Converter<net::HttpResponseHeaders*>::FromV8(isolate, value,
                                                      (*headers.first).get());
  }
}

}  // namespace

gin::WrapperInfo WebRequest::kWrapperInfo = {gin::kEmbedderNativeGin};

WebRequest::RequestFilter::RequestFilter(
    std::set<URLPattern> url_patterns,
    std::set<extensions::WebRequestResourceType> types)
    : url_patterns_(std::move(url_patterns)), types_(std::move(types)) {}
WebRequest::RequestFilter::RequestFilter(const RequestFilter&) = default;
WebRequest::RequestFilter::RequestFilter() = default;
WebRequest::RequestFilter::~RequestFilter() = default;

void WebRequest::RequestFilter::AddUrlPattern(URLPattern pattern) {
  url_patterns_.emplace(std::move(pattern));
}

void WebRequest::RequestFilter::AddType(
    extensions::WebRequestResourceType type) {
  types_.insert(type);
}

bool WebRequest::RequestFilter::MatchesURL(const GURL& url) const {
  if (url_patterns_.empty())
    return true;

  for (const auto& pattern : url_patterns_) {
    if (pattern.MatchesURL(url))
      return true;
  }
  return false;
}

bool WebRequest::RequestFilter::MatchesType(
    extensions::WebRequestResourceType type) const {
  return types_.empty() || base::Contains(types_, type);
}

bool WebRequest::RequestFilter::MatchesRequest(
    extensions::WebRequestInfo* info) const {
  return MatchesURL(info->url) && MatchesType(info->web_request_type);
}

WebRequest::SimpleListenerInfo::SimpleListenerInfo(RequestFilter filter_,
                                                   SimpleListener listener_)
    : filter(std::move(filter_)), listener(listener_) {}
WebRequest::SimpleListenerInfo::SimpleListenerInfo() = default;
WebRequest::SimpleListenerInfo::~SimpleListenerInfo() = default;

WebRequest::ResponseListenerInfo::ResponseListenerInfo(
    RequestFilter filter_,
    ResponseListener listener_)
    : filter(std::move(filter_)), listener(listener_) {}
WebRequest::ResponseListenerInfo::ResponseListenerInfo() = default;
WebRequest::ResponseListenerInfo::~ResponseListenerInfo() = default;

WebRequest::WebRequest(v8::Isolate* isolate,
                       content::BrowserContext* browser_context)
    : browser_context_(browser_context) {
  browser_context_->SetUserData(kUserDataKey, std::make_unique<UserData>(this));
}

WebRequest::~WebRequest() {
  browser_context_->RemoveUserData(kUserDataKey);
}

gin::ObjectTemplateBuilder WebRequest::GetObjectTemplateBuilder(
    v8::Isolate* isolate) {
  return gin::Wrappable<WebRequest>::GetObjectTemplateBuilder(isolate)
      .SetMethod(
          "onBeforeRequest",
          &WebRequest::SetResponseListener<ResponseEvent::kOnBeforeRequest>)
      .SetMethod(
          "onBeforeSendHeaders",
          &WebRequest::SetResponseListener<ResponseEvent::kOnBeforeSendHeaders>)
      .SetMethod(
          "onHeadersReceived",
          &WebRequest::SetResponseListener<ResponseEvent::kOnHeadersReceived>)
      .SetMethod("onSendHeaders",
                 &WebRequest::SetSimpleListener<SimpleEvent::kOnSendHeaders>)
      .SetMethod("onBeforeRedirect",
                 &WebRequest::SetSimpleListener<SimpleEvent::kOnBeforeRedirect>)
      .SetMethod(
          "onResponseStarted",
          &WebRequest::SetSimpleListener<SimpleEvent::kOnResponseStarted>)
      .SetMethod("onErrorOccurred",
                 &WebRequest::SetSimpleListener<SimpleEvent::kOnErrorOccurred>)
      .SetMethod("onCompleted",
                 &WebRequest::SetSimpleListener<SimpleEvent::kOnCompleted>);
}

const char* WebRequest::GetTypeName() {
  return "WebRequest";
}

bool WebRequest::HasListener() const {
  return !(simple_listeners_.empty() && response_listeners_.empty());
}

int WebRequest::OnBeforeRequest(extensions::WebRequestInfo* info,
                                const network::ResourceRequest& request,
                                net::CompletionOnceCallback callback,
                                GURL* new_url) {
  return HandleResponseEvent(ResponseEvent::kOnBeforeRequest, info,
                             std::move(callback), new_url, request);
}

int WebRequest::OnBeforeSendHeaders(extensions::WebRequestInfo* info,
                                    const network::ResourceRequest& request,
                                    BeforeSendHeadersCallback callback,
                                    net::HttpRequestHeaders* headers) {
  return HandleResponseEvent(
      ResponseEvent::kOnBeforeSendHeaders, info,
      base::BindOnce(std::move(callback), std::set<std::string>(),
                     std::set<std::string>()),
      headers, request, *headers);
}

int WebRequest::OnHeadersReceived(
    extensions::WebRequestInfo* info,
    const network::ResourceRequest& request,
    net::CompletionOnceCallback callback,
    const net::HttpResponseHeaders* original_response_headers,
    scoped_refptr<net::HttpResponseHeaders>* override_response_headers,
    GURL* allowed_unsafe_redirect_url) {
  const std::string& status_line =
      original_response_headers ? original_response_headers->GetStatusLine()
                                : std::string();
  return HandleResponseEvent(
      ResponseEvent::kOnHeadersReceived, info, std::move(callback),
      std::make_pair(override_response_headers, status_line), request);
}

void WebRequest::OnSendHeaders(extensions::WebRequestInfo* info,
                               const network::ResourceRequest& request,
                               const net::HttpRequestHeaders& headers) {
  HandleSimpleEvent(SimpleEvent::kOnSendHeaders, info, request, headers);
}

void WebRequest::OnBeforeRedirect(extensions::WebRequestInfo* info,
                                  const network::ResourceRequest& request,
                                  const GURL& new_location) {
  HandleSimpleEvent(SimpleEvent::kOnBeforeRedirect, info, request,
                    new_location);
}

void WebRequest::OnResponseStarted(extensions::WebRequestInfo* info,
                                   const network::ResourceRequest& request) {
  HandleSimpleEvent(SimpleEvent::kOnResponseStarted, info, request);
}

void WebRequest::OnErrorOccurred(extensions::WebRequestInfo* info,
                                 const network::ResourceRequest& request,
                                 int net_error) {
  callbacks_.erase(info->id);

  HandleSimpleEvent(SimpleEvent::kOnErrorOccurred, info, request, net_error);
}

void WebRequest::OnCompleted(extensions::WebRequestInfo* info,
                             const network::ResourceRequest& request,
                             int net_error) {
  callbacks_.erase(info->id);

  HandleSimpleEvent(SimpleEvent::kOnCompleted, info, request, net_error);
}

void WebRequest::OnRequestWillBeDestroyed(extensions::WebRequestInfo* info) {
  callbacks_.erase(info->id);
}

template <WebRequest::SimpleEvent event>
void WebRequest::SetSimpleListener(gin::Arguments* args) {
  SetListener<SimpleListener>(event, &simple_listeners_, args);
}

template <WebRequest::ResponseEvent event>
void WebRequest::SetResponseListener(gin::Arguments* args) {
  SetListener<ResponseListener>(event, &response_listeners_, args);
}

template <typename Listener, typename Listeners, typename Event>
void WebRequest::SetListener(Event event,
                             Listeners* listeners,
                             gin::Arguments* args) {
  v8::Local<v8::Value> arg;

  // { urls, types }.
  std::set<std::string> filter_patterns, filter_types;
  gin::Dictionary dict(args->isolate());
  if (args->GetNext(&arg) && !arg->IsFunction()) {
    // Note that gin treats Function as Dictionary when doing conversions, so we
    // have to explicitly check if the argument is Function before trying to
    // convert it to Dictionary.
    if (gin::ConvertFromV8(args->isolate(), arg, &dict)) {
      if (!dict.Get("urls", &filter_patterns)) {
        args->ThrowTypeError("Parameter 'filter' must have property 'urls'.");
        return;
      }
      dict.Get("types", &filter_types);
      args->GetNext(&arg);
    }
  }

  RequestFilter filter;

  for (const std::string& filter_pattern : filter_patterns) {
    URLPattern pattern(URLPattern::SCHEME_ALL);
    const URLPattern::ParseResult result = pattern.Parse(filter_pattern);
    if (result == URLPattern::ParseResult::kSuccess) {
      filter.AddUrlPattern(std::move(pattern));
    } else {
      const char* error_type = URLPattern::GetParseResultString(result);
      args->ThrowTypeError("Invalid url pattern " + filter_pattern + ": " +
                           error_type);
      return;
    }
  }

  for (const std::string& filter_type : filter_types) {
    auto type = ParseResourceType(filter_type);
    if (type != extensions::WebRequestResourceType::OTHER) {
      filter.AddType(type);
    } else {
      args->ThrowTypeError("Invalid type " + filter_type);
      return;
    }
  }

  // Function or null.
  Listener listener;
  if (arg.IsEmpty() ||
      !(gin::ConvertFromV8(args->isolate(), arg, &listener) || arg->IsNull())) {
    args->ThrowTypeError("Must pass null or a Function");
    return;
  }

  if (listener.is_null())
    listeners->erase(event);
  else
    (*listeners)[event] = {std::move(filter), std::move(listener)};
}

template <typename... Args>
void WebRequest::HandleSimpleEvent(SimpleEvent event,
                                   extensions::WebRequestInfo* request_info,
                                   Args... args) {
  const auto iter = simple_listeners_.find(event);
  if (iter == std::end(simple_listeners_))
    return;

  const auto& info = iter->second;
  if (!info.filter.MatchesRequest(request_info))
    return;

  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  v8::HandleScope handle_scope(isolate);
  gin_helper::Dictionary details(isolate, v8::Object::New(isolate));
  FillDetails(&details, request_info, args...);
  info.listener.Run(gin::ConvertToV8(isolate, details));
}

template <typename Out, typename... Args>
int WebRequest::HandleResponseEvent(ResponseEvent event,
                                    extensions::WebRequestInfo* request_info,
                                    net::CompletionOnceCallback callback,
                                    Out out,
                                    Args... args) {
  const auto iter = response_listeners_.find(event);
  if (iter == std::end(response_listeners_))
    return net::OK;

  const auto& info = iter->second;
  if (!info.filter.MatchesRequest(request_info))
    return net::OK;

  callbacks_[request_info->id] = std::move(callback);

  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  v8::HandleScope handle_scope(isolate);
  gin_helper::Dictionary details(isolate, v8::Object::New(isolate));
  FillDetails(&details, request_info, args...);

  ResponseCallback response =
      base::BindOnce(&WebRequest::OnListenerResult<Out>, base::Unretained(this),
                     request_info->id, out);
  info.listener.Run(gin::ConvertToV8(isolate, details), std::move(response));
  return net::ERR_IO_PENDING;
}

template <typename T>
void WebRequest::OnListenerResult(uint64_t id,
                                  T out,
                                  v8::Local<v8::Value> response) {
  const auto iter = callbacks_.find(id);
  if (iter == std::end(callbacks_))
    return;

  int result = net::OK;
  if (response->IsObject()) {
    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
    gin::Dictionary dict(isolate, response.As<v8::Object>());

    bool cancel = false;
    dict.Get("cancel", &cancel);
    if (cancel)
      result = net::ERR_BLOCKED_BY_CLIENT;
    else
      ReadFromResponse(isolate, &dict, out);
  }

  // The ProxyingURLLoaderFactory expects the callback to be executed
  // asynchronously, because it used to work on IO thread before NetworkService.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callbacks_[id]), result));
  callbacks_.erase(iter);
}

// static
gin::Handle<WebRequest> WebRequest::FromOrCreate(
    v8::Isolate* isolate,
    content::BrowserContext* browser_context) {
  gin::Handle<WebRequest> handle = From(isolate, browser_context);
  if (handle.IsEmpty()) {
    // Make sure the |Session| object has the |webRequest| property created.
    v8::Local<v8::Value> web_request =
        Session::CreateFrom(
            isolate, static_cast<ElectronBrowserContext*>(browser_context))
            ->WebRequest(isolate);
    gin::ConvertFromV8(isolate, web_request, &handle);
  }
  DCHECK(!handle.IsEmpty());
  return handle;
}

// static
gin::Handle<WebRequest> WebRequest::Create(
    v8::Isolate* isolate,
    content::BrowserContext* browser_context) {
  DCHECK(From(isolate, browser_context).IsEmpty())
      << "WebRequest already created";
  return gin::CreateHandle(isolate, new WebRequest(isolate, browser_context));
}

// static
gin::Handle<WebRequest> WebRequest::From(
    v8::Isolate* isolate,
    content::BrowserContext* browser_context) {
  if (!browser_context)
    return gin::Handle<WebRequest>();
  auto* user_data =
      static_cast<UserData*>(browser_context->GetUserData(kUserDataKey));
  if (!user_data)
    return gin::Handle<WebRequest>();
  return gin::CreateHandle(isolate, user_data->data.get());
}

}  // namespace electron::api
