// 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.

#ifndef CONTENT_BROWSER_COOKIE_STORE_COOKIE_STORE_MANAGER_H_
#define CONTENT_BROWSER_COOKIE_STORE_COOKIE_STORE_MANAGER_H_

#include <memory>
#include <string>
#include <vector>

#include "base/callback.h"
#include "base/containers/linked_list.h"
#include "base/macros.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "content/browser/cookie_store/cookie_change_subscription.h"
#include "content/browser/cookie_store/cookie_store_host.h"
#include "content/browser/service_worker/service_worker_context_core_observer.h"
#include "mojo/public/cpp/bindings/strong_binding_set.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "third_party/blink/public/mojom/cookie_store/cookie_store.mojom.h"
#include "url/origin.h"

class GURL;

namespace content {

class ServiceWorkerContextWrapper;
class ServiceWorkerRegistration;

// Manages cookie change subscriptions for a StoragePartition's service workers.
//
// Subscriptions are stored along with their associated service worker
// registrations in ServiceWorkerStorage, as user data. When a service worker is
// unregistered, its cookie change subscriptions are removed. The storage method
// (user data) is an implementation detail. Callers should not rely on it, as
// the storage method may change in the future.
//
// Instances of this class must be accessed exclusively on the IO thread,
// because they call into ServiceWorkerContextWrapper methods that are
// restricted to the IO thread.
class CookieStoreManager : public ServiceWorkerContextCoreObserver,
                           public ::network::mojom::CookieChangeListener {
 public:
  // Creates a CookieStoreManager with an empty in-memory subscription database.
  //
  // The in-memory subscription database must be populated with data from disk,
  // by calling ReadAllSubscriptions().
  CookieStoreManager(
      scoped_refptr<ServiceWorkerContextWrapper> service_worker_context);

  ~CookieStoreManager() override;

  // Creates a mojo connection to a service worker.
  //
  // This is called when service workers use the Cookie Store API to subscribe
  // to cookie changes or obtain the list of cookie changes.
  void CreateService(blink::mojom::CookieStoreRequest request,
                     const url::Origin& origin);

  // Starts loading the on-disk subscription data.
  //
  // Returns after scheduling the work. The callback is called with a boolean
  // that indicates if the load operation succeeded.
  //
  // It is safe to call all the other CookieStoreManager methods during the
  // loading operation. The CookieStoreManager has well-defined semantics if
  // loading fails, so it is not necessary to handle loading errors.
  void LoadAllSubscriptions(base::OnceCallback<void(bool)> callback);

  // Processes cookie changes from a network service instance.
  void ListenToCookieChanges(::network::mojom::CookieManagerPtr cookie_manager,
                             base::OnceCallback<void(bool)> callback);

  // content::mojom::CookieStore implementation
  void AppendSubscriptions(
      int64_t service_worker_registration_id,
      const url::Origin& origin,
      std::vector<blink::mojom::CookieChangeSubscriptionPtr> mojo_subscriptions,
      blink::mojom::CookieStore::AppendSubscriptionsCallback callback);
  void GetSubscriptions(
      int64_t service_worker_registration_id,
      const url::Origin& origin,
      blink::mojom::CookieStore::GetSubscriptionsCallback callback);

  // ServiceWorkerContextCoreObserver
  void OnRegistrationStored(int64_t service_worker_registration_id,
                            const GURL& pattern) override;
  void OnRegistrationDeleted(int64_t service_worker_registration_id,
                             const GURL& pattern) override;
  void OnNewLiveRegistration(int64_t service_worker_registration_id,
                             const GURL& pattern) override;
  void OnStorageWiped() override;

  // ::network::mojom::CookieChangeListener
  void OnCookieChange(const net::CanonicalCookie& cookie,
                      ::network::mojom::CookieChangeCause cause) override;

 private:
  // Updates internal state with the result of loading disk subscription data.
  //
  // Called exactly once.
  void ProcessOnDiskSubscriptions(
      base::OnceCallback<void(bool)> load_callback,
      const std::vector<std::pair<int64_t, std::string>>& user_data,
      blink::ServiceWorkerStatusCode status);

  // Runs all the callbacks waiting for on-disk subscription data.
  //
  // Called exactly once, after on-disk subcriptions have been loaded.
  void DidLoadAllSubscriptions(bool succeeded,
                               base::OnceCallback<void(bool)> load_callback);

  // Starts sending cookie change events to a service worker.
  //
  // All subscriptions must belong to the same service worker registration. This
  // method is not idempotent.
  void ActivateSubscriptions(
      std::vector<CookieChangeSubscription>* subscriptions);

  // Stops sending cookie change events to a service worker.
  //
  // All subscriptions must belong to the same service worker registration. This
  // method is not idempotent.
  void DeactivateSubscriptions(
      std::vector<CookieChangeSubscription>* subscriptions);

  // Sends a cookie change to interested service workers.
  //
  // Must only be called after the on-disk subscription data is successfully
  // loaded.
  void DispatchCookieChange(const net::CanonicalCookie& cookie,
                            ::network::mojom::CookieChangeCause cause);

  // Sends a cookie change event to one service worker.
  void DispatchChangeEvent(
      scoped_refptr<ServiceWorkerRegistration> registration,
      const net::CanonicalCookie& cookie,
      ::network::mojom::CookieChangeCause cause);

  // Called after a service worker was started so it can get a cookie change.
  void DidStartWorkerForChangeEvent(
      scoped_refptr<ServiceWorkerRegistration> registration,
      const net::CanonicalCookie& cookie,
      ::network::mojom::CookieChangeCause cause,
      blink::ServiceWorkerStatusCode start_worker_status);

  // Used to efficiently implement OnRegistrationDeleted().
  //
  // When a service worker registration is removed from the system, the
  // CookieStoreManager needs to remove all the cookie change subscriptions
  // associated with the registration. Looking up the registration ID in the
  // |subscriptions_by_registration_| map is done in O(1) time, and then each
  // subscription is removed from a LinkedList in |subscription_by_url_key_| in
  // O(1) time.
  std::unordered_map<int64_t, std::vector<CookieChangeSubscription>>
      subscriptions_by_registration_;

  // Used to efficiently implement DispatchCookieChange().
  //
  // When a cookie change notification comes from the network service, the
  // CookieStoreManager needs to dispatch events to the workers with relevant
  // subscriptions. |subscriptions_by_url_key_| indexes change subscriptions
  // according to the eTLD+1 of the subscription's scope URL, so each cookie
  // change only needs to be checked against the subscriptions of the service
  // workers in the same eTLD+1. The reduction in work is signficant, given that
  // checking whether a subscription matches a cookie isn't very cheap.
  //
  // The current implementation's performance profile could have been achieved
  // with a map from eTLD+1 to registration IDs, which would not have required
  // linked lists. However, the current approach is more amenable to future
  // optimizations, such as partitioning by (eTLD+1, cookie name).
  std::map<std::string, base::LinkedList<CookieChangeSubscription>>
      subscriptions_by_url_key_;

  // Used to look up and modify service worker registration data.
  scoped_refptr<ServiceWorkerContextWrapper> service_worker_context_;

  // Tracks the open mojo pipes created by CreateService().
  //
  // Each pipe is associated with the CookieStoreHost instance that it is
  // connected to. When the pipe is closed, the StrongBindingSet automatically
  // deletes the CookieStoreHost.
  mojo::StrongBindingSet<blink::mojom::CookieStore> bindings_;

  // Used to receive cookie changes from the network service.
  ::network::mojom::CookieManagerPtr cookie_manager_;
  mojo::Binding<::network::mojom::CookieChangeListener>
      cookie_change_listener_binding_;

  // The service worker registration user data key for subscription data.
  //
  // All the subscriptions associated with a registration are stored in a single
  // user data entry whose key is |registration_user_data_key_|, and whose value
  // is a serialized CookieChangeSubscriptionsProto.
  const std::string registration_user_data_key_;

  // Called after all subscriptions have been loaded.
  //
  // Callbacks can assume that |done_loading_subscriptions_| is true
  // and |succeeded_loading_subscriptions_| is set. If the latter is true,
  // |subscriptions_by_registration_| and |subscriptions_by_url_key_| will also
  // be populated.
  std::vector<base::OnceClosure> subscriptions_loaded_callbacks_;

  // Set to true once all subscriptions have been loaded.
  bool done_loading_subscriptions_ = false;

  // Only defined when |done_loading_subscriptions_| is true.
  bool succeeded_loading_subscriptions_ = false;

  // Instances of this class are currently bound to the IO thread, because they
  // call ServiceWorkerContextWrapper methods that are restricted to the IO
  // thread. However, the class implementation itself is thread-friendly, so it
  // only checks that methods are called on the same sequence.
  SEQUENCE_CHECKER(sequence_checker_);

  // Supports having the manager destroyed while waiting for disk I/O.
  base::WeakPtrFactory<CookieStoreManager> weak_factory_;

  DISALLOW_COPY_AND_ASSIGN(CookieStoreManager);
};

}  // namespace content

#endif  // CONTENT_BROWSER_COOKIE_STORE_COOKIE_STORE_MANAGER_H_
