Bug 1710077 - Part 1: Support target switching for cookies / indexedDB r=jdescottes,ochameau

This adds support for server target-switching for parent process storage resources (cookies and indexedDB at the moment)

Differential Revision: https://phabricator.services.mozilla.com/D114600
This commit is contained in:
Belén Albeza 2021-06-09 15:34:01 +00:00
Родитель 3e40a36745
Коммит df2ff6decf
8 изменённых файлов: 231 добавлений и 64 удалений

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

@ -5,7 +5,20 @@
/* import-globals-from head.js */
"use strict";
const TARGET_SWITCHING_PREF = "devtools.target-switching.server.enabled";
// test without target switching
add_task(async function() {
await testNavigation();
});
// test with target switching enabled
add_task(async function() {
await pushPref(TARGET_SWITCHING_PREF, true);
await testNavigation();
});
async function testNavigation() {
const URL1 = buildURLWithContent(
"example.com",
`<h1>example.com</h1>` + `<script>document.cookie = "lorem=ipsum";</script>`
@ -35,6 +48,25 @@ add_task(async function() {
info("Waiting for storage tree to refresh and show correct host…");
await waitUntil(() => isInTree(doc, ["cookies", "http://example.net"]));
// check the table for values
// NOTE: there's an issue with the TreeWidget in which `selectedItem` is set
// but we have nothing selected in the UI. See Bug 1712706.
// Here we are forcing selecting a different item first.
await selectTreeItem(["cookies"]);
await selectTreeItem(["cookies", "http://example.net"]);
checkCookieData("foo", "bar");
});
info("Waiting for table data to update and show correct values");
await waitUntil(() => hasCookieData("foo", "bar"));
// reload the current page, and check again
await refreshTab();
// wait for storage tree refresh, and check host
info("Waiting for storage tree to refresh and show correct host…");
await waitUntil(() => isInTree(doc, ["cookies", "http://example.net"]));
// check the table for values
// NOTE: there's an issue with the TreeWidget in which `selectedItem` is set
// but we have nothing selected in the UI. See Bug 1712706.
// Here we are forcing selecting a different item first.
await selectTreeItem(["cookies"]);
await selectTreeItem(["cookies", "http://example.net"]);
info("Waiting for table data to update and show correct values");
await waitUntil(() => hasCookieData("foo", "bar"));
}

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

@ -8,7 +8,20 @@
requestLongerTimeout(3);
const TARGET_SWITCHING_PREF = "devtools.target-switching.server.enabled";
// test without target switching
add_task(async function() {
await testNavigation(true);
});
// test with target switching enabled
add_task(async function() {
await pushPref(TARGET_SWITCHING_PREF, true);
await testNavigation();
});
async function testNavigation(shallCleanup = false) {
const URL1 = URL_ROOT_COM + "storage-indexeddb-simple.html";
const URL2 = URL_ROOT_NET + "storage-indexeddb-simple-alt.html";
@ -30,10 +43,7 @@ add_task(async function() {
// clear db before navigating to a new domain
info("Removing database…");
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
const win = content.wrappedJSObject;
await win.clear();
});
await clearStorage();
// Check second domain
await navigateTo(URL2);
@ -48,4 +58,22 @@ add_task(async function() {
// TODO: select tree and check on storage data.
// We cannot do it yet since we do not detect newly created indexed db's when
// navigating. See Bug 1273802
});
// reload the current tab, and check again
await refreshTab();
// wait for storage tree refresh, and check host
info("Checking storage tree…");
await waitUntil(() => isInTree(doc, ["indexedDB", "http://example.net"]));
// clean up if needed
if (shallCleanup) {
await clearStorage();
}
}
async function clearStorage() {
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
const win = content.wrappedJSObject;
await win.clear();
});
}

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

@ -1133,12 +1133,21 @@ function buildURLWithContent(domain, html) {
* @param {String} value
*/
function checkCookieData(name, value) {
const rows = Array.from(gUI.table.items);
const cookie = rows.map(([, data]) => data).find(x => x.name === name);
is(
cookie?.value,
value,
ok(
hasCookieData(name, value),
`Table row has an entry for: ${name} with value: ${value}`
);
}
/**
* Returns whether the given cookie holds the provided value in the data table
* @param {String} name
* @param {String} value
*/
function hasCookieData(name, value) {
const rows = Array.from(gUI.table.items);
const cookie = rows.map(([, data]) => data).find(x => x.name === name);
info(`found ${cookie?.value}`);
return cookie?.value === value;
}

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

@ -3,7 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const { LocalizationHelper, ELLIPSIS } = require("devtools/shared/l10n");
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
@ -318,11 +317,13 @@ class StorageUI {
const { resourceKey } = resource;
// NOTE: We might be getting more than 1 resource per storage type when
// we have remote frames, so we need an array to store these.
// we have remote frames in content process resources, so we need
// an array to store these.
if (!this.storageResources[resourceKey]) {
this.storageResources[resourceKey] = [];
}
this.storageResources[resourceKey].push(resource);
resource.on(
"single-store-update",
this._onStoreUpdate.bind(this, resource)
@ -353,7 +354,8 @@ class StorageUI {
this.storageResources[type] = this.storageResources[type].filter(
storage => {
// Note that the storage front may already be destroyed,
// and have a null targetFront attribute. So also remove all already destroyed fronts.
// and have a null targetFront attribute. So also remove all already
// destroyed fronts.
return !storage.isDestroyed() && storage.targetFront != targetFront;
}
);

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

@ -37,47 +37,23 @@ class ParentProcessStorage {
this.onStoresUpdate = this.onStoresUpdate.bind(this);
this.onStoresCleared = this.onStoresCleared.bind(this);
this.observe = this.observe.bind(this);
// Notifications that help us keep track of newly added windows and windows
// that got removed
Services.obs.addObserver(this, "window-global-created");
Services.obs.addObserver(this, "window-global-destroyed");
}
async watch(watcherActor, { onAvailable, onUpdated, onDestroyed }) {
const browsingContext = watcherActor.browserElement.browsingContext;
async watch(watcherActor, { onAvailable }) {
this.watcherActor = watcherActor;
this.onAvailable = onAvailable;
const ActorConstructor = storageTypePool.get(this.storageKey);
const storageActor = new StorageActorMock(watcherActor);
this.storageActor = storageActor;
this.actor = new ActorConstructor(storageActor);
// Some storage types require to prelist their stores
if (typeof this.actor.preListStores === "function") {
await this.actor.preListStores();
}
// We have to manage the actor manually, because ResourceCommand doesn't
// use the protocol.js specification.
// resource-available-form is typed as "json"
// So that we have to manually handle stuff that would normally be
// automagically done by procotol.js
// 1) Manage the actor in order to have an actorID on it
watcherActor.manage(this.actor);
// 2) Convert to JSON "form"
const storage = this.actor.form();
// All resources should have a resourceType, resourceId and resourceKey
// attributes, so available/updated/destroyed callbacks work properly.
storage.resourceType = this.storageType;
storage.resourceId = `${this.storageType}-${browsingContext.id}`;
storage.resourceKey = this.storageKey;
// NOTE: the resource command needs this attribute
storage.browsingContextID = browsingContext.id;
onAvailable([storage]);
// Maps global events from `storageActor` shared for all storage-types,
// down to storage-type's specific actor `storage`.
storageActor.on("stores-update", this.onStoresUpdate);
// When a store gets cleared
storageActor.on("stores-cleared", this.onStoresCleared);
const {
browsingContext,
innerWindowID: innerWindowId,
} = watcherActor.browserElement;
await this._spawnActor(browsingContext.id, innerWindowId);
}
onStoresUpdate(response) {
@ -105,6 +81,54 @@ class ParentProcessStorage {
}
destroy() {
// Remove observers
Services.obs.removeObserver(this, "window-global-created");
Services.obs.removeObserver(this, "window-global-destroyed");
this._cleanActor();
}
async _spawnActor(browsingContextID, innerWindowId) {
const ActorConstructor = storageTypePool.get(this.storageKey);
const storageActor = new StorageActorMock(this.watcherActor);
this.storageActor = storageActor;
this.actor = new ActorConstructor(storageActor);
// Some storage types require to prelist their stores
if (typeof this.actor.preListStores === "function") {
await this.actor.preListStores();
}
// We have to manage the actor manually, because ResourceCommand doesn't
// use the protocol.js specification.
// resource-available-form is typed as "json"
// So that we have to manually handle stuff that would normally be
// automagically done by procotol.js
// 1) Manage the actor in order to have an actorID on it
this.watcherActor.manage(this.actor);
// 2) Convert to JSON "form"
const storage = this.actor.form();
// All resources should have a resourceType, resourceId and resourceKey
// attributes, so available/updated/destroyed callbacks work properly.
storage.resourceType = this.storageType;
storage.resourceId = `${this.storageType}-${innerWindowId}`;
storage.resourceKey = this.storageKey;
// NOTE: the resource command needs this attribute
storage.browsingContextID = browsingContextID;
this.onAvailable([storage]);
// Maps global events from `storageActor` shared for all storage-types,
// down to storage-type's specific actor `storage`.
storageActor.on("stores-update", this.onStoresUpdate);
// When a store gets cleared
storageActor.on("stores-cleared", this.onStoresCleared);
}
_cleanActor() {
this.actor?.destroy();
this.actor = null;
if (this.storageActor) {
@ -114,6 +138,57 @@ class ParentProcessStorage {
this.storageActor = null;
}
}
/**
* Event handler for any docshell update. This lets us figure out whenever
* any new window is added, or an existing window is removed.
*/
async observe(subject, topic) {
// If the watcher is bound to one browser element (i.e. a tab), ignore
// updates related to other browser elements
if (
this.watcherActor.browserId &&
subject.browsingContext.browserId != this.watcherActor.browserId
) {
return;
}
// ignore about:blank
if (subject.documentURI.displaySpec === "about:blank") {
return;
}
const isTargetSwitching = Services.prefs.getBoolPref(
"devtools.target-switching.server.enabled",
false
);
const isTopContext = subject.browsingContext.top == subject.browsingContext;
// When server side target switching is enabled, we replace the StorageActor
// with a new one.
// On the frontend, the navigation will destroy the previous target, which
// will destroy the previous storage front, so we must notify about a new
// one.
if (isTopContext && isTargetSwitching) {
if (topic === "window-global-created") {
// When we are target switching we keep the storage watcher, so we need
// to send a new resource to the client.
// However, we must ensure that we do this when the new target is
// already available, so we check innerWindowId to do it.
await new Promise(resolve => {
const listener = targetActorForm => {
if (targetActorForm.innerWindowId != subject.innerWindowId) {
return;
}
this.watcherActor.off("target-available-form", listener);
resolve();
};
this.watcherActor.on("target-available-form", listener);
});
this._cleanActor();
this._spawnActor(subject.browsingContext.id, subject.innerWindowId);
}
}
}
}
module.exports = ParentProcessStorage;
@ -125,23 +200,22 @@ class StorageActorMock extends EventEmitter {
this.conn = watcherActor.conn;
this.watcherActor = watcherActor;
this.observe = this.observe.bind(this);
this.boundUpdate = {};
// Notifications that help us keep track of newly added windows and windows
// that got removed
this.observe = this.observe.bind(this);
Services.obs.addObserver(this, "window-global-created");
Services.obs.addObserver(this, "window-global-destroyed");
this.boundUpdate = {};
}
destroy() {
// Remove observers
Services.obs.removeObserver(this, "window-global-created");
Services.obs.removeObserver(this, "window-global-destroyed");
// clear update throttle timeout
clearTimeout(this.batchTimer);
this.batchTimer = null;
// Remove observers
Services.obs.removeObserver(this, "window-global-created");
Services.obs.removeObserver(this, "window-global-destroyed");
}
get windows() {
@ -200,7 +274,7 @@ class StorageActorMock extends EventEmitter {
* Event handler for any docshell update. This lets us figure out whenever
* any new window is added, or an existing window is removed.
*/
observe(subject, topic) {
async observe(subject, topic) {
// If the watcher is bound to one browser element (i.e. a tab), ignore
// updates related to other browser elements
if (
@ -214,6 +288,19 @@ class StorageActorMock extends EventEmitter {
return;
}
// Only notify about remote iframe windows when JSWindowActor based targets are enabled
// We will create a new StorageActor for the top level tab documents when server side target
// switching is enabled
const isTargetSwitching = Services.prefs.getBoolPref(
"devtools.target-switching.server.enabled",
false
);
const isTopContext = subject.browsingContext.top == subject.browsingContext;
if (isTopContext && isTargetSwitching) {
return;
}
// emit window-wready and window-destroyed events when needed
const windowMock = { location: subject.documentURI };
if (topic === "window-global-created") {
this.emit("window-ready", windowMock);

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

@ -554,11 +554,14 @@ const browsingContextTargetPrototype = {
);
assert(this.actorID, "Actor should have an actorID.");
const innerWindowId = this.window ? getInnerId(this.window) : null;
const response = {
actor: this.actorID,
browsingContextID: this.browsingContextID,
// True for targets created by JSWindowActors, see constructor JSDoc.
followWindowGlobalLifeCycle: this.followWindowGlobalLifeCycle,
innerWindowId,
isTopLevelTarget: this.isTopLevelTarget,
traits: {
// @backward-compat { version 64 } Exposes a new trait to help identify

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

@ -12,11 +12,14 @@ const { Front, types } = require("devtools/shared/protocol.js");
module.exports = function({ resource, watcherFront, targetFront }) {
if (!(resource instanceof Front) && watcherFront) {
// instantiate front for cookies
const { innerWindowId } = resource;
// it's safe to instantiate the front now, so we do it.
resource = types.getType("cookies").read(resource, targetFront);
resource.resourceType = COOKIE;
resource.resourceId = `${COOKIE}-${targetFront.browsingContextID}`;
resource.resourceKey = "cookies";
resource.innerWindowId = innerWindowId;
}
return resource;

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

@ -12,11 +12,14 @@ const { Front, types } = require("devtools/shared/protocol.js");
module.exports = function({ resource, watcherFront, targetFront }) {
if (!(resource instanceof Front) && watcherFront) {
// instantiate front for indexedDB storage
const { innerWindowId } = resource;
// it's safe to instantiate the front now, so we do it.
resource = types.getType("indexedDB").read(resource, targetFront);
resource.resourceType = INDEXED_DB;
resource.resourceId = `${INDEXED_DB}-${targetFront.browsingContextID}`;
resource.resourceKey = "indexedDB";
resource.innerWindowId = innerWindowId;
}
return resource;