/* 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 "ContentBlockingLog.h"

#include <bit>

#include "nsIEffectiveTLDService.h"
#include "nsITrackingDBService.h"
#include "nsIWebProgressListener.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsRFPService.h"
#include "nsServiceManagerUtils.h"
#include "nsTArray.h"
#include "nsTHashSet.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/ReverseIterator.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/StaticPrefs_telemetry.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/glean/AntitrackingMetrics.h"

static mozilla::LazyLogModule gFingerprinterDetection("FingerprinterDetection");

namespace mozilla {

namespace {

StaticAutoPtr<nsCString> gEmailWebAppDomainsPref;
static constexpr char kEmailWebAppDomainPrefName[] =
    "privacy.trackingprotection.emailtracking.webapp.domains";

void EmailWebAppDomainPrefChangeCallback(const char* aPrefName, void*) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!strcmp(aPrefName, kEmailWebAppDomainPrefName));
  MOZ_ASSERT(gEmailWebAppDomainsPref);

  Preferences::GetCString(kEmailWebAppDomainPrefName, *gEmailWebAppDomainsPref);
}

}  // namespace

Maybe<uint32_t> ContentBlockingLog::RecordLogParent(
    const nsACString& aOrigin, uint32_t aType, bool aBlocked,
    const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
        aReason,
    const nsTArray<nsCString>& aTrackingFullHashes,
    const Maybe<CanvasFingerprintingEvent>& aCanvasFingerprintingEvent) {
  MOZ_ASSERT(XRE_IsParentProcess());

  uint32_t events = GetContentBlockingEventsInLog();

  bool blockedValue = aBlocked;
  bool unblocked = false;
  OriginEntry* entry;

  switch (aType) {
    case nsIWebProgressListener::STATE_COOKIES_LOADED:
      MOZ_ASSERT(!aBlocked,
                 "We don't expected to see blocked STATE_COOKIES_LOADED");
      [[fallthrough]];

    case nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER:
      MOZ_ASSERT(
          !aBlocked,
          "We don't expected to see blocked STATE_COOKIES_LOADED_TRACKER");
      [[fallthrough]];

    case nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER:
      MOZ_ASSERT(!aBlocked,
                 "We don't expected to see blocked "
                 "STATE_COOKIES_LOADED_SOCIALTRACKER");
      // Note that the logic in these branches are the logical negation of the
      // logic in other branches, since the Document API we have is phrased
      // in "loaded" terms as opposed to "blocked" terms.
      blockedValue = !aBlocked;
      [[fallthrough]];

    case nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT:
    case nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT:
    case nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT:
    case nsIWebProgressListener::STATE_BLOCKED_FINGERPRINTING_CONTENT:
    case nsIWebProgressListener::STATE_LOADED_FINGERPRINTING_CONTENT:
    case nsIWebProgressListener::STATE_BLOCKED_CRYPTOMINING_CONTENT:
    case nsIWebProgressListener::STATE_LOADED_CRYPTOMINING_CONTENT:
    case nsIWebProgressListener::STATE_BLOCKED_SOCIALTRACKING_CONTENT:
    case nsIWebProgressListener::STATE_LOADED_SOCIALTRACKING_CONTENT:
    case nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION:
    case nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL:
    case nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN:
    case nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT:
    case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT:
    case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT:
    case nsIWebProgressListener::STATE_PURGED_BOUNCETRACKER:
    case nsIWebProgressListener::STATE_COOKIES_PARTITIONED_TRACKER:
      (void)RecordLogInternal(aOrigin, aType, blockedValue);
      break;

    case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER:
    case nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER:
      (void)RecordLogInternal(aOrigin, aType, blockedValue, aReason,
                              aTrackingFullHashes);
      break;

    case nsIWebProgressListener::STATE_REPLACED_FINGERPRINTING_CONTENT:
    case nsIWebProgressListener::STATE_ALLOWED_FINGERPRINTING_CONTENT:
    case nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT:
    case nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT:
      (void)RecordLogInternal(aOrigin, aType, blockedValue, aReason,
                              aTrackingFullHashes);
      break;
    case nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING:
      MOZ_ASSERT(!aBlocked,
                 "We don't expected to see blocked "
                 "STATE_ALLOWED_FONT_FINGERPRINTING");
      entry = RecordLogInternal(aOrigin, aType, blockedValue);

      // Replace the flag using the suspicious fingerprinting event so that we
      // can report the event if we detect suspicious fingerprinting.
      aType = nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING;

      // Report blocking if we detect suspicious fingerprinting activity.
      if (entry && entry->mData->mHasSuspiciousFingerprintingActivity) {
        blockedValue = true;
      }
      break;

    case nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING:
      MOZ_ASSERT(!aBlocked,
                 "We don't expected to see blocked "
                 "STATE_ALLOWED_CANVAS_FINGERPRINTING");
      entry = RecordLogInternal(aOrigin, aType, blockedValue, Nothing(), {},
                                aCanvasFingerprintingEvent);

      // Replace the flag using the suspicious fingerprinting event so that we
      // can report the event if we detect suspicious fingerprinting.
      aType = nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING;

      // Report blocking if we detect suspicious fingerprinting activity.
      if (entry && entry->mData->mHasSuspiciousFingerprintingActivity) {
        blockedValue = true;
      }
      break;

    default:
      // Ignore nsIWebProgressListener::STATE_BLOCKED_UNSAFE_CONTENT;
      break;
  }

  if (!aBlocked) {
    unblocked = (events & aType) != 0;
  }

  const uint32_t oldEvents = events;
  if (blockedValue) {
    events |= aType;
  } else if (unblocked) {
    events &= ~aType;
  }

  if (events == oldEvents
#ifdef ANDROID
      // GeckoView always needs to notify about blocked trackers,
      // since the GeckoView API always needs to report the URI and
      // type of any blocked tracker. We use a platform-dependent code
      // path here because reporting this notification on desktop
      // platforms isn't necessary and doing so can have a big
      // performance cost.
      && aType != nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT
#endif
  ) {
    // Avoid dispatching repeated notifications when nothing has
    // changed
    return Nothing();
  }

  return Some(events);
}

void ContentBlockingLog::ReportLog() {
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(NS_IsMainThread());

  if (!StaticPrefs::browser_contentblocking_database_enabled()) {
    return;
  }

  if (mLog.IsEmpty()) {
    return;
  }

  // Emit only the delta since the last flush. This lets ReportLog() be called
  // multiple times during a document's lifetime (e.g. from a flush-on-query
  // barrier in TrackingDBService) without double-counting entries against the
  // daily aggregated counters in protections.sqlite.
  nsAutoCString payload = Stringify(/* aOnlyUnreported */ true);

  // Stringify() emits "{}" when every origin's cursor is already at the end
  // and all origin-level flags have been reported. Skip the IPC round-trip.
  if (payload.EqualsLiteral("{}")) {
    return;
  }

  nsCOMPtr<nsITrackingDBService> trackingDBService =
      do_GetService("@mozilla.org/tracking-db-service;1");
  if (NS_WARN_IF(!trackingDBService)) {
    return;
  }

  trackingDBService->RecordContentBlockingLog(payload);
  MarkAsReported();
}

void ContentBlockingLog::ReportCanvasFingerprintingLog(
    nsIPrincipal* aFirstPartyPrincipal) {
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aFirstPartyPrincipal);

  // We don't need to report if the first party is not a content.
  if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) {
    return;
  }

  bool hasCanvasFingerprinter = false;

  // Track unique (key, source) combinations to avoid counting duplicates per
  // page
  nsTHashSet<nsCString> seenTextSourceCombos;
  nsTHashSet<nsCString> seenAliasSourceCombos;

  for (const auto& originEntry : mLog) {
    if (!originEntry.mData) {
      continue;
    }

    for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) {
      if (logEntry.mType !=
          nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING) {
        continue;
      } else if (logEntry.mCanvasFingerprintingEvent.isSome() == false) {
        // Little confused about how we could get here, but I did see a crash
        {
          nsAutoCString firstPartyOrigin;
          aFirstPartyPrincipal->GetOriginNoSuffix(firstPartyOrigin);
          MOZ_LOG(gFingerprinterDetection, LogLevel::Error,
                  ("ContentBlockingLog::ReportCanvasFingerprintingLog: "
                   "logEntry has no CanvasFingerprintingEvent "
                   "(firstPartyOrigin=%s)",
                   firstPartyOrigin.get()));
        }
        continue;
      }
      hasCanvasFingerprinter = true;

      auto canvasFingerprintingEvent =
          logEntry.mCanvasFingerprintingEvent.value();

      // ----------------------------------
      // First cover canvas_fingerprinting_type_text_by_source_per_tab2
      // Only count each unique (text, source) combination once per page
      if (!canvasFingerprintingEvent.knownTextBitmask) {
        nsAutoCString key, category;
        key.AppendLiteral("none");
        category.AppendInt(canvasFingerprintingEvent.sourcesBitmask);

        nsAutoCString comboKey(key + ":"_ns + category);
        if (!seenTextSourceCombos.Contains(comboKey)) {
          seenTextSourceCombos.Insert(comboKey);
          glean::contentblocking::
              canvas_fingerprinting_type_text_by_source_per_tab2
                  .Get(key, category)
                  .Add();
        }
      } else {
        // Iterate over each set bit in the bitmask
        for (uint32_t b = canvasFingerprintingEvent.knownTextBitmask; b;
             b &= (b - 1)) {
          uint32_t singleSetBit_Text = b & (~b + 1);
          uint32_t exponent = std::countr_zero(singleSetBit_Text);

          nsAutoCString key, category;
          key.AppendInt(exponent);
          category.AppendInt(canvasFingerprintingEvent.sourcesBitmask);

          nsAutoCString comboKey(key + ":"_ns + category);
          if (!seenTextSourceCombos.Contains(comboKey)) {
            seenTextSourceCombos.Insert(comboKey);
            glean::contentblocking::
                canvas_fingerprinting_type_text_by_source_per_tab2
                    .Get(key, category)
                    .Add();
          }
        }
      }

      // ----------------------------------
      // Second, cover canvas_fingerprinting_type_alias_by_source_per_tab2
      // Only count each unique (alias, source) combination once per page
      {
        nsAutoCString key, category;
        key.AppendInt(static_cast<uint32_t>(canvasFingerprintingEvent.alias));
        category.AppendInt(canvasFingerprintingEvent.sourcesBitmask);

        nsAutoCString comboKey(key + ":"_ns + category);
        if (!seenAliasSourceCombos.Contains(comboKey)) {
          seenAliasSourceCombos.Insert(comboKey);
          glean::contentblocking::
              canvas_fingerprinting_type_alias_by_source_per_tab2
                  .Get(key, category)
                  .Add();
        }
      }
    }
  }

  // ----------------------------------
  // Finally, cover the overall 'was there any canvas fingerprinting' metric
  // canvas_fingerprinting_per_tab2

  if (!hasCanvasFingerprinter) {
    // Increment the global 'did the page have any' metric
    glean::contentblocking::canvas_fingerprinting_per_tab2
        .EnumGet(
            glean::contentblocking::CanvasFingerprintingPerTab2Label::eNotFound)
        .Add();
  } else {
    // Increment the global 'did the page have any' metric
    glean::contentblocking::canvas_fingerprinting_per_tab2
        .EnumGet(
            glean::contentblocking::CanvasFingerprintingPerTab2Label::eFound)
        .Add();
  }
}

void ContentBlockingLog::ReportFontFingerprintingLog(
    nsIPrincipal* aFirstPartyPrincipal) {
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aFirstPartyPrincipal);

  // We don't need to report if the first party is not a content.
  if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) {
    return;
  }

  bool hasFontFingerprinter = false;
  for (const auto& originEntry : mLog) {
    if (!originEntry.mData) {
      continue;
    }

    for (const auto& logEntry : originEntry.mData->mLogs) {
      if (logEntry.mType !=
          nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) {
        continue;
      }

      hasFontFingerprinter = true;
    }

    if (hasFontFingerprinter) {
      break;
    }
  }

  glean::contentblocking::font_fingerprinting_per_tab
      .EnumGet(
          static_cast<glean::contentblocking::FontFingerprintingPerTabLabel>(
              hasFontFingerprinter))
      .Add();
}

void ContentBlockingLog::ReportEmailTrackingLog(
    nsIPrincipal* aFirstPartyPrincipal) {
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aFirstPartyPrincipal);

  // We don't need to report if the first party is not a content.
  if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) {
    return;
  }

  nsCOMPtr<nsIEffectiveTLDService> tldService =
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);

  if (!tldService) {
    return;
  }

  nsTHashtable<nsCStringHashKey> level1SiteSet;
  nsTHashtable<nsCStringHashKey> level2SiteSet;

  for (const auto& originEntry : mLog) {
    if (!originEntry.mData) {
      continue;
    }

    bool isLevel1EmailTracker = false;
    bool isLevel2EmailTracker = false;

    for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) {
      // Check if the email tracking related event had been filed for the given
      // origin entry. Note that we currently only block level 1 email trackers,
      // so blocking event represents the page has embedded a level 1 tracker.
      if (logEntry.mType ==
          nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT) {
        isLevel2EmailTracker = true;
        break;
      }

      if (logEntry.mType ==
              nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT ||
          logEntry.mType == nsIWebProgressListener::
                                STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT) {
        isLevel1EmailTracker = true;
        break;
      }
    }

    if (isLevel1EmailTracker || isLevel2EmailTracker) {
      nsCOMPtr<nsIURI> uri;
      nsresult rv = NS_NewURI(getter_AddRefs(uri), originEntry.mOrigin);

      if (NS_FAILED(rv)) {
        continue;
      }

      nsAutoCString baseDomain;
      rv = tldService->GetBaseDomain(uri, 0, baseDomain);

      if (NS_FAILED(rv)) {
        continue;
      }

      if (isLevel1EmailTracker) {
        (void)level1SiteSet.EnsureInserted(baseDomain);
      } else {
        (void)level2SiteSet.EnsureInserted(baseDomain);
      }
    }
  }

  // Cache the email webapp domains pref value and register the callback
  // function to update the cached value when the pref changes.
  if (!gEmailWebAppDomainsPref) {
    gEmailWebAppDomainsPref = new nsCString();

    Preferences::RegisterCallbackAndCall(EmailWebAppDomainPrefChangeCallback,
                                         kEmailWebAppDomainPrefName);
    RunOnShutdown([]() {
      Preferences::UnregisterCallback(EmailWebAppDomainPrefChangeCallback,
                                      kEmailWebAppDomainPrefName);
      gEmailWebAppDomainsPref = nullptr;
    });
  }

  bool isTopEmailWebApp =
      aFirstPartyPrincipal->IsURIInList(*gEmailWebAppDomainsPref);
  uint32_t level1Count = level1SiteSet.Count();
  uint32_t level2Count = level2SiteSet.Count();

  glean::contentblocking::email_tracker_embedded_per_tab
      .EnumGet(isTopEmailWebApp
                   ? glean::contentblocking::EmailTrackerEmbeddedPerTabLabel::
                         eBaseEmailapp
                   : glean::contentblocking::EmailTrackerEmbeddedPerTabLabel::
                         eBaseNormal)
      .AccumulateSingleSample(level1Count);
  glean::contentblocking::email_tracker_embedded_per_tab
      .EnumGet(isTopEmailWebApp
                   ? glean::contentblocking::EmailTrackerEmbeddedPerTabLabel::
                         eContentEmailapp
                   : glean::contentblocking::EmailTrackerEmbeddedPerTabLabel::
                         eContentNormal)
      .AccumulateSingleSample(level2Count);
  glean::contentblocking::email_tracker_embedded_per_tab
      .EnumGet(isTopEmailWebApp
                   ? glean::contentblocking::EmailTrackerEmbeddedPerTabLabel::
                         eAllEmailapp
                   : glean::contentblocking::EmailTrackerEmbeddedPerTabLabel::
                         eAllNormal)
      .AccumulateSingleSample(level1Count + level2Count);
}

ContentBlockingLog::OriginEntry* ContentBlockingLog::RecordLogInternal(
    const nsACString& aOrigin, uint32_t aType, bool aBlocked,
    const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
        aReason,
    const nsTArray<nsCString>& aTrackingFullHashes,
    const Maybe<CanvasFingerprintingEvent>& aCanvasFingerprintingEvent) {
  DebugOnly<bool> isCookiesBlockedTracker =
      aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER ||
      aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER;
  MOZ_ASSERT_IF(aBlocked, aReason.isNothing());
  MOZ_ASSERT_IF(!isCookiesBlockedTracker, aReason.isNothing());
  MOZ_ASSERT_IF(isCookiesBlockedTracker && !aBlocked, aReason.isSome());

  if (aOrigin.IsVoid()) {
    return nullptr;
  }
  auto index = mLog.IndexOf(aOrigin, 0, Comparator());
  if (index != OriginDataTable::NoIndex) {
    OriginEntry& entry = mLog[index];
    if (!entry.mData) {
      return nullptr;
    }

    if (RecordLogEntryInCustomField(aType, entry, aBlocked)) {
      return &entry;
    }
    if (!entry.mData->mLogs.IsEmpty()) {
      auto& last = entry.mData->mLogs.LastElement();
      if (last.mType == aType && last.mBlocked == aBlocked &&
          last.mCanvasFingerprintingEvent == aCanvasFingerprintingEvent) {
        ++last.mRepeatCount;
        // Don't record recorded events.  This helps compress our log.
        // We don't care about if the the reason is the same, just keep the
        // first one.
        // Note: {aReason, aTrackingFullHashes} are not compared here and we
        // simply keep the first for the reason, and merge hashes to make sure
        // they can be correctly recorded.
        for (const auto& hash : aTrackingFullHashes) {
          if (!last.mTrackingFullHashes.Contains(hash)) {
            last.mTrackingFullHashes.AppendElement(hash);
          }
        }
        return &entry;
      }
    }
    if (entry.mData->mLogs.Length() ==
        std::max(1u, StaticPrefs::browser_contentblocking_originlog_length())) {
      // Cap the size at the maximum length adjustable by the pref
      entry.mData->mLogs.RemoveElementAt(0);
    }
    entry.mData->mLogs.AppendElement(LogEntry{aType, 1u, aBlocked, aReason,
                                              aTrackingFullHashes.Clone(),
                                              aCanvasFingerprintingEvent});

    // Check suspicious fingerprinting activities if the origin hasn't already
    // been marked.
    // TODO(Bug 1864909): Moving the suspicious fingerprinting detection call
    // out of here.
    if (aType == nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING ||
        aType == nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) {
      entry.mData->mHasSuspiciousFingerprintingActivity = true;
    }
    return &entry;
  }

  // The entry has not been found.
  OriginEntry* entry = mLog.AppendElement();
  if (NS_WARN_IF(!entry || !entry->mData)) {
    return nullptr;
  }

  entry->mOrigin = aOrigin;

  if (aType == nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) {
    entry->mData->mHasLevel1TrackingContentLoaded = aBlocked;
  } else if (aType ==
             nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) {
    entry->mData->mHasLevel2TrackingContentLoaded = aBlocked;
  } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) {
    MOZ_ASSERT(entry->mData->mHasCookiesLoaded.isNothing());
    entry->mData->mHasCookiesLoaded.emplace(aBlocked);
  } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) {
    MOZ_ASSERT(entry->mData->mHasTrackerCookiesLoaded.isNothing());
    entry->mData->mHasTrackerCookiesLoaded.emplace(aBlocked);
  } else if (aType ==
             nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) {
    MOZ_ASSERT(entry->mData->mHasSocialTrackerCookiesLoaded.isNothing());
    entry->mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked);
  } else {
    entry->mData->mLogs.AppendElement(LogEntry{aType, 1u, aBlocked, aReason,
                                               aTrackingFullHashes.Clone(),
                                               aCanvasFingerprintingEvent});

    // Check suspicious fingerprinting activities if the origin hasn't been
    // marked.
    // TODO(Bug 1864909): Moving the suspicious fingerprinting detection call
    // out of here.
    if (aType == nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING ||
        aType == nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) {
      entry->mData->mHasSuspiciousFingerprintingActivity = true;
    }
  }

  return entry;
}

}  // namespace mozilla
