mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-02 09:18:36 +02:00
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:
parent
287dacd96f
commit
14de68880a
9 changed files with 790 additions and 32 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
12
browser/components/contentanalysis/moz.build
Normal file
12
browser/components/contentanalysis/moz.build
Normal 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",
|
||||
]
|
||||
|
|
@ -30,6 +30,7 @@ DIRS += [
|
|||
"about",
|
||||
"aboutlogins",
|
||||
"attribution",
|
||||
"contentanalysis",
|
||||
"contextualidentity",
|
||||
"customizableui",
|
||||
"doh",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }.
|
||||
Loading…
Reference in a new issue