Bug 1586841 - Port ShieldFrameChild.jsm to JSWindowActors. r=Gijs

These are the actors that handle messaging on the about:studies page.

Differential Revision: https://phabricator.services.mozilla.com/D48911

--HG--
extra : moz-landing-system : lando
This commit is contained in:
James Jahns 2019-10-23 21:00:05 +00:00
Родитель a457606700
Коммит 8719f5bc3e
6 изменённых файлов: 312 добавлений и 232 удалений

Просмотреть файл

@ -147,6 +147,21 @@ let ACTORS = {
allFrames: true,
},
ShieldFrame: {
parent: {
moduleURI: "resource://normandy-content/ShieldFrameParent.jsm",
},
child: {
moduleURI: "resource://normandy-content/ShieldFrameChild.jsm",
events: {
pageshow: {},
pagehide: {},
ShieldPageEvent: { wantUntrusted: true },
},
},
matches: ["about:studies"],
},
SwitchDocumentDirection: {
child: {
moduleURI: "resource:///actors/SwitchDocumentDirectionChild.jsm",
@ -322,16 +337,6 @@ let LEGACY_ACTORS = {
},
},
ShieldFrame: {
child: {
module: "resource://normandy-content/ShieldFrameChild.jsm",
events: {
ShieldPageEvent: { wantUntrusted: true },
},
matches: ["about:studies"],
},
},
UITour: {
child: {
module: "resource:///modules/UITourChild.jsm",

Просмотреть файл

@ -18,11 +18,6 @@ ChromeUtils.defineModuleGetter(
"AddonStudyAction",
"resource://normandy/actions/AddonStudyAction.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"CleanupManager",
"resource://normandy/lib/CleanupManager.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"PreferenceExperiments",
@ -85,30 +80,18 @@ AboutPage.prototype.QueryInterface = ChromeUtils.generateQI([
/**
* The module exported by this file.
*/
var AboutPages = {
async init() {
// Load scripts in content processes and tabs
// Register about: pages and their listeners
this.aboutStudies.registerParentListeners();
CleanupManager.addCleanupHandler(() => {
// Stop loading process scripts and notify existing scripts to clean up.
Services.ppmm.broadcastAsyncMessage("Shield:ShuttingDown");
Services.mm.broadcastAsyncMessage("Shield:ShuttingDown");
// Clean up about pages
this.aboutStudies.unregisterParentListeners();
});
},
};
let AboutPages = {};
/**
* The weak set that keeps track of which browsing contexts
* have an about:studies page.
*/
let BrowsingContexts = new WeakSet();
/**
* about:studies page for displaying in-progress and past Shield studies.
* @type {AboutPage}
* @implements {nsIMessageListener}
*/
XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
XPCOMUtils.defineLazyGetter(AboutPages, "aboutStudies", () => {
const aboutStudies = new AboutPage({
chromeUrl: "resource://normandy-content/about-studies/about-studies.html",
aboutHost: "studies",
@ -122,126 +105,57 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
// Extra methods for about:study-specific behavior.
Object.assign(aboutStudies, {
/**
* Register listeners for messages from the content processes.
getAddonStudyList() {
return AddonStudies.getAll();
},
getPreferenceStudyList() {
return PreferenceExperiments.getAll();
},
/** Add a browsing context to the weak set;
* this weak set keeps track of all contexts
* that are housing an about:studies page.
*/
registerParentListeners() {
Services.mm.addMessageListener("Shield:GetAddonStudyList", this);
Services.mm.addMessageListener("Shield:GetPreferenceStudyList", this);
Services.mm.addMessageListener("Shield:RemoveAddonStudy", this);
Services.mm.addMessageListener("Shield:RemovePreferenceStudy", this);
Services.mm.addMessageListener("Shield:OpenDataPreferences", this);
Services.mm.addMessageListener("Shield:GetStudiesEnabled", this);
addToWeakSet(browsingContext) {
BrowsingContexts.add(browsingContext);
},
/** Remove a browsing context to the weak set;
* this weak set keeps track of all contexts
* that are housing an about:studies page.
*/
removeFromWeakSet(browsingContext) {
BrowsingContexts.delete(browsingContext);
},
/**
* Unregister listeners for messages from the content process.
* Sends a message to every about:studies page,
* by iterating over the BrowsingContexts weakset.
* @param {string} message The message string to send to.
* @param {object} data The data object to send.
*/
unregisterParentListeners() {
Services.mm.removeMessageListener("Shield:GetAddonStudyList", this);
Services.mm.removeMessageListener("Shield:GetPreferenceStudyList", this);
Services.mm.removeMessageListener("Shield:RemoveAddonStudy", this);
Services.mm.removeMessageListener("Shield:RemovePreferenceStudy", this);
Services.mm.removeMessageListener("Shield:OpenDataPreferences", this);
Services.mm.removeMessageListener("Shield:GetStudiesEnabled", this);
_sendToAll(message, data) {
ChromeUtils.nondeterministicGetWeakSetKeys(BrowsingContexts).forEach(
browser =>
browser.currentWindowGlobal
.getActor("ShieldFrame")
.sendAsyncMessage(message, data)
);
},
/**
* Dispatch messages from the content process to the appropriate handler.
* @param {Object} message
* See the nsIMessageListener documentation for details about this object.
*/
receiveMessage(message) {
switch (message.name) {
case "Shield:GetAddonStudyList":
this.sendAddonStudyList(message.target);
break;
case "Shield:GetPreferenceStudyList":
this.sendPreferenceStudyList(message.target);
break;
case "Shield:RemoveAddonStudy":
this.removeAddonStudy(message.data.recipeId, message.data.reason);
break;
case "Shield:RemovePreferenceStudy":
this.removePreferenceStudy(
message.data.experimentName,
message.data.reason
);
break;
case "Shield:OpenDataPreferences":
this.openDataPreferences();
break;
case "Shield:GetStudiesEnabled":
this.sendStudiesEnabled(message.target);
break;
}
},
/**
* Fetch a list of add-on studies from storage and send it to the process
* that requested them.
* @param {<browser>} target
* XUL <browser> element for the tab containing the about:studies page
* that requested a study list.
*/
async sendAddonStudyList(target) {
try {
target.messageManager.sendAsyncMessage("Shield:ReceiveAddonStudyList", {
studies: await AddonStudies.getAll(),
});
} catch (err) {
// The child process might be gone, so no need to throw here.
Cu.reportError(err);
}
},
/**
* Fetch a list of preference studies from storage and send it to the
* process that requested them.
* @param {<browser>} target
* XUL <browser> element for the tab containing the about:studies page
* that requested a study list.
*/
async sendPreferenceStudyList(target) {
try {
target.messageManager.sendAsyncMessage(
"Shield:ReceivePreferenceStudyList",
{
studies: await PreferenceExperiments.getAll(),
}
);
} catch (err) {
// The child process might be gone, so no need to throw here.
Cu.reportError(err);
}
},
/**
* Get if studies are enabled and send it to the process that
* requested them. This has to be in the parent process, since
* RecipeRunner is stateful, and can't be interacted with from
* Get if studies are enabled. This has to be in the parent process,
* since RecipeRunner is stateful, and can't be interacted with from
* content processes safely.
*
* @param {<browser>} target
* XUL <browser> element for the tab containing the about:studies page
* that requested a study list.
*/
sendStudiesEnabled(target) {
RecipeRunner.checkPrefs();
const studiesEnabled = RecipeRunner.enabled && gOptOutStudiesEnabled;
try {
target.messageManager.sendAsyncMessage("Shield:ReceiveStudiesEnabled", {
studiesEnabled,
});
} catch (err) {
// The child process might be gone, so no need to throw here.
Cu.reportError(err);
}
getStudiesEnabled() {
return RecipeRunner.enabled && gOptOutStudiesEnabled;
},
/**
* Disable an active add-on study and remove its add-on.
* @param {String} studyName
* @param {String} recipeId the id of the addon to remove
* @param {String} reason the reason for removal
*/
async removeAddonStudy(recipeId, reason) {
try {
@ -256,15 +170,16 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
} finally {
// Update any open tabs with the new study list now that it has changed,
// even if the above failed.
Services.mm.broadcastAsyncMessage("Shield:ReceiveAddonStudyList", {
studies: await AddonStudies.getAll(),
});
this.getAddonStudyList().then(list =>
this._sendToAll("Shield:UpdateAddonStudyList", list)
);
}
},
/**
* Disable an active preference study
* @param {String} studyName
* Disable an active preference study.
* @param {String} experimentName the name of the experiment to remove
* @param {String} reason the reason for removal
*/
async removePreferenceStudy(experimentName, reason) {
try {
@ -278,9 +193,9 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
} finally {
// Update any open tabs with the new study list now that it has changed,
// even if the above failed.
Services.mm.broadcastAsyncMessage("Shield:ReceivePreferenceStudyList", {
studies: await PreferenceExperiments.getAll(),
});
this.getPreferenceStudyList().then(list =>
this._sendToAll("Shield:UpdatePreferenceStudyList", list)
);
}
},

Просмотреть файл

@ -10,14 +10,8 @@ var EXPORTED_SYMBOLS = ["ShieldFrameChild"];
* privileged actions in response to them. If we need to do anything that the
* content process can't handle (such as reading IndexedDB), we send a message
* to the parent process and handle it there.
*
* This file is loaded as a frame script. It will be loaded once per tab that
* is opened.
*/
const { ActorChild } = ChromeUtils.import(
"resource://gre/modules/ActorChild.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
@ -43,41 +37,58 @@ XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
});
/**
* Handles incoming events from the parent process and about:studies.
* @implements nsIMessageListener
* @implements EventListener
* Listen for DOM events bubbling up from the about:studies page, and perform
* privileged actions in response to them. If we need to do anything that the
* content process can't handle (such as reading IndexedDB), we send a message
* to the parent process and handle it there.
*/
class ShieldFrameChild extends ActorChild {
handleEvent(event) {
// We waited until after we received an event to register message listeners
// in order to save resources for tabs that don't ever load about:studies.
this.mm.addMessageListener("Shield:ShuttingDown", this);
this.mm.addMessageListener("Shield:ReceiveAddonStudyList", this);
this.mm.addMessageListener("Shield:ReceivePreferenceStudyList", this);
this.mm.addMessageListener("Shield:ReceiveStudiesEnabled", this);
class ShieldFrameChild extends JSWindowActorChild {
async handleEvent(event) {
// On page show or page hide,
// add this child to the WeakSet in AboutStudies.
switch (event.type) {
case "pageshow":
this.sendAsyncMessage("Shield:AddToWeakSet");
return;
case "pagehide":
this.sendAsyncMessage("Shield:RemoveFromWeakSet");
return;
}
switch (event.detail.action) {
// Actions that require the parent process
case "GetRemoteValue:AddonStudyList":
this.mm.sendAsyncMessage("Shield:GetAddonStudyList");
let addonStudies = await this.sendQuery("Shield:GetAddonStudyList");
this.triggerPageCallback(
"ReceiveRemoteValue:AddonStudyList",
addonStudies
);
break;
case "GetRemoteValue:PreferenceStudyList":
this.mm.sendAsyncMessage("Shield:GetPreferenceStudyList");
let prefStudies = await this.sendQuery("Shield:GetPreferenceStudyList");
this.triggerPageCallback(
"ReceiveRemoteValue:PreferenceStudyList",
prefStudies
);
break;
case "RemoveAddonStudy":
this.mm.sendAsyncMessage("Shield:RemoveAddonStudy", event.detail.data);
this.sendAsyncMessage("Shield:RemoveAddonStudy", event.detail.data);
break;
case "RemovePreferenceStudy":
this.mm.sendAsyncMessage(
this.sendAsyncMessage(
"Shield:RemovePreferenceStudy",
event.detail.data
);
break;
case "GetRemoteValue:StudiesEnabled":
this.mm.sendAsyncMessage("Shield:GetStudiesEnabled");
let studiesEnabled = await this.sendQuery("Shield:GetStudiesEnabled");
this.triggerPageCallback(
"ReceiveRemoteValue:StudiesEnabled",
studiesEnabled
);
break;
case "NavigateToDataPreferences":
this.mm.sendAsyncMessage("Shield:OpenDataPreferences");
this.sendAsyncMessage("Shield:OpenDataPreferences");
break;
// Actions that can be performed in the content process
case "GetRemoteValue:ShieldLearnMoreHref":
@ -105,56 +116,30 @@ class ShieldFrameChild extends ActorChild {
}
}
/**
* Handle messages from the parent process.
* @param {Object} message
* See the nsIMessageListener docs.
*/
receiveMessage(message) {
switch (message.name) {
case "Shield:ReceiveAddonStudyList":
this.triggerPageCallback(
"ReceiveRemoteValue:AddonStudyList",
message.data.studies
);
receiveMessage(msg) {
switch (msg.name) {
case "Shield:UpdateAddonStudyList":
this.triggerPageCallback("ReceiveRemoteValue:AddonStudyList", msg.data);
break;
case "Shield:ReceivePreferenceStudyList":
case "Shield:UpdatePreferenceStudyList":
this.triggerPageCallback(
"ReceiveRemoteValue:PreferenceStudyList",
message.data.studies
msg.data
);
break;
case "Shield:ReceiveStudiesEnabled":
this.triggerPageCallback(
"ReceiveRemoteValue:StudiesEnabled",
message.data.studiesEnabled
);
break;
case "Shield:ShuttingDown":
this.onShutdown();
break;
}
}
/**
* Trigger an event to communicate with the unprivileged about: page.
* @param {String} type
* @param {Object} detail
* Trigger an event to communicate with the unprivileged about:studies page.
* @param {String} type The type of event to trigger.
* @param {Object} detail The data to pass along to the event.
*/
triggerPageCallback(type, detail) {
let { content } = this.mm;
// Clone details and use the event class from the unprivileged context.
const event = new content.document.defaultView.CustomEvent(type, {
const event = new this.document.defaultView.CustomEvent(type, {
bubbles: true,
detail: Cu.cloneInto(detail, content.document.defaultView),
detail: Cu.cloneInto(detail, this.document.defaultView),
});
content.document.dispatchEvent(event);
}
onShutdown() {
this.mm.removeMessageListener("Shield:SendStudyList", this);
this.mm.removeMessageListener("Shield:ShuttingDown", this);
this.mm.removeEventListener("Shield", this);
this.document.dispatchEvent(event);
}
}

Просмотреть файл

@ -0,0 +1,46 @@
/* 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/. */
var EXPORTED_SYMBOLS = ["ShieldFrameParent"];
const frameGlobal = {};
ChromeUtils.defineModuleGetter(
frameGlobal,
"AboutPages",
"resource://normandy-content/AboutPages.jsm"
);
class ShieldFrameParent extends JSWindowActorParent {
async receiveMessage(msg) {
let { aboutStudies } = frameGlobal.AboutPages;
switch (msg.name) {
case "Shield:AddToWeakSet":
aboutStudies.addToWeakSet(this.browsingContext);
break;
case "Shield:RemoveFromWeakSet":
aboutStudies.removeFromWeakSet(this.browsingContext);
break;
case "Shield:GetAddonStudyList":
return aboutStudies.getAddonStudyList();
case "Shield:GetPreferenceStudyList":
return aboutStudies.getPreferenceStudyList();
case "Shield:RemoveAddonStudy":
aboutStudies.removeAddonStudy(msg.data.recipeId, msg.data.reason);
break;
case "Shield:RemovePreferenceStudy":
aboutStudies.removePreferenceStudy(
msg.data.experimentName,
msg.data.reason
);
break;
case "Shield:OpenDataPreferences":
aboutStudies.openDataPreferences();
break;
case "Shield:GetStudiesEnabled":
return aboutStudies.getStudiesEnabled();
}
return null;
}
}

Просмотреть файл

@ -7,7 +7,6 @@ ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
ChromeUtils.import("resource://normandy/lib/PreferenceRollouts.jsm", this);
ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", this);
ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);
const experimentPref1 = "test.initExperimentPrefs1";
const experimentPref2 = "test.initExperimentPrefs2";
@ -16,7 +15,6 @@ const experimentPref4 = "test.initExperimentPrefs4";
function withStubInits(testFunction) {
return decorate(
withStub(AboutPages, "init"),
withStub(AddonRollouts, "init"),
withStub(AddonStudies, "init"),
withStub(PreferenceRollouts, "init"),
@ -198,7 +196,6 @@ decorate_task(
decorate_task(withStubInits, async function testStartup() {
const initObserved = TestUtils.topicObserved("shield-init-complete");
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(
PreferenceExperiments.init.called,
@ -212,23 +209,6 @@ decorate_task(withStubInits, async function testStartupPrefInitFail() {
PreferenceExperiments.init.rejects();
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(AddonRollouts.init.called, "startup calls AddonRollouts.init");
ok(
PreferenceExperiments.init.called,
"startup calls PreferenceExperiments.init"
);
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
ok(PreferenceRollouts.init.called, "startup calls PreferenceRollouts.init");
});
decorate_task(withStubInits, async function testStartupAboutPagesInitFail() {
AboutPages.init.rejects();
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(AddonRollouts.init.called, "startup calls AddonRollouts.init");
ok(
@ -244,7 +224,6 @@ decorate_task(withStubInits, async function testStartupAddonStudiesInitFail() {
AddonStudies.init.rejects();
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(AddonRollouts.init.called, "startup calls AddonRollouts.init");
ok(
@ -262,7 +241,6 @@ decorate_task(
TelemetryEvents.init.throws();
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(AddonRollouts.init.called, "startup calls AddonRollouts.init");
ok(
@ -281,7 +259,6 @@ decorate_task(
PreferenceRollouts.init.throws();
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(AddonRollouts.init.called, "startup calls AddonRollouts.init");
ok(

Просмотреть файл

@ -465,3 +465,155 @@ decorate_task(
);
}
);
// Test that clicking remove on a study updates even about:studies pages
// that are not currently in focus.
decorate_task(
AddonStudies.withStudies([
addonStudyFactory({
slug: "fake-addon-study",
userFacingName: "Fake Add-on Study",
active: true,
userFacingDescription: "A fake description",
studyStartDate: new Date(2018, 0, 4),
}),
]),
PreferenceExperiments.withMockExperiments([
preferenceStudyFactory({
slug: "fake-pref-study",
userFacingName: "Fake Preference Study",
lastSeen: new Date(2018, 0, 3),
expired: false,
}),
]),
withAboutStudies,
async function testOtherTabsUpdated([addonStudy], [prefStudy], browser) {
// Ensure that both our studies are active in the current tab.
await ContentTask.spawn(
browser,
{ addonStudy, prefStudy },
async ({ addonStudy, prefStudy }) => {
const doc = content.document;
await ContentTaskUtils.waitForCondition(
() => doc.querySelectorAll(".remove-button").length == 2,
"waiting for page to load"
);
let activeNames = Array.from(
doc.querySelectorAll(".active-study-list .study")
).map(row => row.dataset.studySlug);
let inactiveNames = Array.from(
doc.querySelectorAll(".inactive-study-list .study")
).map(row => row.dataset.studySlug);
Assert.deepEqual(
activeNames,
[addonStudy.slug, prefStudy.slug],
"Both studies should be listed as active"
);
Assert.deepEqual(
inactiveNames,
[],
"No studies should be listed as inactive"
);
}
);
// Open a new about:studies tab.
await BrowserTestUtils.withNewTab("about:studies", async browser => {
// Delete both studies in this tab; this should pass if previous tests have passed.
await ContentTask.spawn(
browser,
{ addonStudy, prefStudy },
async ({ addonStudy, prefStudy }) => {
const doc = content.document;
function getStudyRow(docElem, slug) {
return docElem.querySelector(`.study[data-study-slug="${slug}"]`);
}
await ContentTaskUtils.waitForCondition(
() => doc.querySelectorAll(".remove-button").length == 2,
"waiting for page to load"
);
let activeNames = Array.from(
doc.querySelectorAll(".active-study-list .study")
).map(row => row.dataset.studySlug);
let inactiveNames = Array.from(
doc.querySelectorAll(".inactive-study-list .study")
).map(row => row.dataset.studySlug);
Assert.deepEqual(
activeNames,
[addonStudy.slug, prefStudy.slug],
"Both studies should be listed as active in the new tab"
);
Assert.deepEqual(
inactiveNames,
[],
"No studies should be listed as inactive in the new tab"
);
const activeAddonStudy = getStudyRow(doc, addonStudy.slug);
const activePrefStudy = getStudyRow(doc, prefStudy.slug);
activeAddonStudy.querySelector(".remove-button").click();
await ContentTaskUtils.waitForCondition(() =>
getStudyRow(doc, addonStudy.slug).matches(".study.disabled")
);
ok(
getStudyRow(doc, addonStudy.slug).matches(".study.disabled"),
"Clicking the remove button updates the UI in the new tab"
);
activePrefStudy.querySelector(".remove-button").click();
await ContentTaskUtils.waitForCondition(() =>
getStudyRow(doc, prefStudy.slug).matches(".study.disabled")
);
ok(
getStudyRow(doc, prefStudy.slug).matches(".study.disabled"),
"Clicking the remove button updates the UI in the new tab"
);
activeNames = Array.from(
doc.querySelectorAll(".active-study-list .study")
).map(row => row.dataset.studySlug);
Assert.deepEqual(
activeNames,
[],
"No studies should be listed as active"
);
}
);
});
// Ensure that the original tab has updated correctly.
await ContentTask.spawn(
browser,
{ addonStudy, prefStudy },
async ({ addonStudy, prefStudy }) => {
const doc = content.document;
await ContentTaskUtils.waitForCondition(
() => doc.querySelectorAll(".inactive-study-list .study").length == 2,
"Two studies should load into the inactive list, since they were disabled in a different tab"
);
let activeNames = Array.from(
doc.querySelectorAll(".active-study-list .study")
).map(row => row.dataset.studySlug);
let inactiveNames = Array.from(
doc.querySelectorAll(".inactive-study-list .study")
).map(row => row.dataset.studySlug);
Assert.deepEqual(
activeNames,
[],
"No studies should be listed as active, since they were disabled in a different tab"
);
Assert.deepEqual(
inactiveNames,
[addonStudy.slug, prefStudy.slug],
"Both studies should be listed as inactive, since they were disabled in a different tab"
);
}
);
}
);