/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifdef XP_WIN # include <windows.h> # define SECURITY_WIN32 1 # include <security.h> # include "mozilla/NativeNt.h" # include "mozilla/WinDllServices.h" #endif// XP_WIN
// Allow up to this many threads to be concurrently engaged in synchronous // communcations with the agent. That limit is set by // browser.contentanalysis.max_connections but is clamped to not exceed // this value. constunsignedlong kMaxContentAnalysisAgentThreads = 256; // Max number of threads that we keep even if they have no tasks to run. constunsignedlong kMaxIdleContentAnalysisAgentThreads = 2; // Time (ms) we wait before declaring a thread idle. 100ms is the // threadpool default. constunsignedlong kIdleContentAnalysisAgentTimeoutMs = 100; // Time we wait before destroying the kMaxIdleContentAnalysisAgentThreads // threads. Content Analysis never does this, which is what UINT32_MAX // means. constunsignedlong kMaxIdleContentAnalysisAgentTimeoutMs = UINT32_MAX;
// How long the threadpool will wait at shutdown for the agent to complete any // in-progress operations before it abandons the threads (they will keep // running). const uint32_t kShutdownThreadpoolTimeoutMs = 2 * 1000;
// kTextMime must be the first entry. auto kTextFormatsToAnalyze = {kTextMime, kHTMLMime};
constchar* SafeGetStaticErrorName(nsresult aRv) { constauto* ret = mozilla::GetStaticErrorName(aRv); return ret ? ret : "<illegal value>";
}
/* static */ bool nsIContentAnalysis::MightBeActive() { // A DLP connection is not permitted to be added/removed while the // browser is running, so we can cache this. // Furthermore, if this is set via enterprise policy the pref will be locked // so users won't be able to change it. // Ideally we would make this a mirror: once pref, but this interacts in // some weird ways with the enterprise policy for testing purposes. staticbool sIsEnabled =
mozilla::StaticPrefs::browser_contentanalysis_enabled(); // Note that we can't check gAllowContentAnalysis here because it // only gets set in the parent process. return sIsEnabled;
}
// The windowGlobal is allowed to be null at this point in gtests (only). // The URL must be set in that case. We check that below.
RefPtr<dom::WindowGlobalParent> windowGlobal;
NS_ENSURE_SUCCESS(
aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal)), false);
// Any DataTransfer should have been expanded into individual requests.
nsCOMPtr<dom::DataTransfer> dataTransfer;
NS_ENSURE_SUCCESS(aRequest->GetDataTransfer(getter_AddRefs(dataTransfer)), false);
NS_ENSURE_TRUE(!dataTransfer, false);
// Any nsITransferable should have been expanded into individual requests.
nsCOMPtr<nsITransferable> transferable;
NS_ENSURE_SUCCESS(aRequest->GetTransferable(getter_AddRefs(transferable)), false);
NS_ENSURE_TRUE(!transferable, false);
nsCOMPtr<nsIURI> url;
NS_ENSURE_SUCCESS(aRequest->GetUrl(getter_AddRefs(url)), false); if (!url) { // If no URL is given then we use the one for the window.
NS_ENSURE_TRUE(windowGlobal, false);
url = ContentAnalysis::GetURIForBrowsingContext(
windowGlobal->Canonical()->GetBrowsingContext());
NS_ENSURE_TRUE(url, false);
}
int32_t timeout = StaticPrefs::browser_contentanalysis_agent_timeout(); // Non-positive timeout values indicate testing, and the test agent does not // care about this value.
timeout = std::max(timeout, 1);
uint32_t timeoutMultiplier;
rv = aIn->GetTimeoutMultiplier(&timeoutMultiplier);
NS_ENSURE_SUCCESS(rv, rv);
timeoutMultiplier = std::max(timeoutMultiplier, static_cast<uint32_t>(1)); auto checkedTimeout = CheckedInt64(time(nullptr)) +
timeout * userActionRequestsCount * timeoutMultiplier; if (!checkedTimeout.isValid()) { return NS_ERROR_FAILURE;
}
aOut->set_expires_at(checkedTimeout.value());
const std::string tag = "dlp"; // TODO:
*aOut->add_tags() = tag;
auto* requestData = aOut->mutable_request_data();
RefPtr<dom::WindowGlobalParent> windowGlobal;
rv = aIn->GetWindowGlobalParent(getter_AddRefs(windowGlobal));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> url;
rv = aIn->GetUrl(getter_AddRefs(url));
NS_ENSURE_SUCCESS(rv, rv); if (!url) { // We already checked that this exists.
MOZ_ASSERT(windowGlobal); // If no URL is given then we use the one for the window.
url = ContentAnalysis::GetURIForBrowsingContext(
windowGlobal->Canonical()->GetBrowsingContext()); // We also already checked for this.
MOZ_ASSERT(url);
}
nsCString urlString;
rv = url->GetSpec(urlString);
NS_ENSURE_SUCCESS(rv, rv); if (!urlString.IsEmpty()) {
requestData->set_url(urlString.get());
}
if (windowGlobal) {
nsString title;
windowGlobal->GetDocumentTitle(title);
requestData->set_tab_title(NS_ConvertUTF16toUTF8(title).get());
}
namespace { // We don't want this overload to be called for string parameters, so // use std::enable_if template <typename T> typename std::enable_if_t<!std::is_same<std::string, std::decay_t<T>>::value, void>
LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) {
ss << value;
}
// 0 indicates no max length template <typename T> typename std::enable_if_t<std::is_same<std::string, std::decay_t<T>>::value, void>
LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) { if (!maxLength || value.length() < maxLength) {
ss << value;
} else {
ss << value.substr(0, maxLength) << " (truncated)";
}
}
} // namespace
staticvoid LogRequest( const content_analysis::sdk::ContentAnalysisRequest* aPbRequest) { // We cannot use Protocol Buffer's DebugString() because we optimize for // lite runtime. if (!static_cast<LogModule*>(gContentAnalysisLog)
->ShouldLog(LogLevel::Debug)) { return;
}
std::stringstream ss;
ss << "ContentAnalysisRequest:"
<< "\n";
#define ADD_FIELD_WITH_VALFUNC(PBUF, NAME, FUNC, VALFUNC) \
ss << " " << (NAME) << ": "; \ if ((PBUF)->has_##FUNC()) { \
LogWithMaxLength(ss, VALFUNC(), 500); \
ss << "\n"; \
} else \
ss << "<none>" \
<< "\n";
ContentAnalysisResponse::ContentAnalysisResponse(
content_analysis::sdk::ContentAnalysisResponse&& aResponse, const nsCString& aUserActionId)
: mUserActionId(aUserActionId) {
mAction = Action::eUnspecified; for (constauto& result : aResponse.results()) { if (!result.has_status() ||
result.status() !=
content_analysis::sdk::ContentAnalysisResponse::Result::SUCCESS) {
mAction = Action::eUnspecified; return;
} // The action values increase with severity, so the max is the most severe. for (constauto& rule : result.triggered_rules()) {
mAction = static_cast<Action>(std::max(static_cast<uint32_t>(mAction), static_cast<uint32_t>(rule.action())));
}
}
// If no rules blocked then we should allow. if (mAction == Action::eUnspecified) {
mAction = Action::eAllow;
}
NS_IMETHODIMP ContentAnalysisNoResult::GetShouldAllowContent( bool* aShouldAllowContent) { // Make sure to use the non-timeout pref here, because timeouts won't // go through this code path. if (GetDefaultResultFromPref(/* isTimeout */ false) ==
DefaultResult::eAllow) {
*aShouldAllowContent =
mValue != NoContentAnalysisResult::DENY_DUE_TO_CANCELED;
} else { // Note that we allow content if we're unable to get it (for example, if // there's clipboard content that is not text or file)
*aShouldAllowContent =
mValue ==
NoContentAnalysisResult::ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE ||
mValue == NoContentAnalysisResult::
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS ||
mValue == NoContentAnalysisResult::ALLOW_DUE_TO_SAME_TAB_SOURCE ||
mValue == NoContentAnalysisResult::ALLOW_DUE_TO_COULD_NOT_GET_DATA;
} return NS_OK;
}
void ContentAnalysis::EnsureParsedUrlFilters() {
MOZ_ASSERT(NS_IsMainThread()); if (mParsedUrlLists) { return;
}
mParsedUrlLists = true;
nsAutoCString allowList;
MOZ_ALWAYS_SUCCEEDS(Preferences::GetCString(kAllowUrlPref, allowList)); for (const nsACString& regexSubstr : allowList.Split(u' ')) { if (!regexSubstr.IsEmpty()) { auto flatStr = PromiseFlatCString(regexSubstr); constchar* regex = flatStr.get();
LOGD("CA will allow URLs that match %s", regex);
mAllowUrlList.push_back(std::regex(regex));
}
}
nsAutoCString denyList;
MOZ_ALWAYS_SUCCEEDS(Preferences::GetCString(kDenyUrlPref, denyList)); for (const nsACString& regexSubstr : denyList.Split(u' ')) { if (!regexSubstr.IsEmpty()) { auto flatStr = PromiseFlatCString(regexSubstr); constchar* regex = flatStr.get();
LOGD("CA will block URLs that match %s", regex);
mDenyUrlList.push_back(std::regex(regex));
}
}
}
ContentAnalysis::ContentAnalysis()
: mRequestTokenToBasicRequestInfoMap( "ContentAnalysis::mRequestTokenToBasicRequestInfoMap"),
mSetByEnterprise(false) { // Limit one per process
[[maybe_unused]] staticbool sCreated = false;
MOZ_ASSERT(!sCreated);
sCreated = true;
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService(); if (!obsServ) { // We must be shutting down so don't init anything. return;
}
obsServ->AddObserver(this, "xpcom-shutdown-threads", false);
mCaClientPromise = new ClientPromise::Private("ContentAnalysis::ContentAnalysis");
mThreadPool = new nsThreadPool();
MOZ_ALWAYS_SUCCEEDS(
mThreadPool->SetName(nsAutoCString("ContentAnalysisAgentIO")));
// Update thread limit if the pref changes, for testing (otherwise it is // locked). We cannot use RegisterCallbackAndCall since the callback needs // to get the service that we are currently constructing.
Preferences::RegisterCallback(
[](constchar* aPref, void*) { auto self = GetContentAnalysisFromService(); if (!self) { return;
} unsignedlong threadLimit = std::min( static_cast<unsignedlong>(
StaticPrefs::browser_contentanalysis_max_connections()),
kMaxContentAnalysisAgentThreads);
MOZ_ALWAYS_SUCCEEDS(self->mThreadPool->SetThreadLimit(threadLimit));
},
nsDependentCString(
StaticPrefs::GetPrefName_browser_contentanalysis_max_connections()));
void ContentAnalysis::Close() {
AssertIsOnMainThread();
{ // Make sure that we don't try to reconnect to the agent. auto lock = mIsShutDown.Lock(); if (*lock) { // was previously called return;
}
*lock = true;
}
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService(); if (obsServ) {
obsServ->RemoveObserver(this, "xpcom-shutdown-threads");
}
// Reject the promise to avoid assertions when it gets destroyed // Note that if the promise has already been resolved or rejected this is a // noop
mCaClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
// In case the promise _was_ resolved before, create a new one and reject // that.
mCaClientPromise = new ClientPromise::Private("ContentAnalysis:ShutdownReject");
mCaClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
// The userActionMap must be cleared before the object is destroyed.
mUserActionMap.Clear();
mThreadPool->ShutdownWithTimeout(kShutdownThreadpoolTimeoutMs);
mThreadPool = nullptr;
LOGD("Content Analysis service is closed");
}
bool ContentAnalysis::IsShutDown() { auto lock = mIsShutDown.ConstLock(); return *lock;
}
nsCString pipePathName;
nsresult rv = Preferences::GetCString(kPipePathNamePref, pipePathName); if (NS_WARN_IF(NS_FAILED(rv))) {
mCaClientPromise->Reject(rv, __func__); return rv;
} if (mHaveResolvedClientPromise && !aForceCreate) { return NS_OK;
} // mCreatingClient is only accessed on the main thread if (mCreatingClient) { return NS_OK;
}
mCreatingClient = true;
mHaveResolvedClientPromise = false; // Reject the promise to avoid assertions when it gets destroyed // Note that if the promise has already been resolved or rejected this is a // noop
mCaClientPromise->Reject(NS_ERROR_FAILURE, __func__);
mCaClientPromise = new ClientPromise::Private("ContentAnalysis::ContentAnalysis");
bool isPerUser = StaticPrefs::browser_contentanalysis_is_per_user();
nsString clientSignature; // It's OK if this fails, we will default to the empty string
Preferences::GetString(kClientSignature, clientSignature);
RecordConnectionSettingsTelemetry(clientSignature);
LOGD("Dispatching background task to create Content Analysis client");
glean::content_analysis::connection_attempt.Add(); if (aForceCreate) { // indicates this is a retry attempt
glean::content_analysis::connection_attempt_retry.Add();
}
rv = NS_DispatchBackgroundTask(NS_NewCancelableRunnableFunction( "ContentAnalysis::CreateContentAnalysisClient",
[owner = RefPtr{this}, pipePathName = std::move(pipePathName),
clientSignature = std::move(clientSignature), isPerUser]() mutable {
owner->CreateContentAnalysisClient(
std::move(pipePathName), std::move(clientSignature), isPerUser);
})); if (NS_WARN_IF(NS_FAILED(rv))) {
glean::content_analysis::connection_failure
.Get(nsCString{SafeGetStaticErrorName(rv)})
.Add();
mCaClientPromise->Reject(rv, __func__); return rv;
} return NS_OK;
}
void ContentAnalysis::RecordConnectionSettingsTelemetry( const nsString& clientSignature) {
AssertIsOnMainThread();
{
nsCString agentName;
Preferences::GetCString(kAgentNamePref, agentName);
glean::content_analysis::agent_name.Set(agentName);
}
AutoTArray<nsCString, 1> interceptionPointsOff; for (constchar* interceptionPointPrefName : kInterceptionPointPrefNames) { bool interceptionPointPrefValue = false;
Preferences::GetBool(interceptionPointPrefName,
&interceptionPointPrefValue); if (!interceptionPointPrefValue) {
interceptionPointsOff.AppendElement(interceptionPointPrefName);
}
} if (!interceptionPointsOff.IsEmpty()) {
glean::content_analysis::interception_points_turned_off.Set(
interceptionPointsOff);
}
glean::content_analysis::show_blocked_result.Set(
StaticPrefs::browser_contentanalysis_show_blocked_result());
glean::content_analysis::default_result.Set(
StaticPrefs::browser_contentanalysis_default_result());
glean::content_analysis::timeout_result.Set(
StaticPrefs::browser_contentanalysis_timeout_result()); if (!clientSignature.IsEmpty()) {
glean::content_analysis::client_signature.Set(
NS_ConvertUTF16toUTF8(clientSignature));
}
glean::content_analysis::bypass_for_same_tab_operations.Set(
StaticPrefs::browser_contentanalysis_bypass_for_same_tab_operations());
{
nsCString allowUrlRegexList;
Preferences::GetCString(kAllowUrlPref, allowUrlRegexList); // Unfortunately because of the way enterprise policies set and lock prefs, // we can't check if the value is different than the default in // StaticPrefList.yaml, and instead we have to duplicate that value here. At // least we have a test around this so we can update this value if the // default changes. constchar* defaultAllowUrlRegexList = "^about:(?!blank|srcdoc).*";
glean::content_analysis::allow_url_regex_list_set.Set(
!allowUrlRegexList.Equals(defaultAllowUrlRegexList));
}
{
nsCString denyUrlRegexList;
Preferences::GetCString(kDenyUrlPref, denyUrlRegexList);
glean::content_analysis::deny_url_regex_list_set.Set(
!denyUrlRegexList.IsEmpty());
}
}
NS_IMETHODIMP
ContentAnalysis::GetIsActive(bool* aIsActive) {
*aIsActive = false; if (!StaticPrefs::browser_contentanalysis_enabled()) {
LOGD("Local DLP Content Analysis is not enabled"); return NS_OK;
} // Accessing mSetByEnterprise and non-static prefs // so need to be on the main thread
AssertIsOnMainThread(); // gAllowContentAnalysisArgPresent is only set in the parent process
MOZ_ASSERT(XRE_IsParentProcess()); if (!gAllowContentAnalysisArgPresent && !mSetByEnterprise) {
LOGE( "The content analysis pref is enabled but not by an enterprise " "policy and -allow-content-analysis was not present on the " "command-line. Content Analysis will not be active."); return NS_OK;
}
Maybe<nsIContentAnalysisResponse::Action>
ContentAnalysis::CachedClipboardResponse::GetCachedResponse(
nsIURI* aURI, int32_t aClipboardSequenceNumber) {
MOZ_ASSERT(NS_IsMainThread(), "Expecting main thread access only to avoid synchronization"); if (Some(aClipboardSequenceNumber) != mClipboardSequenceNumber) {
LOGD("CachedClipboardResponse seqno does not match cached value"); return Nothing();
} for (constauto& entry : mData) { bool uriEquals = false; // URI will not be set for some chrome contexts if ((!aURI && !entry.first) ||
(aURI && NS_SUCCEEDED(aURI->Equals(entry.first, &uriEquals)) &&
uriEquals)) {
LOGD("CachedClipboardResponse match"); return Some(entry.second);
}
}
LOGD("CachedClipboardResponse did not match any cached URI"); return Nothing();
}
void ContentAnalysis::CachedClipboardResponse::SetCachedResponse( const nsCOMPtr<nsIURI>& aURI, int32_t aClipboardSequenceNumber,
nsIContentAnalysisResponse::Action aAction) {
MOZ_ASSERT(NS_IsMainThread(), "Expecting main thread access only to avoid synchronization"); if (mClipboardSequenceNumber != Some(aClipboardSequenceNumber)) {
LOGD("CachedClipboardResponse caching new clipboard seqno");
mData.Clear();
mClipboardSequenceNumber = Some(aClipboardSequenceNumber);
} else {
LOGD( "CachedClipboardResponse caching new URI for existing cached clipboard " "seqno");
}
// Update the cached action for this URI if it already exists in the cache, // otherwise add a new cache entry for this URI. for (auto& entry : mData) { bool uriEquals = false; // URI will not be set for some chrome contexts if ((!aURI && !entry.first) ||
(aURI && NS_SUCCEEDED(aURI->Equals(entry.first, &uriEquals)) &&
uriEquals)) {
entry.second = aAction; return;
}
}
void ContentAnalysis::CancelWithError(nsCString&& aUserActionId,
nsresult aResult) {
MOZ_ASSERT(!aUserActionId.IsEmpty()); if (!NS_IsMainThread()) {
NS_DispatchToMainThread(NS_NewCancelableRunnableFunction( "CancelWithError",
[aUserActionId = std::move(aUserActionId), aResult]() mutable { auto self = GetContentAnalysisFromService(); if (!self) { // May be shutting down return;
}
self->CancelWithError(std::move(aUserActionId), aResult);
})); return;
}
AssertIsOnMainThread();
LOGD("CancelWithError | aUserActionId: %s | aResult: %s\n",
aUserActionId.get(), SafeGetStaticErrorName(aResult));
AutoTArray<nsCString, 1> tokens;
RefPtr<nsIContentAnalysisCallback> callback; bool autoAcknowledge; if (auto maybeUserActionData = mUserActionMap.Lookup(aUserActionId)) { // We are cancelling all existing requests for this user action.
tokens =
ToTArray<AutoTArray<nsCString, 1>>(maybeUserActionData->mRequestTokens);
callback = maybeUserActionData->mCallback;
autoAcknowledge = maybeUserActionData->mAutoAcknowledge;
} else {
LOGD( "ContentAnalysis::CancelWithError user action not found -- already " "responded | userActionId: %s",
aUserActionId.get()); auto userActionIdToCanceledResponseMap =
mUserActionIdToCanceledResponseMap.Lock(); if (auto entry = userActionIdToCanceledResponseMap->Lookup(aUserActionId)) {
entry->mNumExpectedResponses--; if (!entry->mNumExpectedResponses) {
entry.Remove();
}
} return;
}
if (tokens.IsEmpty()) { // There are two cases where this happens. // (1) This Cancel was for the last request in the user action. We don't // have any other tokens to cancel and we have nothing to tell the agent to // cancel. Note that this case is only possible if this cancel call is // due to a negative verdict from the agent, and that handler will remove // our userActionId from mUserActionMap, so there is nothing left to do. // (2) We canceled before the final request list was formed. We still // need to call the callback -- we do this when the final request list // is complete.
MOZ_ASSERT(
aResult == NS_ERROR_ABORT, "Token list can only be empty when canceling all remaining requests");
LOGD( "ContentAnalysis::CancelWithError user action not found -- either was " "after last response or before first request was submitted | " "userActionId: %s",
aUserActionId.get());
RemoveFromUserActionMap(std::move(aUserActionId)); return;
}
LOGD( "ContentAnalysis::CancelWithError cancelling user action: %s with error: " "%s",
aUserActionId.get(), SafeGetStaticErrorName(aResult));
// Propagate shutdown error to the callback as that same error. All other // cases use the default response, except user cancel, which always uses // cancel response. // Note that, for shutdown errors, if we returned a default warn response // (as opposed to some other value -- we currently return the error), // the result would be a shutdown hang while the dialog waited for a user // response (bug 1912245).
nsIContentAnalysisResponse::Action action =
nsIContentAnalysisResponse::Action::eCanceled; if (!isShutdown && !isCancel) {
DefaultResult defaultResponse = GetDefaultResultFromPref(isTimeout); switch (defaultResponse) { case DefaultResult::eAllow:
action = nsIContentAnalysisResponse::Action::eAllow; break; case DefaultResult::eWarn:
action = nsIContentAnalysisResponse::Action::eWarn; break; case DefaultResult::eBlock: // eBlock would show a block dialog but eCanceled will not.
action = nsIContentAnalysisResponse::Action::eCanceled; break; default:
MOZ_ASSERT(false);
action = nsIContentAnalysisResponse::Action::eCanceled;
}
}
nsIContentAnalysisResponse::CancelError cancelError; switch (aResult) { case NS_ERROR_NOT_AVAILABLE: case NS_ERROR_CONNECTION_REFUSED:
cancelError = nsIContentAnalysisResponse::CancelError::eNoAgent; break; case NS_ERROR_INVALID_SIGNATURE:
cancelError =
nsIContentAnalysisResponse::CancelError::eInvalidAgentSignature; break; case NS_ERROR_WONT_HANDLE_CONTENT: case NS_ERROR_ABORT:
cancelError = nsIContentAnalysisResponse::CancelError::
eOtherRequestInGroupCancelled; break; case NS_ERROR_ILLEGAL_DURING_SHUTDOWN:
cancelError = nsIContentAnalysisResponse::CancelError::eShutdown; break; case NS_ERROR_DOM_TIMEOUT_ERR:
cancelError = nsIContentAnalysisResponse::CancelError::eTimeout; break; default:
cancelError = nsIContentAnalysisResponse::CancelError::eErrorOther; break;
}
bool calledError = false; for (constauto& token : tokens) { auto response =
MakeRefPtr<ContentAnalysisResponse>(action, token, aUserActionId);
response->SetCancelError(cancelError); // Alert the UI and (if action is not warn) the callback. We aren't // handling an actual response so we have nothing to acknowledge.
NotifyResponseObservers(response, nsCString(aUserActionId), autoAcknowledge,
isTimeout); if (action != nsIContentAnalysisResponse::Action::eWarn) { if (callback) { if (isShutdown) { // One Error response call is sufficient to complete the // MultipartRequestCallback. if (!calledError) {
callback->Error(aResult);
calledError = true;
}
} else {
callback->ContentResult(response);
}
}
}
}
if (action == nsIContentAnalysisResponse::Action::eWarn) { // A default warn response will handle the rest after the user chooses // a result. return;
}
// NS_ERROR_WONT_HANDLE_CONTENT and NS_ERROR_CONNECTION_REFUSED mean the // request was never sent to the agent, so we don't cancel it. if (aResult != NS_ERROR_WONT_HANDLE_CONTENT &&
aResult != NS_ERROR_CONNECTION_REFUSED) { auto userActionIdToCanceledResponseMap =
mUserActionIdToCanceledResponseMap.Lock();
userActionIdToCanceledResponseMap->InsertOrUpdate(
aUserActionId,
CanceledResponse{ConvertResult(action), tokens.Length()});
} else {
LOGD("CancelWithError cancelling unsubmitted request with error %s.",
SafeGetStaticErrorName(aResult)); return;
}
// Re-get service in case the registered service is mocked for testing.
nsCOMPtr<nsIContentAnalysis> contentAnalysis =
mozilla::components::nsIContentAnalysis::Service(); if (contentAnalysis) {
contentAnalysis->SendCancelToAgent(aUserActionId);
} else {
LOGD( "Content Analysis Service has been shut down. Cancel will not be " "sent to agent.");
}
}
NS_IMETHODIMP ContentAnalysis::SendCancelToAgent( const nsACString& aUserActionId) {
CallClientWithRetry<std::nullptr_t>(
__func__,
[userActionId = nsCString(aUserActionId)](
std::shared_ptr<content_analysis::sdk::Client> client) mutable
-> Result<std::nullptr_t, nsresult> {
MOZ_ASSERT(!NS_IsMainThread()); auto owner = GetContentAnalysisFromService(); if (!owner) { // May be shutting down return nullptr;
}
content_analysis::sdk::ContentAnalysisCancelRequests cancelRequest;
cancelRequest.set_user_action_id(userActionId.get(),
userActionId.Length()); int err = client->CancelRequests(cancelRequest); if (err != 0) {
LOGE( "SendCancelToAgent got error %d for " "user_action_id: %s",
err, userActionId.get()); return Err(NS_ERROR_FAILURE);
}
LOGD( "SendCancelToAgent successfully sent CancelRequests to " "agent for user_action_id: %s",
userActionId.get()); return nullptr;
})
->Then(
GetCurrentSerialEventTarget(), __func__, []() { /* nothing to do */ },
[](nsresult rv) {
LOGE("SendCancelToAgent failed to get the client with error %s",
SafeGetStaticErrorName(rv));
}); return NS_OK;
}
nsresult rv = NS_ERROR_FAILURE; // Set up the scope exit before checking the return // value so we will call Error() if this call failed. auto callbackCopy = aCallback; auto se = MakeScopeExit([&]() { if (!NS_SUCCEEDED(rv)) {
LOGE("RunAnalyzeRequestTask failed");
callbackCopy->Error(rv);
}
});
// We will need to submit the request to the agent.
content_analysis::sdk::ContentAnalysisRequest pbRequest;
rv = ConvertToProtobuf(aRequest, &pbRequest);
NS_ENSURE_SUCCESS(rv, rv);
LOGD("Issuing ContentAnalysisRequest for token %s", requestToken.get());
LogRequest(&pbRequest);
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService(); // Avoid serializing the string here if no one is observing this message if (obsServ && obsServ->HasObservers("dlp-request-sent-raw")) {
std::string requestString = pbRequest.SerializeAsString();
nsTArray<char16_t> requestArray;
requestArray.SetLength(requestString.size() + 1); for (size_t i = 0; i < requestString.size(); ++i) { // Since NotifyObservers() expects a null-terminated string, // make sure none of these values are 0.
requestArray[i] = requestString[i] + 0xFF00;
}
requestArray[requestString.size()] = 0;
obsServ->NotifyObservers(static_cast<nsIContentAnalysis*>(this), "dlp-request-sent-raw", requestArray.Elements());
}
CallClientWithRetry<std::nullptr_t>(
__func__,
[userActionId = userActionId, pbRequest = std::move(pbRequest),
aAutoAcknowledge, ignoreCanceled](
std::shared_ptr<content_analysis::sdk::Client> client) mutable {
MOZ_ASSERT(!NS_IsMainThread()); return DoAnalyzeRequest(std::move(userActionId), std::move(pbRequest),
aAutoAcknowledge, client, ignoreCanceled);
})
->Then(
GetMainThreadSerialEventTarget(), __func__, []() { /* do nothing */ },
[userActionId = std::move(userActionId),
requestToken = std::move(requestToken)](nsresult rv) mutable {
LOGD( "RunAnalyzeRequestTask failed to get client a second time for " "requestToken=%s, userActionId=%s",
requestToken.get(), userActionId.get());
RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService(); if (!owner) { // May be shutting down return;
}
owner->CancelWithError(std::move(userActionId), rv);
});
return NS_OK;
}
Result<std::nullptr_t, nsresult> ContentAnalysis::DoAnalyzeRequest(
nsCString&& aUserActionId,
content_analysis::sdk::ContentAnalysisRequest&& aRequest, bool aAutoAcknowledge, const std::shared_ptr<content_analysis::sdk::Client>& aClient, bool aTestOnlyIgnoreCanceled) {
MOZ_ASSERT(!NS_IsMainThread());
RefPtr<ContentAnalysis> owner =
ContentAnalysis::GetContentAnalysisFromService(); if (!owner) { // May be shutting down // Don't return an error because we don't want to retry return nullptr;
}
if (aRequest.has_file_path() && !aRequest.file_path().empty() &&
(!aRequest.request_data().has_digest() ||
aRequest.request_data().digest().empty())) { // Calculate the digest
nsCString digest;
nsCString fileCPath(aRequest.file_path().data(),
aRequest.file_path().length());
nsString filePath = NS_ConvertUTF8toUTF16(fileCPath);
nsresult rv = ContentAnalysisRequest::GetFileDigest(filePath, digest); if (NS_FAILED(rv)) {
owner->CancelWithError(std::move(aUserActionId), rv); // Don't return an error because we don't want to retry return nullptr;
} if (!digest.IsEmpty()) {
aRequest.mutable_request_data()->set_digest(digest.get());
}
}
bool actionWasCanceled = false; if (!aTestOnlyIgnoreCanceled) { auto userActionIdToCanceledResponseMap =
owner->mUserActionIdToCanceledResponseMap.Lock();
actionWasCanceled =
userActionIdToCanceledResponseMap->Contains(aUserActionId);
} if (actionWasCanceled) {
LOGD( "DoAnalyzeRequest | userAction: %s | requestToken: %s | was already " "canceled",
aUserActionId.get(), aRequest.request_token().c_str()); return Err(NS_ERROR_WONT_HANDLE_CONTENT);
}
// Run request, then dispatch back to main thread to resolve // aCallback
content_analysis::sdk::ContentAnalysisResponse pbResponse;
nsDependentCString analysisConnectorName(
content_analysis::sdk::AnalysisConnector_Name(
aRequest.analysis_connector())
.c_str()); auto timerId = glean::content_analysis::response_duration_by_analysis_type
.Get(analysisConnectorName)
.Start();
{ // Insert this into the map before calling Send() because another thread // calling Send() may get a response before our Send() call finishes. auto map = owner->mRequestTokenToBasicRequestInfoMap.Lock();
map->InsertOrUpdate(
nsCString(aRequest.request_token()),
BasicRequestInfo{aUserActionId, timerId,
std::move(analysisConnectorName), aAutoAcknowledge});
}
void ContentAnalysis::HandleResponseFromAgent(
content_analysis::sdk::ContentAnalysisResponse&& aResponse) {
MOZ_ASSERT(!NS_IsMainThread());
NS_DispatchToMainThread(NS_NewRunnableFunction(
__func__, [aResponse = std::move(aResponse)]() mutable {
LOGD("RunAnalyzeRequestTask on main thread about to send response");
LogResponse(&aResponse);
RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService(); if (!owner) { // May be shutting down return;
}
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService(); // This message is only used for testing purposes, so avoid // serializing the string here if no one is observing this message. // This message is only really useful if we're in a timeout // situation, otherwise dlp-response is fine. if (obsServ && obsServ->HasObservers("dlp-response-received-raw")) {
std::string responseString = aResponse.SerializeAsString();
nsTArray<char16_t> responseArray;
responseArray.SetLength(responseString.size() + 1); for (size_t i = 0; i < responseString.size(); ++i) { // Since NotifyObservers() expects a null-terminated string, // make sure none of these values are 0.
responseArray[i] = responseString[i] + 0xFF00;
}
responseArray[responseString.size()] = 0;
obsServ->NotifyObservers(static_cast<nsIContentAnalysis*>(owner), "dlp-response-received-raw",
responseArray.Elements());
}
Maybe<BasicRequestInfo> maybeBasicRequestInfo;
{ auto map = owner->mRequestTokenToBasicRequestInfoMap.Lock();
maybeBasicRequestInfo =
map->Extract(nsCString(aResponse.request_token()));
} if (maybeBasicRequestInfo.isNothing()) {
LOGE( "RunAnalyzeRequestTask could not find userActionId for " "request token %s",
aResponse.request_token().c_str()); // We have no hope of doing anything useful, so just early return. return;
}
glean::content_analysis::response_duration_by_analysis_type
.Get(maybeBasicRequestInfo->mAnalysisTypeStr)
.StopAndAccumulate(std::move(maybeBasicRequestInfo->mTimerId));
nsCString userActionId = maybeBasicRequestInfo->mUserActionId;
RefPtr<ContentAnalysisResponse> response =
ContentAnalysisResponse::FromProtobuf(std::move(aResponse),
userActionId); if (!response) {
LOGE("Content analysis got invalid response!"); return;
} // We add our own values for action here (eAllow and eCancel) // so just use the numeric value for glean.
nsAutoCString actionStr;
actionStr.AppendInt(static_cast<int>(response->GetAction()));
glean::content_analysis::response_action.Get(actionStr).Add(); // Normally, if we timeout/user-cancel a request, we remove the // adjacent entry in mUserActionMap. However, we don't do that if // the chosen default behavior is to warn. We don't want to issue // a response in that case.
nsCString requestToken;
MOZ_ALWAYS_SUCCEEDS(response->GetRequestToken(requestToken)); if (owner->mWarnResponseDataMap.Contains(requestToken)) { return;
}
if (aResponse->GetAction() == nsIContentAnalysisResponse::Action::eWarn) { // Store data so we can asynchronously run the warn dialog, then call // IssueResponse with the result.
nsCString requestToken;
MOZ_ALWAYS_SUCCEEDS(aResponse->GetRequestToken(requestToken));
// Call the callback and maybe send an auto acknowledge.
nsCString token;
MOZ_ALWAYS_SUCCEEDS(aResponse->GetRequestToken(token));
RefPtr<nsIContentAnalysisCallback> callback; if (auto maybeUserActionData = mUserActionMap.Lookup(aUserActionId)) {
callback = maybeUserActionData->mCallback;
} else {
LOGD( "ContentAnalysis::IssueResponse user action not found -- already " "responded | userActionId: %s",
aUserActionId.get());
if (aAcknowledge) { // Respond to the agent with TOO_LATE because the response arrived // after the request was cancelled (for any reason).
nsIContentAnalysisAcknowledgement::FinalAction action; auto userActionIdToCanceledResponseMap =
mUserActionIdToCanceledResponseMap.Lock();
userActionIdToCanceledResponseMap->WithEntryHandle(
aUserActionId, [&](auto&& canceledResponseEntry) { if (canceledResponseEntry) {
action = canceledResponseEntry->mAction;
--canceledResponseEntry->mNumExpectedResponses; if (!canceledResponseEntry->mNumExpectedResponses) { // We've handled all responses for canceled requests for this // user action.
canceledResponseEntry.Remove();
}
} else { if (mWarnResponseDataMap.Contains(token)) { // We got a response from the agent but we're still waiting // for a warn response from the user. This can basically only // happen if the request timed out but TimeoutResult=1 (i.e. // warn) is set.
LOGD( "Got response from agent for token %s but user hasn't " "replied to warn dialog yet",
token.get()); return;
}
MOZ_ASSERT_UNREACHABLE("missing canceled response action");
action =
nsIContentAnalysisAcknowledgement::FinalAction::eUnspecified;
}
RefPtr<ContentAnalysisAcknowledgement> acknowledgement =
MakeRefPtr<ContentAnalysisAcknowledgement>(
nsIContentAnalysisAcknowledgement::Result::eTooLate,
action);
aResponse->Acknowledge(acknowledgement);
});
} return;
}
if (aAcknowledge) { // Acknowledge every response we receive. auto acknowledgement = MakeRefPtr<ContentAnalysisAcknowledgement>(
aIsTimeout ? nsIContentAnalysisAcknowledgement::Result::eTooLate
: nsIContentAnalysisAcknowledgement::Result::eSuccess,
ConvertResult(aResponse->GetAction()));
aResponse->Acknowledge(acknowledgement);
}
LOGD("Content analysis notifying observers and calling callback for token %s",
token.get());
callback->ContentResult(aResponse);
// A negative verdict should have removed our user action. (This method // is not called for warn verdicts.)
MOZ_ASSERT(aResponse->GetShouldAllowContent() ||
!mUserActionMap.Contains(aUserActionId));
}
// For warn responses, IssueResponse will be called later by // RespondToWarnDialog, with the action replaced with the user's selection. if (aResponse->GetAction() != nsIContentAnalysisResponse::Action::eWarn) { // This is a response from the agent, so not a timeout.
IssueResponse(aResponse, std::move(aUserActionId), aAutoAcknowledge, false/* aIsTimeout */);
}
}
staticvoid AddCARForText(
nsString&& text, nsIContentAnalysisRequest::Reason aReason,
nsIContentAnalysisRequest::OperationType aOperationType, nsIURI* aURI,
mozilla::dom::WindowGlobalParent* aWindowGlobal,
mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
nsCString&& aUserActionId,
nsTArray<RefPtr<nsIContentAnalysisRequest>>* aRequests) { if (text.IsEmpty()) { // Content Analysis doesn't expect to analyze an empty string. // Just skip it. return;
}
LOGD("Adding CA request for text: '%s'", NS_ConvertUTF16toUTF8(text).get()); auto contentAnalysisRequest = MakeRefPtr<ContentAnalysisRequest>(
nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, aReason,
std::move(text), false, EmptyCString(), aURI, aOperationType,
aWindowGlobal, aSourceWindowGlobal, std::move(aUserActionId));
aRequests->AppendElement(contentAnalysisRequest);
}
if (NS_FAILED(aTrans->GetTransferData(kCustomTypesMime,
getter_AddRefs(transferData)))) { return NS_OK; // nothing to check and not an error
}
nsCOMPtr<nsISupportsCString> cStringData = do_QueryInterface(transferData); if (!cStringData) { return NS_OK; // nothing to check and not an error
}
nsCString str;
nsresult rv = cStringData->GetData(str); if (NS_FAILED(rv)) { return NS_OK; // nothing to check and not an error
}
nsTArray<nsString> texts;
dom::DataTransfer::ParseExternalCustomTypesString(
mozilla::Span(str.Data(), str.Length()),
[&](dom::DataTransfer::ParseExternalCustomTypesStringData&& aData) {
texts.AppendElement(std::move(std::move(aData).second));
}); for (auto& text : texts) {
AddCARForText(std::move(text),
nsIContentAnalysisRequest::Reason::eClipboardPaste,
nsIContentAnalysisRequest::OperationType::eClipboard, aURI,
aWindowGlobal, aSourceWindowGlobal, nsCString(aUserActionId),
aRequests);
} return NS_OK;
}
static nsresult AddClipboardCARForText(
mozilla::dom::WindowGlobalParent* aWindowGlobal,
nsITransferable* aTextTrans, constchar* aFlavor, nsIURI* aURI,
mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
nsCString&& aUserActionId,
nsTArray<RefPtr<nsIContentAnalysisRequest>>* aRequests) {
nsCOMPtr<nsISupports> transferData; if (NS_FAILED(
aTextTrans->GetTransferData(aFlavor, getter_AddRefs(transferData)))) { return NS_OK; // nothing to check and not an error
}
nsString text;
nsCOMPtr<nsISupportsString> textData = do_QueryInterface(transferData); if (MOZ_LIKELY(textData)) { if (NS_FAILED(textData->GetData(text))) { return NS_ERROR_FAILURE;
}
} if (text.IsEmpty()) {
nsCOMPtr<nsISupportsCString> cStringData = do_QueryInterface(transferData); if (cStringData) {
nsCString cText; if (NS_FAILED(cStringData->GetData(cText))) { return NS_ERROR_FAILURE;
}
text = NS_ConvertUTF8toUTF16(cText);
}
}
auto& principal = *nsContentUtils::GetSystemPrincipal(); for (constauto& textFormat : kTextFormatsToAnalyze) {
nsAutoString text;
ErrorResult error; // If format is not found then 'text' will be empty.
dataTransfer->GetData(nsString(NS_ConvertUTF8toUTF16(textFormat)), text,
principal, error);
NS_ENSURE_TRUE(!error.Failed(), Err(error.StealNSResult()));
AddCARForText(std::move(text),
nsIContentAnalysisRequest::Reason::eDragAndDrop,
nsIContentAnalysisRequest::OperationType::eDroppedText, aUri,
aWindowGlobal, aSourceWindowGlobal, nsCString(userActionId),
aNewRequests); if (StaticPrefs::
browser_contentanalysis_interception_point_drag_and_drop_plain_text_only()) { // kTextMime is the first entry in kTextFormatsToAnalyze break;
}
}
if (dataTransfer->HasFile()) {
RefPtr fileList = dataTransfer->GetFiles(principal); for (uint32_t i = 0; i < fileList->Length(); ++i) { auto* file = fileList->Item(i); if (!file) { continue;
}
nsString filePath;
ErrorResult error;
file->GetMozFullPathInternal(filePath, error);
NS_ENSURE_TRUE(!error.Failed(), Err(error.StealNSResult()));
mNumCARequestsRemaining = 0;
nsTHashSet<nsCString> requestTokens; if (!aRequests.IsEmpty()) { for (constauto& requests : aRequests) {
mNumCARequestsRemaining += requests.Length();
}
for (constauto& requests : aRequests) { for (constauto& request : requests) { // Pull the user action ID from the first entry we find. They will // all have the same ID. If that ID isn't in the user action map // then we were canceled while we were building the request list. // In that case, we haven't called the callback, so do that here. if (mUserActionId.IsEmpty()) {
MOZ_ALWAYS_SUCCEEDS(request->GetUserActionId(mUserActionId));
MOZ_ASSERT(!mUserActionId.IsEmpty()); if (!mWeakContentAnalysis->mUserActionMap.Contains(mUserActionId)) {
LOGD( "ContentAnalysis::MultipartRequestCallback created after " "request was canceled. Calling callback.");
RefPtr result = MakeRefPtr<ContentAnalysisActionResult>(
nsIContentAnalysisResponse::Action::eCanceled);
mCallback->ContentResult(result);
mResponded = true; return;
}
}
MOZ_ALWAYS_SUCCEEDS(
request->SetUserActionRequestsCount(mNumCARequestsRemaining));
nsCString requestToken;
MOZ_ALWAYS_SUCCEEDS(request->GetRequestToken(requestToken)); if (requestToken.IsEmpty()) {
requestToken = GenerateUUID();
MOZ_ALWAYS_SUCCEEDS(request->SetRequestToken(requestToken));
}
requestTokens.Insert(requestToken);
}
}
}
if (mNumCARequestsRemaining == 0) { // No requests will be submitted so no response will be sent by agent. // Respond now instead.
LOGD( "Content analysis requested but nothing needs to be checked. " "Request is approved.");
RefPtr result = MakeRefPtr<ContentAnalysisActionResult>(
nsIContentAnalysisResponse::Action::eAllow);
aCallback->ContentResult(result); return;
}
LOGD("ContentAnalysis processing %zu given and synthesized requests",
mNumCARequestsRemaining);
auto checkedTimeoutMs =
CheckedInt32(StaticPrefs::browser_contentanalysis_agent_timeout()) * 1000 * mNumCARequestsRemaining; auto timeoutMs = checkedTimeoutMs.isValid()
? checkedTimeoutMs.value()
: std::numeric_limits<int32_t>::max(); // Non-positive timeout values indicate testing, and the test agent does not // care about this value. Use 25ms (unscaled) in that case.
timeoutMs = std::max(timeoutMs, 25);
RefPtr timeoutRunnable = NS_NewCancelableRunnableFunction( "ContentAnalysis timeout",
[userActionId = mUserActionId,
weakContentAnalysis = mWeakContentAnalysis]() mutable { if (!weakContentAnalysis) { return;
} // Entries awaiting a warn-dialog-selection should not be // considered as part of timeout. Ignore timeout if all remaining // requests are awaiting a warn respones. Otherwise cancel all of // them (including any awaiting a warn response) as timed out. bool found = false; if (auto remainingEntry =
weakContentAnalysis->mUserActionMap.Lookup(userActionId)) {
MOZ_ASSERT(!remainingEntry->mIsHandlingTimeout); for (constauto& remainingToken : remainingEntry->mRequestTokens) { if (!weakContentAnalysis->mWarnResponseDataMap.Contains(
remainingToken)) { // This request is not awaiting warn so cancel the entire user // action.
found = true; // We do not allow calling Cancel() on runnables while they are // running, so this makes sure that CA does not do that.
remainingEntry->mIsHandlingTimeout = true; break;
}
}
} if (found) {
weakContentAnalysis->CancelWithError(std::move(userActionId),
NS_ERROR_DOM_TIMEOUT_ERR);
}
});
NS_DelayedDispatchToCurrentThread((RefPtr{timeoutRunnable}).forget(),
timeoutMs);
// Update our entry in the user action map with the request tokens and a // timeout event. auto uaData = UserActionData{this, std::move(requestTokens), timeoutRunnable,
aAutoAcknowledge};
MOZ_ASSERT(mWeakContentAnalysis->mUserActionMap.Lookup(mUserActionId));
mWeakContentAnalysis->mUserActionMap.InsertOrUpdate(mUserActionId,
std::move(uaData));
}
NS_IMETHODIMP
ContentAnalysis::MultipartRequestCallback::ContentResult(
nsIContentAnalysisResult* aResult) {
MOZ_ASSERT(NS_IsMainThread()); if (mWeakContentAnalysis) { // Remove aResult's request token from the remaining requests list. if (auto maybeUserActionData =
mWeakContentAnalysis->mUserActionMap.Lookup(mUserActionId)) {
nsCOMPtr<nsIContentAnalysisResponse> response =
do_QueryInterface(aResult);
MOZ_ASSERT(response);
nsAutoCString token;
MOZ_ALWAYS_SUCCEEDS(response->GetRequestToken(token));
DebugOnly<bool> removed =
maybeUserActionData->mRequestTokens.EnsureRemoved(token); // Either we removed the token or it was previously removed, along with // all others, as part of a cancellation.
MOZ_ASSERT(removed || maybeUserActionData->mRequestTokens.IsEmpty(), "Request token was not found");
}
}
// Either we have called our callback and removed our userActionId or we are // shutting down.
MOZ_ASSERT(!mWeakContentAnalysis || mWeakContentAnalysis->IsShutDown() ||
!mWeakContentAnalysis->mUserActionMap.Contains(mUserActionId));
}
void ContentAnalysis::MultipartRequestCallback::CancelRequests() {
MOZ_ASSERT(mResponded); // If any request fails to be submitted or is rejected then we need to // cancel all of the other outstanding requests. Note that we may be // getting here as part of being cancelled already, in which case we // have nothing to cancel but our caller may still be cancelling requests // from our user action, which is fine. if (mWeakContentAnalysis) {
mWeakContentAnalysis->CancelRequestsByUserAction(mUserActionId);
}
}
void ContentAnalysis::MultipartRequestCallback::RemoveFromUserActionMap() { if (mWeakContentAnalysis) {
mWeakContentAnalysis->RemoveFromUserActionMap(nsCString(mUserActionId));
}
}
void ContentAnalysis::RemoveFromUserActionMap(nsCString&& aUserActionId) { if (auto entry = mUserActionMap.Lookup(aUserActionId)) { // Implementation note: we need mIsHandlingTimeout because this is called // during mTimeoutRunnable and CancelableRunnable is not robust to having // Cancel called at that time. if (entry->mTimeoutRunnable && !entry->mIsHandlingTimeout) { // Timeout may or may not have been called.
entry->mTimeoutRunnable->Cancel();
}
entry.Remove();
}
}
Result<RefPtr<ContentAnalysis::RequestsPromise>, nsresult>
ContentAnalysis::ExpandFolderRequest(nsIContentAnalysisRequest* aRequest,
nsIFile* file) { // We just need to iterate over the directory, so use the junk scope
RefPtr<mozilla::dom::Directory> directory = mozilla::dom::Directory::Create(
xpc::NativeGlobal(xpc::PrivilegedJunkScope()), file);
NS_ENSURE_TRUE(directory, Err(NS_ERROR_FAILURE));
auto gfhPromise = MakeRefPtr<GetFilesHelper::MozPromiseType>(__func__);
helper->AddMozPromise(gfhPromise,
xpc::NativeGlobal(xpc::PrivilegedJunkScope()));
// Use MozPromise chaining (the undocumented feature where returning a // MozPromise from handlers chains to that new promise). The chained // promise is the RequestsPromise that will resolve to requests for each // file in the folder.
RefPtr<RequestsPromise> requestPromise = gfhPromise->Then(
GetMainThreadSerialEventTarget(), "make ca file requests",
[request = RefPtr{aRequest}]( const nsTArray<RefPtr<mozilla::dom::File>>& aFiles) {
ContentAnalysisRequestArray requests(aFiles.Length()); for (constauto& file : aFiles) { auto requestOrError = MakeRequestForFileInFolder(file, request); if (requestOrError.isErr()) { return RequestsPromise::CreateAndReject(requestOrError.unwrapErr(),
__func__);
}
requests.AppendElement(requestOrError.unwrap());
} return RequestsPromise::CreateAndResolve(requests, __func__);
},
[](nsresult rv) { return RequestsPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
});
return requestPromise;
}
// Asynchronously expand/filter requests based on policies that bypass // the agent. This includes replacing folder requests with requests to scan // their contents (files), etc. Returns either promises for all remaining // requests (provided and synthetic) or a ContentAnalysisResult if no // requests need to be run.
Result<RefPtr<ContentAnalysis::RequestsPromise::AllPromiseType>,
RefPtr<nsIContentAnalysisResult>>
ContentAnalysis::GetFinalRequestList( const ContentAnalysisRequestArray& aRequests) {
Maybe<NoContentAnalysisResult> allowResult;
// We keep allowResult just in case all requests end up getting filtered. // It gives us an explanation for that. If any requests survive this // function then allowResult isn't returned. Negative results should // be returned early. They should not set allowResult. auto setAllowResult = [&allowResult](NoContentAnalysisResult aVal) {
DebugOnly checkResult = [aVal]() { return MakeRefPtr<ContentAnalysisNoResult>(aVal)->GetShouldAllowContent();
}; // shouldAllowContent must be true.
MOZ_ASSERT(checkResult.value());
if (!allowResult) {
allowResult = Some(aVal); return;
} if (*allowResult == NoContentAnalysisResult::
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS) { // Allow aVal to override the prior allow result.
allowResult = Some(aVal);
}
};
// Expand the DataTransfer and Transferable requests into requests for // their individual contents. Also filter out the requests that don't // need to be run.
ContentAnalysisRequestArray expandedTransferRequests(aRequests.Length()); for (constauto& request : aRequests) { // Check request's reason to see if prefs always permit this operation.
nsIContentAnalysisRequest::Reason reason;
MOZ_ALWAYS_SUCCEEDS(request->GetReason(&reason)); if (!ShouldCheckReason(reason)) {
LOGD("Allowing request -- operations of this type are always permitted.");
setAllowResult(NoContentAnalysisResult::
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS); continue;
}
// Content analysis is only needed if an outside webpage has access to // the data. So, skip content analysis if there is: // - the window is a chrome docshell // - the window is being rendered in the parent process (for example, // about:support and the like)
RefPtr<mozilla::dom::WindowGlobalParent> windowGlobal;
request->GetWindowGlobalParent(getter_AddRefs(windowGlobal));
nsCOMPtr<nsIURI> uri;
request->GetUrl(getter_AddRefs(uri)); // NOTE: We only consider uri here (when windowGlobal isn't specified) // for current tests to work. gtests specify URI but no window. // We should never "really" hit that condition. if ((!windowGlobal && !uri) ||
(windowGlobal && (windowGlobal->GetBrowsingContext()->IsChrome() ||
windowGlobal->IsInProcess()))) {
LOGD("Allowing request -- window was null or chrome or in-process.");
setAllowResult(NoContentAnalysisResult::
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS); continue;
}
// Maybe skip check if source of operation is same tab. if (mozilla::StaticPrefs::
browser_contentanalysis_bypass_for_same_tab_operations() &&
SourceIsSameTab(request)) { // ALLOW_DUE_TO_SAME_TAB_SOURCE may replace a result of // ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS from an earlier // request.
LOGD( "Allowing request -- same tab operations are always permitted by " "pref.");
setAllowResult(NoContentAnalysisResult::ALLOW_DUE_TO_SAME_TAB_SOURCE); continue;
}
// Check if the context is privileged. if (!uri) { // If no URL is given then use the one for the window.
uri = ContentAnalysis::GetURIForBrowsingContext(
windowGlobal->Canonical()->GetBrowsingContext()); if (!uri) { // if we still have no URL then the request is from a privileged window
LOGD("Allowing request -- priviledged window.");
setAllowResult(NoContentAnalysisResult::
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS); continue;
}
}
// Check URLs of requested info against // browser.contentanalysis.allow_url_regex_list/deny_url_regex_list. // Build the list once since creating regexs is slow. // Requests with URLs that match the allow list are removed from the check. // There is only one URL in all cases except downloads. If all contents // are removed or the page URL is allowed (for downloads) then the // operation is allowed. // Requests with URLs that match the deny list block the entire operation. auto filterResult = FilterByUrlLists(request, uri); if (filterResult == ContentAnalysis::UrlFilterResult::eDeny) {
LOGD("Blocking request due to deny URL filter.");
glean::content_analysis::request_blocked_by_deny_url.Add(); return Err(MakeRefPtr<ContentAnalysisActionResult>(
nsIContentAnalysisResponse::Action::eBlock));
} if (filterResult == ContentAnalysis::UrlFilterResult::eAllow) {
LOGD("Allowing request -- all operations match allow URL filter.");
glean::content_analysis::request_allowed_by_allow_url.Add();
setAllowResult(NoContentAnalysisResult::
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS); continue;
}
Result<bool, nsresult> hadTransferOrError =
AddRequestsFromTransferableIfAny(request, uri, windowGlobal,
sourceWindowGlobal,
&expandedTransferRequests); if (hadTransferOrError.isOk() && !hadTransferOrError.unwrap()) { // Request didn't have a Transferable with contents. Check for a // DataTransfer.
hadTransferOrError = AddRequestsFromDataTransferIfAny(
request, uri, windowGlobal, sourceWindowGlobal,
&expandedTransferRequests); if (hadTransferOrError.isOk() && !hadTransferOrError.unwrap()) { // Request didn't have a Transferable or DataTransfer with contents. // Copy it as-is.
expandedTransferRequests.AppendElement(request);
}
} if (hadTransferOrError.isErr()) {
LOGD( "Denying request -- error expanding nsITransferable or " "DataTransfer."); return RequestsPromise::AllPromiseType::CreateAndReject(
hadTransferOrError.unwrapErr(), __func__);
}
}
// We have expanded all Transferable and DataTransfer requests. We now // look for folder requests to expand.
ContentAnalysisRequestArray nonFolderRequests;
nsTArray<RefPtr<RequestsPromise>> promises; for (auto& request : expandedTransferRequests) { // Always add request to nonFolderRequests unless we process a folder for // it. Note that the scope for this MakeScopeExit is the for loop, not the // function. auto copyRequest =
MakeScopeExit([&]() { nonFolderRequests.AppendElement(request); });
nsAutoString filename;
nsresult rv = request->GetFilePath(filename);
NS_ENSURE_SUCCESS(
rv, RequestsPromise::AllPromiseType::CreateAndReject(rv, __func__)); if (filename.IsEmpty()) { // Not a file so just copy the request to nonFolderRequests. continue;
}
#ifdef DEBUG // Confirm that there is no text content to analyze. See comment on // mFilePath.
nsAutoString textContent;
rv = request->GetTextContent(textContent);
MOZ_ASSERT(NS_SUCCEEDED(rv));
MOZ_ASSERT(textContent.IsEmpty()); #endif
// We have expanded all requests to check folders, Transferables and // DataTransfers. if (!nonFolderRequests.IsEmpty()) {
promises.AppendElement(RequestsPromise::CreateAndResolve(
std::move(nonFolderRequests), "non folder requests"));
}
if (promises.IsEmpty()) { if (allowResult) {
LOGD( "Allowing request -- all requests were permitted early. " "NoContentAnalysisResult = %d",
(int)*allowResult); return Err(MakeRefPtr<ContentAnalysisNoResult>(*allowResult));
}
// This can happen e.g. if the requests were for empty folders, etc.
LOGD("Allowing request -- no requests need to be checked."); return Err(MakeRefPtr<ContentAnalysisNoResult>(
NoContentAnalysisResult::
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS));
}
// If there were any requests then ignore any allowResult because we still // have to do the remaining checks. return RequestsPromise::All(GetMainThreadSerialEventTarget(), promises);
}
// Wrap callback in a ContentAnalysisCallback, which will assert if the // callback is not called exactly once. auto safeCallback = MakeRefPtr<ContentAnalysisCallback>(aCallback);
// If any member of aRequests has a different user action ID than another, // throw an error. If the user action IDs are empty, generate one and set // it for the requests.
nsAutoCString userActionId; bool isSettingId = false; if (!aRequests.IsEmpty()) {
MOZ_ALWAYS_SUCCEEDS(aRequests[0]->GetUserActionId(userActionId)); if (userActionId.IsEmpty()) {
userActionId = GenerateUUID();
isSettingId = true;
}
}
Result<RefPtr<RequestsPromise::AllPromiseType>,
RefPtr<nsIContentAnalysisResult>>
requestListResult = GetFinalRequestList(aRequests); if (requestListResult.isErr()) { auto result = requestListResult.unwrapErr();
LOGD( "ContentAnalysis::AnalyzeContentRequestsCallback received early result " "before creating the final request list | shouldAllow = %s",
result->GetShouldAllowContent() ? "yes" : "no"); // On a negative result, create only one failure dialog. For a positive // result, we don't bother since there is no visual indication needed. if (!result->GetShouldAllowContent()) { if (!aRequests.IsEmpty()) {
ShowBlockedRequestDialog(aRequests[0]);
} else { // No dialog could be shown since we have no window.
LOGD("Got a negative response for an empty request?");
}
}
safeCallback->ContentResult(result);
mUserActionMap.Remove(userActionId); return NS_OK;
}
// We need to pass this object to the lambda below because we need to // guarantee that we can get this "real" object, not a mock, for // MultipartRequestCallback.
WeakPtr<ContentAnalysis> weakThis = this;
RefPtr<RequestsPromise::AllPromiseType> finalRequests =
requestListResult.unwrap();
finalRequests->Then(
GetMainThreadSerialEventTarget(), "issue ca requests",
[aAutoAcknowledge, safeCallback, weakThis,
userActionId](nsTArray<ContentAnalysisRequestArray>&& aRequests) { // We already have weakThis but we also get the nsIContentAnalysis // object from the service, since we do want the mock service (if // any) for the call to AnalyzeContentRequestPrivate. // In non-test runs, they will always be the same object.
nsCOMPtr<nsIContentAnalysis> contentAnalysis =
mozilla::components::nsIContentAnalysis::Service(); if (!contentAnalysis || !weakThis) {
LOGD( "ContentAnalysis::AnalyzeContentRequestsCallback received " "response during shutdown | userActionId = %s",
userActionId.get());
safeCallback->Error(NS_ERROR_NOT_AVAILABLE); return;
}
RefPtr<MultipartRequestCallback> mpcb =
MultipartRequestCallback::Create(weakThis, aRequests, safeCallback,
aAutoAcknowledge); if (mpcb->HasResponded()) { // Already responded because the request has been canceled already // (or some other error) return;
}
// We check this here so that async calls to this method (e.g. via a promise // resolve) don't send requests after being told not to. if (mForbidFutureRequests) {
nsCString requestToken;
nsresult rv = aRequest->GetRequestToken(requestToken);
NS_ENSURE_SUCCESS(rv, rv);
LOGD( "ContentAnalysis received request [%p](%s) " "after forbidding future requests. Request is rejected.",
aRequest, requestToken.get());
aCallback->Error(NS_ERROR_ILLEGAL_DURING_SHUTDOWN); return NS_OK;
}
LOGD( "ContentAnalysis::AnalyzeContentRequestPrivate analyzing request [%p] " "with callback [%p]",
aRequest, aCallback); auto se = MakeScopeExit([&]() {
LOGE("AnalyzeContentRequestPrivate failed");
aCallback->Error(NS_ERROR_FAILURE);
});
// Make sure we send the notification first, so if we later return // an error the JS will handle it correctly.
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService(); if (obsServ) {
obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr);
}
// since we're on the main thread, don't need to synchronize this return RunAnalyzeRequestTask(aRequest, aAutoAcknowledge, aCallback);
}
NS_IMETHODIMP
ContentAnalysis::CancelAllRequestsAssociatedWithUserAction( const nsACString& aUserActionId) {
MOZ_ASSERT(NS_IsMainThread()); // Find the compound action containing aUserActionId, if any.
RefPtr<const UserActionSet> compoundUserAction; for (auto iter = mCompoundUserActions.iter(); !iter.done(); iter.next()) { auto& entry = iter.get(); if (entry->has(nsCString(aUserActionId))) {
compoundUserAction = entry; break;
}
}
if (!compoundUserAction) { // It was not a compound request, just a single one. return CancelRequestsByUserAction(aUserActionId);
}
MOZ_ASSERT(!compoundUserAction->empty());
// NB: We don't filter out completed user actions from the compound list // since we may need to look them up for this function later. So we may // end up canceling requests that are already completed here -- that is a // no-op.
LOGD("Cancelling %u requests associated with user action ID: %s",
compoundUserAction->count(), PromiseFlatCString(aUserActionId).get());
nsresult rv = NS_OK; for (auto iter = compoundUserAction->iter(); !iter.done(); iter.next()) {
nsresult rv2 = CancelRequestsByUserAction(iter.get()); if (NS_FAILED(rv2)) {
rv = rv2;
} // If we find a user action ID for a request that is not yet complete then // canceling it will cancel and remove the entire compound action. In that // case, we are done. if (!mCompoundUserActions.has(compoundUserAction)) { break;
}
}
// Keys() iterates in-place and we will change the map so we need a copy. for (constauto& userActionId :
mozilla::ToTArray<nsTArray<nsCString>>(mUserActionMap.Keys())) {
CancelRequestsByUserAction(userActionId);
}
// Again, Keys() iterates in-place and we change the map so we need a copy. for (constauto& requestToken :
mozilla::ToTArray<nsTArray<nsCString>>(mWarnResponseDataMap.Keys())) {
LOGD( "Responding to warn dialog (from CancelAllRequests) for " "request %s",
requestToken.get());
RespondToWarnDialog(requestToken, false);
} return NS_OK;
}
NS_IMETHODIMP
ContentAnalysis::RespondToWarnDialog(const nsACString& aRequestToken, bool aAllowContent) {
MOZ_ASSERT(NS_IsMainThread());
nsCString token(aRequestToken);
LOGD("Content analysis getting warn response %d for request %s",
aAllowContent ? 1 : 0, token.get()); auto entry = mWarnResponseDataMap.Extract(token); if (!entry) {
LOGD( "Content analysis request not found when trying to send warn " "response for request %s",
token.get()); return NS_OK;
}
entry->mResponse->ResolveWarnAction(aAllowContent); if (entry->mWasTimeout) {
LOGD( "Warn response was for a previous timeout, inserting into " "mUserActionIdToCanceledResponseMap for " "userActionId %s",
entry->mUserActionId.get());
size_t count = 1; auto userActionIdToCanceledResponseMap =
mUserActionIdToCanceledResponseMap.Lock(); if (auto maybeData =
userActionIdToCanceledResponseMap->Lookup(entry->mUserActionId)) {
count += maybeData->mNumExpectedResponses;
}
// Don't acknowledge if we haven't gotten a response from the agent yet
IssueResponse(entry->mResponse, nsCString(entry->mUserActionId),
entry->mAutoAcknowledge && haveGottenResponse,
entry->mWasTimeout); return NS_OK;
}
NS_IMETHODIMP
ContentAnalysis::ShowBlockedRequestDialog(nsIContentAnalysisRequest* aRequest) {
RefPtr<mozilla::dom::WindowGlobalParent> windowGlobal;
MOZ_ALWAYS_SUCCEEDS(
aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal))); if (!windowGlobal) { // Privileged context or gtest. Either way we show no dialog. return NS_OK;
}
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService(); if (!obsServ) { // We must be shutting down, so we can't show a blocked request dialog. return NS_OK;
}
#ifdefined(XP_WIN)
RefPtr<ContentAnalysis::PrintAllowedPromise>
ContentAnalysis::PrintToPDFToDetermineIfPrintAllowed(
dom::CanonicalBrowsingContext* aBrowsingContext,
nsIPrintSettings* aPrintSettings) { if (!mozilla::StaticPrefs::
browser_contentanalysis_interception_point_print_enabled()) { return PrintAllowedPromise::CreateAndResolve(PrintAllowedResult(true),
__func__);
} // Note that the IsChrome() check here excludes a few // common about pages like about:config, about:preferences, // and about:support, but other about: pages may still // go through content analysis. if (aBrowsingContext->IsChrome()) { return PrintAllowedPromise::CreateAndResolve(PrintAllowedResult(true),
__func__);
}
nsCOMPtr<nsIPrintSettings> contentAnalysisPrintSettings; if (NS_WARN_IF(NS_FAILED(aPrintSettings->Clone(
getter_AddRefs(contentAnalysisPrintSettings)))) ||
NS_WARN_IF(!aBrowsingContext->GetCurrentWindowGlobal())) { return PrintAllowedPromise::CreateAndReject(
PrintAllowedError(NS_ERROR_FAILURE), __func__);
}
contentAnalysisPrintSettings->SetOutputDestination(
nsIPrintSettings::OutputDestinationType::kOutputDestinationStream);
contentAnalysisPrintSettings->SetOutputFormat(
nsIPrintSettings::kOutputFormatPDF);
nsCOMPtr<nsIStorageStream> storageStream =
do_CreateInstance("@mozilla.org/storagestream;1"); if (!storageStream) { return PrintAllowedPromise::CreateAndReject(
PrintAllowedError(NS_ERROR_FAILURE), __func__);
} // Use segment size of 512K
nsresult rv = storageStream->Init(0x80000, UINT32_MAX); if (NS_WARN_IF(NS_FAILED(rv))) { return PrintAllowedPromise::CreateAndReject(PrintAllowedError(rv),
__func__);
}
auto* windowParent = browsingContext->GetCurrentWindowGlobal(); if (!windowParent) { // The print window may have been closed by the user by now. // Cancel the print.
promise->Reject(
PrintAllowedError(NS_ERROR_ABORT,
cachedStaticBrowsingContext),
__func__); return;
}
nsCOMPtr<nsIURI> uri = GetURIForBrowsingContext(
windowParent->Canonical()->GetBrowsingContext()); if (!uri) {
promise->Reject(
PrintAllowedError(NS_ERROR_FAILURE,
cachedStaticBrowsingContext),
__func__); return;
} // It's a little unclear what we should pass to the agent if // print.always_print_silent is true, because in that case we // don't show the print preview dialog or the system print // dialog. // // I'm thinking of the print preview dialog case as the "normal" // one, so to me printing without a dialog is closer to the // system print dialog case. bool isFromPrintPreviewDialog =
!Preferences::GetBool("print.prefer_system_dialog") &&
!Preferences::GetBool("print.always_print_silent");
RefPtr<nsIContentAnalysisRequest> contentAnalysisRequest = new contentanalysis::ContentAnalysisRequest(
std::move(printData), std::move(uri),
std::move(printerName),
isFromPrintPreviewDialog
? nsIContentAnalysisRequest::Reason::
ePrintPreviewPrint
: nsIContentAnalysisRequest::Reason::
eSystemDialogPrint,
windowParent); auto callback =
MakeRefPtr<contentanalysis::ContentAnalysisCallback>(
[browsingContext, cachedStaticBrowsingContext, promise,
finalPrintSettings = std::move(finalPrintSettings)](
nsIContentAnalysisResult* aResult)
MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable {
promise->Resolve(
PrintAllowedResult(
aResult->GetShouldAllowContent(),
cachedStaticBrowsingContext),
__func__);
},
[promise,
cachedStaticBrowsingContext](nsresult aError) {
promise->Reject(
PrintAllowedError(aError,
cachedStaticBrowsingContext),
__func__);
});
nsCOMPtr<nsIContentAnalysis> contentAnalysis =
mozilla::components::nsIContentAnalysis::Service(); if (NS_WARN_IF(!contentAnalysis)) {
promise->Reject(
PrintAllowedError(rv, cachedStaticBrowsingContext),
__func__);
} else { bool isActive = false;
nsresult rv = contentAnalysis->GetIsActive(&isActive); // Should not be called if content analysis is not active
MOZ_ASSERT(isActive);
(void)NS_WARN_IF(NS_FAILED(rv));
AutoTArray<RefPtr<nsIContentAnalysisRequest>, 1> requests{
contentAnalysisRequest};
rv = contentAnalysis->AnalyzeContentRequestsCallback(
requests, /* aAutoAcknowledge */ true, callback); if (NS_WARN_IF(NS_FAILED(rv))) {
promise->Reject(
PrintAllowedError(rv, cachedStaticBrowsingContext),
__func__);
}
}
},
[promise](nsresult aError) {
promise->Reject(PrintAllowedError(aError), __func__);
}); return promise;
} #endif
nsCOMPtr<nsIURI> uri =
aWindowGlobal ? ContentAnalysis::GetURIForBrowsingContext(
aWindowGlobal->Canonical()->GetBrowsingContext())
: nullptr;
auto request = MakeRefPtr<ContentAnalysisRequest>(
nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry,
nsIContentAnalysisRequest::Reason::eClipboardPaste, aTransferable,
aWindowGlobal, aSourceWindowGlobal);
// Don't use the cache if the request can store to the cache -- that // is an indication that this is a separate operation from the previous // one. if (!aStoreInCache && aClipboardSequenceNumber.isSome()) { bool isValid = false;
nsIContentAnalysisResponse::Action action =
nsIContentAnalysisResponse::Action::eUnspecified;
contentAnalysis->GetCachedResponse(uri, *aClipboardSequenceNumber, &action,
&isValid); if (isValid) {
LOGD("Content analysis returning cached clipboard response %d", action);
respondOnFailure.release();
RefPtr actionResult = MakeRefPtr<ContentAnalysisActionResult>(action); if (!actionResult->GetShouldAllowContent()) {
contentAnalysis->ShowBlockedRequestDialog(request);
}
aCallback->ContentResult(actionResult); return NS_OK;
}
}
RefPtr wrapperCallback = aCallback; if (aStoreInCache && aClipboardSequenceNumber.isSome()) { // Add the result to the result cache before we call the caller's callback.
wrapperCallback = MakeRefPtr<ContentAnalysisCallback>(
[aClipboardSequenceNumber, uri,
callback = RefPtr(aCallback)](nsIContentAnalysisResult* aResult) { bool allow = aResult->GetShouldAllowContent();
nsCOMPtr<nsIContentAnalysis> contentAnalysis =
mozilla::components::nsIContentAnalysis::Service(); if (contentAnalysis) {
LOGD("Content analysis setting cached clipboard response: %s",
allow ? "allow" : "block");
contentAnalysis->SetCachedResponse(
uri, *aClipboardSequenceNumber,
allow ? nsIContentAnalysisResponse::Action::eAllow
: nsIContentAnalysisResponse::Action::eBlock);
}
// This method must stay in sync with ContentAnalysis::kKnownClipboardTypes. All // of those types must be analyzed here, and if we start analyzing more types // here we should add it to ContentAnalysis::kKnownClipboardTypes. void ContentAnalysis::CheckClipboardContentAnalysis(
nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow,
nsITransferable* aTransferable, nsIClipboard::ClipboardType aClipboardType,
ContentAnalysisCallback* aResolver, bool aForFullClipboard) { // Make sure we call aResolver on error. Use the current value of // noCAResult.
NoContentAnalysisResult noCAResult =
NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR; auto issueNoAnalysisResponse = MakeScopeExit([&]() {
LOGD("CheckClipboardContentAnalysis skipping CA. Response = %d",
(int)noCAResult); auto result = MakeRefPtr<ContentAnalysisNoResult>(noCAResult);
aResolver->ContentResult(result);
});
RefPtr<ContentAnalysis::FilesAllowedPromise>
ContentAnalysis::CheckUploadsInBatchMode(
nsCOMArray<nsIFile>&& aFiles, bool aAutoAcknowledge,
mozilla::dom::WindowGlobalParent* aWindow,
nsIContentAnalysisRequest::Reason aReason, nsIURI* aURI /* = nullptr */) {
nsresult rv; auto contentAnalysis = GetContentAnalysisFromService(); // Ideally the caller would check all of this before going through the work // of building up aFiles, but we'll double-check here. if (NS_WARN_IF(!contentAnalysis)) { return FilesAllowedPromise::CreateAndReject(rv, __func__);
} bool contentAnalysisIsActive = false;
rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive); if (NS_WARN_IF(NS_FAILED(rv))) { return FilesAllowedPromise::CreateAndReject(rv, __func__);
} if (!contentAnalysisIsActive) { return FilesAllowedPromise::CreateAndResolve(std::move(aFiles), __func__);
}
auto numberOfRequestsLeft = std::make_shared<size_t>(aFiles.Length()); auto allowedFiles = MakeRefPtr<media::Refcountable<nsCOMArray<nsIFile>>>(); auto userActionIds =
MakeRefPtr<media::Refcountable<mozilla::HashSet<nsCString>>>(); auto promise = MakeRefPtr<FilesAllowedPromise::Private>(__func__);
nsCOMPtr<nsIURI> uri; if (aWindow) {
uri = aWindow->GetDocumentURI(); // Clients should only pass aURI if they're not passing aWindow.
MOZ_ASSERT(!aURI);
} else { // Should only be used in tests
uri = aURI;
}
if (!contentAnalysis->mCompoundUserActions.put(userActionIds)) { return FilesAllowedPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY,
__func__);
}
auto cancelOnError = MakeScopeExit([&]() { // Cancel one request to cancel the compound request. if (!userActionIds->empty()) {
contentAnalysis->CancelRequestsByUserAction(userActionIds->iter().get());
}
});
// For requests with the same userActionId, we multiply the timeout by the // number of requests to make sure the agent has enough time to handle all // of them. However, in this case we're using separate userActionIds for // each of these files to get the batch mode behavior, so set a timeout // multiplier to get the correct timeout. // // Note that this could theoretically be wrong, because if one of these // files is actually a folder this could expand into many more requests, and // using aFiles.Count() will undercount the total number of requests. But in // practice, from the Windows file dialog users can only select multiple // individual files that are not folders, or one single folder.
request->SetTimeoutMultiplier(static_cast<uint32_t>(aFiles.Count()));
nsTArray<RefPtr<nsIContentAnalysisRequest>> singleRequest{
std::move(request)}; auto callback =
mozilla::MakeRefPtr<mozilla::contentanalysis::ContentAnalysisCallback>( // Note that this gets coerced to a std::function<>, which means it // has to be copyable, so everything captured here must be copyable, // which is why allowedFiles needs to be wrapped in a RefPtr and not // simply std::move()d.
[promise, allowedFiles, numberOfRequestsLeft, file = RefPtr{file},
userActionIds](nsIContentAnalysisResult* aResult) { // Since we're on the main thread, don't need to synchronize // access to allowedFiles or numberOfRequestsLeft
AssertIsOnMainThread();
nsCOMPtr<nsIContentAnalysisResponse> response =
do_QueryInterface(aResult);
LOGD( "Processing callback for batched file request, " "numberOfRequestsLeft=%zu",
*(numberOfRequestsLeft.get()));
RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService(); if (response && response->GetAction() ==
nsIContentAnalysisResponse::eCanceled) { // This was cancelled, so even if some other files have been // allowed we want to return an empty result.
LOGD("Batched file request got cancel response"); // Some of these may have finished already, but that's OK. // Remove the userActionIds array, then cancel its entries, so // that we only cancel them once. if (owner) { if (auto entry =
owner->mCompoundUserActions.lookup(userActionIds)) {
owner->mCompoundUserActions.remove(entry); for (auto iter = userActionIds->iter(); !iter.done();
iter.next()) {
owner->CancelRequestsByUserAction(iter.get());
}
}
}
nsCOMArray<nsIFile> emptyFiles; // Note that Resolve() will do nothing if the promise has // already been resolved.
promise->Resolve(std::move(emptyFiles), __func__); return;
} if (aResult->GetShouldAllowContent()) {
allowedFiles->AppendElement(file);
}
(*numberOfRequestsLeft)--; if (*numberOfRequestsLeft == 0) {
promise->Resolve(std::move(*allowedFiles), __func__); if (owner) {
owner->mCompoundUserActions.remove(userActionIds);
}
}
},
[promise, userActionIds](nsresult aError) { // cancel all requests
AssertIsOnMainThread();
LOGE("Batched file request got error %s",
SafeGetStaticErrorName(aError));
RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService(); // Some of these may have finished already, but that's OK. // Remove the userActionIds array, then cancel its entries, so // that we only cancel these once. if (owner) { if (auto entry =
owner->mCompoundUserActions.lookup(userActionIds)) {
owner->mCompoundUserActions.remove(entry); for (auto iter = userActionIds->iter(); !iter.done();
iter.next()) {
owner->CancelRequestsByUserAction(iter.get());
}
}
}
nsCOMArray<nsIFile> emptyFiles; // Note that Resolve() will do nothing if the promise has already // been resolved.
promise->Resolve(std::move(emptyFiles), __func__);
});
contentAnalysis->AnalyzeContentRequestsCallback(singleRequest,
aAutoAcknowledge, callback);
}
cancelOnError.release(); return promise;
}
NS_IMETHODIMP
ContentAnalysis::AnalyzeBatchContentRequest(nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge,
JSContext* aCx,
mozilla::dom::Promise** aPromise) {
AssertIsOnMainThread(); // Get the ContentAnalysis service again to make this work with // the mock service
nsCOMPtr<nsIContentAnalysis> contentAnalysis =
mozilla::components::nsIContentAnalysis::Service(); if (!contentAnalysis) { return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
} // Ideally the caller would check all of this before going through the work // of building up aFiles, but we'll double-check here. bool contentAnalysisIsActive = false;
nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive); if (NS_WARN_IF(NS_FAILED(rv))) { return rv;
} // Should not be called if content analysis is not active
MOZ_ASSERT(contentAnalysisIsActive); if (!contentAnalysisIsActive) { return NS_ERROR_NOT_AVAILABLE;
}
nsCOMPtr<dom::DataTransfer> dataTransfer;
rv = aRequest->GetDataTransfer(getter_AddRefs(dataTransfer));
NS_ENSURE_SUCCESS(rv, rv); // This method expects dataTransfer to be present
MOZ_ASSERT(dataTransfer); if (!dataTransfer) { return NS_ERROR_FAILURE;
}
nsCOMArray<nsIFile> files; auto& systemPrincipal = *nsContentUtils::GetSystemPrincipal(); if (dataTransfer->HasFile()) { // Get any files in the DataTransfer and pass them to // CheckUploadsInBatchMode() so they will be analyzed individually.
RefPtr fileList = dataTransfer->GetFiles(systemPrincipal);
files.SetCapacity(fileList->Length()); for (uint32_t i = 0; i < fileList->Length(); ++i) {
dom::File* file = fileList->Item(i); if (!file) { continue;
}
nsString filePath;
mozilla::ErrorResult result;
file->GetMozFullPathInternal(filePath, result); if (NS_WARN_IF(result.Failed())) {
rv = result.StealNSResult(); return rv;
} #ifdef XP_WIN const nsString& nativePathString = filePath; #else
nsCString nativePathString(NS_ConvertUTF16toUTF8(std::move(filePath))); #endif
nsCOMPtr<nsIFile> nsFile;
rv = NS_NewPathStringLocalFile(nativePathString, getter_AddRefs(nsFile));
NS_ENSURE_SUCCESS(rv, rv);
files.AppendElement(nsFile);
}
}
RefPtr<mozilla::dom::Promise> filesPromise;
rv = MakePromise(aCx, getter_AddRefs(filesPromise));
NS_ENSURE_SUCCESS(rv, rv);
if (!files.IsEmpty()) {
RefPtr<mozilla::dom::WindowGlobalParent> windowGlobal;
MOZ_ALWAYS_SUCCEEDS(
aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal)));
CheckUploadsInBatchMode(std::move(files), aAutoAcknowledge, windowGlobal,
nsIContentAnalysisRequest::Reason::eDragAndDrop)
->Then(
mozilla::GetMainThreadSerialEventTarget(), __func__,
[filesPromise,
request = RefPtr{aRequest}](nsCOMArray<nsIFile> aAllowedFiles) {
nsTArray<RefPtr<nsIFile>> allowedFiles;
allowedFiles.AppendElements(mozilla::Span(
aAllowedFiles.Elements(), aAllowedFiles.Length()));
filesPromise->MaybeResolve(std::move(allowedFiles));
},
[filesPromise](nsresult aError) {
filesPromise->MaybeReject(aError);
});
} else { // Handle the case where there are files in fileList but // all of them are null.
filesPromise->MaybeResolve(nsTArray<RefPtr<nsIFile>>());
}
RefPtr<dom::DataTransfer> transferWithoutFiles; if (dataTransfer->HasFile()) {
rv = dataTransfer->Clone(
dataTransfer->GetParentObject(), dataTransfer->GetEventMessage(), false/* aUserCancelled */, dataTransfer->IsCrossDomainSubFrameDrop(),
getter_AddRefs(transferWithoutFiles));
NS_ENSURE_SUCCESS(rv, rv);
transferWithoutFiles->SetMode(dom::DataTransfer::Mode::ReadWrite); auto* items = transferWithoutFiles->Items(); if (items->Length() > 0) { auto idx = items->Length(); do {
--idx; bool found; auto* item = items->IndexedGetter(idx, found);
MOZ_ASSERT(found); if (item->Kind() == dom::DataTransferItem::KIND_FILE) {
items->Remove(idx, systemPrincipal, IgnoreErrors());
}
} while (idx);
}
} else { // There were no files to begin with, so avoid cloning dataTransfer.
transferWithoutFiles = dataTransfer;
}
AutoTArray<RefPtr<dom::Promise>, 2> promises{filesPromise}; if (transferWithoutFiles->Items()->Length() > 0) {
RefPtr<ContentAnalysisRequest> requestWithoutFiles =
ContentAnalysisRequest::Clone(aRequest);
MOZ_ALWAYS_SUCCEEDS(
requestWithoutFiles->SetDataTransfer(transferWithoutFiles.get()));
AutoTArray<RefPtr<nsIContentAnalysisRequest>, 1> singleRequestWithoutFiles{
std::move(requestWithoutFiles)};
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService(); // Do an early check here to avoid an extra dispatch to the main // thread if no one is observing the message bool rawMessageHasObserver = false; if (obsServ) {
rawMessageHasObserver =
obsServ->HasObservers("dlp-acknowledgement-sent-raw");
}
// The content analysis connection is synchronous so run in the background.
LOGD("RunAcknowledgeTask dispatching acknowledge task");
CallClientWithRetry<std::nullptr_t>(
__func__,
[pbAck = std::move(pbAck), rawMessageHasObserver](
std::shared_ptr<content_analysis::sdk::Client> client) mutable
-> Result<std::nullptr_t, nsresult> {
MOZ_ASSERT(!NS_IsMainThread());
RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService(); if (!owner) { // May be shutting down return nullptr;
}
int err = client->Acknowledge(pbAck);
LOGD( "RunAcknowledgeTask sent transaction acknowledgement, " "err=%d",
err); // Wait until the acknowledgement is sent before sending // the dlp-acknowledgement-sent-raw notification to make tests // more reliable. if (rawMessageHasObserver) {
NS_DispatchToMainThread(NS_NewRunnableFunction(
__func__, [owner, pbAck = std::move(pbAck)]() {
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService(); if (!obsServ) { // Shutting down. We don't have a connection to the agent // anymore so sending acknowledgement would fail anyway. return;
}
std::string acknowledgementString = pbAck.SerializeAsString();
nsTArray<char16_t> acknowledgementArray;
acknowledgementArray.SetLength(acknowledgementString.size() + 1); for (size_t i = 0; i < acknowledgementString.size(); ++i) { // Since NotifyObservers() expects a null-terminated string, // make sure none of these values are 0.
acknowledgementArray[i] = acknowledgementString[i] + 0xFF00;
}
acknowledgementArray[acknowledgementString.size()] = 0;
obsServ->NotifyObservers( static_cast<nsIContentAnalysis*>(owner.get()), "dlp-acknowledgement-sent-raw",
acknowledgementArray.Elements());
}));
} if (err != 0) { return Err(NS_ERROR_FAILURE);
} return nullptr;
})
->Then(
GetMainThreadSerialEventTarget(), __func__, []() { /* do nothing */ },
[](nsresult rv) {
LOGE("RunAcknowledgeTask failed to get the client");
}); return NS_OK;
}
NS_IMETHODIMP
ContentAnalysis::GetDiagnosticInfo(JSContext* aCx, dom::Promise** aPromise) {
RefPtr<dom::Promise> promise;
nsresult rv = MakePromise(aCx, getter_AddRefs(promise));
nsMainThreadPtrHandle<dom::Promise> promiseHolder( new nsMainThreadPtrHolder<dom::Promise>( "ContentAnalysis::GetDiagnosticInfo promise", promise));
NS_ENSURE_SUCCESS(rv, rv);
AssertIsOnMainThread();
CallClientWithRetry<std::nullptr_t>(
__func__,
[promiseHolder](
std::shared_ptr<content_analysis::sdk::Client> client) mutable
-> Result<std::nullptr_t, nsresult> {
MOZ_ASSERT(!NS_IsMainThread()); // I don't think this will be slow, but do it on the background thread // just to be safe
std::string agentPath = client->GetAgentInfo().binary_path; // Need to switch back to main thread to create the // ContentAnalysisDiagnosticInfo and resolve the promise
NS_DispatchToMainThread(NS_NewRunnableFunction(
__func__, [promiseHolder = std::move(promiseHolder),
agentPath = std::move(agentPath)]() {
RefPtr<ContentAnalysis> self = GetContentAnalysisFromService(); if (!self) { // may be quitting
promiseHolder->MaybeReject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN); return;
}
nsString agentWidePath = NS_ConvertUTF8toUTF16(agentPath); // Note that if we made it here, we have successfully connected to // the agent. auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>( /* mConnectedToAgent */ true, std::move(agentWidePath), false,
self ? self->mRequestCount : 0);
promiseHolder->MaybeResolve(info);
})); return nullptr;
})
->Then(
GetMainThreadSerialEventTarget(), __func__, []() {},
[promiseHolder](nsresult rv) {
RefPtr<ContentAnalysis> self = GetContentAnalysisFromService(); auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>( false, EmptyString(), rv == NS_ERROR_INVALID_SIGNATURE,
self ? self->mRequestCount : 0);
promiseHolder->MaybeResolve(info);
});
promise.forget(aPromise); return NS_OK;
}
/* static */ nsCOMPtr<nsIURI> ContentAnalysis::GetURIForBrowsingContext(
dom::CanonicalBrowsingContext* aBrowsingContext) {
dom::WindowGlobalParent* windowGlobal =
aBrowsingContext->GetCurrentWindowGlobal(); if (!windowGlobal) { return nullptr;
}
dom::CanonicalBrowsingContext* oldBrowsingContext = aBrowsingContext;
nsIPrincipal* principal = windowGlobal->DocumentPrincipal();
dom::CanonicalBrowsingContext* curBrowsingContext =
aBrowsingContext->GetParent(); while (curBrowsingContext) {
dom::WindowGlobalParent* newWindowGlobal =
curBrowsingContext->GetCurrentWindowGlobal(); if (!newWindowGlobal) { break;
}
nsIPrincipal* newPrincipal = newWindowGlobal->DocumentPrincipal(); if (!(newPrincipal->Subsumes(principal))) { break;
}
principal = newPrincipal;
oldBrowsingContext = curBrowsingContext;
curBrowsingContext = curBrowsingContext->GetParent();
} if (nsContentUtils::IsPDFJS(principal)) { // the principal's URI is the URI of the pdf.js reader // so get the document's URI
dom::WindowContext* windowContext =
oldBrowsingContext->GetCurrentWindowContext(); if (!windowContext) { return nullptr;
} return windowContext->Canonical()->GetDocumentURI();
} return principal->GetURI();
}
NS_IMETHODIMP ContentAnalysis::MakeResponseForTest(
nsIContentAnalysisResponse::Action aAction, const nsACString& aToken, const nsACString& aUserActionId,
nsIContentAnalysisResponse** aNewResponse) { auto response =
MakeRefPtr<ContentAnalysisResponse>(aAction, aToken, aUserActionId); // Pretend this is not synthetic so dialogs will show in tests
response->SetIsSyntheticResponse(false);
response.forget(aNewResponse); return NS_OK;
}
NS_IMETHODIMP ContentAnalysisCallback::ContentResult(
nsIContentAnalysisResult* aResult) {
LOGD("[%p] Called ContentAnalysisCallback::ContentResult", this); // Grab a reference to the parameter.
RefPtr result = aResult; if (mPromise) {
mPromise->MaybeResolve(aResult);
} elseif (mContentResponseCallback) {
mContentResponseCallback(aResult);
} else {
MOZ_ASSERT_UNREACHABLE("ContentAnalysisCallback called multiple times");
}
ClearCallbacks(); return NS_OK;
}
NS_IMETHODIMP ContentAnalysisCallback::Error(nsresult aError) {
LOGD("[%p] Called ContentAnalysisCallback::Error", this); if (mPromise) {
mPromise->MaybeReject(aError);
} elseif (mErrorCallback) {
mErrorCallback(aError);
} else {
MOZ_ASSERT_UNREACHABLE("ContentAnalysisCallback called multiple times");
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.