Port 1570631 - Implement feature promotion doorhangers using CFR (#5308)
This commit is contained in:
Родитель
7e4d3e082f
Коммит
806786f713
|
@ -161,6 +161,7 @@ for (const type of [
|
|||
"SHOW_FIREFOX_ACCOUNTS",
|
||||
"PIN_CURRENT_TAB",
|
||||
"ENABLE_FIREFOX_MONITOR",
|
||||
"OPEN_PROTECTION_PANEL",
|
||||
]) {
|
||||
ASRouterActions[type] = type;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
}
|
||||
},
|
||||
"properties": {
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"description": "The layout style of the pop-over."
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Attribute used for different groups of messages from the same provider"
|
||||
|
@ -24,10 +28,18 @@
|
|||
"description": "Attribute used for different groups of messages from the same provider",
|
||||
"enum": ["message_and_animation", "icon_and_message", "addon_recommendation"]
|
||||
},
|
||||
"anchor_id": {
|
||||
"type": "string",
|
||||
"description": "A DOM element ID that the pop-over will be anchored."
|
||||
},
|
||||
"bucket_id": {
|
||||
"type": "string",
|
||||
"description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting."
|
||||
},
|
||||
"skip_address_bar_notifier": {
|
||||
"type": "boolean",
|
||||
"description": "Skip the 'Recommend' notifier and show directly."
|
||||
},
|
||||
"notification_text": {
|
||||
"description": "The text in the small blue chicklet that appears in the URL bar. This can be a reference to a localized string in Firefox or just a plain string.",
|
||||
"oneOf": [
|
||||
|
@ -88,6 +100,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"learn_more": {
|
||||
"type": "string",
|
||||
"description": "Last part of the path in the SUMO URL to the support page with the information about the doorhanger.",
|
||||
"examples": ["extensionpromotions", "extensionrecommendations"]
|
||||
},
|
||||
"heading_text": {
|
||||
"description": "The larger heading text displayed in the pop-over. This can be a reference to a localized string in Firefox or just a plain string.",
|
||||
"oneOf": [
|
||||
|
@ -108,12 +125,20 @@
|
|||
]
|
||||
},
|
||||
"icon": {
|
||||
"description": "The icon displayed in the pop-over. Should be 64x64px and png/svg.",
|
||||
"description": "The icon displayed in the pop-over. Should be 32x32px or 64x64px and png/svg.",
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/linkUrl"},
|
||||
{"description": "Icon associated with the message"}
|
||||
]
|
||||
},
|
||||
"icon_dark_theme": {
|
||||
"type": "string",
|
||||
"description": "Pop-over icon, dark theme variant. Should be 32x32px or 64x64px and png/svg."
|
||||
},
|
||||
"icon_class": {
|
||||
"type": "string",
|
||||
"description": "CSS class of the pop-over icon."
|
||||
},
|
||||
"addon": {
|
||||
"description": "Addon information including AMO URL.",
|
||||
"type": "object",
|
||||
|
@ -336,5 +361,5 @@
|
|||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["category", "bucket_id", "notification_text", "heading_text", "text", "buttons"]
|
||||
"required": ["layout", "category", "bucket_id", "notification_text", "heading_text", "text", "buttons"]
|
||||
}
|
||||
|
|
|
@ -1844,6 +1844,10 @@ class _ASRouter {
|
|||
csp: null,
|
||||
});
|
||||
break;
|
||||
case ra.OPEN_PROTECTION_PANEL:
|
||||
let { gProtectionsHandler } = target.browser.ownerGlobal;
|
||||
gProtectionsHandler.showProtectionsPopup({});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -488,8 +488,13 @@ this.ASRouterTargeting = {
|
|||
|
||||
return (
|
||||
(candidateMessageTrigger.params &&
|
||||
trigger.param.host &&
|
||||
candidateMessageTrigger.params.includes(trigger.param.host)) ||
|
||||
(candidateMessageTrigger.params &&
|
||||
trigger.param.type &&
|
||||
candidateMessageTrigger.params.includes(trigger.param.type)) ||
|
||||
(candidateMessageTrigger.patterns &&
|
||||
trigger.param.url &&
|
||||
new MatchPatternSet(candidateMessageTrigger.patterns).matches(
|
||||
trigger.param.url
|
||||
))
|
||||
|
|
|
@ -429,6 +429,107 @@ this.ASRouterTriggerListeners = new Map([
|
|||
},
|
||||
},
|
||||
],
|
||||
|
||||
/**
|
||||
* Attach listener to count location changes and notify the trigger handler
|
||||
* on content blocked event
|
||||
*/
|
||||
[
|
||||
"trackingProtection",
|
||||
{
|
||||
_initialized: false,
|
||||
_triggerHandler: null,
|
||||
_events: [],
|
||||
_sessionPageLoad: 0,
|
||||
onLocationChange: null,
|
||||
|
||||
async init(triggerHandler, params, patterns) {
|
||||
params.forEach(p => this._events.push(p));
|
||||
|
||||
if (!this._initialized) {
|
||||
Services.obs.addObserver(this, "SiteProtection:ContentBlockingEvent");
|
||||
|
||||
this.onLocationChange = this._onLocationChange.bind(this);
|
||||
|
||||
// Add listeners to all existing browser windows
|
||||
for (let win of Services.wm.getEnumerator("navigator:browser")) {
|
||||
if (isPrivateWindow(win)) {
|
||||
continue;
|
||||
}
|
||||
await checkStartupFinished(win);
|
||||
win.gBrowser.addTabsProgressListener(this);
|
||||
}
|
||||
|
||||
this._initialized = true;
|
||||
}
|
||||
this._triggerHandler = triggerHandler;
|
||||
},
|
||||
|
||||
uninit() {
|
||||
if (this._initialized) {
|
||||
Services.obs.removeObserver(
|
||||
this,
|
||||
"SiteProtection:ContentBlockingEvent"
|
||||
);
|
||||
|
||||
for (let win of Services.wm.getEnumerator("navigator:browser")) {
|
||||
if (isPrivateWindow(win)) {
|
||||
continue;
|
||||
}
|
||||
win.gBrowser.removeTabsProgressListener(this);
|
||||
}
|
||||
|
||||
this.onLocationChange = null;
|
||||
this._initialized = false;
|
||||
}
|
||||
this._triggerHandler = null;
|
||||
this._events = [];
|
||||
this._sessionPageLoad = 0;
|
||||
},
|
||||
|
||||
observe(aSubject, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
case "SiteProtection:ContentBlockingEvent":
|
||||
const { browser, host, event } = aSubject.wrappedJSObject;
|
||||
if (this._events.includes(event)) {
|
||||
this._triggerHandler(browser, {
|
||||
id: "trackingProtection",
|
||||
param: {
|
||||
host,
|
||||
type: event,
|
||||
},
|
||||
context: {
|
||||
pageLoad: this._sessionPageLoad,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_onLocationChange(
|
||||
aBrowser,
|
||||
aWebProgress,
|
||||
aRequest,
|
||||
aLocationURI,
|
||||
aFlags
|
||||
) {
|
||||
// Some websites trigger redirect events after they finish loading even
|
||||
// though the location remains the same. This results in onLocationChange
|
||||
// events to be fired twice.
|
||||
const isSameDocument = !!(
|
||||
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
|
||||
);
|
||||
if (
|
||||
["http", "https"].includes(aLocationURI.scheme) &&
|
||||
aWebProgress.isTopLevel &&
|
||||
!isSameDocument
|
||||
) {
|
||||
this._sessionPageLoad += 1;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const EXPORTED_SYMBOLS = ["ASRouterTriggerListeners"];
|
||||
|
|
|
@ -546,6 +546,7 @@ const CFR_MESSAGES = [
|
|||
string_id: "cfr-doorhanger-sync-logins-body",
|
||||
},
|
||||
icon: "chrome://browser/content/aboutlogins/icons/intro-illustration.svg",
|
||||
icon_class: "cfr-doorhanger-large-icon",
|
||||
buttons: {
|
||||
secondary: [
|
||||
{
|
||||
|
@ -604,6 +605,144 @@ const CFR_MESSAGES = [
|
|||
id: "newSavedLogin",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "SOCIAL_TRACKING_PROTECTION",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
layout: "icon_and_message",
|
||||
category: "cfrFeatures",
|
||||
anchor_id: "tracking-protection-icon-box",
|
||||
skip_address_bar_notifier: true,
|
||||
bucket_id: "CFR_SOCIAL_TRACKING_PROTECTION",
|
||||
heading_text: { string_id: "cfr-doorhanger-socialtracking-heading" },
|
||||
notification_text: "",
|
||||
info_icon: {
|
||||
label: {
|
||||
string_id: "cfr-doorhanger-extension-sumo-link",
|
||||
},
|
||||
sumo_path: "extensionrecommendations",
|
||||
},
|
||||
learn_more: "social-media-tracking-report",
|
||||
text: { string_id: "cfr-doorhanger-socialtracking-description" },
|
||||
icon: "chrome://browser/skin/notification-icons/block-social.svg",
|
||||
icon_dark_theme:
|
||||
"chrome://browser/skin/notification-icons/block-social-dark.svg",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: { string_id: "cfr-doorhanger-socialtracking-ok-button" },
|
||||
action: { type: "OPEN_PROTECTION_PANEL" },
|
||||
event: "PROTECTION",
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
label: { string_id: "cfr-doorhanger-socialtracking-close-button" },
|
||||
event: "BLOCK",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
targeting: "pageLoad >= 4",
|
||||
frequency: {
|
||||
lifetime: 2,
|
||||
custom: [{ period: 2 * 86400 * 1000, cap: 1 }],
|
||||
},
|
||||
trigger: {
|
||||
id: "trackingProtection",
|
||||
params: [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "FINGERPRINTERS_PROTECTION",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
layout: "icon_and_message",
|
||||
category: "cfrFeatures",
|
||||
anchor_id: "tracking-protection-icon-box",
|
||||
skip_address_bar_notifier: true,
|
||||
bucket_id: "CFR_SOCIAL_TRACKING_PROTECTION",
|
||||
heading_text: { string_id: "cfr-doorhanger-fingerprinters-heading" },
|
||||
notification_text: "",
|
||||
info_icon: {
|
||||
label: {
|
||||
string_id: "cfr-doorhanger-extension-sumo-link",
|
||||
},
|
||||
sumo_path: "extensionrecommendations",
|
||||
},
|
||||
learn_more: "fingerprinters-report",
|
||||
text: { string_id: "cfr-doorhanger-fingerprinters-description" },
|
||||
icon: "chrome://browser/skin/notification-icons/block-fingerprinter.svg",
|
||||
icon_dark_theme:
|
||||
"chrome://browser/skin/notification-icons/block-fingerprinter-dark.svg",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: { string_id: "cfr-doorhanger-socialtracking-ok-button" },
|
||||
action: { type: "OPEN_PROTECTION_PANEL" },
|
||||
event: "PROTECTION",
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
label: { string_id: "cfr-doorhanger-socialtracking-close-button" },
|
||||
event: "BLOCK",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
targeting: "pageLoad >= 4",
|
||||
frequency: {
|
||||
lifetime: 2,
|
||||
custom: [{ period: 2 * 86400 * 1000, cap: 1 }],
|
||||
},
|
||||
trigger: {
|
||||
id: "trackingProtection",
|
||||
params: [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "CRYPTOMINERS_PROTECTION",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
layout: "icon_and_message",
|
||||
category: "cfrFeatures",
|
||||
anchor_id: "tracking-protection-icon-box",
|
||||
skip_address_bar_notifier: true,
|
||||
bucket_id: "CFR_SOCIAL_TRACKING_PROTECTION",
|
||||
heading_text: { string_id: "cfr-doorhanger-cryptominers-heading" },
|
||||
notification_text: "",
|
||||
info_icon: {
|
||||
label: {
|
||||
string_id: "cfr-doorhanger-extension-sumo-link",
|
||||
},
|
||||
sumo_path: "extensionrecommendations",
|
||||
},
|
||||
learn_more: "cryptominers-report",
|
||||
text: { string_id: "cfr-doorhanger-cryptominers-description" },
|
||||
icon: "chrome://browser/skin/notification-icons/block-cryptominer.svg",
|
||||
icon_dark_theme:
|
||||
"chrome://browser/skin/notification-icons/block-cryptominer-dark.svg",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: { string_id: "cfr-doorhanger-socialtracking-ok-button" },
|
||||
action: { type: "OPEN_PROTECTION_PANEL" },
|
||||
event: "PROTECTION",
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
label: { string_id: "cfr-doorhanger-socialtracking-close-button" },
|
||||
event: "BLOCK",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
targeting: "pageLoad >= 4",
|
||||
frequency: {
|
||||
lifetime: 2,
|
||||
custom: [{ period: 2 * 86400 * 1000, cap: 1 }],
|
||||
},
|
||||
trigger: {
|
||||
id: "trackingProtection",
|
||||
params: [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const CFRMessageProvider = {
|
||||
|
|
|
@ -77,6 +77,31 @@ class PageAction {
|
|||
|
||||
// Saved timeout IDs for scheduled state changes, so they can be cancelled
|
||||
this.stateTransitionTimeoutIDs = [];
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "isDarkTheme", () => {
|
||||
try {
|
||||
return this.window.document.documentElement.hasAttribute(
|
||||
"lwt-toolbar-field-brighttext"
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addImpression(recommendation) {
|
||||
this._dispatchImpression(recommendation);
|
||||
// Only send an impression ping upon the first expansion.
|
||||
// Note that when the user clicks on the "show" button on the asrouter admin
|
||||
// page (both `bucket_id` and `id` will be set as null), we don't want to send
|
||||
// the impression ping in that case.
|
||||
if (!!recommendation.id && !!recommendation.content.bucket_id) {
|
||||
this._sendTelemetry({
|
||||
message_id: recommendation.id,
|
||||
bucket_id: recommendation.content.bucket_id,
|
||||
event: "IMPRESSION",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async showAddressBarNotifier(recommendation, shouldExpand = false) {
|
||||
|
@ -114,18 +139,7 @@ class PageAction {
|
|||
// After one second, expand
|
||||
this._expand(DELAY_BEFORE_EXPAND_MS);
|
||||
|
||||
this._dispatchImpression(recommendation);
|
||||
// Only send an impression ping upon the first expansion.
|
||||
// Note that when the user clicks on the "show" button on the asrouter admin
|
||||
// page (both `bucket_id` and `id` will be set as null), we don't want to send
|
||||
// the impression ping in that case.
|
||||
if (!!recommendation.id && !!recommendation.content.bucket_id) {
|
||||
this._sendTelemetry({
|
||||
message_id: recommendation.id,
|
||||
bucket_id: recommendation.content.bucket_id,
|
||||
event: "IMPRESSION",
|
||||
});
|
||||
}
|
||||
this.addImpression(recommendation);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -473,6 +487,9 @@ class PageAction {
|
|||
this.window.document
|
||||
.getElementById("contextual-feature-recommendation-notification")
|
||||
.setAttribute("data-notification-category", content.layout);
|
||||
this.window.document
|
||||
.getElementById("contextual-feature-recommendation-notification")
|
||||
.setAttribute("data-notification-bucket", content.bucket_id);
|
||||
|
||||
switch (content.layout) {
|
||||
case "icon_and_message":
|
||||
|
@ -491,10 +508,23 @@ class PageAction {
|
|||
});
|
||||
RecommendationMap.delete(browser);
|
||||
};
|
||||
|
||||
let getIcon = () => {
|
||||
if (content.icon_dark_theme && this.isDarkTheme) {
|
||||
return content.icon_dark_theme;
|
||||
}
|
||||
return content.icon;
|
||||
};
|
||||
|
||||
let learnMoreURL = content.learn_more
|
||||
? SUMO_BASE_URL + content.learn_more
|
||||
: null;
|
||||
|
||||
panelTitle = await this.getStrings(content.heading_text);
|
||||
options = {
|
||||
popupIconURL: content.icon,
|
||||
popupIconClass: "cfr-doorhanger-large-icon",
|
||||
popupIconURL: getIcon(),
|
||||
popupIconClass: content.icon_class,
|
||||
learnMoreURL,
|
||||
};
|
||||
break;
|
||||
case "message_and_animation":
|
||||
|
@ -579,52 +609,41 @@ class PageAction {
|
|||
callback: primaryActionCallback,
|
||||
};
|
||||
|
||||
// For each secondary action, get the strings and attributes
|
||||
const secondaryBtnStrings = [];
|
||||
for (let button of secondary) {
|
||||
let _renderSecondaryButtonAction = async (event, button) => {
|
||||
let label = await this.getStrings(button.label);
|
||||
secondaryBtnStrings.push({ label, attributes: label.attributes });
|
||||
}
|
||||
const secondaryActions = [
|
||||
{
|
||||
label: secondaryBtnStrings[0].label,
|
||||
accessKey: secondaryBtnStrings[0].attributes.accesskey,
|
||||
let { attributes } = label;
|
||||
|
||||
return {
|
||||
label,
|
||||
accessKey: attributes.accesskey,
|
||||
callback: () => {
|
||||
this.dispatchUserAction(secondary[0].action);
|
||||
if (button.action) {
|
||||
this.dispatchUserAction(button.action);
|
||||
} else {
|
||||
this._blockMessage(id);
|
||||
this.hideAddressBarNotifier();
|
||||
RecommendationMap.delete(browser);
|
||||
}
|
||||
|
||||
this._sendTelemetry({
|
||||
message_id: id,
|
||||
bucket_id: content.bucket_id,
|
||||
event: "DISMISS",
|
||||
event,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: secondaryBtnStrings[1].label,
|
||||
accessKey: secondaryBtnStrings[1].attributes.accesskey,
|
||||
callback: () => {
|
||||
this._blockMessage(id);
|
||||
this.hideAddressBarNotifier();
|
||||
this._sendTelemetry({
|
||||
message_id: id,
|
||||
bucket_id: content.bucket_id,
|
||||
event: "BLOCK",
|
||||
});
|
||||
RecommendationMap.delete(browser);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: secondaryBtnStrings[2].label,
|
||||
accessKey: secondaryBtnStrings[2].attributes.accesskey,
|
||||
callback: () => {
|
||||
this.dispatchUserAction(secondary[2].action);
|
||||
this._sendTelemetry({
|
||||
message_id: id,
|
||||
bucket_id: content.bucket_id,
|
||||
event: "MANAGE",
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
// For each secondary action, define default telemetry event
|
||||
const defaultSecondaryEvent = ["DISMISS", "BLOCK", "MANAGE"];
|
||||
const secondaryActions = await Promise.all(
|
||||
secondary.map((button, i) => {
|
||||
return _renderSecondaryButtonAction(
|
||||
button.event || defaultSecondaryEvent[i],
|
||||
button
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// Actually show the notification
|
||||
this.currentNotification = this.window.PopupNotifications.show(
|
||||
|
@ -655,15 +674,23 @@ class PageAction {
|
|||
return;
|
||||
}
|
||||
const message = RecommendationMap.get(browser);
|
||||
const { id, content } = message;
|
||||
|
||||
// The recommendation should remain either collapsed or expanded while the
|
||||
// doorhanger is showing
|
||||
this._clearScheduledStateChanges(browser, message);
|
||||
|
||||
await this.showPopup();
|
||||
}
|
||||
|
||||
async showPopup() {
|
||||
const browser = this.window.gBrowser.selectedBrowser;
|
||||
const message = RecommendationMap.get(browser);
|
||||
const { id, content } = message;
|
||||
|
||||
// A hacky way of setting the popup anchor outside the usual url bar icon box
|
||||
// See https://searchfox.org/mozilla-central/rev/847b64cc28b74b44c379f9bff4f415b97da1c6d7/toolkit/modules/PopupNotifications.jsm#42
|
||||
browser.cfrpopupnotificationanchor = this.container;
|
||||
browser.cfrpopupnotificationanchor =
|
||||
this.window.document.getElementById(content.anchor_id) || this.container;
|
||||
|
||||
this._sendTelemetry({
|
||||
message_id: id,
|
||||
|
@ -699,10 +726,11 @@ const CFRPageActions = {
|
|||
if (RecommendationMap.has(browser)) {
|
||||
const recommendation = RecommendationMap.get(browser);
|
||||
if (
|
||||
isHostMatch(browser, recommendation.host) ||
|
||||
// If there is no host associated we assume we're back on a tab
|
||||
// that had a CFR message so we should show it again
|
||||
!recommendation.host
|
||||
!recommendation.content.skip_address_bar_notifier &&
|
||||
(isHostMatch(browser, recommendation.host) ||
|
||||
// If there is no host associated we assume we're back on a tab
|
||||
// that had a CFR message so we should show it again
|
||||
!recommendation.host)
|
||||
) {
|
||||
// The browser has a recommendation specified with this host, so show
|
||||
// the page action
|
||||
|
@ -762,7 +790,13 @@ const CFRPageActions = {
|
|||
if (!PageActionMap.has(win)) {
|
||||
PageActionMap.set(win, new PageAction(win, dispatchToASRouter));
|
||||
}
|
||||
await PageActionMap.get(win).showAddressBarNotifier(recommendation, true);
|
||||
|
||||
if (content.skip_address_bar_notifier) {
|
||||
await PageActionMap.get(win).showPopup();
|
||||
PageActionMap.get(win).addImpression(recommendation);
|
||||
} else {
|
||||
await PageActionMap.get(win).showAddressBarNotifier(recommendation, true);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
|
@ -795,7 +829,13 @@ const CFRPageActions = {
|
|||
if (!PageActionMap.has(win)) {
|
||||
PageActionMap.set(win, new PageAction(win, dispatchToASRouter));
|
||||
}
|
||||
await PageActionMap.get(win).showAddressBarNotifier(recommendation, true);
|
||||
|
||||
if (content.skip_address_bar_notifier) {
|
||||
await PageActionMap.get(win).showPopup();
|
||||
PageActionMap.get(win).addImpression(recommendation);
|
||||
} else {
|
||||
await PageActionMap.get(win).showAddressBarNotifier(recommendation, true);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
|
@ -156,3 +156,16 @@ cfr-doorhanger-firefox-send-header = Share this PDF securely
|
|||
cfr-doorhanger-firefox-send-body = Keep your sensitive documents safe from prying eyes with end-to-end encryption and a link that disappears when you’re done.
|
||||
cfr-doorhanger-firefox-send-ok-button = Try { -send-brand-name }
|
||||
.accesskey = T
|
||||
|
||||
## Social Tracking Protection
|
||||
|
||||
cfr-doorhanger-socialtracking-ok-button = See Protections
|
||||
.accesskey = P
|
||||
cfr-doorhanger-socialtracking-close-button = Close
|
||||
.accesskey = C
|
||||
cfr-doorhanger-socialtracking-heading = { -brand-short-name } stopped a social network from tracking you here
|
||||
cfr-doorhanger-socialtracking-description = Your privacy matters. { -brand-short-name } now blocks common social media trackers, limiting how much data they can collect about what you do online.
|
||||
cfr-doorhanger-fingerprinters-heading = { -brand-short-name } blocked a fingerprinter on this page
|
||||
cfr-doorhanger-fingerprinters-description = Your privacy matters. { -brand-short-name } now blocks fingerprinters, which collect pieces of uniquely identifiable information about your device to track you.
|
||||
cfr-doorhanger-cryptominers-heading = { -brand-short-name } blocked a cryptominer on this page
|
||||
cfr-doorhanger-cryptominers-description = Your privacy matters. { -brand-short-name } now blocks cryptominers, which use your system’s computing power to mine digital money.
|
||||
|
|
|
@ -13,16 +13,22 @@ const createDummyRecommendation = ({
|
|||
category,
|
||||
heading_text,
|
||||
layout,
|
||||
skip_address_bar_notifier,
|
||||
}) => ({
|
||||
content: {
|
||||
layout: layout || "addon_recommendation",
|
||||
category,
|
||||
anchor_id: "page-action-buttons",
|
||||
skip_address_bar_notifier,
|
||||
notification_text: "Mochitest",
|
||||
heading_text: heading_text || "Mochitest",
|
||||
info_icon: {
|
||||
label: { attributes: { tooltiptext: "Why am I seeing this" } },
|
||||
sumo_path: "extensionrecommendations",
|
||||
},
|
||||
icon: "foo",
|
||||
icon_dark_theme: "bar",
|
||||
learn_more: "extensionrecommendations",
|
||||
addon: {
|
||||
id: "addon-id",
|
||||
title: "Addon name",
|
||||
|
@ -107,6 +113,19 @@ function checkCFRAddonsElements(notification) {
|
|||
);
|
||||
}
|
||||
|
||||
function checkCFRSocialTrackingProtection(notification) {
|
||||
Assert.ok(notification.hidden === false, "Panel should be visible");
|
||||
Assert.ok(
|
||||
notification.getAttribute("data-notification-category") ===
|
||||
"icon_and_message",
|
||||
"Panel have corret data attribute"
|
||||
);
|
||||
Assert.ok(
|
||||
notification.querySelector("#cfr-notification-footer-learn-more-link"),
|
||||
"Panel should have learn more link"
|
||||
);
|
||||
}
|
||||
|
||||
function clearNotifications() {
|
||||
for (let notification of PopupNotifications._currentNotifications) {
|
||||
notification.remove();
|
||||
|
@ -128,6 +147,8 @@ function trigger_cfr_panel(
|
|||
heading_text,
|
||||
category = "cfrAddons",
|
||||
layout,
|
||||
skip_address_bar_notifier = false,
|
||||
use_single_secondary_button = false,
|
||||
} = {}
|
||||
) {
|
||||
// a fake action type will result in the action being ignored
|
||||
|
@ -136,10 +157,16 @@ function trigger_cfr_panel(
|
|||
category,
|
||||
heading_text,
|
||||
layout,
|
||||
skip_address_bar_notifier,
|
||||
});
|
||||
if (category !== "cfrAddons") {
|
||||
delete recommendation.content.addon;
|
||||
}
|
||||
if (use_single_secondary_button) {
|
||||
recommendation.content.buttons.secondary = [
|
||||
recommendation.content.buttons.secondary[0],
|
||||
];
|
||||
}
|
||||
|
||||
clearNotifications();
|
||||
|
||||
|
@ -400,6 +427,49 @@ add_task(async function test_cfr_pin_tab_notification_show() {
|
|||
);
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_cfr_social_tracking_protection_notification_show() {
|
||||
// addRecommendation checks that scheme starts with http and host matches
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
await BrowserTestUtils.loadURI(browser, "http://example.com/");
|
||||
await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
|
||||
|
||||
const showPanel = BrowserTestUtils.waitForEvent(
|
||||
PopupNotifications.panel,
|
||||
"popupshown"
|
||||
);
|
||||
|
||||
const response = await trigger_cfr_panel(browser, "example.com", {
|
||||
action: { type: "OPEN_PROTECTION_PANEL" },
|
||||
category: "cfrFeatures",
|
||||
layout: "icon_and_message",
|
||||
skip_address_bar_notifier: true,
|
||||
use_single_secondary_button: true,
|
||||
});
|
||||
Assert.ok(
|
||||
response,
|
||||
"Should return true if addRecommendation checks were successful"
|
||||
);
|
||||
await showPanel;
|
||||
|
||||
const notification = document.getElementById(
|
||||
"contextual-feature-recommendation-notification"
|
||||
);
|
||||
checkCFRSocialTrackingProtection(notification);
|
||||
|
||||
// Check there is a primary button and click it. It will trigger the callback.
|
||||
Assert.ok(notification.button);
|
||||
let hidePanel = BrowserTestUtils.waitForEvent(
|
||||
PopupNotifications.panel,
|
||||
"popuphidden"
|
||||
);
|
||||
document
|
||||
.getElementById("contextual-feature-recommendation-notification")
|
||||
.button.click();
|
||||
await hidePanel;
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_cfr_features_and_addon_show() {
|
||||
// addRecommendation checks that scheme starts with http and host matches
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
|
|
|
@ -141,3 +141,97 @@ add_task(async function check_newSavedLogin_listener() {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function check_trackingProtection_listener() {
|
||||
const TEST_URL =
|
||||
"https://example.com/browser/browser/components/newtab/test/browser/red_page.html";
|
||||
|
||||
const contentBlockingEvent = 1234;
|
||||
let observerEvent = 0;
|
||||
let pageLoadSum = 0;
|
||||
const triggerHandler = (target, trigger) => {
|
||||
const {
|
||||
id,
|
||||
param: { host },
|
||||
context: { pageLoad },
|
||||
} = trigger;
|
||||
is(id, "trackingProtection", "should match event name");
|
||||
is(host, TEST_URL, "should match test URL");
|
||||
|
||||
observerEvent += 1;
|
||||
pageLoadSum += pageLoad;
|
||||
};
|
||||
const trackingProtectionListener = ASRouterTriggerListeners.get(
|
||||
"trackingProtection"
|
||||
);
|
||||
|
||||
// Previously initialized by the Router
|
||||
trackingProtectionListener.uninit();
|
||||
|
||||
// Initialise listener
|
||||
await trackingProtectionListener.init(triggerHandler, [contentBlockingEvent]);
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
TEST_URL,
|
||||
async function triggerTrackingProtection(browser) {
|
||||
Services.obs.notifyObservers(
|
||||
{
|
||||
wrappedJSObject: {
|
||||
browser,
|
||||
host: TEST_URL,
|
||||
event: contentBlockingEvent + 1,
|
||||
},
|
||||
},
|
||||
"SiteProtection:ContentBlockingEvent"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
is(observerEvent, 0, "shouldn't receive unrelated observer notification");
|
||||
is(pageLoadSum, 0, "shouldn't receive unrelated observer notification");
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
TEST_URL,
|
||||
async function triggerTrackingProtection(browser) {
|
||||
Services.obs.notifyObservers(
|
||||
{
|
||||
wrappedJSObject: {
|
||||
browser,
|
||||
host: TEST_URL,
|
||||
event: contentBlockingEvent,
|
||||
},
|
||||
},
|
||||
"SiteProtection:ContentBlockingEvent"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => observerEvent !== 0,
|
||||
"Wait for the observer notification to run"
|
||||
);
|
||||
is(observerEvent, 1, "should receive observer notification");
|
||||
is(pageLoadSum, 2, "should receive observer notification");
|
||||
}
|
||||
);
|
||||
|
||||
// Uninitialise listener
|
||||
trackingProtectionListener.uninit();
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
TEST_URL,
|
||||
async function triggerTrackingProtectionAfterUninit(browser) {
|
||||
Services.obs.notifyObservers(
|
||||
{
|
||||
wrappedJSObject: {
|
||||
browser,
|
||||
host: TEST_URL,
|
||||
event: contentBlockingEvent,
|
||||
},
|
||||
},
|
||||
"SiteProtection:ContentBlockingEvent"
|
||||
);
|
||||
await new Promise(resolve => executeSoon(resolve));
|
||||
is(observerEvent, 1, "shouldn't receive obs. notification after uninit");
|
||||
is(pageLoadSum, 2, "shouldn't receive obs. notification after uninit");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1971,6 +1971,18 @@ describe("ASRouter", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#onMessage: OPEN_PROTECTION_PANEL", () => {
|
||||
it("should open protection panel", async () => {
|
||||
const msg = fakeExecuteUserAction({ type: "OPEN_PROTECTION_PANEL" });
|
||||
let { gProtectionsHandler } = msg.target.browser.ownerGlobal;
|
||||
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.calledOnce(gProtectionsHandler.showProtectionsPopup);
|
||||
assert.calledWithExactly(gProtectionsHandler.showProtectionsPopup, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#dispatch(action, target)", () => {
|
||||
it("should an action and target to onMessage", async () => {
|
||||
// use the IMPRESSION action to make sure actions are actually getting processed
|
||||
|
|
|
@ -11,8 +11,8 @@ const REGULAR_IDS = [
|
|||
];
|
||||
|
||||
describe("CFRMessageProvider", () => {
|
||||
it("should have a total of 5 messages", () => {
|
||||
assert.lengthOf(messages, 5);
|
||||
it("should have a total of 8 messages", () => {
|
||||
assert.lengthOf(messages, 8);
|
||||
});
|
||||
it("should have one message each for the three regular addons", () => {
|
||||
for (const id of REGULAR_IDS) {
|
||||
|
|
|
@ -120,7 +120,6 @@ export const FAKE_RECOMMENDATION = {
|
|||
},
|
||||
{
|
||||
label: { string_id: "secondary_button_id_2" },
|
||||
action: { id: "secondary_action" },
|
||||
},
|
||||
{
|
||||
label: { string_id: "secondary_button_id_3" },
|
||||
|
@ -150,6 +149,9 @@ export class FakeRemotePageManager {
|
|||
ConfirmationHint: {
|
||||
show: sinon.stub(),
|
||||
},
|
||||
gProtectionsHandler: {
|
||||
showProtectionsPopup: sinon.stub(),
|
||||
},
|
||||
},
|
||||
};
|
||||
this.portID = "6000:2";
|
||||
|
|
|
@ -2,6 +2,7 @@ import { CFRMessageProvider } from "lib/CFRMessageProvider.jsm";
|
|||
import schema from "content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json";
|
||||
|
||||
const DEFAULT_CONTENT = {
|
||||
layout: "addon_recommendation",
|
||||
category: "dummyCategory",
|
||||
bucket_id: "some_bucket_id",
|
||||
notification_text: "Recommendation",
|
||||
|
@ -40,6 +41,7 @@ const DEFAULT_CONTENT = {
|
|||
};
|
||||
|
||||
const L10N_CONTENT = {
|
||||
layout: "addon_recommendation",
|
||||
category: "dummyL10NCategory",
|
||||
bucket_id: "some_bucket_id",
|
||||
notification_text: { string_id: "notification_text_id" },
|
||||
|
|
Загрузка…
Ссылка в новой задаче