Files
sousa-gecko/toolkit/components/content-classifier/ContentClassifierService.cpp
T
Benjamin VanderSloot b6181f191c Bug 2044696 - Add ContentClassifier feature for harmful-addon - r=timhuang,extension-reviewers,robwu,baku
This ended up being a bit of a mess because the cancellation logic for
this protection is also custom and has a custom telemetry ping.
This means two sources of custom logic for each probe. Luckily it's less
custom than before in spite of this.

Differential Revision: https://phabricator.services.mozilla.com/D307323
2026-07-01 17:07:31 +00:00

1457 lines
54 KiB
C++

/* 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 "ContentClassifierService.h"
#include "ContentClassifierFeatureUtils.h"
#include "ErrorList.h"
#include "ContentClassifierPrefMirror.h"
#include "mozilla/Logging.h"
#include "mozilla/net/ChannelClassifierUtils.h"
#include "MainThreadUtils.h"
#include "nsDebug.h"
#include "mozilla/ContentClassifierEngine.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/Promise-inl.h"
#include "mozilla/dom/TypedArray.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/Components.h"
#include "mozilla/MozPromise.h"
#include "mozilla/StaticPtr.h"
#include "nsIAsyncShutdown.h"
#include "nsIChannel.h"
#include "nsIClassifiedChannel.h"
#include "nsIObserverService.h"
#include "nsILoadInfo.h"
#include "nsIStreamLoader.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsContentUtils.h"
#include "nsIWebProgressListener.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTHashSet.h"
#include "nsThreadUtils.h"
namespace mozilla {
static LazyLogModule gContentClassifierLog("ContentClassifier");
StaticRefPtr<ContentClassifierService> ContentClassifierService::sInstance;
bool ContentClassifierService::sEnabled = false;
namespace {
constexpr nsLiteralCString kTrackersListIds[] = {"disconnect-tracker-base"_ns};
constexpr nsLiteralCString kTrackersContentListIds[] = {
"disconnect-tracker-content"_ns};
constexpr nsLiteralCString kSocialTrackersListIds[] = {"mozilla-social"_ns};
constexpr nsLiteralCString kFingerprintersListIds[] = {
"disconnect-fingerprinters-base"_ns};
constexpr nsLiteralCString kEmailTrackersListIds[] = {
"disconnect-email-base"_ns};
constexpr nsLiteralCString kCryptominersListIds[] = {
"disconnect-cryptominer-base"_ns};
constexpr nsLiteralCString kMajorExceptionListIds[] = {
"mozilla-major-exceptions"_ns};
constexpr nsLiteralCString kMinorExceptionListIds[] = {
"mozilla-minor-exceptions"_ns};
constexpr nsLiteralCString kHarmfulAddonListIds[] = {
"mozilla-harmful-addon"_ns};
constexpr nsLiteralCString kTestBlockListIds[] = {"test_block"_ns};
constexpr nsLiteralCString kTestAnnotateListIds[] = {"test_annotate"_ns};
constexpr ContentClassifierFeature kFeatures[] = {
{"trackers"_ns, Span<const nsLiteralCString>(kTrackersListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING,
nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT,
nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT,
NS_ERROR_TRACKING_URI, false,
Some(nsIScopedPrefs::PRIVACY_TRACKINGPROTECTION_CONTENT_ENABLED),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
// The annotation variant adds content-track-digest256, which mirrors
// url-classifier's promotion to STATE_LOADED_LEVEL_2_TRACKING_CONTENT
// when a content-track-* table matches.
{"trackers-content"_ns,
Span<const nsLiteralCString>(kTrackersContentListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING,
nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT,
nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT,
nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT,
NS_ERROR_TRACKING_URI, false, Nothing(),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
{"social-trackers"_ns, Span<const nsLiteralCString>(kSocialTrackersListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_SOCIALTRACKING,
nsIWebProgressListener::STATE_LOADED_SOCIALTRACKING_CONTENT,
nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT,
nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT,
NS_ERROR_SOCIALTRACKING_URI, false,
Some(nsIScopedPrefs::
PRIVACY_TRACKINGPROTECTION_CONTENT_SOCIALTRACKING_ENABLED),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
{"fingerprinters"_ns, Span<const nsLiteralCString>(kFingerprintersListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_FINGERPRINTING,
nsIWebProgressListener::STATE_LOADED_FINGERPRINTING_CONTENT,
nsIWebProgressListener::STATE_REPLACED_FINGERPRINTING_CONTENT,
nsIWebProgressListener::STATE_ALLOWED_FINGERPRINTING_CONTENT,
NS_ERROR_FINGERPRINTING_URI, false,
Some(nsIScopedPrefs::
PRIVACY_TRACKINGPROTECTION_CONTENT_FINGERPRINTING_ENABLED),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
{"email-trackers"_ns, Span<const nsLiteralCString>(kEmailTrackersListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_EMAILTRACKING,
nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT,
nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT,
nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT,
NS_ERROR_EMAILTRACKING_URI, false,
Some(nsIScopedPrefs::
PRIVACY_TRACKINGPROTECTION_CONTENT_EMAILTRACKING_ENABLED),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
{"cryptominers"_ns, Span<const nsLiteralCString>(kCryptominersListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_CRYPTOMINING,
nsIWebProgressListener::STATE_LOADED_CRYPTOMINING_CONTENT,
nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT,
nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT,
NS_ERROR_CRYPTOMINING_URI, false,
Some(nsIScopedPrefs::
PRIVACY_TRACKINGPROTECTION_CONTENT_CRYPTOMINING_ENABLED),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
{"harmful-addon"_ns, Span<const nsLiteralCString>(kHarmfulAddonListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING,
0, // mLoadedState: blocking-only feature, not annotated
0, // mReplacedState
0, // mAllowedState
NS_ERROR_HARMFULADDON_URI, false, Nothing(),
&ContentClassifierFeatureUtils::IsNonRecommendedAddonRequest,
&ContentClassifierFeatureUtils::HarmfulAddonCancelChannelCallback},
{"minor-exceptions"_ns,
Span<const nsLiteralCString>(kMinorExceptionListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING, 0, 0, 0,
NS_OK, true, Nothing(),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
{"major-exceptions"_ns,
Span<const nsLiteralCString>(kMajorExceptionListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING, 0, 0, 0,
NS_OK, true, Nothing(),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
// Test-only features. Their engines are built directly by the HTTP
// test loader (driven by the *.test_list_urls prefs) and installed
// into mEngines under these names. They behave like any other
// feature once built and do not go through the RS client.
{"test_block"_ns, Span<const nsLiteralCString>(kTestBlockListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING,
nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT,
nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT,
NS_ERROR_TRACKING_URI, false,
Some(nsIScopedPrefs::PRIVACY_TRACKINGPROTECTION_CONTENT_TEST_ENABLED),
&ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
{"test_annotate"_ns, Span<const nsLiteralCString>(kTestAnnotateListIds),
nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING,
nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT,
nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT, NS_OK, false,
Nothing(), &ContentClassifierFeatureUtils::IsThirdPartyRequest, nullptr},
};
// Prefs that name feature engines built into mEngines.
constexpr const char* kFeatureEnginesPrefs[] = {
"privacy.trackingprotection.content.protection.engines",
"privacy.trackingprotection.content.protection.engines.pbmode",
"privacy.trackingprotection.content.annotation.engines",
"privacy.trackingprotection.content.annotation.engines.pbmode",
};
bool HasAnyActiveRemoteSettingsFeatures() {
nsTArray<nsCString> names;
for (const char* pref : kFeatureEnginesPrefs) {
names.Clear();
EnginesPrefsSnapshot::AppendFeatureNamesFromPref(pref, names);
for (const auto& name : names) {
if (!name.Equals("test_block") && !name.Equals("test_annotate")) {
return true;
}
}
}
return false;
}
void NotifyListsLoadedForTesting() {
if (!StaticPrefs::privacy_trackingprotection_content_testing()) {
return;
}
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (obs) {
obs->NotifyObservers(
nullptr, NS_CONTENT_CLASSIFIER_FILTER_LISTS_LOADED_TOPIC, nullptr);
}
}
} // namespace
NS_IMPL_ISUPPORTS(ContentClassifierService, nsIAsyncShutdownBlocker,
nsIContentClassifierService)
ContentClassifierService::ContentClassifierService()
: mLock("ContentClassifierService::mLock"),
mInitPhase(InitPhase::NotInited) {
sEnabled =
Preferences::GetBool(
"privacy.trackingprotection.content.protection.enabled", false) ||
Preferences::GetBool(
"privacy.trackingprotection.content.annotation.enabled", false);
}
ContentClassifierService::~ContentClassifierService() = default;
// static
bool ContentClassifierService::IsEnabled() {
if (!sInstance) {
return false;
}
return sEnabled;
}
// static
Span<const ContentClassifierFeature> ContentClassifierService::GetFeatures() {
return Span<const ContentClassifierFeature>(kFeatures);
}
// static
Maybe<const ContentClassifierFeature&>
ContentClassifierService::GetFeatureByName(const nsACString& aName) {
for (const auto& feature : kFeatures) {
if (feature.mName.Equals(aName)) {
return SomeRef(feature);
}
}
return Nothing();
}
// static
bool ContentClassifierService::IsInitialized() {
if (!sInstance) {
return false;
}
MutexAutoLock lock(sInstance->mLock);
return sInstance->mInitPhase == InitPhase::InitSucceeded;
}
// static
void ContentClassifierService::OnPrefChange(const char* aPref, void*) {
MOZ_ASSERT(NS_IsMainThread());
// Access sInstance directly rather than GetInstance(), because
// GetInstance() returns nullptr when the feature is disabled, but we
// need to handle enable/disable transitions here.
RefPtr<ContentClassifierService> service = sInstance;
if (!service) {
return;
}
if (!IsInitialized()) {
return;
}
bool wasEnabled = sEnabled;
sEnabled =
Preferences::GetBool(
"privacy.trackingprotection.content.protection.enabled", false) ||
Preferences::GetBool(
"privacy.trackingprotection.content.annotation.enabled", false);
// mRSClient is main-thread only (see header); the NS_IsMainThread
// assert at the top of this function covers this read and the
// subsequent Init/Shutdown calls.
const bool hasRSClient = !!service->mRSClient;
if (!wasEnabled && sEnabled && !hasRSClient) {
// Feature just became enabled. Start the RS client if list names are set.
if (HasAnyActiveRemoteSettingsFeatures()) {
service->InitRSClient();
}
return;
}
if (wasEnabled && !sEnabled) {
// Feature just became disabled. Tear down the RS client and engines.
service->ShutdownRSClient();
return;
}
// Feature enabled state unchanged. Handle individual pref changes.
const nsDependentCString prefStr(aPref);
const bool isFeatureSelectionPref =
prefStr.EqualsLiteral(
"privacy.trackingprotection.content.protection.engines") ||
prefStr.EqualsLiteral(
"privacy.trackingprotection.content.protection.engines.pbmode") ||
prefStr.EqualsLiteral(
"privacy.trackingprotection.content.annotation.engines") ||
prefStr.EqualsLiteral(
"privacy.trackingprotection.content.annotation.engines.pbmode");
if (isFeatureSelectionPref) {
if (!sEnabled) {
// The feature is disabled; nothing to rebuild or fetch. Enabling
// will pick up the new pref.
return;
}
// Active feature selection changed. Start RS client if needed; its
// init will deliver onListsChanged notifications and trigger builds.
if (!hasRSClient && HasAnyActiveRemoteSettingsFeatures()) {
service->InitRSClient();
return;
}
if (hasRSClient && !HasAnyActiveRemoteSettingsFeatures()) {
service->ShutdownRSClient();
return;
}
// Reshuffles into the per-mode arrays and picks up any newly-active
// feature whose engine isn't built.
service->ProcessListChanges({}, {});
return;
}
// Redownload the test_* rule lists when the prefs controlling them are
// updated
nsTArray<nsCString> testEnginesToUpdate;
if (prefStr.EqualsLiteral(
"privacy.trackingprotection.content.protection.test_list_urls")) {
testEnginesToUpdate.AppendElement("test_block"_ns);
}
if (prefStr.EqualsLiteral(
"privacy.trackingprotection.content.annotation.test_list_urls")) {
testEnginesToUpdate.AppendElement("test_annotate"_ns);
}
if (!testEnginesToUpdate.IsEmpty()) {
service->ProcessListChanges(testEnginesToUpdate, {});
return;
}
// An .enabled pref changed but the combined enabled state didn't flip
// (e.g. one was already true). Nothing to do - engines are already
// populated via whichever path is active.
}
void ContentClassifierService::Init() {
MOZ_ASSERT(XRE_IsParentProcess());
AssertIsOnMainThread();
{
MutexAutoLock lock(mLock);
if (mInitPhase != InitPhase::NotInited) {
return;
}
MOZ_LOG(gContentClassifierLog, LogLevel::Info,
("ContentClassifierService::Init - initializing"));
nsCOMPtr<nsIAsyncShutdownClient> shutdownBarrier =
GetAsyncShutdownBarrier();
if (!shutdownBarrier) {
mInitPhase = InitPhase::InitFailed;
return;
}
bool closed;
nsresult rv = shutdownBarrier->GetIsClosed(&closed);
if (NS_FAILED(rv) || closed) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = shutdownBarrier->AddBlocker(
this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = Preferences::RegisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.protection.enabled"_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = Preferences::RegisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.annotation.enabled"_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = Preferences::RegisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.protection.test_list_urls"_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = Preferences::RegisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.annotation.test_list_urls"_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = Preferences::RegisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.protection.engines"_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = Preferences::RegisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.annotation.engines"_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = Preferences::RegisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.protection.engines.pbmode"_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = Preferences::RegisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.annotation.engines.pbmode"_ns);
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
rv = NS_CreateBackgroundTaskQueue("ContentClassifier",
getter_AddRefs(mBuildThread));
if (NS_FAILED(rv)) {
mInitPhase = InitPhase::InitFailed;
return;
}
mInitPhase = InitPhase::InitSucceeded;
}
// Lock released; safe to call into JS.
// Only initialize the RS client if list_names prefs are set,
// to avoid interfering with the test-only HTTP loading path.
if (sEnabled && HasAnyActiveRemoteSettingsFeatures()) {
InitRSClient();
}
ContentClassifierPrefMirror::Init();
}
void ContentClassifierService::InitRSClient() {
MOZ_ASSERT(NS_IsMainThread());
if (mRSClient) {
return;
}
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Info,
"InitRSClient - creating RS client");
nsresult rv;
mRSClient =
do_GetService(NS_CONTENTCLASSIFIERREMOTESETTINGSCLIENT_CONTRACTID, &rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Error,
"InitRSClient - failed to get RS client service: {:#x}",
static_cast<uint32_t>(rv));
return;
}
// The returned Promise is ignored: C++ doesn't need to await the
// initial import. Callers that do (such as tests) observe the
// NS_CONTENT_CLASSIFIER_FILTER_LISTS_LOADED_TOPIC notification.
RefPtr<dom::Promise> unused;
rv = mRSClient->Init(this, getter_AddRefs(unused));
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Error,
"InitRSClient - failed to init RS client: {:#x}",
static_cast<uint32_t>(rv));
mRSClient = nullptr;
return;
}
}
void ContentClassifierService::ShutdownRSClient() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Info, "ShutdownRSClient");
if (mRSClient) {
// Release mRSClient before reacquiring mLock. The JS Shutdown()
// implementation does not call back into us, but drop the strong
// reference first to be defensive.
nsCOMPtr<nsIContentClassifierRemoteSettingsClient> client =
std::move(mRSClient);
client->Shutdown();
}
MutexAutoLock lock(mLock);
mEngines.Clear();
mCancelEngines.Clear();
mCancelEnginesPBM.Clear();
mAnnotateEngines.Clear();
mAnnotateEnginesPBM.Clear();
}
// static
already_AddRefed<ContentClassifierService>
ContentClassifierService::GetInstance() {
if (!sInstance) {
sInstance = new ContentClassifierService();
ClearOnShutdown(&sInstance);
sInstance->Init();
}
if (!IsInitialized() || !IsEnabled()) {
return nullptr;
}
return do_AddRef(sInstance);
}
already_AddRefed<nsIAsyncShutdownClient>
ContentClassifierService::GetAsyncShutdownBarrier() const {
nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service();
MOZ_RELEASE_ASSERT(svc);
nsCOMPtr<nsIAsyncShutdownClient> client;
nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(client));
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
MOZ_RELEASE_ASSERT(client);
return client.forget();
}
NS_IMETHODIMP ContentClassifierService::BlockShutdown(
nsIAsyncShutdownClient* aClient) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_LOG(gContentClassifierLog, LogLevel::Info,
("ContentClassifierService::BlockShutdown - shutting down"));
// ShutdownRSClient clears the filter list data and engines. It also
// tears down the RS client if one was created (the HTTP-only test
// path leaves mRSClient null).
ShutdownRSClient();
nsCOMPtr<nsISerialEventTarget> buildThread;
{
MutexAutoLock lock(mLock);
mInitPhase = InitPhase::ShutdownStarted;
// Clearing mBuildThread closes the dispatch window for any
// subsequent UpdateFeatures call. In-flight closures on the queue
// are gated by the mInitPhase check above before they touch state.
buildThread = std::move(mBuildThread);
Preferences::UnregisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.protection.enabled"_ns);
Preferences::UnregisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.annotation.enabled"_ns);
Preferences::UnregisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.protection.test_list_urls"_ns);
Preferences::UnregisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.annotation.test_list_urls"_ns);
Preferences::UnregisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.protection.engines"_ns);
Preferences::UnregisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.annotation.engines"_ns);
Preferences::UnregisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.protection.engines.pbmode"_ns);
Preferences::UnregisterCallback(
&ContentClassifierService::OnPrefChange,
"privacy.trackingprotection.content.annotation.engines.pbmode"_ns);
content_classifier_teardown_domain_resolver();
if (!buildThread) {
RemoveBlocker();
return NS_OK;
}
}
// Drain mBuildThread, then post back to the main thread to remove
// the shutdown blocker. Because mBuildThread is serial, the fence
// runs strictly after every already-dispatched build closure, so by
// the time FinishShutdown lands no off-thread work is in flight.
RefPtr<ContentClassifierService> self = this;
buildThread->Dispatch(NS_NewRunnableFunction(
"ContentClassifierService::ShutdownFence", [self]() {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"ContentClassifierService::FinishShutdown", [self]() {
MutexAutoLock lock(self->mLock);
self->RemoveBlocker();
}));
}));
return NS_OK;
}
void ContentClassifierService::RemoveBlocker() {
MOZ_ASSERT(NS_IsMainThread());
mLock.AssertCurrentThreadOwns();
nsCOMPtr<nsIAsyncShutdownClient> asc = GetAsyncShutdownBarrier();
MOZ_ASSERT(asc);
DebugOnly<nsresult> rv = asc->RemoveBlocker(this);
MOZ_ASSERT(NS_SUCCEEDED(rv));
mInitPhase = InitPhase::ShutdownEnded;
}
// Fold a single per-engine outcome into the aggregate. All matched engine
// results are appended so callers can attribute per-feature annotations,
// but the aggregate Status is promoted monotonically: a later
// non-Important Hit cannot demote an earlier Exception, and Important
// pins the status.
void ContentClassifierResult::Accumulate(
ContentClassifierEngineResult aEngineResult) {
Status engineStatus = Status::Miss;
if (aEngineResult.Exception()) {
engineStatus = aEngineResult.Important() ? Status::ImportantException
: Status::Exception;
} else if (aEngineResult.Matched()) {
engineStatus =
aEngineResult.Important() ? Status::ImportantHit : Status::Hit;
}
if (engineStatus > mStatus) {
mStatus = engineStatus;
}
mEngineResults.AppendElement(std::move(aEngineResult));
}
ContentClassifierResult ContentClassifierService::ClassifyWithEngines(
const nsTArray<RefPtr<ContentClassifierEngine>>& aEngines,
const ContentClassifierRequest& aRequest, bool aIndependentEngines) {
MOZ_ASSERT(!NS_IsMainThread());
mLock.AssertCurrentThreadOwns();
ContentClassifierResult result;
if (mInitPhase != InitPhase::InitSucceeded) {
MOZ_LOG(gContentClassifierLog, LogLevel::Warning,
("ClassifyWithEngines - service not initialized; returning Miss"));
return result;
}
if (!aRequest.Valid()) {
MOZ_LOG(gContentClassifierLog, LogLevel::Warning,
("ClassifyWithEngines - invalid request; returning Miss"));
return result;
}
bool matchedSoFar = false;
for (const auto& engine : aEngines) {
if (engine->Feature().mRequestFilter &&
!engine->Feature().mRequestFilter(aRequest)) {
continue;
}
ContentClassifierEngineResult er = engine->CheckNetworkRequest(
aRequest, aIndependentEngines ? false : matchedSoFar);
result.Accumulate(er);
const auto status = result.GetStatus();
if (!aIndependentEngines &&
(status == ContentClassifierResult::Status::ImportantException ||
status == ContentClassifierResult::Status::ImportantHit)) {
break;
}
if (er.Matched() && !er.Exception()) {
matchedSoFar = true;
}
}
return result;
}
NS_IMETHODIMP ContentClassifierService::GetName(nsAString& aName) {
aName.AssignLiteral("ContentClassifierService: Shutting down");
return NS_OK;
}
NS_IMETHODIMP ContentClassifierService::GetState(nsIPropertyBag** aState) {
*aState = nullptr;
return NS_OK;
}
ContentClassifierResult ContentClassifierService::ClassifyForAnnotate(
const ContentClassifierRequest& aRequest) {
MutexAutoLock lock(mLock);
const nsTArray<RefPtr<ContentClassifierEngine>>& engines =
aRequest.PrivateBrowsing() ? mAnnotateEnginesPBM : mAnnotateEngines;
ContentClassifierResult result =
ClassifyWithEngines(engines, aRequest, /* aIndependentEngines */ true);
MOZ_LOG(gContentClassifierLog, LogLevel::Debug,
("ClassifyForAnnotate - url=%s hit=%d exception=%d",
aRequest.Url().get(), result.Hit(), result.Exception()));
return result;
}
ContentClassifierResult ContentClassifierService::ClassifyForCancel(
const ContentClassifierRequest& aRequest) {
MutexAutoLock lock(mLock);
const nsTArray<RefPtr<ContentClassifierEngine>>& engines =
aRequest.PrivateBrowsing() ? mCancelEnginesPBM : mCancelEngines;
// Cancel mode threads matchedSoFar across engines so trailing exception
// engines can suppress an earlier hit, but ClassifyWithEngines bails out
// as soon as the aggregated status reaches ImportantHit / ImportantException
// because either pins the outcome.
ContentClassifierResult result =
ClassifyWithEngines(engines, aRequest, /* aIndependentEngines */ false);
MOZ_LOG(gContentClassifierLog, LogLevel::Debug,
("ClassifyForCancel - url=%s hit=%d exception=%d",
aRequest.Url().get(), result.Hit(), result.Exception()));
return result;
}
void ContentClassifierService::MaybeAnnotateChannel(
nsIChannel* aChannel, const ContentClassifierResult& aResult) {
NS_ENSURE_TRUE_VOID(aChannel);
if (!aResult.Hit()) {
return;
}
nsCOMPtr<nsIURI> uri;
aChannel->GetURI(getter_AddRefs(uri));
for (const auto& engineResult : aResult.EngineResults()) {
if (!engineResult.Matched() || engineResult.Exception()) {
continue;
}
const ContentClassifierFeature& feature = engineResult.Feature();
if (feature.mLoadedState == 0) {
continue;
}
if (uri) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Debug,
"MaybeAnnotateChannel - url={} feature={}",
uri->GetSpecOrDefault(), feature.mName);
}
net::ChannelClassifierUtils::AnnotateChannel(
aChannel, feature.mClassificationFlag, feature.mLoadedState);
}
}
net::ChannelBlockDecision ContentClassifierService::MaybeCancelChannel(
nsIChannel* aChannel, const ContentClassifierResult& aResult) {
NS_ENSURE_TRUE(aChannel, net::ChannelBlockDecision::Allowed);
if (!aResult.Hit()) {
return net::ChannelBlockDecision::Allowed;
}
// Closest analogue to the URLClassifier's "first cancelling feature
// wins" rule. Classification itself keeps evaluating all engines (so
// exception rules can suppress a Hit), but at decision time we pick
// the first matched feature whose definition carries a non-NS_OK
// blocking error code. Iteration order is the order in
// ClassifyWithEngines, which is the order of the engines pref.
const ContentClassifierFeature* blockingFeature = nullptr;
for (const auto& engineResult : aResult.EngineResults()) {
if (!engineResult.Matched() || engineResult.Exception()) {
continue;
}
const ContentClassifierFeature& feature = engineResult.Feature();
if (feature.mBlockingErrorCode != NS_OK) {
blockingFeature = &feature;
break;
}
}
if (!blockingFeature) {
MOZ_LOG(gContentClassifierLog, LogLevel::Warning,
("MaybeCancelChannel - no matched feature carries a blocking error "
"code; nothing to cancel"));
return net::ChannelBlockDecision::Allowed;
}
nsCOMPtr<nsIURI> uri;
aChannel->GetURI(getter_AddRefs(uri));
if (uri) {
MOZ_LOG(gContentClassifierLog, LogLevel::Debug,
("MaybeCancelChannel - url=%s", uri->GetSpecOrDefault().get()));
}
if (net::ChannelClassifierUtils::IsAllowListed(aChannel)) {
return net::ChannelBlockDecision::Allowed;
}
// Check if the scoped pref overrides the blocking decision.
if (blockingFeature->mReferencedScopedPref.isSome() &&
!ScopedPrefs::BoolPrefScoped(blockingFeature->mReferencedScopedPref.ref(),
aChannel)) {
return net::ChannelBlockDecision::Allowed;
}
net::ChannelBlockDecision decision = net::ChannelBlockDecision::Allowed;
net::ChannelClassifierUtils::MaybeBlockChannel(
aChannel, "content-classifier"_ns, "content-classifier-block"_ns,
blockingFeature->mBlockingErrorCode, blockingFeature->mReplacedState,
blockingFeature->mAllowedState, blockingFeature->mCancelChannelCallback,
&decision);
return decision;
}
// nsIContentClassifierService
NS_IMETHODIMP ContentClassifierService::OnListsChanged(
const nsTArray<nsCString>& aUpdated, const nsTArray<nsCString>& aRemoved) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Debug,
"OnListsChanged - updated={} removed={}", aUpdated.Length(),
aRemoved.Length());
{
MutexAutoLock lock(mLock);
if (mInitPhase != InitPhase::InitSucceeded) {
return NS_ERROR_NOT_INITIALIZED;
}
}
ProcessListChanges(aUpdated, aRemoved);
return NS_OK;
}
NS_IMETHODIMP ContentClassifierService::GetFeatureNames(
nsTArray<nsCString>& aNames) {
aNames.Clear();
for (const auto& feature : GetFeatures()) {
aNames.AppendElement(feature.mName);
}
return NS_OK;
}
// Parses a byte buffer of adblock-format filter list text into rules.
// Tolerates both LF and CRLF line endings; skips empty lines.
static void ParseFilterListRules(const nsTArray<uint8_t>& aData,
nsTArray<nsCString>& aRules) {
nsDependentCSubstring content(reinterpret_cast<const char*>(aData.Elements()),
aData.Length());
for (const auto& line : content.Split('\n')) {
nsCString rule(line);
// Trim trailing CR for CRLF line endings.
if (!rule.IsEmpty() && rule.Last() == '\r') {
rule.Truncate(rule.Length() - 1);
}
if (!rule.IsEmpty()) {
aRules.AppendElement(std::move(rule));
}
}
}
// MozPromise resolved with the parsed rules for one feature's list IDs,
// rejected with the first nsresult error we encountered. Used to fan in
// the per-list getListBytes calls before building the engine.
using ListBytesPromise = MozPromise<nsTArray<uint8_t>, nsresult,
/* IsExclusive = */ true>;
// Convert the JS Promise returned by getListBytes into a MozPromise
// resolved with the raw bytes (Uint8Array contents).
static RefPtr<ListBytesPromise> FetchListBytesFromRemoteSettings(
nsIContentClassifierRemoteSettingsClient* aClient,
const nsACString& aName) {
MOZ_ASSERT(NS_IsMainThread());
RefPtr<dom::Promise> jsPromise;
nsresult rv = aClient->GetListBytes(aName, getter_AddRefs(jsPromise));
if (NS_FAILED(rv) || !jsPromise) {
return ListBytesPromise::CreateAndReject(
NS_FAILED(rv) ? rv : NS_ERROR_FAILURE, __func__);
}
RefPtr<ListBytesPromise::Private> result =
new ListBytesPromise::Private(__func__);
jsPromise->AddCallbacksWithCycleCollectedArgs(
[result](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult&) {
if (!aValue.isObject()) {
result->Reject(NS_ERROR_FAILURE, __func__);
return;
}
JS::Rooted<JSObject*> jsObj(aCx, &aValue.toObject());
dom::Uint8Array arr;
if (!arr.Init(jsObj)) {
result->Reject(NS_ERROR_FAILURE, __func__);
return;
}
nsTArray<uint8_t> bytes;
if (!arr.AppendDataTo(bytes)) {
result->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
return;
}
result->Resolve(std::move(bytes), __func__);
},
[result](JSContext*, JS::Handle<JS::Value>, ErrorResult&) {
result->Reject(NS_ERROR_FAILURE, __func__);
});
return result;
}
nsresult BuildEngineFromRules(const ContentClassifierFeature& aFeature,
const nsTArray<nsCString>& aRules,
RefPtr<ContentClassifierEngine>& aEngineOutput) {
if (aRules.IsEmpty()) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Info,
"BuildEngineFromRules - no rules for feature \"{}\"; ",
aFeature.mName);
aEngineOutput = nullptr;
return NS_OK;
}
aEngineOutput = new ContentClassifierEngine(aFeature);
nsresult rv = aEngineOutput->InitFromRules(aRules);
if (NS_FAILED(rv)) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Warning,
"BuildEngineFromRules - InitFromRules failed for feature "
"\"{}\": {:#x}",
aFeature.mName, static_cast<uint32_t>(rv));
aEngineOutput = nullptr;
}
return rv;
}
nsresult ContentClassifierService::InstallEngine(
const nsACString& aFeatureName, RefPtr<ContentClassifierEngine>&& aEngine) {
mLock.AssertCurrentThreadOwns();
if (!aEngine) {
mEngines.Remove(nsCString(aFeatureName));
} else {
MOZ_ASSERT(aEngine->Feature().mName.Equals(aFeatureName));
mEngines.InsertOrUpdate(nsCString(aFeatureName), std::move(aEngine));
}
return NS_OK;
}
void ContentClassifierService::PopulateAllActiveEnginesFromPreferenceSnapshot(
const EnginesPrefsSnapshot& aPreferenceSnapshot) {
mLock.AssertCurrentThreadOwns();
PopulateActiveEngineListFromFeatureNames(aPreferenceSnapshot.mCancel,
mCancelEngines);
PopulateActiveEngineListFromFeatureNames(aPreferenceSnapshot.mCancelPBM,
mCancelEnginesPBM);
PopulateActiveEngineListFromFeatureNames(aPreferenceSnapshot.mAnnotate,
mAnnotateEngines);
PopulateActiveEngineListFromFeatureNames(aPreferenceSnapshot.mAnnotatePBM,
mAnnotateEnginesPBM);
}
void ContentClassifierService::PopulateActiveEngineListFromFeatureNames(
const nsTArray<nsCString>& aFeatureNames,
nsTArray<RefPtr<ContentClassifierEngine>>& aEngineList) {
mLock.AssertCurrentThreadOwns();
aEngineList.Clear();
bool sawExceptionOnly = false;
for (const auto& name : aFeatureNames) {
auto entry = mEngines.Lookup(name);
if (entry) {
RefPtr<ContentClassifierEngine> engine = entry.Data();
MOZ_ASSERT(engine);
if (sawExceptionOnly && !engine->Feature().mExceptionOnly) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Warning,
"PopulateActiveEngineListFromFeatureNames - pref lists "
"non-exception feature \"{}\" after an exception-only "
"feature; matched_rule state will not reach it",
name);
}
if (engine->Feature().mExceptionOnly) {
sawExceptionOnly = true;
}
aEngineList.AppendElement(engine);
}
}
}
nsTHashSet<nsCString> ContentClassifierService::ActiveFeatureNames(
const EnginesPrefsSnapshot& aPreferenceSnapshot) {
nsTHashSet<nsCString> names;
for (const auto& name : aPreferenceSnapshot.mCancel) {
names.Insert(name);
}
for (const auto& name : aPreferenceSnapshot.mCancelPBM) {
names.Insert(name);
}
for (const auto& name : aPreferenceSnapshot.mAnnotate) {
names.Insert(name);
}
for (const auto& name : aPreferenceSnapshot.mAnnotatePBM) {
names.Insert(name);
}
return names;
}
// static
bool ContentClassifierService::IsBlockingFeatureActive(
const nsACString& aFeatureName, bool aIsPrivate) {
if (!StaticPrefs::privacy_trackingprotection_content_protection_enabled()) {
return false;
}
// The feature is active only if it is listed in the protection engines pref.
nsTArray<nsCString> names;
EnginesPrefsSnapshot::AppendFeatureNamesFromPref(
aIsPrivate
? "privacy.trackingprotection.content.protection.engines.pbmode"
: "privacy.trackingprotection.content.protection.engines",
names);
return names.Contains(aFeatureName);
}
void ContentClassifierService::PruneInactiveEngines(
const EnginesPrefsSnapshot& aPreferenceSnapshot) {
mLock.AssertCurrentThreadOwns();
nsTHashSet<nsCString> activeFeatureNames =
ActiveFeatureNames(aPreferenceSnapshot);
for (auto iter = mEngines.Iter(); !iter.Done(); iter.Next()) {
if (!activeFeatureNames.Contains(iter.Key())) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Debug,
"PruneInactiveEngines - dropping engine for \"{}\"",
iter.Key());
iter.Remove();
}
}
for (auto iter = mFeatureVersions.Iter(); !iter.Done(); iter.Next()) {
if (!activeFeatureNames.Contains(iter.Key())) {
iter.Remove();
}
}
}
void ContentClassifierService::ProcessListChanges(
const nsTArray<nsCString>& aUpdated, const nsTArray<nsCString>& aRemoved) {
MOZ_ASSERT(NS_IsMainThread());
EnginesPrefsSnapshot snapshot;
nsTHashSet<nsCString> activeNames = ActiveFeatureNames(snapshot);
nsTArray<const ContentClassifierFeature*> toUpdate;
{
MutexAutoLock lock(mLock);
for (const auto& feature : GetFeatures()) {
if (!activeNames.Contains(nsCString(feature.mName))) {
continue;
}
bool affected = !mEngines.Contains(nsCString(feature.mName));
if (!affected) {
for (const auto& listId : feature.mListIds) {
if (aUpdated.Contains(listId) || aRemoved.Contains(listId)) {
affected = true;
break;
}
}
}
if (affected) {
toUpdate.AppendElement(&feature);
}
}
}
UpdateFeatures(toUpdate, std::move(snapshot));
}
void ContentClassifierService::UpdateFeatures(
const nsTArray<const ContentClassifierFeature*>& aFeatures,
EnginesPrefsSnapshot aPreferenceSnapshot) {
MOZ_ASSERT(NS_IsMainThread());
// Drop inactive features before issuing fetches: their engines will be
// removed by PruneInactiveEngines below, so fetching list bytes just to
// throw the engine away would be wasted work.
nsTHashSet<nsCString> activeNames = ActiveFeatureNames(aPreferenceSnapshot);
nsTArray<const ContentClassifierFeature*> features;
nsTArray<RefPtr<EngineRulesPromise>> fetches;
for (const auto* feature : aFeatures) {
if (!activeNames.Contains(nsCString(feature->mName))) {
continue;
}
features.AppendElement(feature);
fetches.AppendElement(FetchEngineDataForFeature(*feature));
}
// mBuildThread is non-null iff mInitPhase == InitSucceeded. A null
// value here means we're either pre-Init, init failed, or shutdown
// has already extracted the queue; in all of those cases the rebuild
// has nothing useful to do.
nsCOMPtr<nsISerialEventTarget> buildThread = mBuildThread;
if (!buildThread) {
return;
}
// Bump the global generation and (where applicable) per-feature
// versions under the lock. The build closure compares each captured
// version against the current one before installing, so a later
// UpdateFeatures call for the same feature can't be overwritten by
// an earlier in-flight rebuild. The global generation gates
// Populate / Prune / Notify so an older closure's stale snapshot
// can't clobber the active-engine lists set up by a newer call.
nsTArray<uint64_t> featureVersions;
featureVersions.SetCapacity(features.Length());
uint64_t generation;
{
MutexAutoLock lock(mLock);
generation = ++mUpdateGeneration;
for (const auto* feature : features) {
uint64_t& v = mFeatureVersions.LookupOrInsert(feature->mName);
featureVersions.AppendElement(++v);
}
}
RefPtr<ContentClassifierService> self = this;
if (features.IsEmpty()) {
// No fetches needed; still refresh the active lists and prune in
// case the snapshot changed. Run the lock-holding work on the
// build thread so it never blocks the main thread. The Notify is
// dispatched only when this is still the latest UpdateFeatures
// call — otherwise a newer call's closure will fire it.
buildThread->Dispatch(NS_NewRunnableFunction(
"ContentClassifierService::UpdateFeaturesNoFetch",
[self, snapshot = std::move(aPreferenceSnapshot), generation]() {
{
MutexAutoLock lock(self->mLock);
if (self->mInitPhase != InitPhase::InitSucceeded) {
return;
}
if (self->mUpdateGeneration != generation) {
return;
}
self->PopulateAllActiveEnginesFromPreferenceSnapshot(snapshot);
self->PruneInactiveEngines(snapshot);
}
NS_DispatchToMainThread(NS_NewRunnableFunction(
"ContentClassifierService::NotifyListsLoaded",
[self]() { NotifyListsLoadedForTesting(); }));
}));
return;
}
EngineRulesPromise::AllSettled(GetMainThreadSerialEventTarget(), fetches)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[self, features = std::move(features),
featureVersions = std::move(featureVersions),
snapshot = std::move(aPreferenceSnapshot),
buildThread = std::move(buildThread), generation](
EngineRulesPromise::AllSettledPromiseType::ResolveOrRejectValue&&
aValue) mutable {
MOZ_ASSERT(NS_IsMainThread());
// Collect per-feature rule arrays out of the settled promises;
// defer the expensive parsing / InitFromRules to mBuildThread.
nsTArray<nsTArray<nsCString>> perFeatureRules;
perFeatureRules.SetLength(features.Length());
if (aValue.IsResolve()) {
auto& settled = aValue.ResolveValue();
MOZ_ASSERT(settled.Length() == features.Length());
for (size_t i = 0; i < settled.Length(); ++i) {
if (settled[i].IsReject()) {
MOZ_LOG_FMT(
gContentClassifierLog, LogLevel::Warning,
"UpdateFeatures - fetch rejected for feature \"{}\"",
features[i]->mName);
continue;
}
perFeatureRules[i] = std::move(settled[i].ResolveValue());
}
}
buildThread->Dispatch(NS_NewRunnableFunction(
"ContentClassifierService::UpdateFeaturesBuild",
[self, features = std::move(features),
featureVersions = std::move(featureVersions),
perFeatureRules = std::move(perFeatureRules),
snapshot = std::move(snapshot), generation]() mutable {
MOZ_ASSERT(!NS_IsMainThread());
// Build engines outside the lock; InitFromRules can be
// expensive. A null engine — from a fetch reject, build
// failure, or empty rules — clobbers any existing entry
// for that feature via InstallEngine.
nsTArray<RefPtr<ContentClassifierEngine>> builtEngines;
builtEngines.SetLength(features.Length());
for (size_t i = 0; i < features.Length(); ++i) {
if (perFeatureRules[i].IsEmpty()) {
continue;
}
RefPtr<ContentClassifierEngine> engine;
if (NS_FAILED(BuildEngineFromRules(
*features[i], perFeatureRules[i], engine))) {
continue;
}
builtEngines[i] = std::move(engine);
}
bool didFullWork = false;
{
MutexAutoLock lock(self->mLock);
if (self->mInitPhase != InitPhase::InitSucceeded) {
// Shutdown raced us; drop everything we built.
return;
}
// Install non-stale engines (per-feature versioning).
for (size_t i = 0; i < builtEngines.Length(); ++i) {
uint64_t current =
self->mFeatureVersions.Get(features[i]->mName);
if (current != featureVersions[i]) {
MOZ_LOG_FMT(
gContentClassifierLog, LogLevel::Debug,
"UpdateFeatures - skipping stale install for "
"feature \"{}\" (have v{}, current v{})",
features[i]->mName, featureVersions[i], current);
continue;
}
self->InstallEngine(features[i]->mName,
std::move(builtEngines[i]));
}
// Only run Populate / Prune (and the Notify below)
// when this is still the latest UpdateFeatures call.
// A newer call's closure will run those itself with
// its own (more recent) snapshot.
if (self->mUpdateGeneration == generation) {
self->PopulateAllActiveEnginesFromPreferenceSnapshot(
snapshot);
self->PruneInactiveEngines(snapshot);
didFullWork = true;
}
}
if (didFullWork) {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"ContentClassifierService::NotifyListsLoaded",
[self]() { NotifyListsLoadedForTesting(); }));
}
}));
});
}
class FilterListLoader final : public nsIStreamLoaderObserver {
public:
NS_DECL_THREADSAFE_ISUPPORTS
explicit FilterListLoader(nsTArray<nsCString>* aRules) : mRules(aRules) {}
NS_IMETHOD
OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aCtxt,
nsresult aStatus, uint32_t aResultLength,
const uint8_t* aResult) override {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_SUCCESS(aStatus, aStatus);
if (NS_FAILED(aStatus)) {
MOZ_LOG(gContentClassifierLog, LogLevel::Debug,
("FilterListLoader::OnStreamComplete - failed with status 0x%x",
static_cast<uint32_t>(aStatus)));
mPromiseHolder.RejectIfExists(aStatus, __func__);
return aStatus;
}
nsAutoCString content(reinterpret_cast<const char*>(aResult),
aResultLength);
for (const auto& line : content.Split('\n')) {
if (!line.IsEmpty()) {
mRules->AppendElement(line);
}
}
MOZ_LOG(gContentClassifierLog, LogLevel::Debug,
("FilterListLoader::OnStreamComplete - loaded %zu rules",
mRules->Length()));
mPromiseHolder.ResolveIfExists(true, __func__);
return NS_OK;
}
RefPtr<GenericPromise> Load(const nsACString& aURL) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL);
NS_ENSURE_SUCCESS(rv, GenericPromise::CreateAndReject(rv, __func__));
nsCOMPtr<nsIChannel> channel;
uint32_t loadFlags = nsIChannel::LOAD_BYPASS_URL_CLASSIFIER;
rv = NS_NewChannel(getter_AddRefs(channel), uri,
nsContentUtils::GetSystemPrincipal(),
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
nsIContentPolicy::TYPE_OTHER,
nullptr, // nsICookieJarSettings
nullptr, // aPerformanceStorage
nullptr, // aLoadGroup
nullptr, // aInterfaceRequestor
loadFlags);
NS_ENSURE_SUCCESS(rv, GenericPromise::CreateAndReject(rv, __func__));
nsCOMPtr<nsIStreamLoader> loader;
rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
NS_ENSURE_SUCCESS(rv, GenericPromise::CreateAndReject(rv, __func__));
rv = channel->AsyncOpen(loader);
NS_ENSURE_SUCCESS(rv, GenericPromise::CreateAndReject(rv, __func__));
return mPromiseHolder.Ensure(__func__);
}
private:
~FilterListLoader() {
mPromiseHolder.RejectIfExists(NS_ERROR_ABORT, __func__);
}
nsTArray<nsCString>* mRules;
MozPromiseHolder<GenericPromise> mPromiseHolder;
};
NS_IMPL_ISUPPORTS(FilterListLoader, nsIStreamLoaderObserver)
RefPtr<ContentClassifierService::EngineRulesPromise>
ContentClassifierService::FetchEngineDataForFeature(
const ContentClassifierFeature& aFeature) {
if (aFeature.mName.Equals("test_block") ||
aFeature.mName.Equals("test_annotate")) {
return FetchEngineDataForTestFeature(aFeature);
}
if (!mRSClient) {
return EngineRulesPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
__func__);
}
nsTArray<RefPtr<ListBytesPromise>> fetches;
nsTArray<nsCString> listIdsInOrder;
for (const auto& listId : aFeature.mListIds) {
listIdsInOrder.AppendElement(listId);
fetches.AppendElement(FetchListBytesFromRemoteSettings(mRSClient, listId));
}
if (fetches.IsEmpty()) {
return EngineRulesPromise::CreateAndResolve(nsTArray<nsCString>{},
__func__);
}
RefPtr<EngineRulesPromise::Private> result =
new EngineRulesPromise::Private(__func__);
RefPtr<ContentClassifierService> self = this;
ListBytesPromise::AllSettled(GetMainThreadSerialEventTarget(), fetches)
->Then(GetMainThreadSerialEventTarget(), __func__,
[self, result, feature = &aFeature,
listIdsInOrder = std::move(listIdsInOrder)](
const ListBytesPromise::AllSettledPromiseType::
ResolveOrRejectValue& aValue) mutable {
if (aValue.IsReject()) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Error,
"FetchEngineDataForFeature - failed to fetch "
"list bytes for feature \"{}\"",
feature->mName);
result->Reject(NS_ERROR_NOT_AVAILABLE, __func__);
return;
}
const auto& fetchPromises = aValue.ResolveValue();
nsTArray<nsCString> rules;
for (size_t i = 0; i < fetchPromises.Length(); ++i) {
if (fetchPromises[i].IsReject()) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Warning,
"FetchEngineDataForFeature - list \"{}\" for "
"feature \"{}\" rejected",
listIdsInOrder[i], feature->mName);
continue;
}
const auto& fetchResult = fetchPromises[i].ResolveValue();
if (fetchResult.IsEmpty()) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Warning,
"FetchEngineDataForFeature - list \"{}\" for "
"feature \"{}\" returned no bytes",
listIdsInOrder[i], feature->mName);
continue;
}
ParseFilterListRules(fetchResult, rules);
}
result->Resolve(std::move(rules), __func__);
});
return result;
}
RefPtr<ContentClassifierService::EngineRulesPromise>
ContentClassifierService::FetchEngineDataForTestFeature(
const ContentClassifierFeature& aFeature) {
MOZ_LOG_FMT(gContentClassifierLog, LogLevel::Debug,
"FetchEngineDataForTestFeature - loading filter lists for "
"feature \"{}\"",
aFeature.mName);
nsAutoCString testListURLPref;
if (aFeature.mName.Equals("test_annotate")) {
Preferences::GetCString(
"privacy.trackingprotection.content.annotation.test_list_urls",
testListURLPref);
} else if (aFeature.mName.Equals("test_block")) {
Preferences::GetCString(
"privacy.trackingprotection.content.protection.test_list_urls",
testListURLPref);
} else {
MOZ_LOG(gContentClassifierLog, LogLevel::Warning,
("FetchEngineDataForTestFeature - incorrect feature name"));
return EngineRulesPromise::CreateAndResolve(nsTArray<nsCString>{},
__func__);
}
nsTArray<nsCString> listURLS;
for (const nsACString& url : testListURLPref.Split('|')) {
if (!url.IsEmpty()) {
listURLS.AppendElement(url);
MOZ_LOG(gContentClassifierLog, LogLevel::Debug,
("FetchEngineDataForTestFeature - test list URL: %s",
nsAutoCString(url).get()));
}
}
nsTArray<RefPtr<GenericPromise>> promises;
nsTArray<nsTArray<nsCString>> filterRules;
filterRules.SetLength(listURLS.Length());
for (size_t i = 0; i < listURLS.Length(); ++i) {
RefPtr<FilterListLoader> loader = new FilterListLoader(&filterRules[i]);
promises.AppendElement(loader->Load(listURLS[i]));
}
RefPtr<EngineRulesPromise::Private> result =
new EngineRulesPromise::Private(__func__);
GenericPromise::AllSettled(GetMainThreadSerialEventTarget(), promises)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[self = RefPtr{this}, result, filterRules = std::move(filterRules)](
const GenericPromise::AllSettledPromiseType::ResolveOrRejectValue&
aResults) {
nsTArray<nsCString> rules;
for (const auto& fromUrl : filterRules) {
rules.AppendElements(fromUrl);
}
result->Resolve(std::move(rules), __func__);
});
return result;
}
} // namespace mozilla