summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors
diff options
context:
space:
mode:
authorjanekptacijarabaci <janekptacijarabaci@seznam.cz>2018-03-27 15:57:18 +0200
committerjanekptacijarabaci <janekptacijarabaci@seznam.cz>2018-03-27 15:57:18 +0200
commitd990d8ab2cade6c928e8bbe56ae038d020cef599 (patch)
treec7561ae0f303cb0d4a7a7507178531b4852e4dea /devtools/server/actors
parent0c36b27511c1fbca594f0426c493ef601fda3e4c (diff)
parent8d5ec757ece850fb7ad5c712868f305636e41177 (diff)
downloadUXP-d990d8ab2cade6c928e8bbe56ae038d020cef599.tar
UXP-d990d8ab2cade6c928e8bbe56ae038d020cef599.tar.gz
UXP-d990d8ab2cade6c928e8bbe56ae038d020cef599.tar.lz
UXP-d990d8ab2cade6c928e8bbe56ae038d020cef599.tar.xz
UXP-d990d8ab2cade6c928e8bbe56ae038d020cef599.zip
Merge branch 'master' of https://github.com/MoonchildProductions/UXP into js_array_values_1
Diffstat (limited to 'devtools/server/actors')
-rw-r--r--devtools/server/actors/errordocs.js1
-rw-r--r--devtools/server/actors/highlighters/box-model.js15
-rw-r--r--devtools/server/actors/object.js5
-rw-r--r--devtools/server/actors/storage.js548
-rw-r--r--devtools/server/actors/stylesheets.js120
-rw-r--r--devtools/server/actors/webbrowser.js6
6 files changed, 506 insertions, 189 deletions
diff --git a/devtools/server/actors/errordocs.js b/devtools/server/actors/errordocs.js
index 27f687dc7..7c4b3acff 100644
--- a/devtools/server/actors/errordocs.js
+++ b/devtools/server/actors/errordocs.js
@@ -18,7 +18,6 @@ const ErrorDocs = {
JSMSG_RESULTING_STRING_TOO_LARGE: "Resulting_string_too_large",
JSMSG_BAD_RADIX: "Bad_radix",
JSMSG_PRECISION_RANGE: "Precision_range",
- JSMSG_BAD_FORMAL: "Malformed_formal_parameter",
JSMSG_STMT_AFTER_RETURN: "Stmt_after_return",
JSMSG_NOT_A_CODEPOINT: "Not_a_codepoint",
JSMSG_BAD_SORT_ARG: "Array_sort_argument",
diff --git a/devtools/server/actors/highlighters/box-model.js b/devtools/server/actors/highlighters/box-model.js
index 35f201a04..ae4284424 100644
--- a/devtools/server/actors/highlighters/box-model.js
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -15,7 +15,10 @@ const {
isNodeValid,
moveInfobar,
} = require("./utils/markup");
-const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
+const {
+ setIgnoreLayoutChanges,
+ getCurrentZoom,
+ } = require("devtools/shared/layout/utils");
const inspector = require("devtools/server/actors/inspector");
const nodeConstants = require("devtools/shared/dom-node-constants");
@@ -670,10 +673,14 @@ BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
pseudos += ":" + pseudo;
}
- let rect = this._getOuterQuad("border").bounds;
- let dim = parseFloat(rect.width.toPrecision(6)) +
+ // We want to display the original `width` and `height`, instead of the ones affected
+ // by any zoom. Since the infobar can be displayed also for text nodes, we can't
+ // access the computed style for that, and this is why we recalculate them here.
+ let zoom = getCurrentZoom(this.win);
+ let { width, height } = this._getOuterQuad("border").bounds;
+ let dim = parseFloat((width / zoom).toPrecision(6)) +
" \u00D7 " +
- parseFloat(rect.height.toPrecision(6));
+ parseFloat((height / zoom).toPrecision(6));
this.getElement("infobar-tagname").setTextContent(displayName);
this.getElement("infobar-id").setTextContent(id);
diff --git a/devtools/server/actors/object.js b/devtools/server/actors/object.js
index 1f417b951..06e95a5f0 100644
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -1158,11 +1158,6 @@ DebuggerServer.ObjectActorPreviewers = {
}],
RegExp: [function ({obj, hooks}, grip) {
- // Avoid having any special preview for the RegExp.prototype itself.
- if (!obj.proto || obj.proto.class != "RegExp") {
- return false;
- }
-
let str = RegExp.prototype.toString.call(obj.unsafeDereference());
grip.displayString = hooks.createValueGrip(str);
return true;
diff --git a/devtools/server/actors/storage.js b/devtools/server/actors/storage.js
index 572cd6b68..c702e8145 100644
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -15,6 +15,17 @@ const {isWindowIncluded} = require("devtools/shared/layout/utils");
const specs = require("devtools/shared/specs/storage");
const { Task } = require("devtools/shared/task");
+const DEFAULT_VALUE = "value";
+
+loader.lazyRequireGetter(this, "naturalSortCaseInsensitive",
+ "devtools/client/shared/natural-sort", true);
+
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/client/storage/ui.js,
+// devtools/client/storage/test/head.js and
+// devtools/server/tests/browser/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
loader.lazyImporter(this, "Sqlite", "resource://gre/modules/Sqlite.jsm");
@@ -87,7 +98,7 @@ var StorageActors = {};
* - observe : Method which gets triggered on the notificaiton of the watched
* topic.
* - getNamesForHost : Given a host, get list of all known store names.
- * - getValuesForHost : Given a host (and optianally a name) get all known
+ * - getValuesForHost : Given a host (and optionally a name) get all known
* store objects.
* - toStoreObject : Given a store object, convert it to the required format
* so that it can be transferred over wire.
@@ -118,7 +129,11 @@ StorageActors.defaults = function (typeName, observationTopic) {
get hosts() {
let hosts = new Set();
for (let {location} of this.storageActor.windows) {
- hosts.add(this.getHostName(location));
+ let host = this.getHostName(location);
+
+ if (host) {
+ hosts.add(host);
+ }
}
return hosts;
},
@@ -132,10 +147,35 @@ StorageActors.defaults = function (typeName, observationTopic) {
},
/**
- * Converts the window.location object into host.
+ * Converts the window.location object into a URL (e.g. http://domain.com).
*/
getHostName(location) {
- return location.hostname || location.href;
+ if (!location) {
+ // Debugging a legacy Firefox extension... no hostname available and no
+ // storage possible.
+ return null;
+ }
+
+ switch (location.protocol) {
+ case "data:":
+ // data: URLs do not support storage of any type.
+ return null;
+ case "about:":
+ // Fallthrough.
+ case "chrome:":
+ // Fallthrough.
+ case "file:":
+ return location.protocol + location.pathname;
+ case "resource:":
+ return location.origin + location.pathname;
+ case "moz-extension:":
+ return location.origin;
+ case "javascript:":
+ return location.href;
+ default:
+ // http: or unknown protocol.
+ return `${location.protocol}//${location.host}`;
+ }
},
initialize(storageActor) {
@@ -188,7 +228,7 @@ StorageActors.defaults = function (typeName, observationTopic) {
*/
onWindowReady: Task.async(function* (window) {
let host = this.getHostName(window.location);
- if (!this.hostVsStores.has(host)) {
+ if (host && !this.hostVsStores.has(host)) {
yield this.populateStoresForHost(host, window);
let data = {};
data[host] = this.getNamesForHost(host);
@@ -209,7 +249,7 @@ StorageActors.defaults = function (typeName, observationTopic) {
return;
}
let host = this.getHostName(window.location);
- if (!this.hosts.has(host)) {
+ if (host && !this.hosts.has(host)) {
this.hostVsStores.delete(host);
let data = {};
data[host] = [];
@@ -315,15 +355,20 @@ StorageActors.defaults = function (typeName, observationTopic) {
toReturn.data.push(...values);
}
}
+
toReturn.total = this.getObjectsSize(host, names, options);
+
if (offset > toReturn.total) {
// In this case, toReturn.data is an empty array.
toReturn.offset = toReturn.total;
toReturn.data = [];
} else {
- toReturn.data = toReturn.data.sort((a, b) => {
- return a[sortOn] - b[sortOn];
- }).slice(offset, offset + size).map(a => this.toStoreObject(a));
+ // We need to use natural sort before slicing.
+ let sorted = toReturn.data.sort((a, b) => {
+ return naturalSortCaseInsensitive(a[sortOn], b[sortOn]);
+ });
+ let sliced = sorted.slice(offset, offset + size);
+ toReturn.data = sliced.map(a => this.toStoreObject(a));
}
} else {
let obj = yield this.getValuesForHost(host, undefined, undefined,
@@ -333,15 +378,18 @@ StorageActors.defaults = function (typeName, observationTopic) {
}
toReturn.total = obj.length;
+
if (offset > toReturn.total) {
// In this case, toReturn.data is an empty array.
toReturn.offset = offset = toReturn.total;
toReturn.data = [];
} else {
- toReturn.data = obj.sort((a, b) => {
- return a[sortOn] - b[sortOn];
- }).slice(offset, offset + size)
- .map(object => this.toStoreObject(object));
+ // We need to use natural sort before slicing.
+ let sorted = obj.sort((a, b) => {
+ return naturalSortCaseInsensitive(a[sortOn], b[sortOn]);
+ });
+ let sliced = sorted.slice(offset, offset + size);
+ toReturn.data = sliced.map(object => this.toStoreObject(object));
}
}
@@ -445,6 +493,9 @@ StorageActors.createActor({
if (cookie.host == null) {
return host == null;
}
+
+ host = trimHttpHttpsPort(host);
+
if (cookie.host.startsWith(".")) {
return ("." + host).endsWith(cookie.host);
}
@@ -460,11 +511,13 @@ StorageActors.createActor({
}
return {
+ uniqueKey: `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+ `${SEPARATOR_GUID}${cookie.path}`,
name: cookie.name,
- path: cookie.path || "",
host: cookie.host || "",
+ path: cookie.path || "",
- // because expires is in seconds
+ // because creationTime is in micro seconds
expires: (cookie.expires || 0) * 1000,
// because it is in micro seconds
@@ -488,7 +541,10 @@ StorageActors.createActor({
for (let cookie of cookies) {
if (this.isCookieAtHost(cookie, host)) {
- this.hostVsStores.get(host).set(cookie.name, cookie);
+ let uniqueKey = `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+ `${SEPARATOR_GUID}${cookie.path}`;
+
+ this.hostVsStores.get(host).set(uniqueKey, cookie);
}
}
},
@@ -521,8 +577,11 @@ StorageActors.createActor({
case "changed":
if (hosts.length) {
for (let host of hosts) {
- this.hostVsStores.get(host).set(subject.name, subject);
- data[host] = [subject.name];
+ let uniqueKey = `${subject.name}${SEPARATOR_GUID}${subject.host}` +
+ `${SEPARATOR_GUID}${subject.path}`;
+
+ this.hostVsStores.get(host).set(uniqueKey, subject);
+ data[host] = [uniqueKey];
}
this.storageActor.update(action, "cookies", data);
}
@@ -531,8 +590,11 @@ StorageActors.createActor({
case "deleted":
if (hosts.length) {
for (let host of hosts) {
- this.hostVsStores.get(host).delete(subject.name);
- data[host] = [subject.name];
+ let uniqueKey = `${subject.name}${SEPARATOR_GUID}${subject.host}` +
+ `${SEPARATOR_GUID}${subject.path}`;
+
+ this.hostVsStores.get(host).delete(uniqueKey);
+ data[host] = [uniqueKey];
}
this.storageActor.update("deleted", "cookies", data);
}
@@ -543,8 +605,11 @@ StorageActors.createActor({
for (let host of hosts) {
let stores = [];
for (let cookie of subject) {
- this.hostVsStores.get(host).delete(cookie.name);
- stores.push(cookie.name);
+ let uniqueKey = `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+ `${SEPARATOR_GUID}${cookie.path}`;
+
+ this.hostVsStores.get(host).delete(uniqueKey);
+ stores.push(uniqueKey);
}
data[host] = stores;
}
@@ -566,15 +631,17 @@ StorageActors.createActor({
getFields: Task.async(function* () {
return [
- { name: "name", editable: 1},
- { name: "path", editable: 1},
- { name: "host", editable: 1},
- { name: "expires", editable: 1},
- { name: "lastAccessed", editable: 0},
- { name: "value", editable: 1},
- { name: "isDomain", editable: 0},
- { name: "isSecure", editable: 1},
- { name: "isHttpOnly", editable: 1}
+ { name: "uniqueKey", editable: false, private: true },
+ { name: "name", editable: true, hidden: false },
+ { name: "host", editable: true, hidden: false },
+ { name: "path", editable: true, hidden: false },
+ { name: "expires", editable: true, hidden: false },
+ { name: "lastAccessed", editable: false, hidden: false },
+ { name: "creationTime", editable: false, hidden: true },
+ { name: "value", editable: true, hidden: false },
+ { name: "isDomain", editable: false, hidden: true },
+ { name: "isSecure", editable: true, hidden: true },
+ { name: "isHttpOnly", editable: true, hidden: false }
];
}),
@@ -591,6 +658,14 @@ StorageActors.createActor({
this.editCookie(data);
}),
+ addItem: Task.async(function* (guid) {
+ let doc = this.storageActor.document;
+ let time = new Date().getTime();
+ let expiry = new Date(time + 3600 * 24 * 1000).toGMTString();
+
+ doc.cookie = `${guid}=${DEFAULT_VALUE};expires=${expiry}`;
+ }),
+
removeItem: Task.async(function* (host, name) {
let doc = this.storageActor.document;
this.removeCookie(host, name, doc.nodePrincipal
@@ -603,6 +678,12 @@ StorageActors.createActor({
.originAttributes);
}),
+ removeAllSessionCookies: Task.async(function* (host, domain) {
+ let doc = this.storageActor.document;
+ this.removeAllSessionCookies(host, domain, doc.nodePrincipal
+ .originAttributes);
+ }),
+
maybeSetupChildProcess() {
cookieHelpers.onCookieChanged = this.onCookieChanged.bind(this);
@@ -619,6 +700,8 @@ StorageActors.createActor({
cookieHelpers.removeCookie.bind(cookieHelpers);
this.removeAllCookies =
cookieHelpers.removeAllCookies.bind(cookieHelpers);
+ this.removeAllSessionCookies =
+ cookieHelpers.removeAllSessionCookies.bind(cookieHelpers);
return;
}
@@ -642,6 +725,8 @@ StorageActors.createActor({
callParentProcess.bind(null, "removeCookie");
this.removeAllCookies =
callParentProcess.bind(null, "removeAllCookies");
+ this.removeAllSessionCookies =
+ callParentProcess.bind(null, "removeAllSessionCookies");
addMessageListener("debug:storage-cookie-request-child",
cookieHelpers.handleParentRequest);
@@ -676,6 +761,8 @@ var cookieHelpers = {
host = "";
}
+ host = trimHttpHttpsPort(host);
+
let cookies = Services.cookies.getCookiesFromHost(host, originAttributes);
let store = [];
@@ -696,7 +783,7 @@ var cookieHelpers = {
* {
* host: "http://www.mozilla.org",
* field: "value",
- * key: "name",
+ * editCookie: "name",
* oldValue: "%7BHello%7D",
* newValue: "%7BHelloo%7D",
* items: {
@@ -720,10 +807,14 @@ var cookieHelpers = {
let origPath = field === "path" ? oldValue : data.items.path;
let cookie = null;
- let enumerator = Services.cookies.getCookiesFromHost(origHost, data.originAttributes || {});
+ let enumerator =
+ Services.cookies.getCookiesFromHost(origHost, data.originAttributes || {});
+
while (enumerator.hasMoreElements()) {
let nsiCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
- if (nsiCookie.name === origName && nsiCookie.host === origHost) {
+ if (nsiCookie.name === origName &&
+ nsiCookie.host === origHost &&
+ nsiCookie.path === origPath) {
cookie = {
host: nsiCookie.host,
path: nsiCookie.path,
@@ -743,7 +834,7 @@ var cookieHelpers = {
return;
}
- // If the date is expired set it for 1 minute in the future.
+ // If the date is expired set it for 10 seconds in the future.
let now = new Date();
if (!cookie.isSession && (cookie.expires * 1000) <= now) {
let tenSecondsFromNow = (now.getTime() + 10 * 1000) / 1000;
@@ -797,6 +888,17 @@ var cookieHelpers = {
},
_removeCookies(host, opts = {}) {
+ // We use a uniqueId to emulate compound keys for cookies. We need to
+ // extract the cookie name to remove the correct cookie.
+ if (opts.name) {
+ let split = opts.name.split(SEPARATOR_GUID);
+
+ opts.name = split[0];
+ opts.path = split[2];
+ }
+
+ host = trimHttpHttpsPort(host);
+
function hostMatches(cookieHost, matchHost) {
if (cookieHost == null) {
return matchHost == null;
@@ -807,12 +909,16 @@ var cookieHelpers = {
return cookieHost == host;
}
- let enumerator = Services.cookies.getCookiesFromHost(host, opts.originAttributes || {});
+ let enumerator =
+ Services.cookies.getCookiesFromHost(host, opts.originAttributes || {});
+
while (enumerator.hasMoreElements()) {
let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
if (hostMatches(cookie.host, host) &&
(!opts.name || cookie.name === opts.name) &&
- (!opts.domain || cookie.host === opts.domain)) {
+ (!opts.domain || cookie.host === opts.domain) &&
+ (!opts.path || cookie.path === opts.path) &&
+ (!opts.session || (!cookie.expires && !cookie.maxAge))) {
Services.cookies.remove(
cookie.host,
cookie.name,
@@ -834,6 +940,10 @@ var cookieHelpers = {
this._removeCookies(host, { domain, originAttributes });
},
+ removeAllSessionCookies(host, domain, originAttributes) {
+ this._removeCookies(host, { domain, originAttributes, session: true });
+ },
+
addCookieObservers() {
Services.obs.addObserver(cookieHelpers, "cookie-changed", false);
return null;
@@ -898,6 +1008,12 @@ var cookieHelpers = {
let rowdata = msg.data.args[0];
return cookieHelpers.editCookie(rowdata);
}
+ case "createNewCookie": {
+ let host = msg.data.args[0];
+ let guid = msg.data.args[1];
+ let originAttributes = msg.data.args[2];
+ return cookieHelpers.createNewCookie(host, guid, originAttributes);
+ }
case "removeCookie": {
let host = msg.data.args[0];
let name = msg.data.args[1];
@@ -910,6 +1026,12 @@ var cookieHelpers = {
let originAttributes = msg.data.args[2];
return cookieHelpers.removeAllCookies(host, domain, originAttributes);
}
+ case "removeAllSessionCookies": {
+ let host = msg.data.args[0];
+ let domain = msg.data.args[1];
+ let originAttributes = msg.data.args[2];
+ return cookieHelpers.removeAllSessionCookies(host, domain, originAttributes);
+ }
default:
console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
@@ -1000,13 +1122,6 @@ function getObjectForLocalOrSessionStorage(type) {
}));
},
- getHostName(location) {
- if (!location.host) {
- return location.href;
- }
- return location.protocol + "//" + location.host;
- },
-
populateStoresForHost(host, window) {
try {
this.hostVsStores.set(host, window[type]);
@@ -1018,17 +1133,28 @@ function getObjectForLocalOrSessionStorage(type) {
populateStoresForHosts() {
this.hostVsStores = new Map();
for (let window of this.windows) {
- this.populateStoresForHost(this.getHostName(window.location), window);
+ let host = this.getHostName(window.location);
+ if (host) {
+ this.populateStoresForHost(host, window);
+ }
}
},
getFields: Task.async(function* () {
return [
- { name: "name", editable: 1},
- { name: "value", editable: 1}
+ { name: "name", editable: true },
+ { name: "value", editable: true }
];
}),
+ addItem: Task.async(function* (guid, host) {
+ let storage = this.hostVsStores.get(host);
+ if (!storage) {
+ return;
+ }
+ storage.setItem(guid, DEFAULT_VALUE);
+ }),
+
/**
* Edit localStorage or sessionStorage fields.
*
@@ -1143,6 +1269,11 @@ StorageActors.createActor({
// The |chrome| cache is the cache implicitely cached by the platform,
// hosting the source file of the service worker.
let { CacheStorage } = this.storageActor.window;
+
+ if (!CacheStorage) {
+ return [];
+ }
+
let cache = new CacheStorage("content", principal);
return cache;
}),
@@ -1205,18 +1336,11 @@ StorageActors.createActor({
getFields: Task.async(function* () {
return [
- { name: "url", editable: 0 },
- { name: "status", editable: 0 }
+ { name: "url", editable: false },
+ { name: "status", editable: false }
];
}),
- getHostName(location) {
- if (!location.host) {
- return location.href;
- }
- return location.protocol + "//" + location.host;
- },
-
populateStoresForHost: Task.async(function* (host) {
let storeMap = new Map();
let caches = yield this.getCachesForHost(host);
@@ -1386,12 +1510,15 @@ ObjectStoreMetadata.prototype = {
* The host associated with this indexed db.
* @param {IDBDatabase} db
* The particular indexed db.
+ * @param {String} storage
+ * Storage type, either "temporary", "default" or "persistent".
*/
-function DatabaseMetadata(origin, db) {
+function DatabaseMetadata(origin, db, storage) {
this._origin = origin;
this._name = db.name;
this._version = db.version;
this._objectStores = [];
+ this.storage = storage;
if (db.objectStoreNames.length) {
let transaction = db.transaction(db.objectStoreNames, "readonly");
@@ -1411,7 +1538,9 @@ DatabaseMetadata.prototype = {
toObject() {
return {
+ uniqueKey: `${this._name}${SEPARATOR_GUID}${this.storage}`,
name: this._name,
+ storage: this.storage,
origin: this._origin,
version: this._version,
objectStores: this._objectStores.size
@@ -1483,13 +1612,6 @@ StorageActors.createActor({
this.removeDBRecord(host, principal, db, store, id);
}),
- getHostName(location) {
- if (!location.host) {
- return location.href;
- }
- return location.protocol + "//" + location.host;
- },
-
/**
* This method is overriden and left blank as for indexedDB, this operation
* cannot be performed synchronously. Thus, the preListStores method exists to
@@ -1579,15 +1701,17 @@ StorageActors.createActor({
populateStoresForHost: Task.async(function* (host) {
let storeMap = new Map();
let {names} = yield this.getDBNamesForHost(host);
+
let win = this.storageActor.getWindowFromHost(host);
if (win) {
let principal = win.document.nodePrincipal;
- for (let name of names) {
- let metadata = yield this.getDBMetaData(host, principal, name);
+ for (let {name, storage} of names) {
+ let metadata = yield this.getDBMetaData(host, principal, name, storage);
metadata = indexedDBHelpers.patchMetadataMapsAndProtos(metadata);
- storeMap.set(name, metadata);
+
+ storeMap.set(`${name} (${storage})`, metadata);
}
}
@@ -1614,16 +1738,30 @@ StorageActors.createActor({
if ("objectStores" in item) {
// DB meta data
return {
+ uniqueKey: `${item.name} (${item.storage})`,
db: item.name,
+ storage: item.storage,
origin: item.origin,
version: item.version,
objectStores: item.objectStores
};
}
+
+ let value = JSON.stringify(item.value);
+
+ // FIXME: Bug 1318029 - Due to a bug that is thrown whenever a
+ // LongStringActor string reaches DebuggerServer.LONG_STRING_LENGTH we need
+ // to trim the value. When the bug is fixed we should stop trimming the
+ // string here.
+ let maxLength = DebuggerServer.LONG_STRING_LENGTH - 1;
+ if (value.length > maxLength) {
+ value = value.substr(0, maxLength);
+ }
+
// Indexed db entry
return {
name: item.name,
- value: new LongStringActor(this.conn, JSON.stringify(item.value))
+ value: new LongStringActor(this.conn, value)
};
},
@@ -1659,16 +1797,20 @@ StorageActors.createActor({
maybeSetupChildProcess() {
if (!DebuggerServer.isInChildProcess) {
this.backToChild = (func, rv) => rv;
+ this.clearDBStore = indexedDBHelpers.clearDBStore;
+ this.findIDBPathsForHost = indexedDBHelpers.findIDBPathsForHost;
+ this.findSqlitePathsForHost = indexedDBHelpers.findSqlitePathsForHost;
+ this.findStorageTypePaths = indexedDBHelpers.findStorageTypePaths;
this.getDBMetaData = indexedDBHelpers.getDBMetaData;
- this.openWithPrincipal = indexedDBHelpers.openWithPrincipal;
this.getDBNamesForHost = indexedDBHelpers.getDBNamesForHost;
- this.getSanitizedHost = indexedDBHelpers.getSanitizedHost;
this.getNameFromDatabaseFile = indexedDBHelpers.getNameFromDatabaseFile;
- this.getValuesForHost = indexedDBHelpers.getValuesForHost;
this.getObjectStoreData = indexedDBHelpers.getObjectStoreData;
+ this.getSanitizedHost = indexedDBHelpers.getSanitizedHost;
+ this.getValuesForHost = indexedDBHelpers.getValuesForHost;
+ this.openWithPrincipal = indexedDBHelpers.openWithPrincipal;
this.removeDB = indexedDBHelpers.removeDB;
this.removeDBRecord = indexedDBHelpers.removeDBRecord;
- this.clearDBStore = indexedDBHelpers.clearDBStore;
+ this.splitNameAndStorage = indexedDBHelpers.splitNameAndStorage;
return;
}
@@ -1681,6 +1823,7 @@ StorageActors.createActor({
});
this.getDBMetaData = callParentProcessAsync.bind(null, "getDBMetaData");
+ this.splitNameAndStorage = callParentProcessAsync.bind(null, "splitNameAndStorage");
this.getDBNamesForHost = callParentProcessAsync.bind(null, "getDBNamesForHost");
this.getValuesForHost = callParentProcessAsync.bind(null, "getValuesForHost");
this.removeDB = callParentProcessAsync.bind(null, "removeDB");
@@ -1725,26 +1868,28 @@ StorageActors.createActor({
// Detail of database
case "database":
return [
- { name: "objectStore", editable: 0 },
- { name: "keyPath", editable: 0 },
- { name: "autoIncrement", editable: 0 },
- { name: "indexes", editable: 0 },
+ { name: "objectStore", editable: false },
+ { name: "keyPath", editable: false },
+ { name: "autoIncrement", editable: false },
+ { name: "indexes", editable: false },
];
// Detail of object store
case "object store":
return [
- { name: "name", editable: 0 },
- { name: "value", editable: 0 }
+ { name: "name", editable: false },
+ { name: "value", editable: false }
];
// Detail of indexedDB for one origin
default:
return [
- { name: "db", editable: 0 },
- { name: "origin", editable: 0 },
- { name: "version", editable: 0 },
- { name: "objectStores", editable: 0 },
+ { name: "uniqueKey", editable: false, private: true },
+ { name: "db", editable: false },
+ { name: "storage", editable: false },
+ { name: "origin", editable: false },
+ { name: "version", editable: false },
+ { name: "objectStores", editable: false },
];
}
})
@@ -1776,14 +1921,14 @@ var indexedDBHelpers = {
* `name` for the given `host` with its `principal`. The stored metadata
* information is of `DatabaseMetadata` type.
*/
- getDBMetaData: Task.async(function* (host, principal, name) {
- let request = this.openWithPrincipal(principal, name);
+ getDBMetaData: Task.async(function* (host, principal, name, storage) {
+ let request = this.openWithPrincipal(principal, name, storage);
let success = promise.defer();
request.onsuccess = event => {
let db = event.target.result;
- let dbData = new DatabaseMetadata(host, db);
+ let dbData = new DatabaseMetadata(host, db, storage);
db.close();
success.resolve(this.backToChild("getDBMetaData", dbData));
@@ -1796,21 +1941,37 @@ var indexedDBHelpers = {
return success.promise;
}),
+ splitNameAndStorage: function (name) {
+ let lastOpenBracketIndex = name.lastIndexOf("(");
+ let lastCloseBracketIndex = name.lastIndexOf(")");
+ let delta = lastCloseBracketIndex - lastOpenBracketIndex - 1;
+
+ let storage = name.substr(lastOpenBracketIndex + 1, delta);
+
+ name = name.substr(0, lastOpenBracketIndex - 1);
+
+ return { storage, name };
+ },
+
/**
* Opens an indexed db connection for the given `principal` and
* database `name`.
*/
- openWithPrincipal(principal, name) {
- return indexedDBForStorage.openForPrincipal(principal, name);
+ openWithPrincipal: function (principal, name, storage) {
+ return indexedDBForStorage.openForPrincipal(principal, name,
+ { storage: storage });
},
- removeDB: Task.async(function* (host, principal, name) {
+ removeDB: Task.async(function* (host, principal, dbName) {
let result = new promise(resolve => {
- let request = indexedDBForStorage.deleteForPrincipal(principal, name);
+ let {name, storage} = this.splitNameAndStorage(dbName);
+ let request =
+ indexedDBForStorage.deleteForPrincipal(principal, name,
+ { storage: storage });
request.onsuccess = () => {
resolve({});
- this.onItemUpdated("deleted", host, [name]);
+ this.onItemUpdated("deleted", host, [dbName]);
};
request.onblocked = () => {
@@ -1836,10 +1997,11 @@ var indexedDBHelpers = {
removeDBRecord: Task.async(function* (host, principal, dbName, storeName, id) {
let db;
+ let {name, storage} = this.splitNameAndStorage(dbName);
try {
db = yield new promise((resolve, reject) => {
- let request = this.openWithPrincipal(principal, dbName);
+ let request = this.openWithPrincipal(principal, name, storage);
request.onsuccess = ev => resolve(ev.target.result);
request.onerror = ev => reject(ev.target.error);
});
@@ -1868,10 +2030,11 @@ var indexedDBHelpers = {
clearDBStore: Task.async(function* (host, principal, dbName, storeName) {
let db;
+ let {name, storage} = this.splitNameAndStorage(dbName);
try {
db = yield new promise((resolve, reject) => {
- let request = this.openWithPrincipal(principal, dbName);
+ let request = this.openWithPrincipal(principal, name, storage);
request.onsuccess = ev => resolve(ev.target.result);
request.onerror = ev => reject(ev.target.error);
});
@@ -1903,46 +2066,101 @@ var indexedDBHelpers = {
*/
getDBNamesForHost: Task.async(function* (host) {
let sanitizedHost = this.getSanitizedHost(host);
- let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
- "default", sanitizedHost, "idb");
-
- let exists = yield OS.File.exists(directory);
- if (!exists && host.startsWith("about:")) {
- // try for moz-safe-about directory
- sanitizedHost = this.getSanitizedHost("moz-safe-" + host);
- directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
- "permanent", sanitizedHost, "idb");
- exists = yield OS.File.exists(directory);
- }
- if (!exists) {
- return this.backToChild("getDBNamesForHost", {names: []});
+ let profileDir = OS.Constants.Path.profileDir;
+ let files = [];
+ let names = [];
+ let storagePath = OS.Path.join(profileDir, "storage");
+
+ // We expect sqlite DB paths to look something like this:
+ // - PathToProfileDir/storage/default/http+++www.example.com/
+ // idb/1556056096MeysDaabta.sqlite
+ // - PathToProfileDir/storage/permanent/http+++www.example.com/
+ // idb/1556056096MeysDaabta.sqlite
+ // - PathToProfileDir/storage/temporary/http+++www.example.com/
+ // idb/1556056096MeysDaabta.sqlite
+ // The subdirectory inside the storage folder is determined by the storage
+ // type:
+ // - default: { storage: "default" } or not specified.
+ // - permanent: { storage: "persistent" }.
+ // - temporary: { storage: "temporary" }.
+ let sqliteFiles = yield this.findSqlitePathsForHost(storagePath, sanitizedHost);
+
+ for (let file of sqliteFiles) {
+ let splitPath = OS.Path.split(file).components;
+ let idbIndex = splitPath.indexOf("idb");
+ let storage = splitPath[idbIndex - 2];
+ let relative = file.substr(profileDir.length + 1);
+
+ files.push({
+ file: relative,
+ storage: storage === "permanent" ? "persistent" : storage
+ });
}
- let names = [];
- let dirIterator = new OS.File.DirectoryIterator(directory);
- try {
- yield dirIterator.forEach(file => {
- // Skip directories.
- if (file.isDir) {
- return null;
+ if (files.length > 0) {
+ for (let {file, storage} of files) {
+ let name = yield this.getNameFromDatabaseFile(file);
+ if (name) {
+ names.push({
+ name,
+ storage
+ });
}
+ }
+ }
+ return this.backToChild("getDBNamesForHost", {names});
+ }),
- // Skip any non-sqlite files.
- if (!file.name.endsWith(".sqlite")) {
- return null;
+ /**
+ * Find all SQLite files that hold IndexedDB data for a host, such as:
+ * storage/temporary/http+++www.example.com/idb/1556056096MeysDaabta.sqlite
+ */
+ findSqlitePathsForHost: Task.async(function* (storagePath, sanitizedHost) {
+ let sqlitePaths = [];
+ let idbPaths = yield this.findIDBPathsForHost(storagePath, sanitizedHost);
+ for (let idbPath of idbPaths) {
+ let iterator = new OS.File.DirectoryIterator(idbPath);
+ yield iterator.forEach(entry => {
+ if (!entry.isDir && entry.path.endsWith(".sqlite")) {
+ sqlitePaths.push(entry.path);
}
-
- return this.getNameFromDatabaseFile(file.path).then(name => {
- if (name) {
- names.push(name);
- }
- return null;
- });
});
- } finally {
- dirIterator.close();
+ iterator.close();
+ }
+ return sqlitePaths;
+ }),
+
+ /**
+ * Find all paths that hold IndexedDB data for a host, such as:
+ * storage/temporary/http+++www.example.com/idb
+ */
+ findIDBPathsForHost: Task.async(function* (storagePath, sanitizedHost) {
+ let idbPaths = [];
+ let typePaths = yield this.findStorageTypePaths(storagePath);
+ for (let typePath of typePaths) {
+ let idbPath = OS.Path.join(typePath, sanitizedHost, "idb");
+ if (yield OS.File.exists(idbPath)) {
+ idbPaths.push(idbPath);
+ }
}
- return this.backToChild("getDBNamesForHost", {names: names});
+ return idbPaths;
+ }),
+
+ /**
+ * Find all the storage types, such as "default", "permanent", or "temporary".
+ * These names have changed over time, so it seems simpler to look through all types
+ * that currently exist in the profile.
+ */
+ findStorageTypePaths: Task.async(function* (storagePath) {
+ let iterator = new OS.File.DirectoryIterator(storagePath);
+ let typePaths = [];
+ yield iterator.forEach(entry => {
+ if (entry.isDir) {
+ typePaths.push(entry.path);
+ }
+ });
+ iterator.close();
+ return typePaths;
}),
/**
@@ -1950,6 +2168,9 @@ var indexedDBHelpers = {
* name.
*/
getSanitizedHost(host) {
+ if (host.startsWith("about:")) {
+ host = "moz-safe-" + host;
+ }
return host.replace(ILLEGAL_CHAR_REGEX, "+");
},
@@ -1963,7 +2184,7 @@ var indexedDBHelpers = {
// Content pages might be having an open transaction for the same indexed db
// which this sqlite file belongs to. In that case, sqlite.openConnection
- // will throw. Thus we retey for some time to see if lock is removed.
+ // will throw. Thus we retry for some time to see if lock is removed.
while (!connection && retryCount++ < 25) {
try {
connection = yield Sqlite.openConnection({ path: path });
@@ -2024,8 +2245,14 @@ var indexedDBHelpers = {
return this.backToChild("getValuesForHost", {objectStores: objectStores});
}
// Get either all entries from the object store, or a particular id
- let result = yield this.getObjectStoreData(host, principal, db2,
- objectStore, id, options.index, options.size);
+ let storage = hostVsStores.get(host).get(db2).storage;
+ let result = yield this.getObjectStoreData(host, principal, db2, storage, {
+ objectStore: objectStore,
+ id: id,
+ index: options.index,
+ offset: 0,
+ size: options.size
+ });
return this.backToChild("getValuesForHost", {result: result});
}),
@@ -2039,23 +2266,27 @@ var indexedDBHelpers = {
* The principal of the given document.
* @param {string} dbName
* The name of the indexed db from the above host.
- * @param {string} objectStore
- * The name of the object store from the above db.
- * @param {string} id
- * id of the requested entry from the above object store.
- * null if all entries from the above object store are requested.
- * @param {string} index
- * name of the IDBIndex to be iterated on while fetching entries.
- * null or "name" if no index is to be iterated.
- * @param {number} offset
- * ofsset of the entries to be fetched.
- * @param {number} size
- * The intended size of the entries to be fetched.
+ * @param {String} storage
+ * Storage type, either "temporary", "default" or "persistent".
+ * @param {Object} requestOptions
+ * An object in the following format:
+ * {
+ * objectStore: The name of the object store from the above db,
+ * id: Id of the requested entry from the above object
+ * store. null if all entries from the above object
+ * store are requested,
+ * index: Name of the IDBIndex to be iterated on while fetching
+ * entries. null or "name" if no index is to be
+ * iterated,
+ * offset: offset of the entries to be fetched,
+ * size: The intended size of the entries to be fetched
+ * }
*/
- getObjectStoreData(host, principal, dbName, objectStore, id, index,
- offset, size) {
- let request = this.openWithPrincipal(principal, dbName);
+ getObjectStoreData(host, principal, dbName, storage, requestOptions) {
+ let {name} = this.splitNameAndStorage(dbName);
+ let request = this.openWithPrincipal(principal, name, storage);
let success = promise.defer();
+ let {objectStore, id, index, offset, size} = requestOptions;
let data = [];
let db;
@@ -2157,8 +2388,12 @@ var indexedDBHelpers = {
switch (msg.json.method) {
case "getDBMetaData": {
- let [host, principal, name] = args;
- return indexedDBHelpers.getDBMetaData(host, principal, name);
+ let [host, principal, name, storage] = args;
+ return indexedDBHelpers.getDBMetaData(host, principal, name, storage);
+ }
+ case "splitNameAndStorage": {
+ let [name] = args;
+ return indexedDBHelpers.splitNameAndStorage(name);
}
case "getDBNamesForHost": {
let [host] = args;
@@ -2170,8 +2405,8 @@ var indexedDBHelpers = {
hostVsStores, principal);
}
case "removeDB": {
- let [host, principal, name] = args;
- return indexedDBHelpers.removeDB(host, principal, name);
+ let [host, principal, dbName] = args;
+ return indexedDBHelpers.removeDB(host, principal, dbName);
}
case "removeDBRecord": {
let [host, principal, db, store, id] = args;
@@ -2215,6 +2450,24 @@ exports.setupParentProcessForIndexedDB = function ({ mm, prefix }) {
};
/**
+ * General helpers
+ */
+function trimHttpHttpsPort(url) {
+ let match = url.match(/(.+):\d+$/);
+
+ if (match) {
+ url = match[1];
+ }
+ if (url.startsWith("http://")) {
+ return url.substr(7);
+ }
+ if (url.startsWith("https://")) {
+ return url.substr(8);
+ }
+ return url;
+}
+
+/**
* The main Storage Actor.
*/
let StorageActor = protocol.ActorClassWithSpec(specs.storageSpec, {
@@ -2480,6 +2733,7 @@ let StorageActor = protocol.ActorClassWithSpec(specs.storageSpec, {
// added or changed update
this.removeNamesFromUpdateList("added", storeType, data);
this.removeNamesFromUpdateList("changed", storeType, data);
+
for (let host in data) {
if (data[host].length == 0 && this.boundUpdate.added &&
this.boundUpdate.added[storeType] &&
diff --git a/devtools/server/actors/stylesheets.js b/devtools/server/actors/stylesheets.js
index f20634e6c..7fcbca8c4 100644
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -13,7 +13,6 @@ const events = require("sdk/event/core");
const protocol = require("devtools/shared/protocol");
const {LongStringActor} = require("devtools/server/actors/string");
const {fetch} = require("devtools/shared/DevToolsUtils");
-const {listenOnce} = require("devtools/shared/async-utils");
const {originalSourceSpec, mediaRuleSpec, styleSheetSpec,
styleSheetsSpec} = require("devtools/shared/specs/stylesheets");
const {SourceMapConsumer} = require("source-map");
@@ -251,7 +250,7 @@ var StyleSheetActor = protocol.ActorClassWithSpec(styleSheetSpec, {
},
destroy: function () {
- if (this._transitionTimeout) {
+ if (this._transitionTimeout && this.window) {
this.window.clearTimeout(this._transitionTimeout);
removePseudoClassLock(
this.document.documentElement, TRANSITION_PSEUDO_CLASS);
@@ -801,6 +800,64 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
protocol.Actor.prototype.initialize.call(this, null);
this.parentActor = tabActor;
+
+ this._onNewStyleSheetActor = this._onNewStyleSheetActor.bind(this);
+ this._onSheetAdded = this._onSheetAdded.bind(this);
+ this._onWindowReady = this._onWindowReady.bind(this);
+
+ events.on(this.parentActor, "stylesheet-added", this._onNewStyleSheetActor);
+ events.on(this.parentActor, "window-ready", this._onWindowReady);
+
+ // We listen for StyleSheetApplicableStateChanged rather than
+ // StyleSheetAdded, because the latter will be sent before the
+ // rules are ready. Using the former (with a check to ensure that
+ // the sheet is enabled) ensures that the sheet is ready before we
+ // try to make an actor for it.
+ this.parentActor.chromeEventHandler
+ .addEventListener("StyleSheetApplicableStateChanged", this._onSheetAdded, true);
+
+ // This is used when creating a new style sheet, so that we can
+ // pass the correct flag when emitting our stylesheet-added event.
+ // See addStyleSheet and _onNewStyleSheetActor for more details.
+ this._nextStyleSheetIsNew = false;
+ },
+
+ destroy: function () {
+ for (let win of this.parentActor.windows) {
+ // This flag only exists for devtools, so we are free to clear
+ // it when we're done.
+ win.document.styleSheetChangeEventsEnabled = false;
+ }
+
+ events.off(this.parentActor, "stylesheet-added", this._onNewStyleSheetActor);
+ events.off(this.parentActor, "window-ready", this._onWindowReady);
+
+ this.parentActor.chromeEventHandler.removeEventListener("StyleSheetAdded",
+ this._onSheetAdded, true);
+
+ protocol.Actor.prototype.destroy.call(this);
+ },
+
+ /**
+ * Event handler that is called when a the tab actor emits window-ready.
+ *
+ * @param {Event} evt
+ * The triggering event.
+ */
+ _onWindowReady: function (evt) {
+ this._addStyleSheets(evt.window);
+ },
+
+ /**
+ * Event handler that is called when a the tab actor emits stylesheet-added.
+ *
+ * @param {StyleSheetActor} actor
+ * The new style sheet actor.
+ */
+ _onNewStyleSheetActor: function (actor) {
+ // Forward it to the client side.
+ events.emit(this, "stylesheet-added", actor, this._nextStyleSheetIsNew);
+ this._nextStyleSheetIsNew = false;
},
/**
@@ -808,23 +865,11 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
* all the style sheets in this document.
*/
getStyleSheets: Task.async(function* () {
- // Iframe document can change during load (bug 1171919). Track their windows
- // instead.
- let windows = [this.window];
let actors = [];
- for (let win of windows) {
+ for (let win of this.parentActor.windows) {
let sheets = yield this._addStyleSheets(win);
actors = actors.concat(sheets);
-
- // Recursively handle style sheets of the documents in iframes.
- for (let iframe of win.document.querySelectorAll("iframe, browser, frame")) {
- if (iframe.contentDocument && iframe.contentWindow) {
- // Sometimes, iframes don't have any document, like the
- // one that are over deeply nested (bug 285395)
- windows.push(iframe.contentWindow);
- }
- }
}
return actors;
}),
@@ -832,15 +877,13 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
/**
* Check if we should be showing this stylesheet.
*
- * @param {Document} doc
- * Document for which we're checking
* @param {DOMCSSStyleSheet} sheet
* Stylesheet we're interested in
*
* @return boolean
* Whether the stylesheet should be listed.
*/
- _shouldListSheet: function (doc, sheet) {
+ _shouldListSheet: function (sheet) {
// Special case about:PreferenceStyleSheet, as it is generated on the
// fly and the URI is not registered with the about: handler.
// https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
@@ -852,6 +895,22 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
},
/**
+ * Event handler that is called when a new style sheet is added to
+ * a document. In particular, StyleSheetApplicableStateChanged is
+ * listened for, because StyleSheetAdded is sent too early, before
+ * the rules are ready.
+ *
+ * @param {Event} evt
+ * The triggering event.
+ */
+ _onSheetAdded: function (evt) {
+ let sheet = evt.stylesheet;
+ if (this._shouldListSheet(sheet)) {
+ this.parentActor.createStyleSheetActor(sheet);
+ }
+ },
+
+ /**
* Add all the stylesheets for the document in this window to the map and
* create an actor for each one if not already created.
*
@@ -865,24 +924,16 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
{
return Task.spawn(function* () {
let doc = win.document;
- // readyState can be uninitialized if an iframe has just been created but
- // it has not started to load yet.
- if (doc.readyState === "loading" || doc.readyState === "uninitialized") {
- // Wait for the document to load first.
- yield listenOnce(win, "DOMContentLoaded", true);
-
- // Make sure we have the actual document for this window. If the
- // readyState was initially uninitialized, the initial dummy document
- // was replaced with the actual document (bug 1171919).
- doc = win.document;
- }
+ // We have to set this flag in order to get the
+ // StyleSheetApplicableStateChanged events. See Document.webidl.
+ doc.styleSheetChangeEventsEnabled = true;
let isChrome = Services.scriptSecurityManager.isSystemPrincipal(doc.nodePrincipal);
let styleSheets = isChrome ? DOMUtils.getAllStyleSheets(doc) : doc.styleSheets;
let actors = [];
for (let i = 0; i < styleSheets.length; i++) {
let sheet = styleSheets[i];
- if (!this._shouldListSheet(doc, sheet)) {
+ if (!this._shouldListSheet(sheet)) {
continue;
}
@@ -917,7 +968,7 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
// Associated styleSheet may be null if it has already been seen due
// to duplicate @imports for the same URL.
- if (!rule.styleSheet || !this._shouldListSheet(doc, rule.styleSheet)) {
+ if (!rule.styleSheet || !this._shouldListSheet(rule.styleSheet)) {
continue;
}
let actor = this.parentActor.createStyleSheetActor(rule.styleSheet);
@@ -948,6 +999,13 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
* Object with 'styelSheet' property for form on new actor.
*/
addStyleSheet: function (text) {
+ // This is a bit convoluted. The style sheet actor may be created
+ // by a notification from platform. In this case, we can't easily
+ // pass the "new" flag through to createStyleSheetActor, so we set
+ // a flag locally and check it before sending an event to the
+ // client. See |_onNewStyleSheetActor|.
+ this._nextStyleSheetIsNew = true;
+
let parent = this.document.documentElement;
let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
style.setAttribute("type", "text/css");
diff --git a/devtools/server/actors/webbrowser.js b/devtools/server/actors/webbrowser.js
index 1808895b1..dffe49b91 100644
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -2501,7 +2501,11 @@ DebuggerProgressListener.prototype = {
if (isWindow && isStop) {
// Don't dispatch "navigate" event just yet when there is a redirect to
// about:neterror page.
- if (request.status != Cr.NS_OK) {
+ // Navigating to about:neterror will make `status` be something else than NS_OK.
+ // But for some error like NS_BINDING_ABORTED we don't want to emit any `navigate`
+ // event as the page load has been cancelled and the related page document is going
+ // to be a dead wrapper.
+ if (request.status != Cr.NS_OK && request.status != Cr.NS_BINDING_ABORTED) {
// Instead, listen for DOMContentLoaded as about:neterror is loaded
// with LOAD_BACKGROUND flags and never dispatches load event.
// That may be the same reason why there is no onStateChange event