// Copyright (c) 2012 The Chromium Embedded Framework 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 "libcef/browser/net/cookie_manager_old_impl.h"

#include <set>
#include <string>
#include <vector>

#include "libcef/browser/content_browser_client.h"
#include "libcef/browser/context.h"
#include "libcef/browser/net/network_delegate.h"
#include "libcef/common/net_service/net_service_util.h"
#include "libcef/common/task_runner_impl.h"
#include "libcef/common/time_util.h"

#include "base/bind.h"
#include "base/format_macros.h"
#include "base/logging.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/browser_process.h"
#include "components/net_log/chrome_net_log.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_task_traits.h"
#include "net/url_request/url_request_context.h"
#include "url/gurl.h"

using content::BrowserThread;

namespace {

// Callback class for visiting cookies.
class VisitCookiesCallback
    : public base::RefCountedThreadSafe<VisitCookiesCallback> {
 public:
  explicit VisitCookiesCallback(
      const CefCookieManagerOldImpl::CookieStoreGetter& cookie_store_getter,
      CefRefPtr<CefCookieVisitor> visitor)
      : cookie_store_getter_(cookie_store_getter), visitor_(visitor) {}

  void Run(const net::CookieList& list,
           const net::CookieStatusList& excluded_list) {
    if (!CEF_CURRENTLY_ON_UIT()) {
      CEF_POST_TASK(CEF_UIT, base::Bind(&VisitCookiesCallback::Run, this, list,
                                        excluded_list));
      return;
    }

    int total = list.size(), count = 0;

    net::CookieList::const_iterator it = list.begin();
    for (; it != list.end(); ++it, ++count) {
      CefCookie cookie;
      const net::CanonicalCookie& cc = *(it);
      net_service::MakeCefCookie(cc, cookie);

      bool deleteCookie = false;
      bool keepLooping = visitor_->Visit(cookie, count, total, deleteCookie);
      if (deleteCookie) {
        CEF_POST_TASK(
            CEF_IOT,
            base::Bind(&VisitCookiesCallback::DeleteOnIOThread, this, cc));
      }
      if (!keepLooping)
        break;
    }
  }

 private:
  friend class base::RefCountedThreadSafe<VisitCookiesCallback>;

  ~VisitCookiesCallback() {}

  void DeleteOnIOThread(const net::CanonicalCookie& cc) {
    net::CookieStore* cookie_store = cookie_store_getter_.Run();
    if (cookie_store) {
      cookie_store->DeleteCanonicalCookieAsync(
          cc, net::CookieMonster::DeleteCallback());
    }
  }

  CefCookieManagerOldImpl::CookieStoreGetter cookie_store_getter_;
  CefRefPtr<CefCookieVisitor> visitor_;
};

// Always execute the callback asynchronously.
void RunAsyncCompletionOnUIThread(CefRefPtr<CefCompletionCallback> callback) {
  if (!callback.get())
    return;
  CEF_POST_TASK(CEF_UIT,
                base::Bind(&CefCompletionCallback::OnComplete, callback.get()));
}

// Always execute the callback asynchronously.
void DeleteCookiesCallbackImpl(CefRefPtr<CefDeleteCookiesCallback> callback,
                               uint32_t num_deleted) {
  if (!callback.get())
    return;
  CEF_POST_TASK(CEF_UIT, base::Bind(&CefDeleteCookiesCallback::OnComplete,
                                    callback.get(), num_deleted));
}

// Always execute the callback asynchronously.
void SetCookieCallbackImpl(CefRefPtr<CefSetCookieCallback> callback,
                           net::CanonicalCookie::CookieInclusionStatus status) {
  if (!callback.get())
    return;
  CEF_POST_TASK(
      CEF_UIT,
      base::Bind(
          &CefSetCookieCallback::OnComplete, callback.get(),
          status == net::CanonicalCookie::CookieInclusionStatus::INCLUDE));
}

}  // namespace

CefCookieManagerOldImpl::CefCookieManagerOldImpl() {}

CefCookieManagerOldImpl::~CefCookieManagerOldImpl() {
  CEF_REQUIRE_IOT();
}

void CefCookieManagerOldImpl::Initialize(
    CefRefPtr<CefRequestContextImpl> request_context,
    const CefString& path,
    bool persist_session_cookies,
    CefRefPtr<CefCompletionCallback> callback) {
  DCHECK(request_context.get());
  request_context_ = request_context;
  request_context_->GetRequestContextImpl(
      base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO}),
      base::Bind(&CefCookieManagerOldImpl::InitWithContext, this, callback));
}

void CefCookieManagerOldImpl::GetCookieStore(
    scoped_refptr<base::SingleThreadTaskRunner> task_runner,
    const CookieStoreCallback& callback) {
  if (!task_runner.get())
    task_runner = CefTaskRunnerImpl::GetCurrentTaskRunner();

  if (!CEF_CURRENTLY_ON_IOT()) {
    CEF_POST_TASK(CEF_IOT, base::Bind(&CefCookieManagerOldImpl::GetCookieStore,
                                      this, task_runner, callback));
    return;
  }

  RunMethodWithContext(
      base::Bind(&CefCookieManagerOldImpl::GetCookieStoreWithContext, this,
                 task_runner, callback));
}

net::CookieStore* CefCookieManagerOldImpl::GetExistingCookieStore() {
  CEF_REQUIRE_IOT();
  if (request_context_impl_.get()) {
    net::CookieStore* cookie_store =
        request_context_impl_->GetExistingCookieStore();
    DCHECK(cookie_store);
    return cookie_store;
  }

  LOG(ERROR) << "Cookie store does not exist";
  return nullptr;
}

void CefCookieManagerOldImpl::SetSupportedSchemes(
    const std::vector<CefString>& schemes,
    bool include_defaults,
    CefRefPtr<CefCompletionCallback> callback) {
  if (!CEF_CURRENTLY_ON_IOT()) {
    CEF_POST_TASK(
        CEF_IOT, base::Bind(&CefCookieManagerOldImpl::SetSupportedSchemes, this,
                            schemes, include_defaults, callback));
    return;
  }

  std::vector<std::string> scheme_set;
  std::vector<CefString>::const_iterator it = schemes.begin();
  for (; it != schemes.end(); ++it)
    scheme_set.push_back(*it);

  SetSupportedSchemesInternal(scheme_set, include_defaults, callback);
}

bool CefCookieManagerOldImpl::VisitAllCookies(
    CefRefPtr<CefCookieVisitor> visitor) {
  GetCookieStore(
      base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO}),
      base::Bind(&CefCookieManagerOldImpl::VisitAllCookiesInternal, this,
                 visitor));
  return true;
}

bool CefCookieManagerOldImpl::VisitUrlCookies(
    const CefString& url,
    bool includeHttpOnly,
    CefRefPtr<CefCookieVisitor> visitor) {
  GetCookieStore(
      base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO}),
      base::Bind(&CefCookieManagerOldImpl::VisitUrlCookiesInternal, this, url,
                 includeHttpOnly, visitor));
  return true;
}

bool CefCookieManagerOldImpl::SetCookie(
    const CefString& url,
    const CefCookie& cookie,
    CefRefPtr<CefSetCookieCallback> callback) {
  GURL gurl = GURL(url.ToString());
  if (!gurl.is_valid())
    return false;

  GetCookieStore(
      base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO}),
      base::Bind(&CefCookieManagerOldImpl::SetCookieInternal, this, gurl,
                 cookie, callback));
  return true;
}

bool CefCookieManagerOldImpl::DeleteCookies(
    const CefString& url,
    const CefString& cookie_name,
    CefRefPtr<CefDeleteCookiesCallback> callback) {
  // Empty URLs are allowed but not invalid URLs.
  GURL gurl = GURL(url.ToString());
  if (!gurl.is_empty() && !gurl.is_valid())
    return false;

  GetCookieStore(
      base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO}),
      base::Bind(&CefCookieManagerOldImpl::DeleteCookiesInternal, this, gurl,
                 cookie_name, callback));
  return true;
}

bool CefCookieManagerOldImpl::FlushStore(
    CefRefPtr<CefCompletionCallback> callback) {
  GetCookieStore(
      base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO}),
      base::Bind(&CefCookieManagerOldImpl::FlushStoreInternal, this, callback));
  return true;
}

// static
void CefCookieManagerOldImpl::SetCookieMonsterSchemes(
    net::CookieMonster* cookie_monster,
    const std::vector<std::string>& schemes,
    bool include_defaults) {
  CEF_REQUIRE_IOT();

  std::vector<std::string> all_schemes = schemes;

  if (include_defaults) {
    // Add default schemes that should always support cookies.
    all_schemes.push_back("http");
    all_schemes.push_back("https");
    all_schemes.push_back("ws");
    all_schemes.push_back("wss");
  }

  cookie_monster->SetCookieableSchemes(
      all_schemes, net::CookieStore::SetCookieableSchemesCallback());
}

void CefCookieManagerOldImpl::RunMethodWithContext(
    const CefRequestContextImpl::RequestContextCallback& method) {
  CEF_REQUIRE_IOT();
  if (request_context_impl_.get()) {
    method.Run(request_context_impl_);
  } else if (request_context_.get()) {
    // Try again after the request context is initialized.
    request_context_->GetRequestContextImpl(
        base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO}),
        method);
  } else {
    NOTREACHED();
  }
}

void CefCookieManagerOldImpl::InitWithContext(
    CefRefPtr<CefCompletionCallback> callback,
    scoped_refptr<CefURLRequestContextGetter> request_context) {
  CEF_REQUIRE_IOT();

  DCHECK(!request_context_impl_.get());
  request_context_impl_ = request_context;

  // Clear the CefRequestContextImpl reference here to avoid a potential
  // reference loop between CefRequestContextImpl (which has a reference to
  // CefRequestContextHandler), CefRequestContextHandler (which may keep a
  // reference to this object) and this object.
  request_context_ = NULL;

  RunAsyncCompletionOnUIThread(callback);
}

void CefCookieManagerOldImpl::SetSupportedSchemesWithContext(
    const std::vector<std::string>& schemes,
    bool include_defaults,
    CefRefPtr<CefCompletionCallback> callback,
    scoped_refptr<CefURLRequestContextGetter> request_context) {
  CEF_REQUIRE_IOT();

  request_context->SetCookieSupportedSchemes(schemes, include_defaults);

  RunAsyncCompletionOnUIThread(callback);
}

void CefCookieManagerOldImpl::GetCookieStoreWithContext(
    scoped_refptr<base::SingleThreadTaskRunner> task_runner,
    const CookieStoreCallback& callback,
    scoped_refptr<CefURLRequestContextGetter> request_context) {
  CEF_REQUIRE_IOT();
  DCHECK(request_context->GetExistingCookieStore());

  const CookieStoreGetter& cookie_store_getter = base::Bind(
      &CefURLRequestContextGetter::GetExistingCookieStore, request_context);

  if (task_runner->BelongsToCurrentThread()) {
    // Execute the callback immediately.
    callback.Run(cookie_store_getter);
  } else {
    // Execute the callback on the target thread.
    task_runner->PostTask(FROM_HERE, base::Bind(callback, cookie_store_getter));
  }
}

void CefCookieManagerOldImpl::SetSupportedSchemesInternal(
    const std::vector<std::string>& schemes,
    bool include_defaults,
    CefRefPtr<CefCompletionCallback> callback) {
  CEF_REQUIRE_IOT();

  RunMethodWithContext(
      base::Bind(&CefCookieManagerOldImpl::SetSupportedSchemesWithContext, this,
                 schemes, include_defaults, callback));
}

void CefCookieManagerOldImpl::VisitAllCookiesInternal(
    CefRefPtr<CefCookieVisitor> visitor,
    const CookieStoreGetter& cookie_store_getter) {
  CEF_REQUIRE_IOT();

  net::CookieStore* cookie_store = cookie_store_getter.Run();
  if (!cookie_store)
    return;

  scoped_refptr<VisitCookiesCallback> callback(
      new VisitCookiesCallback(cookie_store_getter, visitor));

  cookie_store->GetAllCookiesAsync(
      base::Bind(&VisitCookiesCallback::Run, callback.get()));
}

void CefCookieManagerOldImpl::VisitUrlCookiesInternal(
    const CefString& url,
    bool includeHttpOnly,
    CefRefPtr<CefCookieVisitor> visitor,
    const CookieStoreGetter& cookie_store_getter) {
  CEF_REQUIRE_IOT();

  net::CookieStore* cookie_store = cookie_store_getter.Run();
  if (!cookie_store)
    return;

  net::CookieOptions options;
  if (includeHttpOnly)
    options.set_include_httponly();

  scoped_refptr<VisitCookiesCallback> callback(
      new VisitCookiesCallback(cookie_store_getter, visitor));

  GURL gurl = GURL(url.ToString());
  cookie_store->GetCookieListWithOptionsAsync(
      gurl, options, base::Bind(&VisitCookiesCallback::Run, callback.get()));
}

void CefCookieManagerOldImpl::SetCookieInternal(
    const GURL& url,
    const CefCookie& cookie,
    CefRefPtr<CefSetCookieCallback> callback,
    const CookieStoreGetter& cookie_store_getter) {
  CEF_REQUIRE_IOT();

  net::CookieStore* cookie_store = cookie_store_getter.Run();
  if (!cookie_store) {
    if (callback.get()) {
      CEF_POST_TASK(CEF_IOT, base::Bind(&CefSetCookieCallback::OnComplete,
                                        callback.get(), false));
    }
    return;
  }

  std::string name = CefString(&cookie.name).ToString();
  std::string value = CefString(&cookie.value).ToString();
  std::string domain = CefString(&cookie.domain).ToString();
  std::string path = CefString(&cookie.path).ToString();

  base::Time expiration_time;
  if (cookie.has_expires)
    cef_time_to_basetime(cookie.expires, expiration_time);

  auto canonical_cookie = net::CanonicalCookie::CreateSanitizedCookie(
      url, name, value, domain, path,
      base::Time(),  // Creation time.
      expiration_time,
      base::Time(),  // Last access time.
      cookie.secure ? true : false, cookie.httponly ? true : false,
      net::CookieSameSite::UNSPECIFIED, net::COOKIE_PRIORITY_DEFAULT);

  net::CookieOptions options;
  if (cookie.httponly)
    options.set_include_httponly();

  if (!canonical_cookie) {
    SetCookieCallbackImpl(
        callback,
        net::CanonicalCookie::CookieInclusionStatus::EXCLUDE_UNKNOWN_ERROR);
    return;
  }

  cookie_store->SetCanonicalCookieAsync(
      std::move(canonical_cookie), url.scheme(), options,
      base::Bind(SetCookieCallbackImpl, callback));
}

void CefCookieManagerOldImpl::DeleteCookiesInternal(
    const GURL& url,
    const CefString& cookie_name,
    CefRefPtr<CefDeleteCookiesCallback> callback,
    const CookieStoreGetter& cookie_store_getter) {
  CEF_REQUIRE_IOT();

  net::CookieStore* cookie_store = cookie_store_getter.Run();
  if (!cookie_store) {
    if (callback.get()) {
      CEF_POST_TASK(CEF_IOT, base::Bind(&CefDeleteCookiesCallback::OnComplete,
                                        callback.get(), 0));
    }
    return;
  }

  if (url.is_empty()) {
    // Delete all cookies.
    cookie_store->DeleteAllAsync(
        base::Bind(DeleteCookiesCallbackImpl, callback));
  } else if (cookie_name.empty()) {
    // Delete all matching host cookies.
    net::CookieDeletionInfo delete_info;
    delete_info.host = url.host();
    cookie_store->DeleteAllMatchingInfoAsync(
        delete_info, base::Bind(DeleteCookiesCallbackImpl, callback));
  } else {
    // Delete all matching host and domain cookies.
    net::CookieDeletionInfo delete_info;
    delete_info.url = url;
    delete_info.name = cookie_name;
    cookie_store->DeleteAllMatchingInfoAsync(
        delete_info, base::Bind(DeleteCookiesCallbackImpl, callback));
  }
}

void CefCookieManagerOldImpl::FlushStoreInternal(
    CefRefPtr<CefCompletionCallback> callback,
    const CookieStoreGetter& cookie_store_getter) {
  CEF_REQUIRE_IOT();

  net::CookieStore* cookie_store = cookie_store_getter.Run();
  if (!cookie_store) {
    RunAsyncCompletionOnUIThread(callback);
    return;
  }

  cookie_store->FlushStore(base::Bind(RunAsyncCompletionOnUIThread, callback));
}
