// Copyright (c) 2015 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_session.h"

#include <algorithm>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/guid.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/post_task.h"
#include "chrome/browser/browser_process.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_url_parameters.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/value_map_pref_store.h"
#include "components/proxy_config/proxy_config_dictionary.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "components/proxy_config/proxy_prefs.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/download_manager_delegate.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/storage_partition.h"
#include "gin/arguments.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/base/completion_repeating_callback.h"
#include "net/base/load_flags.h"
#include "net/http/http_auth_handler_factory.h"
#include "net/http/http_auth_preferences.h"
#include "net/http/http_cache.h"
#include "net/http/http_util.h"
#include "services/network/network_service.h"
#include "services/network/public/cpp/features.h"
#include "shell/browser/api/electron_api_app.h"
#include "shell/browser/api/electron_api_cookies.h"
#include "shell/browser/api/electron_api_data_pipe_holder.h"
#include "shell/browser/api/electron_api_download_item.h"
#include "shell/browser/api/electron_api_net_log.h"
#include "shell/browser/api/electron_api_protocol.h"
#include "shell/browser/api/electron_api_service_worker_context.h"
#include "shell/browser/api/electron_api_web_request.h"
#include "shell/browser/browser.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/electron_browser_main_parts.h"
#include "shell/browser/electron_permission_manager.h"
#include "shell/browser/javascript_environment.h"
#include "shell/browser/media/media_device_id_salt.h"
#include "shell/browser/net/cert_verifier_client.h"
#include "shell/browser/session_preferences.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/content_converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/net_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/node_includes.h"
#include "shell/common/options_switches.h"
#include "shell/common/process_util.h"
#include "ui/base/l10n/l10n_util.h"

#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
#include "extensions/browser/extension_registry.h"
#include "shell/browser/extensions/electron_extension_system.h"
#include "shell/common/gin_converters/extension_converter.h"
#endif

#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
#include "chrome/browser/spellchecker/spellcheck_factory.h"  // nogncheck
#include "chrome/browser/spellchecker/spellcheck_service.h"  // nogncheck
#include "components/spellcheck/browser/pref_names.h"
#include "components/spellcheck/common/spellcheck_common.h"

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
#include "components/spellcheck/browser/spellcheck_platform.h"
#include "components/spellcheck/common/spellcheck_features.h"
#endif
#endif

using content::BrowserThread;
using content::StoragePartition;

namespace predictors {
// NOTE(nornagon): this is copied from
// //chrome/browser/predictors/resource_prefetch_predictor.cc we don't need
// anything in that file other than this constructor. Without it we get a link
// error. Probably upstream the constructor should be moved to
// preconnect_manager.cc.
PreconnectRequest::PreconnectRequest(
    const url::Origin& origin,
    int num_sockets,
    const net::NetworkIsolationKey& network_isolation_key)
    : origin(origin),
      num_sockets(num_sockets),
      network_isolation_key(network_isolation_key) {
  DCHECK_GE(num_sockets, 0);
}
}  // namespace predictors

namespace {

struct ClearStorageDataOptions {
  GURL origin;
  uint32_t storage_types = StoragePartition::REMOVE_DATA_MASK_ALL;
  uint32_t quota_types = StoragePartition::QUOTA_MANAGED_STORAGE_MASK_ALL;
};

uint32_t GetStorageMask(const std::vector<std::string>& storage_types) {
  uint32_t storage_mask = 0;
  for (const auto& it : storage_types) {
    auto type = base::ToLowerASCII(it);
    if (type == "appcache")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_APPCACHE;
    else if (type == "cookies")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_COOKIES;
    else if (type == "filesystem")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_FILE_SYSTEMS;
    else if (type == "indexdb")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_INDEXEDDB;
    else if (type == "localstorage")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_LOCAL_STORAGE;
    else if (type == "shadercache")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_SHADER_CACHE;
    else if (type == "websql")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_WEBSQL;
    else if (type == "serviceworkers")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_SERVICE_WORKERS;
    else if (type == "cachestorage")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_CACHE_STORAGE;
  }
  return storage_mask;
}

uint32_t GetQuotaMask(const std::vector<std::string>& quota_types) {
  uint32_t quota_mask = 0;
  for (const auto& it : quota_types) {
    auto type = base::ToLowerASCII(it);
    if (type == "temporary")
      quota_mask |= StoragePartition::QUOTA_MANAGED_STORAGE_MASK_TEMPORARY;
    else if (type == "persistent")
      quota_mask |= StoragePartition::QUOTA_MANAGED_STORAGE_MASK_PERSISTENT;
    else if (type == "syncable")
      quota_mask |= StoragePartition::QUOTA_MANAGED_STORAGE_MASK_SYNCABLE;
  }
  return quota_mask;
}

}  // namespace

namespace gin {

template <>
struct Converter<ClearStorageDataOptions> {
  static bool FromV8(v8::Isolate* isolate,
                     v8::Local<v8::Value> val,
                     ClearStorageDataOptions* out) {
    gin_helper::Dictionary options;
    if (!ConvertFromV8(isolate, val, &options))
      return false;
    options.Get("origin", &out->origin);
    std::vector<std::string> types;
    if (options.Get("storages", &types))
      out->storage_types = GetStorageMask(types);
    if (options.Get("quotas", &types))
      out->quota_types = GetQuotaMask(types);
    return true;
  }
};

bool SSLProtocolVersionFromString(const std::string& version_str,
                                  network::mojom::SSLVersion* version) {
  if (version_str == switches::kSSLVersionTLSv1) {
    *version = network::mojom::SSLVersion::kTLS1;
    return true;
  }
  if (version_str == switches::kSSLVersionTLSv11) {
    *version = network::mojom::SSLVersion::kTLS11;
    return true;
  }
  if (version_str == switches::kSSLVersionTLSv12) {
    *version = network::mojom::SSLVersion::kTLS12;
    return true;
  }
  if (version_str == switches::kSSLVersionTLSv13) {
    *version = network::mojom::SSLVersion::kTLS13;
    return true;
  }
  return false;
}

template <>
struct Converter<uint16_t> {
  static bool FromV8(v8::Isolate* isolate,
                     v8::Local<v8::Value> val,
                     uint16_t* out) {
    auto maybe = val->IntegerValue(isolate->GetCurrentContext());
    if (maybe.IsNothing())
      return false;
    *out = maybe.FromJust();
    return true;
  }
};

template <>
struct Converter<network::mojom::SSLConfigPtr> {
  static bool FromV8(v8::Isolate* isolate,
                     v8::Local<v8::Value> val,
                     network::mojom::SSLConfigPtr* out) {
    gin_helper::Dictionary options;
    if (!ConvertFromV8(isolate, val, &options))
      return false;
    *out = network::mojom::SSLConfig::New();
    std::string version_min_str;
    if (options.Get("minVersion", &version_min_str)) {
      if (!SSLProtocolVersionFromString(version_min_str, &(*out)->version_min))
        return false;
    }
    std::string version_max_str;
    if (options.Get("maxVersion", &version_max_str)) {
      if (!SSLProtocolVersionFromString(version_max_str,
                                        &(*out)->version_max) ||
          (*out)->version_max < network::mojom::SSLVersion::kTLS12)
        return false;
    }

    if (options.Has("disabledCipherSuites") &&
        !options.Get("disabledCipherSuites", &(*out)->disabled_cipher_suites)) {
      return false;
    }
    std::sort((*out)->disabled_cipher_suites.begin(),
              (*out)->disabled_cipher_suites.end());

    // TODO(nornagon): also support other SSLConfig properties?
    return true;
  }
};

}  // namespace gin

namespace electron {

namespace api {

namespace {

const char kPersistPrefix[] = "persist:";

void DownloadIdCallback(content::DownloadManager* download_manager,
                        const base::FilePath& path,
                        const std::vector<GURL>& url_chain,
                        const std::string& mime_type,
                        int64_t offset,
                        int64_t length,
                        const std::string& last_modified,
                        const std::string& etag,
                        const base::Time& start_time,
                        uint32_t id) {
  download_manager->CreateDownloadItem(
      base::GenerateGUID(), id, path, path, url_chain, GURL(), GURL(), GURL(),
      GURL(), base::nullopt, mime_type, mime_type, start_time, base::Time(),
      etag, last_modified, offset, length, std::string(),
      download::DownloadItem::INTERRUPTED,
      download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
      download::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT, false, base::Time(),
      false, std::vector<download::DownloadItem::ReceivedSlice>());
}

#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
class DictionaryObserver final : public SpellcheckCustomDictionary::Observer {
 private:
  std::unique_ptr<gin_helper::Promise<std::set<std::string>>> promise_;
  base::WeakPtr<SpellcheckService> spellcheck_;

 public:
  DictionaryObserver(gin_helper::Promise<std::set<std::string>> promise,
                     base::WeakPtr<SpellcheckService> spellcheck)
      : spellcheck_(spellcheck) {
    promise_ = std::make_unique<gin_helper::Promise<std::set<std::string>>>(
        std::move(promise));
    if (spellcheck_)
      spellcheck_->GetCustomDictionary()->AddObserver(this);
  }

  ~DictionaryObserver() {
    if (spellcheck_)
      spellcheck_->GetCustomDictionary()->RemoveObserver(this);
  }

  void OnCustomDictionaryLoaded() override {
    if (spellcheck_) {
      promise_->Resolve(spellcheck_->GetCustomDictionary()->GetWords());
    } else {
      promise_->RejectWithErrorMessage(
          "Spellcheck in unexpected state: failed to load custom dictionary.");
    }
    delete this;
  }

  void OnCustomDictionaryChanged(
      const SpellcheckCustomDictionary::Change& dictionary_change) override {
    // noop
  }
};
#endif  // BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)

struct UserDataLink : base::SupportsUserData::Data {
  explicit UserDataLink(Session* ses) : session(ses) {}

  Session* session;
};

const void* kElectronApiSessionKey = &kElectronApiSessionKey;

}  // namespace

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

Session::Session(v8::Isolate* isolate, ElectronBrowserContext* browser_context)
    : network_emulation_token_(base::UnguessableToken::Create()),
      browser_context_(browser_context) {
  // Observe DownloadManager to get download notifications.
  content::BrowserContext::GetDownloadManager(browser_context)
      ->AddObserver(this);

  new SessionPreferences(browser_context);

  protocol_.Reset(isolate, Protocol::Create(isolate, browser_context).ToV8());

  browser_context->SetUserData(kElectronApiSessionKey,
                               std::make_unique<UserDataLink>(this));

#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
  SpellcheckService* service =
      SpellcheckServiceFactory::GetForContext(browser_context_);
  if (service) {
    service->SetHunspellObserver(this);
  }
#endif

#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
  extensions::ExtensionRegistry::Get(browser_context)->AddObserver(this);
#endif
}

Session::~Session() {
  content::BrowserContext::GetDownloadManager(browser_context())
      ->RemoveObserver(this);

#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
  SpellcheckService* service =
      SpellcheckServiceFactory::GetForContext(browser_context_);
  if (service) {
    service->SetHunspellObserver(nullptr);
  }
#endif

#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
  extensions::ExtensionRegistry::Get(browser_context())->RemoveObserver(this);
#endif
}

void Session::OnDownloadCreated(content::DownloadManager* manager,
                                download::DownloadItem* item) {
  if (item->IsSavePackageDownload())
    return;

  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  v8::Locker locker(isolate);
  v8::HandleScope handle_scope(isolate);
  auto handle = DownloadItem::FromOrCreate(isolate, item);
  if (item->GetState() == download::DownloadItem::INTERRUPTED)
    handle->SetSavePath(item->GetTargetFilePath());
  content::WebContents* web_contents =
      content::DownloadItemUtils::GetWebContents(item);
  bool prevent_default = Emit("will-download", handle, web_contents);
  if (prevent_default) {
    item->Cancel(true);
    item->Remove();
  }
}

#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
void Session::OnHunspellDictionaryInitialized(const std::string& language) {
  Emit("spellcheck-dictionary-initialized", language);
}
void Session::OnHunspellDictionaryDownloadBegin(const std::string& language) {
  Emit("spellcheck-dictionary-download-begin", language);
}
void Session::OnHunspellDictionaryDownloadSuccess(const std::string& language) {
  Emit("spellcheck-dictionary-download-success", language);
}
void Session::OnHunspellDictionaryDownloadFailure(const std::string& language) {
  Emit("spellcheck-dictionary-download-failure", language);
}
#endif

v8::Local<v8::Promise> Session::ResolveProxy(gin::Arguments* args) {
  v8::Isolate* isolate = args->isolate();
  gin_helper::Promise<std::string> promise(isolate);
  v8::Local<v8::Promise> handle = promise.GetHandle();

  GURL url;
  args->GetNext(&url);

  browser_context_->GetResolveProxyHelper()->ResolveProxy(
      url, base::BindOnce(gin_helper::Promise<std::string>::ResolvePromise,
                          std::move(promise)));

  return handle;
}

v8::Local<v8::Promise> Session::GetCacheSize() {
  auto* isolate = JavascriptEnvironment::GetIsolate();
  gin_helper::Promise<int64_t> promise(isolate);
  auto handle = promise.GetHandle();

  content::BrowserContext::GetDefaultStoragePartition(browser_context_)
      ->GetNetworkContext()
      ->ComputeHttpCacheSize(
          base::Time(), base::Time::Max(),
          base::BindOnce(
              [](gin_helper::Promise<int64_t> promise, bool is_upper_bound,
                 int64_t size_or_error) {
                if (size_or_error < 0) {
                  promise.RejectWithErrorMessage(
                      net::ErrorToString(size_or_error));
                } else {
                  promise.Resolve(size_or_error);
                }
              },
              std::move(promise)));

  return handle;
}

v8::Local<v8::Promise> Session::ClearCache() {
  auto* isolate = JavascriptEnvironment::GetIsolate();
  gin_helper::Promise<void> promise(isolate);
  auto handle = promise.GetHandle();

  content::BrowserContext::GetDefaultStoragePartition(browser_context_)
      ->GetNetworkContext()
      ->ClearHttpCache(base::Time(), base::Time::Max(), nullptr,
                       base::BindOnce(gin_helper::Promise<void>::ResolvePromise,
                                      std::move(promise)));

  return handle;
}

v8::Local<v8::Promise> Session::ClearStorageData(gin::Arguments* args) {
  v8::Isolate* isolate = args->isolate();
  gin_helper::Promise<void> promise(isolate);
  v8::Local<v8::Promise> handle = promise.GetHandle();

  ClearStorageDataOptions options;
  args->GetNext(&options);

  auto* storage_partition =
      content::BrowserContext::GetStoragePartition(browser_context(), nullptr);
  if (options.storage_types & StoragePartition::REMOVE_DATA_MASK_COOKIES) {
    // Reset media device id salt when cookies are cleared.
    // https://w3c.github.io/mediacapture-main/#dom-mediadeviceinfo-deviceid
    MediaDeviceIDSalt::Reset(browser_context()->prefs());
  }

  storage_partition->ClearData(
      options.storage_types, options.quota_types, options.origin, base::Time(),
      base::Time::Max(),
      base::BindOnce(gin_helper::Promise<void>::ResolvePromise,
                     std::move(promise)));
  return handle;
}

void Session::FlushStorageData() {
  auto* storage_partition =
      content::BrowserContext::GetStoragePartition(browser_context(), nullptr);
  storage_partition->Flush();
}

v8::Local<v8::Promise> Session::SetProxy(gin::Arguments* args) {
  v8::Isolate* isolate = args->isolate();
  gin_helper::Promise<void> promise(isolate);
  v8::Local<v8::Promise> handle = promise.GetHandle();

  gin_helper::Dictionary options;
  args->GetNext(&options);

  if (!browser_context_->in_memory_pref_store()) {
    promise.Resolve();
    return handle;
  }

  std::string mode, proxy_rules, bypass_list, pac_url;

  options.Get("pacScript", &pac_url);
  options.Get("proxyRules", &proxy_rules);
  options.Get("proxyBypassRules", &bypass_list);

  ProxyPrefs::ProxyMode proxy_mode = ProxyPrefs::MODE_FIXED_SERVERS;
  if (!options.Get("mode", &mode)) {
    // pacScript takes precedence over proxyRules.
    if (!pac_url.empty()) {
      proxy_mode = ProxyPrefs::MODE_PAC_SCRIPT;
    } else {
      proxy_mode = ProxyPrefs::MODE_FIXED_SERVERS;
    }
  } else {
    if (!ProxyPrefs::StringToProxyMode(mode, &proxy_mode)) {
      promise.RejectWithErrorMessage(
          "Invalid mode, must be one of direct, auto_detect, pac_script, "
          "fixed_servers or system");
      return handle;
    }
  }

  std::unique_ptr<base::Value> proxy_config;
  if (proxy_mode == ProxyPrefs::MODE_DIRECT) {
    proxy_config =
        std::make_unique<base::Value>(ProxyConfigDictionary::CreateDirect());
  } else if (proxy_mode == ProxyPrefs::MODE_SYSTEM) {
    proxy_config =
        std::make_unique<base::Value>(ProxyConfigDictionary::CreateSystem());
  } else if (proxy_mode == ProxyPrefs::MODE_AUTO_DETECT) {
    proxy_config = std::make_unique<base::Value>(
        ProxyConfigDictionary::CreateAutoDetect());
  } else if (proxy_mode == ProxyPrefs::MODE_PAC_SCRIPT) {
    proxy_config =
        std::make_unique<base::Value>(ProxyConfigDictionary::CreatePacScript(
            pac_url, true /* pac_mandatory */));
  } else {
    proxy_config = std::make_unique<base::Value>(
        ProxyConfigDictionary::CreateFixedServers(proxy_rules, bypass_list));
  }
  browser_context_->in_memory_pref_store()->SetValue(
      proxy_config::prefs::kProxy, std::move(proxy_config),
      WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);

  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(gin_helper::Promise<void>::ResolvePromise,
                                std::move(promise)));

  return handle;
}

v8::Local<v8::Promise> Session::ForceReloadProxyConfig() {
  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  gin_helper::Promise<void> promise(isolate);
  auto handle = promise.GetHandle();

  content::BrowserContext::GetDefaultStoragePartition(browser_context_)
      ->GetNetworkContext()
      ->ForceReloadProxyConfig(base::BindOnce(
          gin_helper::Promise<void>::ResolvePromise, std::move(promise)));

  return handle;
}

void Session::SetDownloadPath(const base::FilePath& path) {
  browser_context_->prefs()->SetFilePath(prefs::kDownloadDefaultDirectory,
                                         path);
}

void Session::EnableNetworkEmulation(const gin_helper::Dictionary& options) {
  auto conditions = network::mojom::NetworkConditions::New();

  options.Get("offline", &conditions->offline);
  options.Get("downloadThroughput", &conditions->download_throughput);
  options.Get("uploadThroughput", &conditions->upload_throughput);
  double latency = 0.0;
  if (options.Get("latency", &latency) && latency) {
    conditions->latency = base::TimeDelta::FromMillisecondsD(latency);
  }

  auto* network_context =
      content::BrowserContext::GetDefaultStoragePartition(browser_context_)
          ->GetNetworkContext();
  network_context->SetNetworkConditions(network_emulation_token_,
                                        std::move(conditions));
}

void Session::DisableNetworkEmulation() {
  auto* network_context =
      content::BrowserContext::GetDefaultStoragePartition(browser_context_)
          ->GetNetworkContext();
  network_context->SetNetworkConditions(
      network_emulation_token_, network::mojom::NetworkConditions::New());
}

void Session::SetCertVerifyProc(v8::Local<v8::Value> val,
                                gin::Arguments* args) {
  CertVerifierClient::CertVerifyProc proc;
  if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &proc))) {
    args->ThrowTypeError("Must pass null or function");
    return;
  }

  mojo::PendingRemote<network::mojom::CertVerifierClient>
      cert_verifier_client_remote;
  if (proc) {
    mojo::MakeSelfOwnedReceiver(
        std::make_unique<CertVerifierClient>(proc),
        cert_verifier_client_remote.InitWithNewPipeAndPassReceiver());
  }
  content::BrowserContext::GetDefaultStoragePartition(browser_context_)
      ->GetNetworkContext()
      ->SetCertVerifierClient(std::move(cert_verifier_client_remote));
}

void Session::SetPermissionRequestHandler(v8::Local<v8::Value> val,
                                          gin::Arguments* args) {
  auto* permission_manager = static_cast<ElectronPermissionManager*>(
      browser_context()->GetPermissionControllerDelegate());
  if (val->IsNull()) {
    permission_manager->SetPermissionRequestHandler(
        ElectronPermissionManager::RequestHandler());
    return;
  }
  auto handler = std::make_unique<ElectronPermissionManager::RequestHandler>();
  if (!gin::ConvertFromV8(args->isolate(), val, handler.get())) {
    args->ThrowTypeError("Must pass null or function");
    return;
  }
  permission_manager->SetPermissionRequestHandler(base::BindRepeating(
      [](ElectronPermissionManager::RequestHandler* handler,
         content::WebContents* web_contents,
         content::PermissionType permission_type,
         ElectronPermissionManager::StatusCallback callback,
         const base::Value& details) {
        handler->Run(web_contents, permission_type,
                     base::AdaptCallbackForRepeating(std::move(callback)),
                     details);
      },
      base::Owned(std::move(handler))));
}

void Session::SetPermissionCheckHandler(v8::Local<v8::Value> val,
                                        gin::Arguments* args) {
  ElectronPermissionManager::CheckHandler handler;
  if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &handler))) {
    args->ThrowTypeError("Must pass null or function");
    return;
  }
  auto* permission_manager = static_cast<ElectronPermissionManager*>(
      browser_context()->GetPermissionControllerDelegate());
  permission_manager->SetPermissionCheckHandler(handler);
}

v8::Local<v8::Promise> Session::ClearHostResolverCache(gin::Arguments* args) {
  v8::Isolate* isolate = args->isolate();
  gin_helper::Promise<void> promise(isolate);
  v8::Local<v8::Promise> handle = promise.GetHandle();

  content::BrowserContext::GetDefaultStoragePartition(browser_context_)
      ->GetNetworkContext()
      ->ClearHostCache(nullptr,
                       base::BindOnce(gin_helper::Promise<void>::ResolvePromise,
                                      std::move(promise)));

  return handle;
}

v8::Local<v8::Promise> Session::ClearAuthCache() {
  auto* isolate = JavascriptEnvironment::GetIsolate();
  gin_helper::Promise<void> promise(isolate);
  v8::Local<v8::Promise> handle = promise.GetHandle();

  content::BrowserContext::GetDefaultStoragePartition(browser_context_)
      ->GetNetworkContext()
      ->ClearHttpAuthCache(
          base::Time(), base::Time::Max(),
          base::BindOnce(gin_helper::Promise<void>::ResolvePromise,
                         std::move(promise)));

  return handle;
}

void Session::AllowNTLMCredentialsForDomains(const std::string& domains) {
  auto* command_line = base::CommandLine::ForCurrentProcess();
  network::mojom::HttpAuthDynamicParamsPtr auth_dynamic_params =
      network::mojom::HttpAuthDynamicParams::New();
  auth_dynamic_params->server_allowlist = domains;
  auth_dynamic_params->enable_negotiate_port =
      command_line->HasSwitch(electron::switches::kEnableAuthNegotiatePort);
  auth_dynamic_params->ntlm_v2_enabled =
      !command_line->HasSwitch(electron::switches::kDisableNTLMv2);
  content::GetNetworkService()->ConfigureHttpAuthPrefs(
      std::move(auth_dynamic_params));
}

void Session::SetUserAgent(const std::string& user_agent,
                           gin::Arguments* args) {
  browser_context_->SetUserAgent(user_agent);
  auto* network_context =
      content::BrowserContext::GetDefaultStoragePartition(browser_context_)
          ->GetNetworkContext();
  network_context->SetUserAgent(user_agent);

  std::string accept_lang;
  if (args->GetNext(&accept_lang)) {
    network_context->SetAcceptLanguage(
        net::HttpUtil::GenerateAcceptLanguageHeader(accept_lang));
  }
}

std::string Session::GetUserAgent() {
  return browser_context_->GetUserAgent();
}

void Session::SetSSLConfig(network::mojom::SSLConfigPtr config) {
  browser_context_->SetSSLConfig(std::move(config));
}

bool Session::IsPersistent() {
  return !browser_context_->IsOffTheRecord();
}

v8::Local<v8::Promise> Session::GetBlobData(v8::Isolate* isolate,
                                            const std::string& uuid) {
  gin::Handle<DataPipeHolder> holder = DataPipeHolder::From(isolate, uuid);
  if (holder.IsEmpty()) {
    gin_helper::Promise<v8::Local<v8::Value>> promise(isolate);
    promise.RejectWithErrorMessage("Could not get blob data handle");
    return promise.GetHandle();
  }

  return holder->ReadAll(isolate);
}

void Session::DownloadURL(const GURL& url) {
  auto* download_manager =
      content::BrowserContext::GetDownloadManager(browser_context());
  auto download_params = std::make_unique<download::DownloadUrlParameters>(
      url, MISSING_TRAFFIC_ANNOTATION);
  download_manager->DownloadUrl(std::move(download_params));
}

void Session::CreateInterruptedDownload(const gin_helper::Dictionary& options) {
  int64_t offset = 0, length = 0;
  double start_time = base::Time::Now().ToDoubleT();
  std::string mime_type, last_modified, etag;
  base::FilePath path;
  std::vector<GURL> url_chain;
  options.Get("path", &path);
  options.Get("urlChain", &url_chain);
  options.Get("mimeType", &mime_type);
  options.Get("offset", &offset);
  options.Get("length", &length);
  options.Get("lastModified", &last_modified);
  options.Get("eTag", &etag);
  options.Get("startTime", &start_time);
  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  if (path.empty() || url_chain.empty() || length == 0) {
    isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
        isolate, "Must pass non-empty path, urlChain and length.")));
    return;
  }
  if (offset >= length) {
    isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
        isolate, "Must pass an offset value less than length.")));
    return;
  }
  auto* download_manager =
      content::BrowserContext::GetDownloadManager(browser_context());
  download_manager->GetNextId(base::BindRepeating(
      &DownloadIdCallback, download_manager, path, url_chain, mime_type, offset,
      length, last_modified, etag, base::Time::FromDoubleT(start_time)));
}

void Session::SetPreloads(const std::vector<base::FilePath>& preloads) {
  auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
  DCHECK(prefs);
  prefs->set_preloads(preloads);
}

std::vector<base::FilePath> Session::GetPreloads() const {
  auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
  DCHECK(prefs);
  return prefs->preloads();
}

#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
v8::Local<v8::Promise> Session::LoadExtension(
    const base::FilePath& extension_path) {
  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  gin_helper::Promise<const extensions::Extension*> promise(isolate);
  v8::Local<v8::Promise> handle = promise.GetHandle();

  if (!extension_path.IsAbsolute()) {
    promise.RejectWithErrorMessage(
        "The path to the extension in 'loadExtension' must be absolute");
    return handle;
  }

  if (browser_context()->IsOffTheRecord()) {
    promise.RejectWithErrorMessage(
        "Extensions cannot be loaded in a temporary session");
    return handle;
  }

  auto* extension_system = static_cast<extensions::ElectronExtensionSystem*>(
      extensions::ExtensionSystem::Get(browser_context()));
  extension_system->LoadExtension(
      extension_path,
      base::BindOnce(
          [](gin_helper::Promise<const extensions::Extension*> promise,
             const extensions::Extension* extension,
             const std::string& error_msg) {
            if (extension) {
              if (!error_msg.empty()) {
                node::Environment* env =
                    node::Environment::GetCurrent(v8::Isolate::GetCurrent());
                EmitWarning(env, error_msg, "ExtensionLoadWarning");
              }
              promise.Resolve(extension);
            } else {
              promise.RejectWithErrorMessage(error_msg);
            }
          },
          std::move(promise)));

  return handle;
}

void Session::RemoveExtension(const std::string& extension_id) {
  auto* extension_system = static_cast<extensions::ElectronExtensionSystem*>(
      extensions::ExtensionSystem::Get(browser_context()));
  extension_system->RemoveExtension(extension_id);
}

v8::Local<v8::Value> Session::GetExtension(const std::string& extension_id) {
  auto* registry = extensions::ExtensionRegistry::Get(browser_context());
  const extensions::Extension* extension =
      registry->GetInstalledExtension(extension_id);
  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  if (extension) {
    return gin::ConvertToV8(isolate, extension);
  } else {
    return v8::Null(isolate);
  }
}

v8::Local<v8::Value> Session::GetAllExtensions() {
  auto* registry = extensions::ExtensionRegistry::Get(browser_context());
  auto installed_extensions = registry->GenerateInstalledExtensionsSet();
  std::vector<const extensions::Extension*> extensions_vector;
  for (const auto& extension : *installed_extensions) {
    if (extension->location() != extensions::Manifest::COMPONENT)
      extensions_vector.emplace_back(extension.get());
  }
  return gin::ConvertToV8(v8::Isolate::GetCurrent(), extensions_vector);
}

void Session::OnExtensionLoaded(content::BrowserContext* browser_context,
                                const extensions::Extension* extension) {
  Emit("extension-loaded", extension);
}

void Session::OnExtensionUnloaded(content::BrowserContext* browser_context,
                                  const extensions::Extension* extension,
                                  extensions::UnloadedExtensionReason reason) {
  Emit("extension-unloaded", extension);
}

void Session::OnExtensionReady(content::BrowserContext* browser_context,
                               const extensions::Extension* extension) {
  Emit("extension-ready", extension);
}
#endif

v8::Local<v8::Value> Session::Cookies(v8::Isolate* isolate) {
  if (cookies_.IsEmpty()) {
    auto handle = Cookies::Create(isolate, browser_context());
    cookies_.Reset(isolate, handle.ToV8());
  }
  return cookies_.Get(isolate);
}

v8::Local<v8::Value> Session::Protocol(v8::Isolate* isolate) {
  return protocol_.Get(isolate);
}

v8::Local<v8::Value> Session::ServiceWorkerContext(v8::Isolate* isolate) {
  if (service_worker_context_.IsEmpty()) {
    v8::Local<v8::Value> handle;
    handle = ServiceWorkerContext::Create(isolate, browser_context()).ToV8();
    service_worker_context_.Reset(isolate, handle);
  }
  return service_worker_context_.Get(isolate);
}

v8::Local<v8::Value> Session::WebRequest(v8::Isolate* isolate) {
  if (web_request_.IsEmpty()) {
    auto handle = WebRequest::Create(isolate, browser_context());
    web_request_.Reset(isolate, handle.ToV8());
  }
  return web_request_.Get(isolate);
}

v8::Local<v8::Value> Session::NetLog(v8::Isolate* isolate) {
  if (net_log_.IsEmpty()) {
    auto handle = NetLog::Create(isolate, browser_context());
    net_log_.Reset(isolate, handle.ToV8());
  }
  return net_log_.Get(isolate);
}

static void StartPreconnectOnUI(ElectronBrowserContext* browser_context,
                                const GURL& url,
                                int num_sockets_to_preconnect) {
  std::vector<predictors::PreconnectRequest> requests = {
      {url::Origin::Create(url), num_sockets_to_preconnect,
       net::NetworkIsolationKey()}};
  browser_context->GetPreconnectManager()->Start(url, requests);
}

void Session::Preconnect(const gin_helper::Dictionary& options,
                         gin::Arguments* args) {
  GURL url;
  if (!options.Get("url", &url) || !url.is_valid()) {
    args->ThrowTypeError(
        "Must pass non-empty valid url to session.preconnect.");
    return;
  }
  int num_sockets_to_preconnect = 1;
  if (options.Get("numSockets", &num_sockets_to_preconnect)) {
    const int kMinSocketsToPreconnect = 1;
    const int kMaxSocketsToPreconnect = 6;
    if (num_sockets_to_preconnect < kMinSocketsToPreconnect ||
        num_sockets_to_preconnect > kMaxSocketsToPreconnect) {
      args->ThrowTypeError(
          base::StringPrintf("numSocketsToPreconnect is outside range [%d,%d]",
                             kMinSocketsToPreconnect, kMaxSocketsToPreconnect));
      return;
    }
  }

  DCHECK_GT(num_sockets_to_preconnect, 0);
  base::PostTask(
      FROM_HERE, {content::BrowserThread::UI},
      base::BindOnce(&StartPreconnectOnUI, base::Unretained(browser_context_),
                     url, num_sockets_to_preconnect));
}

v8::Local<v8::Promise> Session::CloseAllConnections() {
  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  gin_helper::Promise<void> promise(isolate);
  auto handle = promise.GetHandle();

  content::BrowserContext::GetDefaultStoragePartition(browser_context_)
      ->GetNetworkContext()
      ->CloseAllConnections(base::BindOnce(
          gin_helper::Promise<void>::ResolvePromise, std::move(promise)));

  return handle;
}

#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
base::Value Session::GetSpellCheckerLanguages() {
  return browser_context_->prefs()
      ->Get(spellcheck::prefs::kSpellCheckDictionaries)
      ->Clone();
}

void Session::SetSpellCheckerLanguages(
    gin_helper::ErrorThrower thrower,
    const std::vector<std::string>& languages) {
  base::ListValue language_codes;
  for (const std::string& lang : languages) {
    std::string code = spellcheck::GetCorrespondingSpellCheckLanguage(lang);
    if (code.empty()) {
      thrower.ThrowError("Invalid language code provided: \"" + lang +
                         "\" is not a valid language code");
      return;
    }
    language_codes.AppendString(code);
  }
  browser_context_->prefs()->Set(spellcheck::prefs::kSpellCheckDictionaries,
                                 language_codes);
  // Enable spellcheck if > 0 languages, disable if no languages set
  browser_context_->prefs()->SetBoolean(spellcheck::prefs::kSpellCheckEnable,
                                        !languages.empty());
}

void SetSpellCheckerDictionaryDownloadURL(gin_helper::ErrorThrower thrower,
                                          const GURL& url) {
  if (!url.is_valid()) {
    thrower.ThrowError(
        "The URL you provided to setSpellCheckerDictionaryDownloadURL is not a "
        "valid URL");
    return;
  }
  SpellcheckHunspellDictionary::SetBaseDownloadURL(url);
}

v8::Local<v8::Promise> Session::ListWordsInSpellCheckerDictionary() {
  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  gin_helper::Promise<std::set<std::string>> promise(isolate);
  v8::Local<v8::Promise> handle = promise.GetHandle();

  SpellcheckService* spellcheck =
      SpellcheckServiceFactory::GetForContext(browser_context_);

  if (!spellcheck) {
    promise.RejectWithErrorMessage(
        "Spellcheck in unexpected state: failed to load custom dictionary.");
    return handle;
  }

  if (spellcheck->GetCustomDictionary()->IsLoaded()) {
    promise.Resolve(spellcheck->GetCustomDictionary()->GetWords());
  } else {
    new DictionaryObserver(std::move(promise), spellcheck->GetWeakPtr());
    // Dictionary loads by default asynchronously,
    // call the load function anyways just to be sure.
    spellcheck->GetCustomDictionary()->Load();
  }

  return handle;
}

bool Session::AddWordToSpellCheckerDictionary(const std::string& word) {
  // don't let in-memory sessions add spellchecker words
  // because files will persist unintentionally
  bool is_in_memory = browser_context_->IsOffTheRecord();
  if (is_in_memory)
    return false;

  SpellcheckService* service =
      SpellcheckServiceFactory::GetForContext(browser_context_);
  if (!service)
    return false;

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  if (spellcheck::UseBrowserSpellChecker()) {
    spellcheck_platform::AddWord(service->platform_spell_checker(),
                                 base::UTF8ToUTF16(word));
  }
#endif
  return service->GetCustomDictionary()->AddWord(word);
}

bool Session::RemoveWordFromSpellCheckerDictionary(const std::string& word) {
  // don't let in-memory sessions remove spellchecker words
  // because files will persist unintentionally
  bool is_in_memory = browser_context_->IsOffTheRecord();
  if (is_in_memory)
    return false;

  SpellcheckService* service =
      SpellcheckServiceFactory::GetForContext(browser_context_);
  if (!service)
    return false;

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  if (spellcheck::UseBrowserSpellChecker()) {
    spellcheck_platform::RemoveWord(service->platform_spell_checker(),
                                    base::UTF8ToUTF16(word));
  }
#endif
  return service->GetCustomDictionary()->RemoveWord(word);
}

void Session::SetSpellCheckerEnabled(bool b) {
  browser_context_->prefs()->SetBoolean(spellcheck::prefs::kSpellCheckEnable,
                                        b);
}

bool Session::IsSpellCheckerEnabled() const {
  return browser_context_->prefs()->GetBoolean(
      spellcheck::prefs::kSpellCheckEnable);
}

#endif  // BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)

// static
Session* Session::FromBrowserContext(content::BrowserContext* context) {
  auto* data =
      static_cast<UserDataLink*>(context->GetUserData(kElectronApiSessionKey));
  return data ? data->session : nullptr;
}

// static
gin::Handle<Session> Session::CreateFrom(
    v8::Isolate* isolate,
    ElectronBrowserContext* browser_context) {
  Session* existing = FromBrowserContext(browser_context);
  if (existing)
    return gin::CreateHandle(isolate, existing);

  auto handle =
      gin::CreateHandle(isolate, new Session(isolate, browser_context));

  // The Sessions should never be garbage collected, since the common pattern is
  // to use partition strings, instead of using the Session object directly.
  handle->Pin(isolate);

  App::Get()->EmitCustomEvent("session-created",
                              handle.ToV8().As<v8::Object>());

  return handle;
}

// static
gin::Handle<Session> Session::FromPartition(v8::Isolate* isolate,
                                            const std::string& partition,
                                            base::DictionaryValue options) {
  ElectronBrowserContext* browser_context;
  if (partition.empty()) {
    browser_context =
        ElectronBrowserContext::From("", false, std::move(options));
  } else if (base::StartsWith(partition, kPersistPrefix,
                              base::CompareCase::SENSITIVE)) {
    std::string name = partition.substr(8);
    browser_context =
        ElectronBrowserContext::From(name, false, std::move(options));
  } else {
    browser_context =
        ElectronBrowserContext::From(partition, true, std::move(options));
  }
  return CreateFrom(isolate, browser_context);
}

gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
    v8::Isolate* isolate) {
  return gin_helper::EventEmitterMixin<Session>::GetObjectTemplateBuilder(
             isolate)
      .SetMethod("resolveProxy", &Session::ResolveProxy)
      .SetMethod("getCacheSize", &Session::GetCacheSize)
      .SetMethod("clearCache", &Session::ClearCache)
      .SetMethod("clearStorageData", &Session::ClearStorageData)
      .SetMethod("flushStorageData", &Session::FlushStorageData)
      .SetMethod("setProxy", &Session::SetProxy)
      .SetMethod("forceReloadProxyConfig", &Session::ForceReloadProxyConfig)
      .SetMethod("setDownloadPath", &Session::SetDownloadPath)
      .SetMethod("enableNetworkEmulation", &Session::EnableNetworkEmulation)
      .SetMethod("disableNetworkEmulation", &Session::DisableNetworkEmulation)
      .SetMethod("setCertificateVerifyProc", &Session::SetCertVerifyProc)
      .SetMethod("setPermissionRequestHandler",
                 &Session::SetPermissionRequestHandler)
      .SetMethod("setPermissionCheckHandler",
                 &Session::SetPermissionCheckHandler)
      .SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
      .SetMethod("clearAuthCache", &Session::ClearAuthCache)
      .SetMethod("allowNTLMCredentialsForDomains",
                 &Session::AllowNTLMCredentialsForDomains)
      .SetMethod("isPersistent", &Session::IsPersistent)
      .SetMethod("setUserAgent", &Session::SetUserAgent)
      .SetMethod("getUserAgent", &Session::GetUserAgent)
      .SetMethod("setSSLConfig", &Session::SetSSLConfig)
      .SetMethod("getBlobData", &Session::GetBlobData)
      .SetMethod("downloadURL", &Session::DownloadURL)
      .SetMethod("createInterruptedDownload",
                 &Session::CreateInterruptedDownload)
      .SetMethod("setPreloads", &Session::SetPreloads)
      .SetMethod("getPreloads", &Session::GetPreloads)
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
      .SetMethod("loadExtension", &Session::LoadExtension)
      .SetMethod("removeExtension", &Session::RemoveExtension)
      .SetMethod("getExtension", &Session::GetExtension)
      .SetMethod("getAllExtensions", &Session::GetAllExtensions)
#endif
#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
      .SetMethod("getSpellCheckerLanguages", &Session::GetSpellCheckerLanguages)
      .SetMethod("setSpellCheckerLanguages", &Session::SetSpellCheckerLanguages)
      .SetProperty("availableSpellCheckerLanguages",
                   &spellcheck::SpellCheckLanguages)
      .SetMethod("setSpellCheckerDictionaryDownloadURL",
                 &SetSpellCheckerDictionaryDownloadURL)
      .SetMethod("listWordsInSpellCheckerDictionary",
                 &Session::ListWordsInSpellCheckerDictionary)
      .SetMethod("addWordToSpellCheckerDictionary",
                 &Session::AddWordToSpellCheckerDictionary)
      .SetMethod("removeWordFromSpellCheckerDictionary",
                 &Session::RemoveWordFromSpellCheckerDictionary)
      .SetMethod("setSpellCheckerEnabled", &Session::SetSpellCheckerEnabled)
      .SetMethod("isSpellCheckerEnabled", &Session::IsSpellCheckerEnabled)
      .SetProperty("spellCheckerEnabled", &Session::IsSpellCheckerEnabled,
                   &Session::SetSpellCheckerEnabled)
#endif
      .SetMethod("preconnect", &Session::Preconnect)
      .SetMethod("closeAllConnections", &Session::CloseAllConnections)
      .SetProperty("cookies", &Session::Cookies)
      .SetProperty("netLog", &Session::NetLog)
      .SetProperty("protocol", &Session::Protocol)
      .SetProperty("serviceWorkers", &Session::ServiceWorkerContext)
      .SetProperty("webRequest", &Session::WebRequest);
}

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

}  // namespace api

}  // namespace electron

namespace {

using electron::api::Cookies;
using electron::api::Protocol;
using electron::api::ServiceWorkerContext;
using electron::api::Session;

v8::Local<v8::Value> FromPartition(const std::string& partition,
                                   gin::Arguments* args) {
  if (!electron::Browser::Get()->is_ready()) {
    args->ThrowTypeError("Session can only be received when app is ready");
    return v8::Null(args->isolate());
  }
  base::DictionaryValue options;
  args->GetNext(&options);
  return Session::FromPartition(args->isolate(), partition, std::move(options))
      .ToV8();
}

void Initialize(v8::Local<v8::Object> exports,
                v8::Local<v8::Value> unused,
                v8::Local<v8::Context> context,
                void* priv) {
  v8::Isolate* isolate = context->GetIsolate();
  gin_helper::Dictionary dict(isolate, exports);
  dict.SetMethod("fromPartition", &FromPartition);
}

}  // namespace

NODE_LINKED_MODULE_CONTEXT_AWARE(electron_browser_session, Initialize)