// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/cookie_store/cookie_store_manager.h"

#include <utility>

#include "base/optional.h"
#include "content/browser/cookie_store/cookie_change_subscriptions.pb.h"
#include "content/browser/service_worker/embedded_worker_status.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_metrics.h"
#include "content/browser/service_worker/service_worker_registration.h"
#include "content/browser/service_worker/service_worker_version.h"
#include "content/public/browser/browser_context.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_event_status.mojom.h"
#include "url/gurl.h"

namespace content {

namespace {

// ServiceWorkerStorage user data key for cookie change subscriptions.
const char kSubscriptionsUserKey[] = "cookie_store_subscriptions";

// Handles the result of ServiceWorkerContextWrapper::StoreRegistrationUserData.
void HandleStoreRegistrationUserDataStatus(
    blink::ServiceWorkerStatusCode status) {
  // The current implementation does not have a good way to handle errors in
  // StoreRegistrationUserData. Cookie change subscriptions have been added to
  // the registration during the install event, so it's too late to surface the
  // error to the renderer. The registration has already been persisted, and the
  // Service Worker is likely active by now.
  DLOG_IF(ERROR, status != blink::ServiceWorkerStatusCode::kOk)
      << "StoreRegistrationUserData failed";
}

}  // namespace

CookieStoreManager::CookieStoreManager(
    scoped_refptr<ServiceWorkerContextWrapper> service_worker_context)
    : service_worker_context_(std::move(service_worker_context)),
      cookie_change_listener_binding_(this),
      registration_user_data_key_(kSubscriptionsUserKey),
      weak_factory_(this) {
  service_worker_context_->AddObserver(this);
}

CookieStoreManager::~CookieStoreManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  service_worker_context_->RemoveObserver(this);
}

void CookieStoreManager::CreateService(blink::mojom::CookieStoreRequest request,
                                       const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  bindings_.AddBinding(std::make_unique<CookieStoreHost>(this, origin),
                       std::move(request));
}

void CookieStoreManager::LoadAllSubscriptions(
    base::OnceCallback<void(bool)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  DCHECK(!done_loading_subscriptions_) << __func__ << " already called";

  service_worker_context_->GetUserDataForAllRegistrations(
      registration_user_data_key_,
      base::BindOnce(&CookieStoreManager::ProcessOnDiskSubscriptions,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void CookieStoreManager::ListenToCookieChanges(
    ::network::mojom::CookieManagerPtr cookie_manager,
    base::OnceCallback<void(bool)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  DCHECK(!cookie_manager_) << __func__ << " already called";
  cookie_manager_ = std::move(cookie_manager);

  DCHECK(!cookie_change_listener_binding_.is_bound());
  ::network::mojom::CookieChangeListenerPtr cookie_change_listener;
  cookie_change_listener_binding_.Bind(
      mojo::MakeRequest(&cookie_change_listener));

  // TODO(pwnall): Switch to an API with subscription confirmation.
  cookie_manager_->AddGlobalChangeListener(std::move(cookie_change_listener));
  std::move(callback).Run(true);
}

void CookieStoreManager::ProcessOnDiskSubscriptions(
    base::OnceCallback<void(bool)> load_callback,
    const std::vector<std::pair<int64_t, std::string>>& user_data,
    blink::ServiceWorkerStatusCode status) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  DCHECK(!done_loading_subscriptions_) << __func__ << " already called";
  done_loading_subscriptions_ = true;

  if (status != blink::ServiceWorkerStatusCode::kOk) {
    DidLoadAllSubscriptions(false, std::move(load_callback));
    return;
  }

  DCHECK(subscriptions_by_registration_.empty());
  subscriptions_by_registration_.reserve(user_data.size());
  bool load_success = true;
  for (const auto& pair : user_data) {
    int64_t service_worker_registration_id = pair.first;
    const std::string& proto_string = pair.second;

    base::Optional<std::vector<CookieChangeSubscription>> subscriptions_opt =
        CookieChangeSubscription::DeserializeVector(
            proto_string, service_worker_registration_id);
    if (!subscriptions_opt.has_value()) {
      load_success = false;
      continue;
    }

    ActivateSubscriptions(&subscriptions_opt.value());
    DCHECK(
        !subscriptions_by_registration_.count(service_worker_registration_id));
    subscriptions_by_registration_.emplace(
        std::move(service_worker_registration_id),
        std::move(subscriptions_opt).value());
  }

  DidLoadAllSubscriptions(load_success, std::move(load_callback));
}

void CookieStoreManager::DidLoadAllSubscriptions(
    bool succeeded,
    base::OnceCallback<void(bool)> load_callback) {
  DCHECK(done_loading_subscriptions_);
  succeeded_loading_subscriptions_ = succeeded;

  for (auto& callback : subscriptions_loaded_callbacks_)
    std::move(callback).Run();
  subscriptions_loaded_callbacks_.clear();

  std::move(load_callback).Run(succeeded);
}

void CookieStoreManager::AppendSubscriptions(
    int64_t service_worker_registration_id,
    const url::Origin& origin,
    std::vector<blink::mojom::CookieChangeSubscriptionPtr> mojo_subscriptions,
    blink::mojom::CookieStore::AppendSubscriptionsCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!done_loading_subscriptions_) {
    subscriptions_loaded_callbacks_.emplace_back(base::BindOnce(
        &CookieStoreManager::AppendSubscriptions, weak_factory_.GetWeakPtr(),
        service_worker_registration_id, origin, std::move(mojo_subscriptions),
        std::move(callback)));
    return;
  }

  if (!succeeded_loading_subscriptions_) {
    std::move(callback).Run(false);
    return;
  }

  // GetLiveRegistration() is sufficient here (as opposed to a flavor of
  // FindRegistration()) because AppendSubscriptions is only called from the
  // implementation of the Cookie Store API, which is exposed to
  // ServiceWorkerGlobalScope. ServiceWorkerGlobalScope references the
  // service worker's registration via a ServiceWorkerRegistration JavaScript
  // object, so the registration is guaranteed to be live while the service
  // worker is executing.
  //
  // It is possible for the service worker to get killed while this API call is
  // in progress, for example, if the service worker code exceeds an event
  // handling time limit. In that case, the return value will not be observed,
  // so a false negative is acceptable.
  ServiceWorkerRegistration* service_worker_registration =
      service_worker_context_->GetLiveRegistration(
          service_worker_registration_id);
  if (!service_worker_registration ||
      !origin.IsSameOriginWith(
          url::Origin::Create(service_worker_registration->pattern()))) {
    // This error case is a good fit for mojo::ReportBadMessage(), because the
    // renderer has passed an invalid registration ID. However, the code here
    // might run without a mojo call context, if the original call was delayed
    // while loading on-disk subscription data.
    //
    // While it would be possible to have two code paths for the two situations,
    // the extra complexity doesn't seem warranted for the limited debuggig
    // benefits provided by mojo::ReportBadMessage.
    std::move(callback).Run(false);
    return;
  }

  // TODO(crbug.com/843079): This check incorrectly allows an active service
  //                         worker version to call the API, if another version
  //                         is installing at the same time.
  if (!service_worker_registration->installing_version()) {
    // A service worker's cookie change subscriptions can only be modified while
    // the service worker's install event is handled.
    std::move(callback).Run(false);
    return;
  }

  if (mojo_subscriptions.empty()) {
    // Empty subscriptions are special-cased so we never have to serialize an
    // empty array of subscriptions. This is advantageous because the protobuf
    // serialization of an empty array is the empty string, which is also used
    // by the convenience protobuf serialization API to signal serialization
    // failure. So, supporting serializing an empty array would mean we can't
    // use the convenience serialization API.
    std::move(callback).Run(true);
    return;
  }

  std::vector<CookieChangeSubscription> new_subscriptions =
      CookieChangeSubscription::FromMojoVector(
          std::move(mojo_subscriptions), service_worker_registration->id());
  DCHECK(!new_subscriptions.empty());

  auto old_subscriptions_it =
      subscriptions_by_registration_.find(service_worker_registration_id);
  if (old_subscriptions_it == subscriptions_by_registration_.end()) {
    subscriptions_by_registration_.emplace(service_worker_registration_id,
                                           std::move(new_subscriptions));
    std::move(callback).Run(true);
    return;
  }

  std::vector<CookieChangeSubscription>& old_subscriptions =
      old_subscriptions_it->second;
  old_subscriptions.reserve(old_subscriptions.size() +
                            new_subscriptions.size());
  for (auto& new_subscription : new_subscriptions)
    old_subscriptions.emplace_back(std::move(new_subscription));

  std::move(callback).Run(true);
}

void CookieStoreManager::GetSubscriptions(
    int64_t service_worker_registration_id,
    const url::Origin& origin,
    blink::mojom::CookieStore::GetSubscriptionsCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!done_loading_subscriptions_) {
    subscriptions_loaded_callbacks_.emplace_back(base::BindOnce(
        &CookieStoreManager::GetSubscriptions, weak_factory_.GetWeakPtr(),
        service_worker_registration_id, origin, std::move(callback)));
    return;
  }

  if (!succeeded_loading_subscriptions_) {
    std::move(callback).Run(
        std::vector<blink::mojom::CookieChangeSubscriptionPtr>(), false);
    return;
  }

  auto it = subscriptions_by_registration_.find(service_worker_registration_id);
  if (it == subscriptions_by_registration_.end() || it->second.empty()) {
    std::move(callback).Run(
        std::vector<blink::mojom::CookieChangeSubscriptionPtr>(), true);
    return;
  }

  const url::Origin& first_origin = url::Origin::Create(it->second[0].url());
#if DCHECK_IS_ON()
  for (const auto& subscription : it->second) {
    DCHECK(
        first_origin.IsSameOriginWith(url::Origin::Create(subscription.url())))
        << "Service worker's change subscriptions don't have the same origin";
  }
#endif  // DCHECK_IS_ON()

  if (!origin.IsSameOriginWith(first_origin)) {
    // This error case is a good fit for mojo::ReportBadMessage(), because the
    // renderer has passed an invalid registration ID. However, the code here
    // might run without a mojo call context, if the original call was delayed
    // while loading on-disk subscription data.
    //
    // While it would be possible to have two code paths for the two situations,
    // the extra complexity doesn't seem warranted for the limited debuggig
    // benefits provided by mojo::ReportBadMessage.
    std::move(callback).Run(
        std::vector<blink::mojom::CookieChangeSubscriptionPtr>(), false);
    return;
  }

  std::move(callback).Run(CookieChangeSubscription::ToMojoVector(it->second),
                          true);
}

void CookieStoreManager::OnNewLiveRegistration(
    int64_t service_worker_registration_id,
    const GURL& pattern) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void CookieStoreManager::OnRegistrationStored(
    int64_t service_worker_registration_id,
    const GURL& pattern) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Waiting for the on-disk subscriptions to be loaded ensures that the
  // registration's subscriptions aren't activated twice. Without waiting,
  // there's a risk that LoadAllSubscriptions() sees the result of the
  // StoreRegistrationUserData() call below.
  if (!done_loading_subscriptions_) {
    subscriptions_loaded_callbacks_.emplace_back(base::BindOnce(
        &CookieStoreManager::OnRegistrationStored, weak_factory_.GetWeakPtr(),
        service_worker_registration_id, pattern));
    return;
  }

  auto it = subscriptions_by_registration_.find(service_worker_registration_id);
  if (it == subscriptions_by_registration_.end())
    return;

  ActivateSubscriptions(&it->second);

  std::string subscriptions_data =
      CookieChangeSubscription::SerializeVector(it->second);
  DCHECK(!subscriptions_data.empty())
      << "Failed to create cookie change subscriptions protobuf";

  service_worker_context_->StoreRegistrationUserData(
      service_worker_registration_id, pattern.GetOrigin(),
      std::vector<std::pair<std::string, std::string>>(
          {{registration_user_data_key_, subscriptions_data}}),
      base::BindOnce(&HandleStoreRegistrationUserDataStatus));
}

void CookieStoreManager::OnRegistrationDeleted(
    int64_t service_worker_registration_id,
    const GURL& pattern) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Waiting for the on-disk subscriptions to be loaded ensures that the
  // registration's subscriptions are removed. Without waiting, there's a risk
  // that a registration's subscriptions will finish loading (and thus remain
  // active) right after this function runs.
  if (!done_loading_subscriptions_) {
    subscriptions_loaded_callbacks_.emplace_back(base::BindOnce(
        &CookieStoreManager::OnRegistrationDeleted, weak_factory_.GetWeakPtr(),
        service_worker_registration_id, pattern));
    return;
  }

  auto it = subscriptions_by_registration_.find(service_worker_registration_id);
  if (it == subscriptions_by_registration_.end())
    return;

  DeactivateSubscriptions(&it->second);
  subscriptions_by_registration_.erase(it);
}

void CookieStoreManager::ActivateSubscriptions(
    std::vector<CookieChangeSubscription>* subscriptions) {
  if (subscriptions->empty())
    return;

  // Service workers can only observe changes to cookies for URLs under their
  // scope. This means all the URLs that the worker is observing must map to the
  // same domain key (eTLD+1).
  //
  // TODO(pwnall): This is the same as implementation as
  //               net::CookieMonsterChangeDispatcher::DomainKey. Extract that
  //               implementation into net/cookies.cookie_util.h and call it.
  std::string url_key = net::registry_controlled_domains::GetDomainAndRegistry(

      (*subscriptions)[0].url(),
      net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  base::LinkedList<CookieChangeSubscription>& url_key_subscriptions_list =
      subscriptions_by_url_key_[url_key];

  for (auto& subscription : *subscriptions) {
    DCHECK(!subscription.next() && !subscription.previous())
        << "Subscription passed to " << __func__ << " already activated";
    DCHECK_EQ(url_key,
              net::registry_controlled_domains::GetDomainAndRegistry(
                  subscription.url(),
                  net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES))
        << __func__ << " subscriptions belong to different registrations";
    url_key_subscriptions_list.Append(&subscription);
  }
}

void CookieStoreManager::DeactivateSubscriptions(
    std::vector<CookieChangeSubscription>* subscriptions) {
  if (subscriptions->empty())
    return;

  // Service workers can only observe changes to cookies for URLs under their
  // scope. This means all the URLs that the worker is observing must map to the
  // same domain key (eTLD+1).
  //
  // TODO(pwnall): This has the same implementation as
  //               net::CookieMonsterChangeDispatcher::DomainKey. Extract that
  //               implementation into net/cookies.cookie_util.h and call it.
  std::string url_key = net::registry_controlled_domains::GetDomainAndRegistry(
      (*subscriptions)[0].url(),
      net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  for (auto& subscription : *subscriptions) {
    DCHECK(subscription.next() && subscription.previous())
        << "Subscription passed to " << __func__ << " not previously activated";
    DCHECK_EQ(url_key,
              net::registry_controlled_domains::GetDomainAndRegistry(
                  subscription.url(),
                  net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES))
        << __func__ << " subscriptions belong to different registrations";
    subscription.RemoveFromList();
  }
  auto it = subscriptions_by_url_key_.find(url_key);
  DCHECK(it != subscriptions_by_url_key_.end());
  if (it->second.empty())
    subscriptions_by_url_key_.erase(it);
}

void CookieStoreManager::OnStorageWiped() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Waiting for the on-disk subscriptions to be loaded ensures that all
  // subscriptions are removed. Without waiting, there's a risk that some
  // subscriptions will finish loading (and thus remain active) after this
  // function runs.
  if (!done_loading_subscriptions_) {
    subscriptions_loaded_callbacks_.emplace_back(base::BindOnce(
        &CookieStoreManager::OnStorageWiped, weak_factory_.GetWeakPtr()));
    return;
  }

  subscriptions_by_url_key_.clear();
  subscriptions_by_registration_.clear();
}

void CookieStoreManager::OnCookieChange(
    const net::CanonicalCookie& cookie,
    ::network::mojom::CookieChangeCause cause) {
  // Waiting for on-disk subscriptions to be loaded ensures that changes are
  // delivered to all service workers that subscribed to them in previous
  // browser sessions. Without waiting, workers might miss cookie changes.
  if (!done_loading_subscriptions_) {
    subscriptions_loaded_callbacks_.emplace_back(
        base::BindOnce(&CookieStoreManager::OnCookieChange,
                       weak_factory_.GetWeakPtr(), cookie, cause));
    return;
  }

  // Compute the list of service workers interested in this change. A worker
  // might have multiple subscriptions that cover this change, but should still
  // receive a single change event.
  // TODO(pwnall): This has same as implementation as
  //               net::CookieMonsterChangeDispatcher::DomainKey. Extract that
  //               implementation into net/cookies.cookie_util.h and call it.
  std::string url_key = net::registry_controlled_domains::GetDomainAndRegistry(
      cookie.Domain(),
      net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  auto it = subscriptions_by_url_key_.find(url_key);
  if (it == subscriptions_by_url_key_.end())
    return;
  std::set<int64_t> interested_registration_ids;
  const base::LinkedList<CookieChangeSubscription>& subscriptions = it->second;
  for (const base::LinkNode<CookieChangeSubscription>* node =
           subscriptions.head();
       node != subscriptions.end(); node = node->next()) {
    const CookieChangeSubscription* subscription = node->value();
    if (subscription->ShouldObserveChangeTo(cookie)) {
      interested_registration_ids.insert(
          subscription->service_worker_registration_id());
    }
  }

  // Dispatch the change to interested workers.
  for (int64_t registration_id : interested_registration_ids) {
    service_worker_context_->FindReadyRegistrationForIdOnly(
        registration_id,
        base::BindOnce(
            [](base::WeakPtr<CookieStoreManager> manager,
               const net::CanonicalCookie& cookie,
               ::network::mojom::CookieChangeCause cause,
               blink::ServiceWorkerStatusCode find_status,
               scoped_refptr<ServiceWorkerRegistration> registration) {
              if (find_status != blink::ServiceWorkerStatusCode::kOk)
                return;

              DCHECK(registration);
              if (!manager)
                return;
              manager->DispatchChangeEvent(std::move(registration), cookie,
                                           cause);
            },
            weak_factory_.GetWeakPtr(), cookie, cause));
  }
}

void CookieStoreManager::DispatchChangeEvent(
    scoped_refptr<ServiceWorkerRegistration> registration,
    const net::CanonicalCookie& cookie,
    ::network::mojom::CookieChangeCause cause) {
  scoped_refptr<ServiceWorkerVersion> active_version =
      registration->active_version();
  if (active_version->running_status() != EmbeddedWorkerStatus::RUNNING) {
    active_version->RunAfterStartWorker(
        ServiceWorkerMetrics::EventType::COOKIE_CHANGE,
        base::BindOnce(&CookieStoreManager::DidStartWorkerForChangeEvent,
                       weak_factory_.GetWeakPtr(), std::move(registration),
                       cookie, cause));
    return;
  }

  int request_id = active_version->StartRequest(
      ServiceWorkerMetrics::EventType::COOKIE_CHANGE, base::DoNothing());

  active_version->endpoint()->DispatchCookieChangeEvent(
      cookie, cause, active_version->CreateSimpleEventCallback(request_id));
}

void CookieStoreManager::DidStartWorkerForChangeEvent(
    scoped_refptr<ServiceWorkerRegistration> registration,
    const net::CanonicalCookie& cookie,
    ::network::mojom::CookieChangeCause cause,
    blink::ServiceWorkerStatusCode start_worker_status) {
  if (start_worker_status != blink::ServiceWorkerStatusCode::kOk)
    return;
  DispatchChangeEvent(std::move(registration), cookie, cause);
}

}  // namespace content
