Bug 1811076: Part 5 - Add UI notifications for content analysis results r=nika,Gijs,fluent-reviewers,flod

Connects content analysis checks with the tab that their messages
to the user should appear on.  Adds notifications for the
CA messages.

Differential Revision: https://phabricator.services.mozilla.com/D191784
This commit is contained in:
Greg Stoll 2023-11-15 14:53:12 +00:00
parent 287dacd96f
commit 14de68880a
9 changed files with 790 additions and 32 deletions

View file

@ -24,6 +24,7 @@ ChromeUtils.defineESModuleGetters(this, {
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
Color: "resource://gre/modules/Color.sys.mjs",
ContentAnalysis: "resource:///modules/ContentAnalysis.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
@ -1871,6 +1872,7 @@ var gBrowserInit = {
BrowserOffline.init();
CanvasPermissionPromptHelper.init();
WebAuthnPromptHelper.init();
ContentAnalysis.initialize();
// Initialize the full zoom setting.
// We do this before the session restore service gets initialized so we can

View file

@ -0,0 +1,582 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Contains elements of the Content Analysis UI, which are integrated into
* various browser behaviors (uploading, downloading, printing, etc) that
* require content analysis to be done.
* The content analysis itself is done by the clients of this script, who
* use nsIContentAnalysis to talk to the external CA system.
*/
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"silentNotifications",
"browser.contentanalysis.silent_notifications",
false
);
/**
* A class that groups browsing contexts by their top-level one.
* This is necessary because if there may be a subframe that
* is showing a "DLP request busy" dialog when another subframe
* (other the outer frame) wants to show one. This class makes it
* convenient to find if another frame with the same top browsing
* context is currently showing a dialog, and also to find if there
* are any pending dialogs to show when one closes.
*/
class MapByTopBrowsingContext {
#map;
constructor() {
this.#map = new Map();
}
/**
* Gets any existing data associated with the browsing context
*
* @param {BrowsingContext} aBrowsingContext the browsing context to search for
* @returns {object | undefined} the existing data, or `undefined` if there is none
*/
getEntry(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return undefined;
}
return topEntry.get(aBrowsingContext);
}
/**
* Returns whether the browsing context has any data associated with it
*
* @param {BrowsingContext} aBrowsingContext the browsing context to search for
* @returns {boolean} Whether the browsing context has any associated data
*/
hasEntry(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return false;
}
return topEntry.has(aBrowsingContext);
}
/**
* Whether the tab containing the browsing context has a dialog
* currently showing
*
* @param {BrowsingContext} aBrowsingContext the browsing context to search for
* @returns {boolean} whether the tab has a dialog currently showing
*/
hasEntryDisplayingNotification(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return false;
}
for (const otherEntry in topEntry.values()) {
if (otherEntry.notification?.dialogBrowsingContext) {
return true;
}
}
return false;
}
/**
* Gets another browsing context in the same tab that has pending "DLP busy" dialog
* info to show, if any.
*
* @param {BrowsingContext} aBrowsingContext the browsing context to search for
* @returns {BrowsingContext} Another browsing context in the same tab that has pending "DLP busy" dialog info, or `undefined` if there aren't any.
*/
getBrowsingContextWithPendingNotification(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return undefined;
}
if (aBrowsingContext.top.isDiscarded) {
// The top-level tab has already been closed, so remove
// the top-level entry and return there are no pending dialogs.
this.#map.delete(aBrowsingContext.top);
return undefined;
}
for (const otherContext in topEntry.keys()) {
if (
topEntry.get(otherContext).notification?.dialogBrowsingContextArgs &&
otherContext !== aBrowsingContext
) {
return otherContext;
}
}
return undefined;
}
/**
* Deletes the entry for the browsing context, if any
*
* @param {BrowsingContext} aBrowsingContext the browsing context to delete
* @returns {boolean} Whether an entry was deleted or not
*/
deleteEntry(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return false;
}
const toReturn = topEntry.delete(aBrowsingContext);
if (!topEntry.size || aBrowsingContext.top.isDiscarded) {
// Either the inner Map is now empty, or the whole tab
// has been closed. Either way, remove the top-level entry.
this.#map.delete(aBrowsingContext.top);
}
return toReturn;
}
/**
* Sets the associated data for the browsing context
*
* @param {BrowsingContext} aBrowsingContext the browsing context to set the data for
* @param {object} aValue the data to associated with the browsing context
* @returns {MapByTopBrowsingContext} this
*/
setEntry(aBrowsingContext, aValue) {
let topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
topEntry = new Map();
this.#map.set(aBrowsingContext.top, topEntry);
}
topEntry.set(aBrowsingContext, aValue);
return this;
}
}
export const ContentAnalysis = {
_SHOW_NOTIFICATIONS: true,
_SHOW_DIALOGS: false,
_SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS: 250,
_SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS: 3 * 1000,
_RESULT_NOTIFICATION_TIMEOUT_MS: 5 * 60 * 1000, // 5 min
_RESULT_NOTIFICATION_FAST_TIMEOUT_MS: 60 * 1000, // 1 min
isInitialized: false,
dlpBusyViewsByTopBrowsingContext: new MapByTopBrowsingContext(),
requestTokenToRequestInfo: new Map(),
/**
* Registers for various messages/events that will indicate the
* need for communicating something to the user.
*/
initialize() {
if (!this.isInitialized) {
this.isInitialized = true;
this.initializeDownloadCA();
ChromeUtils.defineLazyGetter(this, "l10n", function () {
return new Localization(
["toolkit/contentanalysis/contentanalysis.ftl"],
true
);
});
}
},
async uninitialize() {
if (this.isInitialized) {
this.isInitialized = false;
this.requestTokenToRequestInfo.clear();
}
},
/**
* Register UI for file download CA events.
*/
async initializeDownloadCA() {
Services.obs.addObserver(this, "dlp-request-made");
Services.obs.addObserver(this, "dlp-response");
Services.obs.addObserver(this, "quit-application");
},
// nsIObserver
async observe(aSubj, aTopic, aData) {
switch (aTopic) {
case "quit-application": {
this.uninitialize();
break;
}
case "dlp-request-made":
{
const request = aSubj;
if (!request) {
console.error(
"Showing in-browser Content Analysis notification but no request was passed"
);
return;
}
const operation = request.analysisType;
// For operations that block browser interaction, show the "slow content analysis"
// dialog faster
let slowTimeoutMs = this._shouldShowBlockingNotification(operation)
? this._SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS
: this._SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS;
let browsingContext = request.windowGlobalParent?.browsingContext;
if (!browsingContext) {
throw new Error(
"Got dlp-request-made message but couldn't find a browsingContext!"
);
}
// Start timer that, when it expires,
// presents a "slow CA check" message.
// Note that there should only be one DLP request
// at a time per browsingContext (since we block the UI and
// the content process waits synchronously for the result).
if (this.dlpBusyViewsByTopBrowsingContext.hasEntry(browsingContext)) {
throw new Error(
"Got dlp-request-made message for a browsingContext that already has a busy view!"
);
}
let resourceNameOrL10NId =
this._getResourceNameOrL10NIdFromRequest(request);
this.requestTokenToRequestInfo.set(request.requestToken, {
browsingContext,
resourceNameOrL10NId,
});
this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, {
timer: lazy.setTimeout(() => {
this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, {
notification: this._showSlowCAMessage(
operation,
request,
resourceNameOrL10NId,
browsingContext
),
});
}, slowTimeoutMs),
});
}
break;
case "dlp-response":
const request = aSubj;
// Cancels timer or slow message UI,
// if present, and possibly presents the CA verdict.
if (!request) {
throw new Error("Got dlp-response message but no request was passed");
}
let windowAndResourceNameOrL10NId = this.requestTokenToRequestInfo.get(
request.requestToken
);
if (!windowAndResourceNameOrL10NId) {
// Perhaps this was cancelled just before the response came in from the
// DLP agent.
console.warn(
`Got dlp-response message with unknown token ${request.requestToken}`
);
return;
}
this.requestTokenToRequestInfo.delete(request.requestToken);
let dlpBusyView = this.dlpBusyViewsByTopBrowsingContext.getEntry(
windowAndResourceNameOrL10NId.browsingContext
);
if (dlpBusyView) {
this._disconnectFromView(dlpBusyView);
this.dlpBusyViewsByTopBrowsingContext.deleteEntry(
windowAndResourceNameOrL10NId.browsingContext
);
}
const responseResult =
request?.action ?? Ci.nsIContentAnalysisResponse.ACTION_UNSPECIFIED;
this._showCAResult(
windowAndResourceNameOrL10NId.resourceNameOrL10NId,
windowAndResourceNameOrL10NId.browsingContext,
request.requestToken,
responseResult
);
this._showAnotherPendingDialog(
windowAndResourceNameOrL10NId.browsingContext
);
break;
}
},
_showAnotherPendingDialog(aBrowsingContext) {
const otherBrowsingContext =
this.dlpBusyViewsByTopBrowsingContext.getBrowsingContextWithPendingNotification(
aBrowsingContext
);
if (otherBrowsingContext) {
const args =
this.dlpBusyViewsByTopBrowsingContext.getEntry(otherBrowsingContext);
this.dlpBusyViewsByTopBrowsingContext.setEntry(otherBrowsingContext, {
notification: this._showSlowCABlockingMessage(
otherBrowsingContext,
args.requestToken,
args.resourceNameOrL10NId
),
});
}
},
_disconnectFromView(caView) {
if (!caView) {
return;
}
if (caView.timer) {
lazy.clearTimeout(caView.timer);
} else if (caView.notification) {
if (caView.notification.close) {
// native notification
caView.notification.close();
} else if (caView.notification.dialogBrowsingContext) {
// in-browser notification
let browser =
caView.notification.dialogBrowsingContext.top.embedderElement;
// browser will be null if the tab was closed
let win = browser?.ownerGlobal;
if (win) {
let dialogBox = win.gBrowser.getTabDialogBox(browser);
// Don't close any content-modal dialogs, because we could be doing
// content analysis on something like a prompt() call.
dialogBox.getTabDialogManager().abortDialogs();
}
} else {
console.error(
"Unexpected content analysis notification - can't close it!"
);
}
}
},
_showMessage(aMessage, aBrowsingContext, aTimeout = 0) {
if (this._SHOW_DIALOGS) {
Services.prompt.asyncAlert(
aBrowsingContext,
Ci.nsIPrompt.MODAL_TYPE_WINDOW,
this.l10n.formatValueSync("contentanalysis-alert-title"),
aMessage
);
}
if (this._SHOW_NOTIFICATIONS) {
const notification = new aBrowsingContext.topChromeWindow.Notification(
this.l10n.formatValueSync("contentanalysis-notification-title"),
{
body: aMessage,
silent: lazy.silentNotifications,
}
);
if (aTimeout != 0) {
lazy.setTimeout(() => {
notification.close();
}, aTimeout);
}
return notification;
}
return null;
},
_shouldShowBlockingNotification(aOperation) {
return false;
},
// This function also transforms the nameOrL10NId so we won't have to
// look it up again.
_getResourceNameFromNameOrL10NId(nameOrL10NId) {
if (nameOrL10NId.name) {
return nameOrL10NId.name;
}
nameOrL10NId.name = this.l10n.formatValueSync(nameOrL10NId.l10nId);
return nameOrL10NId.name;
},
_getResourceNameOrL10NIdFromRequest(aRequest) {
if (
aRequest.operationTypeForDisplay ==
Ci.nsIContentAnalysisRequest.OPERATION_CUSTOMDISPLAYSTRING
) {
return { name: aRequest.operationDisplayString };
}
let l10nId;
switch (aRequest.operationTypeForDisplay) {
case Ci.nsIContentAnalysisRequest.OPERATION_CLIPBOARD:
l10nId = "contentanalysis-operationtype-clipboard";
break;
case Ci.nsIContentAnalysisRequest.OPERATION_DROPPEDTEXT:
l10nId = "contentanalysis-operationtype-dropped-text";
break;
}
if (!l10nId) {
console.error(
"Unknown operationTypeForDisplay: " + aRequest.operationTypeForDisplay
);
return { name: "" };
}
return { l10nId };
},
/**
* Show a message to the user to indicate that a CA request is taking
* a long time.
*/
_showSlowCAMessage(
aOperation,
aRequest,
aResourceNameOrL10NId,
aBrowsingContext
) {
if (!this._shouldShowBlockingNotification(aOperation)) {
return this._showMessage(
this.l10n.formatValueSync("contentanalysis-slow-agent-notification", {
content: this._getResourceNameFromNameOrL10NId(aResourceNameOrL10NId),
}),
aBrowsingContext
);
}
if (!aRequest) {
throw new Error(
"Showing in-browser Content Analysis notification but no request was passed"
);
}
if (
this.dlpBusyViewsByTopBrowsingContext.hasEntryDisplayingNotification(
aBrowsingContext
)
) {
// This tab already has a frame displaying a "DLP in progress" message, so we can't
// show another one right now. Record the arguments we will need to show another
// "DLP in progress" message when the existing message goes away.
return {
requestToken: aRequest.requestToken,
dialogBrowsingContextArgs: {
resourceNameOrL10NId: aResourceNameOrL10NId,
},
};
}
return this._showSlowCABlockingMessage(
aBrowsingContext,
aRequest.requestToken,
aResourceNameOrL10NId
);
},
_showSlowCABlockingMessage(
aBrowsingContext,
aRequestToken,
aResourceNameOrL10NId
) {
let promise = Services.prompt.asyncConfirmEx(
aBrowsingContext,
Ci.nsIPromptService.MODAL_TYPE_TAB,
this.l10n.formatValueSync("contentanalysis-slow-agent-dialog-title"),
this.l10n.formatValueSync("contentanalysis-slow-agent-dialog-body", {
content: this._getResourceNameFromNameOrL10NId(aResourceNameOrL10NId),
}),
Ci.nsIPromptService.BUTTON_POS_0 *
Ci.nsIPromptService.BUTTON_TITLE_CANCEL +
Ci.nsIPromptService.SHOW_SPINNER,
null,
null,
null,
null,
false
);
promise
.catch(() => {
// need a catch clause to avoid an unhandled JS exception
// when we programmatically close the dialog.
// Since this only happens when we are programmatically closing
// the dialog, no need to log the exception.
})
.finally(() => {
// This is also be called if the tab/window is closed while a request is in progress,
// in which case we need to cancel the request.
if (this.requestTokenToRequestInfo.delete(aRequestToken)) {
let dlpBusyView =
this.dlpBusyViewsByTopBrowsingContext.getEntry(aBrowsingContext);
if (dlpBusyView) {
this._disconnectFromView(dlpBusyView);
this.dlpBusyViewsByTopBrowsingContext.deleteEntry(aBrowsingContext);
}
}
});
return {
requestToken: aRequestToken,
dialogBrowsingContext: aBrowsingContext,
};
},
/**
* Show a message to the user to indicate the result of a CA request.
*
* @returns {object} a notification object (if shown)
*/
_showCAResult(
aResourceNameOrL10NId,
aBrowsingContext,
aRequestToken,
aCAResult
) {
let message = null;
let timeoutMs = 0;
switch (aCAResult) {
case Ci.nsIContentAnalysisResponse.ALLOW:
// We don't need to show anything
return null;
case Ci.nsIContentAnalysisResponse.REPORT_ONLY:
message = this.l10n.formatValueSync(
"contentanalysis-genericresponse-message",
{
content: this._getResourceNameFromNameOrL10NId(
aResourceNameOrL10NId
),
response: "REPORT_ONLY",
}
);
timeoutMs = this._RESULT_NOTIFICATION_FAST_TIMEOUT_MS;
break;
case Ci.nsIContentAnalysisResponse.WARN:
message = this.l10n.formatValueSync(
"contentanalysis-genericresponse-message",
{
content: this._getResourceNameFromNameOrL10NId(
aResourceNameOrL10NId
),
response: "WARN",
}
);
timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
break;
case Ci.nsIContentAnalysisResponse.BLOCK:
message = this.l10n.formatValueSync("contentanalysis-block-message", {
content: this._getResourceNameFromNameOrL10NId(aResourceNameOrL10NId),
});
timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
break;
case Ci.nsIContentAnalysisResponse.ACTION_UNSPECIFIED:
message = this.l10n.formatValueSync("contentanalysis-error-message", {
content: this._getResourceNameFromNameOrL10NId(aResourceNameOrL10NId),
});
timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
break;
default:
throw new Error("Unexpected CA result value: " + aCAResult);
}
return this._showMessage(message, aBrowsingContext, timeoutMs);
},
};

View file

@ -0,0 +1,12 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
with Files("**"):
BUG_COMPONENT = ("Toolkit", "General")
EXTRA_JS_MODULES += [
"content/ContentAnalysis.sys.mjs",
]

View file

@ -30,6 +30,7 @@ DIRS += [
"about",
"aboutlogins",
"attribution",
"contentanalysis",
"contextualidentity",
"customizableui",
"doh",

View file

@ -1145,6 +1145,12 @@
value: "path_user"
mirror: never
# Should CA ignore the system setting and use silent notifications?
- name: browser.contentanalysis.silent_notifications
type: bool
value: false
mirror: always
# Content blocking for Enhanced Tracking Protection
- name: browser.contentblocking.database.enabled
type: bool

View file

@ -12,9 +12,15 @@
#include "mozilla/dom/Promise.h"
#include "mozilla/Logging.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_browser.h"
#include "nsAppRunner.h"
#include "nsComponentManagerUtils.h"
#include "nsIClassInfoImpl.h"
#include "nsIFile.h"
#include "nsIGlobalObject.h"
#include "nsIObserverService.h"
#include "ScopedNSSTypes.h"
#include "xpcpublic.h"
#include <algorithm>
@ -47,12 +53,19 @@ nsresult MakePromise(JSContext* aCx, RefPtr<mozilla::dom::Promise>* aPromise) {
return NS_OK;
}
std::string GenerateRequestToken() {
static std::atomic<uint32_t> count = 0;
uint32_t tokenValue = count.fetch_add(1, std::memory_order_relaxed);
std::stringstream stm;
stm << std::hex << base::GetCurrentProcId() << "-" << tokenValue;
return stm.str();
nsCString GenerateRequestToken() {
nsID id = nsID::GenerateUUID();
return nsCString(id.ToString().get());
}
static nsresult GetFileDisplayName(const nsString& aFilePath,
nsString& aFileDisplayName) {
nsresult rv;
nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = file->InitWithPath(aFilePath);
NS_ENSURE_SUCCESS(rv, rv);
return file->GetDisplayName(aFileDisplayName);
}
} // anonymous namespace
@ -105,6 +118,32 @@ ContentAnalysisRequest::GetResources(
return NS_OK;
}
NS_IMETHODIMP
ContentAnalysisRequest::GetRequestToken(nsACString& aRequestToken) {
aRequestToken = mRequestToken;
return NS_OK;
}
NS_IMETHODIMP
ContentAnalysisRequest::GetOperationTypeForDisplay(uint32_t* aOperationType) {
*aOperationType = mOperationTypeForDisplay;
return NS_OK;
}
NS_IMETHODIMP
ContentAnalysisRequest::GetOperationDisplayString(
nsAString& aOperationDisplayString) {
aOperationDisplayString = mOperationDisplayString;
return NS_OK;
}
NS_IMETHODIMP
ContentAnalysisRequest::GetWindowGlobalParent(
dom::WindowGlobalParent** aWindowGlobalParent) {
NS_IF_ADDREF(*aWindowGlobalParent = mWindowGlobalParent);
return NS_OK;
}
/* static */
StaticDataMutex<UniquePtr<content_analysis::sdk::Client>>
ContentAnalysis::sCaClient("ContentAnalysisClient");
@ -126,19 +165,29 @@ nsresult ContentAnalysis::EnsureContentAnalysisClient() {
return caClient ? NS_OK : NS_ERROR_NOT_AVAILABLE;
}
ContentAnalysisRequest::ContentAnalysisRequest(unsigned long aAnalysisType,
nsString&& aString,
bool aStringIsFilePath,
nsCString&& aSha256Digest,
nsString&& aUrl)
ContentAnalysisRequest::ContentAnalysisRequest(
unsigned long aAnalysisType, nsString&& aString, bool aStringIsFilePath,
nsCString&& aSha256Digest, nsString&& aUrl, unsigned long aResourceNameType,
dom::WindowGlobalParent* aWindowGlobalParent)
: mAnalysisType(aAnalysisType),
mUrl(std::move(aUrl)),
mSha256Digest(std::move(aSha256Digest)) {
mSha256Digest(std::move(aSha256Digest)),
mWindowGlobalParent(aWindowGlobalParent) {
if (aStringIsFilePath) {
mFilePath = std::move(aString);
} else {
mTextContent = std::move(aString);
}
mOperationTypeForDisplay = aResourceNameType;
if (mOperationTypeForDisplay == OPERATION_CUSTOMDISPLAYSTRING) {
MOZ_ASSERT(aStringIsFilePath);
nsresult rv = GetFileDisplayName(mFilePath, mOperationDisplayString);
if (NS_FAILED(rv)) {
mOperationDisplayString = u"file";
}
}
mRequestToken = GenerateRequestToken();
}
static nsresult ConvertToProtobuf(
@ -171,8 +220,10 @@ static nsresult ConvertToProtobuf(
static_cast<content_analysis::sdk::AnalysisConnector>(analysisType);
aOut->set_analysis_connector(connector);
std::string requestToken = GenerateRequestToken();
aOut->set_request_token(requestToken);
nsCString requestToken;
rv = aIn->GetRequestToken(requestToken);
NS_ENSURE_SUCCESS(rv, rv);
aOut->set_request_token(requestToken.get(), requestToken.Length());
const std::string tag = "dlp"; // TODO:
*aOut->add_tags() = tag;
@ -338,9 +389,14 @@ ContentAnalysisResponse::ContentAnalysisResponse(
mAction = nsIContentAnalysisResponse::ALLOW;
}
mRequestToken = aResponse.request_token();
const auto& requestToken = aResponse.request_token();
mRequestToken.Assign(requestToken.data(), requestToken.size());
}
ContentAnalysisResponse::ContentAnalysisResponse(
unsigned long aAction, const nsACString& aRequestToken)
: mAction(aAction), mRequestToken(aRequestToken) {}
/* static */
already_AddRefed<ContentAnalysisResponse> ContentAnalysisResponse::FromProtobuf(
content_analysis::sdk::ContentAnalysisResponse&& aResponse) {
@ -357,6 +413,22 @@ already_AddRefed<ContentAnalysisResponse> ContentAnalysisResponse::FromProtobuf(
return ret.forget();
}
/* static */
RefPtr<ContentAnalysisResponse> ContentAnalysisResponse::FromAction(
unsigned long aAction, const nsACString& aRequestToken) {
if (aAction == nsIContentAnalysisResponse::ACTION_UNSPECIFIED) {
return nullptr;
}
return RefPtr<ContentAnalysisResponse>(
new ContentAnalysisResponse(aAction, aRequestToken));
}
NS_IMETHODIMP
ContentAnalysisResponse::GetRequestToken(nsACString& aRequestToken) {
aRequestToken = mRequestToken;
return NS_OK;
}
static void LogResponse(
content_analysis::sdk::ContentAnalysisResponse* aPbResponse) {
if (!static_cast<LogModule*>(gContentAnalysisLog)
@ -398,9 +470,9 @@ static void LogResponse(
}
static nsresult ConvertToProtobuf(
nsIContentAnalysisAcknowledgement* aIn, const std::string& aRequestToken,
nsIContentAnalysisAcknowledgement* aIn, const nsACString& aRequestToken,
content_analysis::sdk::ContentAnalysisAcknowledgement* aOut) {
aOut->set_request_token(aRequestToken);
aOut->set_request_token(aRequestToken.Data(), aRequestToken.Length());
uint32_t result;
nsresult rv = aIn->GetResult(&result);
@ -484,8 +556,10 @@ NS_IMETHODIMP ContentAnalysisResult::GetShouldAllowContent(
return NS_OK;
}
NS_IMPL_ISUPPORTS(ContentAnalysisRequest, nsIContentAnalysisRequest);
NS_IMPL_ISUPPORTS(ContentAnalysisResponse, nsIContentAnalysisResponse);
NS_IMPL_CLASSINFO(ContentAnalysisRequest, nullptr, 0, {0});
NS_IMPL_ISUPPORTS_CI(ContentAnalysisRequest, nsIContentAnalysisRequest);
NS_IMPL_CLASSINFO(ContentAnalysisResponse, nullptr, 0, {0});
NS_IMPL_ISUPPORTS_CI(ContentAnalysisResponse, nsIContentAnalysisResponse);
NS_IMPL_ISUPPORTS(ContentAnalysisCallback, nsIContentAnalysisCallback);
NS_IMPL_ISUPPORTS(ContentAnalysisResult, nsIContentAnalysisResult);
NS_IMPL_ISUPPORTS(ContentAnalysis, nsIContentAnalysis);
@ -545,12 +619,16 @@ nsresult ContentAnalysis::RunAnalyzeRequestTask(
LOGD("Issuing ContentAnalysisRequest");
LogRequest(&pbRequest);
nsCString requestToken;
rv = aRequest->GetRequestToken(requestToken);
NS_ENSURE_SUCCESS(rv, rv);
// The content analysis connection is synchronous so run in the background.
rv = NS_DispatchBackgroundTask(
NS_NewRunnableFunction(
"RunAnalyzeRequestTask",
[pbRequest = std::move(pbRequest), aCallback = std::move(aCallback),
owner] {
requestToken = std::move(requestToken), owner] {
nsresult rv = NS_ERROR_FAILURE;
content_analysis::sdk::ContentAnalysisResponse pbResponse;
@ -558,14 +636,21 @@ nsresult ContentAnalysis::RunAnalyzeRequestTask(
NS_DispatchToMainThread(NS_NewRunnableFunction(
"ResolveOnMainThread",
[rv, owner, aCallback = std::move(aCallback),
pbResponse = std::move(pbResponse)]() mutable {
pbResponse = std::move(pbResponse), requestToken]() mutable {
if (NS_SUCCEEDED(rv)) {
LOGD("Content analysis resolving response promise");
LOGD(
"Content analysis resolving response promise for "
"token %s",
requestToken.get());
RefPtr<ContentAnalysisResponse> response =
ContentAnalysisResponse::FromProtobuf(
std::move(pbResponse));
if (response) {
response->SetOwner(owner);
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService();
obsServ->NotifyObservers(response, "dlp-response",
nullptr);
aCallback->ContentResult(response);
} else {
aCallback->Error(NS_ERROR_FAILURE);
@ -629,8 +714,11 @@ ContentAnalysis::AnalyzeContentRequestCallback(
return NS_ERROR_NOT_AVAILABLE;
}
rv = RunAnalyzeRequestTask(aRequest, aCallback);
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService();
obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr);
rv = RunAnalyzeRequestTask(aRequest, aCallback);
return rv;
}
@ -643,7 +731,7 @@ ContentAnalysisResponse::Acknowledge(
nsresult ContentAnalysis::RunAcknowledgeTask(
nsIContentAnalysisAcknowledgement* aAcknowledgement,
const std::string& aRequestToken) {
const nsACString& aRequestToken) {
bool isActive;
nsresult rv = GetIsActive(&isActive);
NS_ENSURE_SUCCESS(rv, rv);

View file

@ -7,6 +7,8 @@
#define mozilla_contentanalysis_h
#include "mozilla/DataMutex.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/Mutex.h"
#include "nsIContentAnalysis.h"
#include "nsProxyRelease.h"
#include "nsString.h"
@ -22,12 +24,13 @@ namespace mozilla::contentanalysis {
class ContentAnalysisRequest final : public nsIContentAnalysisRequest {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_ISUPPORTS
NS_DECL_NSICONTENTANALYSISREQUEST
ContentAnalysisRequest(unsigned long aAnalysisType, nsString&& aString,
bool aStringIsFilePath, nsCString&& aSha256Digest,
nsString&& aUrl);
nsString&& aUrl, unsigned long aResourceNameType,
dom::WindowGlobalParent* aWindowGlobalParent);
private:
~ContentAnalysisRequest() = default;
@ -56,6 +59,18 @@ class ContentAnalysisRequest final : public nsIContentAnalysisRequest {
// Email address of user.
nsString mEmail;
// Unique identifier for this request
nsCString mRequestToken;
// Type of text to display, see nsIContentAnalysisRequest for values
unsigned long mOperationTypeForDisplay;
// String to display if mOperationTypeForDisplay is
// OPERATION_CUSTOMDISPLAYSTRING
nsString mOperationDisplayString;
RefPtr<dom::WindowGlobalParent> mWindowGlobalParent;
};
class ContentAnalysisResponse;
@ -76,7 +91,7 @@ class ContentAnalysis final : public nsIContentAnalysis {
RefPtr<nsIContentAnalysisCallback> aCallback);
nsresult RunAcknowledgeTask(
nsIContentAnalysisAcknowledgement* aAcknowledgement,
const std::string& aRequestToken);
const nsACString& aRequestToken);
static StaticDataMutex<UniquePtr<content_analysis::sdk::Client>> sCaClient;
friend class ContentAnalysisResponse;
@ -84,9 +99,12 @@ class ContentAnalysis final : public nsIContentAnalysis {
class ContentAnalysisResponse final : public nsIContentAnalysisResponse {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_ISUPPORTS
NS_DECL_NSICONTENTANALYSISRESPONSE
static RefPtr<ContentAnalysisResponse> FromAction(
unsigned long aAction, const nsACString& aRequestToken);
void SetOwner(RefPtr<ContentAnalysis> aOwner);
private:
@ -96,13 +114,16 @@ class ContentAnalysisResponse final : public nsIContentAnalysisResponse {
ContentAnalysisResponse& operator=(ContentAnalysisResponse&) = delete;
explicit ContentAnalysisResponse(
content_analysis::sdk::ContentAnalysisResponse&& aResponse);
ContentAnalysisResponse(unsigned long aAction,
const nsACString& aRequestToken);
static already_AddRefed<ContentAnalysisResponse> FromProtobuf(
content_analysis::sdk::ContentAnalysisResponse&& aResponse);
// See nsIContentAnalysisResponse for values
uint32_t mAction;
std::string mRequestToken;
// Identifier for the corresponding nsIContentAnalysisRequest
nsCString mRequestToken;
// ContentAnalysis (or, more precisely, it's Client object) must outlive
// the transaction.

View file

@ -5,6 +5,8 @@
#include "nsISupports.idl"
webidl WindowGlobalParent;
[scriptable, uuid(06e6a60f-3a2b-41fa-a63b-fea7a7f71649)]
interface nsIContentAnalysisAcknowledgement : nsISupports
{
@ -48,12 +50,15 @@ interface nsIContentAnalysisResponse : nsISupports
[infallible] readonly attribute unsigned long action;
[infallible] readonly attribute boolean shouldAllowContent;
// Identifier for the corresponding nsIContentAnalysisRequest
readonly attribute ACString requestToken;
/**
* Acknowledge receipt of an analysis response.
* Should always be called after successful resolution of the promise
* from AnalyzeContentRequest.
*/
void Acknowledge(in nsIContentAnalysisAcknowledgement aCaa);
void acknowledge(in nsIContentAnalysisAcknowledgement aCaa);
};
[scriptable, uuid(48d31df1-204d-42ce-a57f-f156bb870d89)]
@ -104,6 +109,13 @@ interface nsIContentAnalysisRequest : nsISupports
readonly attribute unsigned long analysisType;
// Enumeration of what operation is happening, to be displayed to the user
const unsigned long OPERATION_CUSTOMDISPLAYSTRING = 0;
const unsigned long OPERATION_CLIPBOARD = 1;
const unsigned long OPERATION_DROPPEDTEXT = 2;
readonly attribute unsigned long operationTypeForDisplay;
readonly attribute AString operationDisplayString;
// Text content to analyze. Only one of textContent or filePath is defined.
readonly attribute AString textContent;
@ -122,6 +134,12 @@ interface nsIContentAnalysisRequest : nsISupports
// Email address of user.
readonly attribute AString email;
// Unique identifier for this request
readonly attribute ACString requestToken;
// The window associated with this request
readonly attribute WindowGlobalParent windowGlobalParent;
};
[scriptable, builtinclass, uuid(9679545f-4256-4c90-9654-90292c355d25)]
@ -169,11 +187,11 @@ interface nsIContentAnalysis : nsISupports
* See @nsIContentAnalysisResponse.
*/
[implicit_jscontext]
Promise AnalyzeContentRequest(in nsIContentAnalysisRequest aCar);
Promise analyzeContentRequest(in nsIContentAnalysisRequest aCar);
/**
* Same functionality as AnalyzeContentRequest(), but more convenient to call
* from C++ since it takes a callback instead of returning a Promise.
*/
void AnalyzeContentRequestCallback(in nsIContentAnalysisRequest aCar, in nsIContentAnalysisCallback callback);
void analyzeContentRequestCallback(in nsIContentAnalysisRequest aCar, in nsIContentAnalysisCallback callback);
};

View file

@ -0,0 +1,28 @@
# 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/.
contentanalysis-alert-title = Content Analysis
# Variables:
# $content - Description of the content being warned about, such as "clipboard" or "aFile.txt"
contentanalysis-slow-agent-notification = The Content Analysis tool is taking a long time to respond for resource “{ $content }”
contentanalysis-slow-agent-dialog-title = Content analysis in progress
# Variables:
# $content - Description of the content being warned about, such as "clipboard" or "aFile.txt"
contentanalysis-slow-agent-dialog-body = Content Analysis is analyzing resource “{ $content }”
contentanalysis-operationtype-clipboard = clipboard
contentanalysis-operationtype-dropped-text = dropped text
contentanalysis-notification-title = Content Analysis
# Variables:
# $content - Description of the content being reported, such as "clipboard" or "aFile.txt"
# $response - The response received from the content analysis agent, such as "REPORT_ONLY"
contentanalysis-genericresponse-message = Content Analysis responded with { $response } for resource: { $content }
# Variables:
# $content - Description of the content being blocked, such as "clipboard" or "aFile.txt"
contentanalysis-block-message = Your organization uses data-loss prevention software that has blocked this content: { $content }.
# Variables:
# $content - Description of the content being blocked, such as "clipboard" or "aFile.txt"
contentanalysis-error-message = An error occurred in communicating with the data-loss prevention software. Transfer denied for resource: { $content }.