/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "AntiTrackingLog.h"
#include "DynamicFpiRedirectHeuristic.h"
#include "ContentBlockingAllowList.h"
#include "ContentBlockingUserInteraction.h"
#include "StorageAccessAPIHelper.h"

#include "mozilla/net/HttpBaseChannel.h"
#include "mozilla/net/UrlClassifierCommon.h"
#include "mozilla/Telemetry.h"
#include "nsContentUtils.h"
#include "nsIChannel.h"
#include "nsICookieJarSettings.h"
#include "nsICookieService.h"
#include "nsIEffectiveTLDService.h"
#include "nsINavHistoryService.h"
#include "nsIRedirectHistoryEntry.h"
#include "nsIScriptError.h"
#include "nsIURI.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsScriptSecurityManager.h"
#include "nsToolkitCompsCID.h"

namespace mozilla {

namespace {

nsresult GetBaseDomain(nsIURI* aURI, nsACString& aBaseDomain) {
  nsCOMPtr<nsIEffectiveTLDService> tldService =
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);

  if (!tldService) {
    return NS_ERROR_FAILURE;
  }

  return tldService->GetBaseDomain(aURI, 0, aBaseDomain);
}

// check if there's any interacting visit within the given seconds
bool HasEligibleVisit(
    nsIURI* aURI,
    int64_t aSinceInSec = StaticPrefs::
        privacy_restrict3rdpartystorage_heuristic_recently_visited_time()) {
  nsresult rv;

  nsAutoCString baseDomain;
  rv = GetBaseDomain(aURI, baseDomain);
  NS_ENSURE_SUCCESS(rv, false);

  nsCOMPtr<nsINavHistoryService> histSrv =
      do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID);
  if (!histSrv) {
    return false;
  }
  nsCOMPtr<nsINavHistoryQuery> histQuery;
  rv = histSrv->GetNewQuery(getter_AddRefs(histQuery));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  rv = histQuery->SetDomain(baseDomain);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  rv = histQuery->SetDomainIsHost(false);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  PRTime beginTime = PR_Now() - PRTime(PR_USEC_PER_SEC) * aSinceInSec;
  rv = histQuery->SetBeginTime(beginTime);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  nsCOMPtr<nsINavHistoryQueryOptions> histQueryOpts;
  rv = histSrv->GetNewQueryOptions(getter_AddRefs(histQueryOpts));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  rv =
      histQueryOpts->SetResultType(nsINavHistoryQueryOptions::RESULTS_AS_VISIT);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  rv = histQueryOpts->SetMaxResults(1);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  rv = histQueryOpts->SetQueryType(
      nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  nsCOMPtr<nsINavHistoryResult> histResult;
  rv = histSrv->ExecuteQuery(histQuery, histQueryOpts,
                             getter_AddRefs(histResult));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  nsCOMPtr<nsINavHistoryContainerResultNode> histResultContainer;
  rv = histResult->GetRoot(getter_AddRefs(histResultContainer));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  rv = histResultContainer->SetContainerOpen(true);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  uint32_t childCount = 0;
  rv = histResultContainer->GetChildCount(&childCount);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  rv = histResultContainer->SetContainerOpen(false);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  return childCount > 0;
}

void AddConsoleReport(nsIChannel* aNewChannel, nsIURI* aNewURI,
                      const nsACString& aOldOrigin,
                      const nsACString& aNewOrigin) {
  nsCOMPtr<net::HttpBaseChannel> httpChannel = do_QueryInterface(aNewChannel);
  if (!httpChannel) {
    return;
  }

  nsAutoCString uri;
  nsresult rv = aNewURI->GetSpec(uri);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  AutoTArray<nsString, 2> params = {NS_ConvertUTF8toUTF16(aNewOrigin),
                                    NS_ConvertUTF8toUTF16(aOldOrigin)};

  httpChannel->AddConsoleReport(nsIScriptError::warningFlag,
                                ANTITRACKING_CONSOLE_CATEGORY,
                                nsContentUtils::eNECKO_PROPERTIES, uri, 0, 0,
                                "CookieAllowedForFpiByHeuristic"_ns, params);
}

bool ShouldRedirectHeuristicApplyTrackingResource(nsIChannel* aOldChannel,
                                                  nsIURI* aOldURI,
                                                  nsIChannel* aNewChannel,
                                                  nsIURI* aNewURI) {
  nsCOMPtr<nsIClassifiedChannel> classifiedOldChannel =
      do_QueryInterface(aOldChannel);
  if (!classifiedOldChannel) {
    LOG_SPEC2(("Ignoring redirect for %s to %s because there is not "
               "nsIClassifiedChannel interface",
               _spec1, _spec2),
              aOldURI, aNewURI);
    return false;
  }

  // We're looking at the first-party classification flags because we're
  // interested in first-party redirects.
  uint32_t oldClassificationFlags =
      classifiedOldChannel->GetFirstPartyClassificationFlags();

  if (net::UrlClassifierCommon::IsTrackingClassificationFlag(
          oldClassificationFlags, NS_UsePrivateBrowsing(aOldChannel))) {
    // This is a redirect from tracking.
    LOG_SPEC2(("Ignoring redirect for %s to %s because it's from tracking ",
               _spec1, _spec2),
              aOldURI, aNewURI);
    return false;
  }

  return true;
}

}  // namespace

void DynamicFpiRedirectHeuristic(nsIChannel* aOldChannel, nsIURI* aOldURI,
                                 nsIChannel* aNewChannel, nsIURI* aNewURI) {
  MOZ_ASSERT(aOldChannel);
  MOZ_ASSERT(aOldURI);
  MOZ_ASSERT(aNewChannel);
  MOZ_ASSERT(aNewURI);

  nsresult rv;

  if (!StaticPrefs::privacy_antitracking_enableWebcompat() ||
      !StaticPrefs::
          privacy_restrict3rdpartystorage_heuristic_recently_visited()) {
    return;
  }

  nsCOMPtr<nsIHttpChannel> oldChannel = do_QueryInterface(aOldChannel);
  nsCOMPtr<nsIHttpChannel> newChannel = do_QueryInterface(aNewChannel);
  if (!oldChannel || !newChannel) {
    return;
  }

  LOG_SPEC(("Checking dfpi redirect-heuristic for %s", _spec), aOldURI);

  nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo();
  MOZ_ASSERT(oldLoadInfo);

  nsCOMPtr<nsILoadInfo> newLoadInfo = aNewChannel->LoadInfo();
  MOZ_ASSERT(newLoadInfo);

  ExtContentPolicyType contentType =
      oldLoadInfo->GetExternalContentPolicyType();
  if (contentType != ExtContentPolicy::TYPE_DOCUMENT ||
      !aOldChannel->IsDocument()) {
    LOG_SPEC(("Ignoring redirect for %s because it's not a document", _spec),
             aOldURI);
    // We care about document redirects only.
    return;
  }

  nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
  rv = oldLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    LOG(("Can't get the cookieJarSettings"));
    return;
  }

  int32_t behavior = cookieJarSettings->GetCookieBehavior();
  if (behavior !=
      nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
    LOG(
        ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out "
         "early",
         behavior));
    return;
  }

  nsIScriptSecurityManager* ssm =
      nsScriptSecurityManager::GetScriptSecurityManager();
  MOZ_ASSERT(ssm);

  nsCOMPtr<nsIPrincipal> oldPrincipal;
  const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& chain =
      oldLoadInfo->RedirectChain();
  if (!chain.IsEmpty()) {
    rv = chain[0]->GetPrincipal(getter_AddRefs(oldPrincipal));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      LOG(("Can't obtain the principal from the redirect chain"));
      return;
    }
  } else {
    rv = ssm->GetChannelResultPrincipal(aOldChannel,
                                        getter_AddRefs(oldPrincipal));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      LOG(("Can't obtain the principal from the old channel"));
      return;
    }
  }

  nsCOMPtr<nsIPrincipal> newPrincipal;
  rv =
      ssm->GetChannelResultPrincipal(aNewChannel, getter_AddRefs(newPrincipal));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    LOG(("Can't obtain the principal from the new channel"));
    return;
  }

  if (oldPrincipal->Equals(newPrincipal)) {
    LOG(("No permission needed for same principals."));
    return;
  }

  if (!ShouldRedirectHeuristicApplyTrackingResource(aOldChannel, aOldURI,
                                                    aNewChannel, aNewURI)) {
    LOG_SPEC2(("Ignoring redirect for %s to %s because tracking test failed",
               _spec1, _spec2),
              aOldURI, aNewURI);
    return;
  }

  if (!ContentBlockingUserInteraction::Exists(oldPrincipal) ||
      !ContentBlockingUserInteraction::Exists(newPrincipal)) {
    LOG_SPEC2(("Ignoring redirect for %s to %s because no user-interaction on "
               "both pages",
               _spec1, _spec2),
              aOldURI, aNewURI);
    return;
  }

  nsAutoCString oldOrigin;
  rv = oldPrincipal->GetOriginNoSuffix(oldOrigin);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    LOG(("Can't get the origin from the old Principal"));
    return;
  }

  nsAutoCString newOrigin;
  rv = newPrincipal->GetOriginNoSuffix(newOrigin);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    LOG(("Can't get the origin from the new Principal"));
    return;
  }

  if (!HasEligibleVisit(aOldURI) || !HasEligibleVisit(aNewURI)) {
    LOG(("No previous visit record, bailing out early."));
    return;
  }

  LOG(("Adding a first-party storage exception for %s...",
       PromiseFlatCString(newOrigin).get()));

  LOG(("Saving the permission: oldOrigin=%s, grantedOrigin=%s", oldOrigin.get(),
       newOrigin.get()));

  AddConsoleReport(aNewChannel, aNewURI, oldOrigin, newOrigin);

  Telemetry::AccumulateCategorical(
      Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted);
  Telemetry::AccumulateCategorical(
      Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Redirect);

  // We don't care about this promise because the operation is actually sync.
  RefPtr<StorageAccessAPIHelper::ParentAccessGrantPromise> promise =
      StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
          newPrincipal, oldPrincipal,
          StorageAccessAPIHelper::StorageAccessPromptChoices::eAllow, false,
          StaticPrefs::privacy_restrict3rdpartystorage_expiration_visited());
  Unused << promise;
}

}  // namespace mozilla
