/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "CookieBannerDomainPrefService.h"

#include "mozilla/ClearOnShutdown.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Logging.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Services.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StaticPtr.h"

#include "nsIContentPrefService2.h"
#include "nsICookieBannerService.h"
#include "nsIObserverService.h"
#include "nsServiceManagerUtils.h"
#include "nsVariant.h"

#define COOKIE_BANNER_CONTENT_PREF_NAME u"cookiebanner"_ns
#define COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE u"cookiebannerprivate"_ns

namespace mozilla {

NS_IMPL_ISUPPORTS(CookieBannerDomainPrefService, nsIAsyncShutdownBlocker,
                  nsIObserver)

NS_IMPL_ISUPPORTS(CookieBannerDomainPrefService::DomainPrefData, nsISupports)

LazyLogModule gCookieBannerPerSitePrefLog("CookieBannerDomainPref");

static StaticRefPtr<CookieBannerDomainPrefService>
    sCookieBannerDomainPrefService;

namespace {

// A helper function to get the profile-before-change shutdown barrier.

nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier() {
  nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
  NS_ENSURE_TRUE(svc, nullptr);

  nsCOMPtr<nsIAsyncShutdownClient> barrier;
  nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier));
  NS_ENSURE_SUCCESS(rv, nullptr);

  return barrier;
}

}  // anonymous namespace

/* static */
already_AddRefed<CookieBannerDomainPrefService>
CookieBannerDomainPrefService::GetOrCreate() {
  if (!sCookieBannerDomainPrefService) {
    sCookieBannerDomainPrefService = new CookieBannerDomainPrefService();

    RunOnShutdown([] {
      MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Debug, ("RunOnShutdown."));

      sCookieBannerDomainPrefService->Shutdown();

      sCookieBannerDomainPrefService = nullptr;
    });
  }

  return do_AddRef(sCookieBannerDomainPrefService);
}

void CookieBannerDomainPrefService::Init() {
  // Make sure we won't init again.
  if (mIsInitialized) {
    return;
  }

  nsCOMPtr<nsIContentPrefService2> contentPrefService =
      do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);

  if (!contentPrefService) {
    return;
  }

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();

  if (!obs) {
    return;
  }

  // Register the observer to watch private browsing session ends. We will clean
  // the private domain prefs when this happens.
  nsresult rv = obs->AddObserver(this, "last-pb-context-exited", false);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "Fail to add observer for 'last-pb-context-exited'.");

  auto initCallback = MakeRefPtr<InitialLoadContentPrefCallback>(this, false);

  // Populate the content pref for cookie banner domain preferences.
  rv = contentPrefService->GetByName(COOKIE_BANNER_CONTENT_PREF_NAME, nullptr,
                                     initCallback);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "Fail to get all content prefs during init.");

  auto initPrivateCallback =
      MakeRefPtr<InitialLoadContentPrefCallback>(this, true);

  // Populate the content pref for the private browsing.
  rv = contentPrefService->GetByName(COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE,
                                     nullptr, initPrivateCallback);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "Fail to get all content prefs during init.");

  rv = AddShutdownBlocker();
  NS_ENSURE_SUCCESS_VOID(rv);

  mIsInitialized = true;
}

void CookieBannerDomainPrefService::Shutdown() {
  // Bail out early if the service never gets initialized.
  if (!mIsInitialized) {
    return;
  }

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();

  if (!obs) {
    return;
  }

  DebugOnly<nsresult> rv = obs->RemoveObserver(this, "last-pb-context-exited");
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "Fail to remove observer for 'last-pb-context-exited'.");
}

Maybe<nsICookieBannerService::Modes> CookieBannerDomainPrefService::GetPref(
    const nsACString& aDomain, bool aIsPrivate) {
  bool isContentPrefLoaded =
      aIsPrivate ? mIsPrivateContentPrefLoaded : mIsContentPrefLoaded;

  // We return nothing if the first reading of the content pref is not completed
  // yet. Note that, we won't be able to get the domain pref for early loads.
  // But, we think this is acceptable because the cookie banners on the early
  // load tabs would have interacted before when the user disabled the banner
  // handling. So, there should be consent cookies in place to prevent banner
  // showing. In this case, our cookie injection and banner clicking won't do
  // anything.
  if (!isContentPrefLoaded) {
    return Nothing();
  }

  Maybe<RefPtr<DomainPrefData>> data =
      aIsPrivate ? mPrefsPrivate.MaybeGet(aDomain) : mPrefs.MaybeGet(aDomain);

  if (!data) {
    return Nothing();
  }

  return Some(data.ref()->mMode);
}

nsresult CookieBannerDomainPrefService::SetPref(
    const nsACString& aDomain, nsICookieBannerService::Modes aMode,
    bool aIsPrivate, bool aPersistInPrivateBrowsing) {
  MOZ_ASSERT(NS_IsMainThread());
  // Don't do anything if we are shutting down.
  if (NS_WARN_IF(mIsShuttingDown)) {
    MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning,
            ("Attempt to set a domain pref while shutting down."));
    return NS_OK;
  }

  EnsureInitCompleted(aIsPrivate);

  // Create the domain pref data. The data is always persistent for normal
  // windows. For private windows, the data is only persistent if requested.
  auto domainPrefData = MakeRefPtr<DomainPrefData>(
      aMode, aIsPrivate ? aPersistInPrivateBrowsing : true);
  bool wasPersistentInPrivate = false;

  // Update the in-memory domain preference map.
  if (aIsPrivate) {
    Maybe<RefPtr<DomainPrefData>> data = mPrefsPrivate.MaybeGet(aDomain);

    wasPersistentInPrivate = data ? data.ref()->mIsPersistent : false;
    (void)mPrefsPrivate.InsertOrUpdate(aDomain, domainPrefData);
  } else {
    (void)mPrefs.InsertOrUpdate(aDomain, domainPrefData);
  }

  // For private windows, the domain prefs will only be stored in memory.
  // Unless, this function is instructed to persist setting for private
  // browsing. To make the disk state consistent with the memory state, we need
  // to clear the domain pref in the disk when we no longer need to persist the
  // domain pref for the domain in PBM.
  if (!aPersistInPrivateBrowsing && aIsPrivate) {
    // Clear the domain pref in disk if it was persistent.
    if (wasPersistentInPrivate) {
      return RemoveContentPrefForDomain(aDomain, true);
    }
    return NS_OK;
  }

  // Set the preference to the content pref service.
  nsCOMPtr<nsIContentPrefService2> contentPrefService =
      do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
  NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE);

  RefPtr<nsVariant> variant = new nsVariant();
  nsresult rv = variant->SetAsUint8(aMode);
  NS_ENSURE_SUCCESS(rv, rv);

  auto callback = MakeRefPtr<WriteContentPrefCallback>(this);
  mWritingCount++;

  // Store the domain preference to the content pref service.
  rv = contentPrefService->Set(NS_ConvertUTF8toUTF16(aDomain),
                               aIsPrivate
                                   ? COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE
                                   : COOKIE_BANNER_CONTENT_PREF_NAME,
                               variant, nullptr, callback);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "Fail to set cookie banner domain pref.");

  return rv;
}

nsresult CookieBannerDomainPrefService::RemovePref(const nsACString& aDomain,
                                                   bool aIsPrivate) {
  MOZ_ASSERT(NS_IsMainThread());
  // Don't do anything if we are shutting down.
  if (NS_WARN_IF(mIsShuttingDown)) {
    MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning,
            ("Attempt to remove a domain pref while shutting down."));
    return NS_OK;
  }

  EnsureInitCompleted(aIsPrivate);

  // Clear in-memory domain pref.
  if (aIsPrivate) {
    mPrefsPrivate.Remove(aDomain);
  } else {
    mPrefs.Remove(aDomain);
  }

  return RemoveContentPrefForDomain(aDomain, aIsPrivate);
}

nsresult CookieBannerDomainPrefService::RemoveAll(bool aIsPrivate) {
  MOZ_ASSERT(NS_IsMainThread());
  // Don't do anything if we are shutting down.
  if (NS_WARN_IF(mIsShuttingDown)) {
    MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning,
            ("Attempt to remove all domain prefs while shutting down."));
    return NS_OK;
  }

  EnsureInitCompleted(aIsPrivate);

  // Clear in-memory domain pref.
  if (aIsPrivate) {
    mPrefsPrivate.Clear();
  } else {
    mPrefs.Clear();
  }

  nsCOMPtr<nsIContentPrefService2> contentPrefService =
      do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
  NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE);

  auto callback = MakeRefPtr<WriteContentPrefCallback>(this);
  mWritingCount++;

  // Remove all the domain preferences.
  nsresult rv = contentPrefService->RemoveByName(
      aIsPrivate ? COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE
                 : COOKIE_BANNER_CONTENT_PREF_NAME,
      nullptr, callback);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "Fail to remove all cookie banner domain prefs.");

  return rv;
}

void CookieBannerDomainPrefService::EnsureInitCompleted(bool aIsPrivate) {
  bool& isContentPrefLoaded =
      aIsPrivate ? mIsPrivateContentPrefLoaded : mIsContentPrefLoaded;
  if (isContentPrefLoaded) {
    return;
  }

  // Wait until the service is fully initialized.
  SpinEventLoopUntil("CookieBannerDomainPrefService::EnsureUpdateComplete"_ns,
                     [&] { return isContentPrefLoaded; });
}

nsresult CookieBannerDomainPrefService::AddShutdownBlocker() {
  MOZ_ASSERT(!mIsShuttingDown);

  nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
  NS_ENSURE_TRUE(barrier, NS_ERROR_FAILURE);

  return GetShutdownBarrier()->AddBlocker(
      this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
      u"CookieBannerDomainPrefService: shutdown"_ns);
}

nsresult CookieBannerDomainPrefService::RemoveShutdownBlocker() {
  MOZ_ASSERT(mIsShuttingDown);

  nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
  NS_ENSURE_TRUE(barrier, NS_ERROR_FAILURE);

  return GetShutdownBarrier()->RemoveBlocker(this);
}

nsresult CookieBannerDomainPrefService::RemoveContentPrefForDomain(
    const nsACString& aDomain, bool aIsPrivate) {
  nsCOMPtr<nsIContentPrefService2> contentPrefService =
      do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
  NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE);

  auto callback = MakeRefPtr<WriteContentPrefCallback>(this);
  mWritingCount++;

  // Remove the domain preference from the content pref service.
  nsresult rv = contentPrefService->RemoveByDomainAndName(
      NS_ConvertUTF8toUTF16(aDomain),
      aIsPrivate ? COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE
                 : COOKIE_BANNER_CONTENT_PREF_NAME,
      nullptr, callback);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "Fail to remove cookie banner domain pref.");
  return rv;
}

NS_IMPL_ISUPPORTS(CookieBannerDomainPrefService::BaseContentPrefCallback,
                  nsIContentPrefCallback2)

NS_IMETHODIMP
CookieBannerDomainPrefService::InitialLoadContentPrefCallback::HandleResult(
    nsIContentPref* aPref) {
  NS_ENSURE_ARG_POINTER(aPref);
  MOZ_ASSERT(mService);

  nsAutoString domain;
  nsresult rv = aPref->GetDomain(domain);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIVariant> value;
  rv = aPref->GetValue(getter_AddRefs(value));
  NS_ENSURE_SUCCESS(rv, rv);

  if (!value) {
    return NS_OK;
  }

  uint8_t data;
  rv = value->GetAsUint8(&data);
  NS_ENSURE_SUCCESS(rv, rv);

  // Create the domain pref data and indicate it's persistent.
  auto domainPrefData =
      MakeRefPtr<DomainPrefData>(nsICookieBannerService::Modes(data), true);

  if (mIsPrivate) {
    (void)mService->mPrefsPrivate.InsertOrUpdate(NS_ConvertUTF16toUTF8(domain),
                                                 domainPrefData);
  } else {
    (void)mService->mPrefs.InsertOrUpdate(NS_ConvertUTF16toUTF8(domain),
                                          domainPrefData);
  }

  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::InitialLoadContentPrefCallback::HandleCompletion(
    uint16_t aReason) {
  MOZ_ASSERT(mService);

  if (mIsPrivate) {
    mService->mIsPrivateContentPrefLoaded = true;
  } else {
    mService->mIsContentPrefLoaded = true;
  }

  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::InitialLoadContentPrefCallback::HandleError(
    nsresult error) {
  // We don't need to do anything here because HandleCompletion is always
  // called.

  if (NS_WARN_IF(NS_FAILED(error))) {
    MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning,
            ("Fail to get content pref during initiation."));
  }

  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::WriteContentPrefCallback::HandleResult(
    nsIContentPref* aPref) {
  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::WriteContentPrefCallback::HandleCompletion(
    uint16_t aReason) {
  MOZ_ASSERT(mService);
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mService->mWritingCount > 0);

  mService->mWritingCount--;

  // Remove the shutdown blocker after we complete writing to content pref.
  if (mService->mIsShuttingDown && mService->mWritingCount == 0) {
    mService->RemoveShutdownBlocker();
  }
  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::WriteContentPrefCallback::HandleError(
    nsresult error) {
  if (NS_WARN_IF(NS_FAILED(error))) {
    MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning,
            ("Fail to write content pref."));
    return NS_OK;
  }

  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::Observe(nsISupports* /*aSubject*/,
                                       const char* aTopic,
                                       const char16_t* /*aData*/) {
  if (strcmp(aTopic, "last-pb-context-exited") != 0) {
    MOZ_ASSERT_UNREACHABLE("unexpected topic");
    return NS_ERROR_UNEXPECTED;
  }

  // Clear the private browsing domain prefs that are not persistent when we
  // observe the private browsing session has ended.
  mPrefsPrivate.RemoveIf([](const auto& iter) {
    const RefPtr<DomainPrefData>& data = iter.Data();
    return !data->mIsPersistent;
  });

  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::GetName(nsAString& aName) {
  aName.AssignLiteral(
      "CookieBannerDomainPrefService: write content pref before "
      "profile-before-change.");
  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::BlockShutdown(nsIAsyncShutdownClient*) {
  MOZ_ASSERT(NS_IsMainThread());

  mIsShuttingDown = true;

  // If we are not writing the content pref, we can remove the shutdown blocker
  // directly.
  if (mWritingCount == 0) {
    RemoveShutdownBlocker();
  }
  return NS_OK;
}

NS_IMETHODIMP
CookieBannerDomainPrefService::GetState(nsIPropertyBag**) { return NS_OK; }

}  // namespace mozilla
