зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
3e40a36745
Коммит
df2ff6decf
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче