--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -55,17 +55,17 @@ table.headers.Cache.status=Status
table.headers.indexedDB.name=Key
table.headers.indexedDB.db=Database Name
table.headers.indexedDB.objectStore=Object Store Name
table.headers.indexedDB.value=Value
table.headers.indexedDB.origin=Origin
table.headers.indexedDB.version=Version
table.headers.indexedDB.objectStores=Object Stores
-table.headers.indexedDB.keyPath=Key
+table.headers.indexedDB.keyPath2=Key Path
table.headers.indexedDB.autoIncrement=Auto Increment
table.headers.indexedDB.indexes=Indexes
# LOCALIZATION NOTE (label.expires.session):
# This string is displayed in the expires column when the cookie is Session
# Cookie
label.expires.session=Session
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -2,16 +2,17 @@
tags = devtools
subsuite = devtools
support-files =
storage-cache-error.html
storage-complex-values.html
storage-cookies.html
storage-empty-objectstores.html
storage-idb-delete-blocked.html
+ storage-indexeddb-duplicate-names.html
storage-listings.html
storage-localstorage.html
storage-overflow.html
storage-search.html
storage-secured-iframe.html
storage-sessionstorage.html
storage-unsecured-iframe.html
storage-updates.html
@@ -30,16 +31,17 @@ support-files =
[browser_storage_delete_all.js]
[browser_storage_delete_tree.js]
[browser_storage_dynamic_updates_cookies.js]
[browser_storage_dynamic_updates_localStorage.js]
[browser_storage_dynamic_updates_sessionStorage.js]
[browser_storage_empty_objectstores.js]
[browser_storage_indexeddb_delete.js]
[browser_storage_indexeddb_delete_blocked.js]
+[browser_storage_indexeddb_duplicate_names.js]
[browser_storage_localstorage_edit.js]
[browser_storage_localstorage_error.js]
[browser_storage_overflow.js]
[browser_storage_search.js]
[browser_storage_search_keyboard_trap.js]
[browser_storage_sessionstorage_edit.js]
[browser_storage_sidebar.js]
[browser_storage_sidebar_update.js]
--- a/devtools/client/storage/test/browser_storage_basic.js
+++ b/devtools/client/storage/test/browser_storage_basic.js
@@ -48,38 +48,38 @@ const testCases = [
["iframe-s-ls1"]],
[["sessionStorage", "http://test1.example.org"],
["ss1"]],
[["sessionStorage", "http://sectest1.example.org"],
["iframe-u-ss1", "iframe-u-ss2"]],
[["sessionStorage", "https://sectest1.example.org"],
["iframe-s-ss1"]],
[["indexedDB", "http://test1.example.org"],
- ["idb1", "idb2"]],
- [["indexedDB", "http://test1.example.org", "idb1"],
+ ["idb1 (default)", "idb2 (default)"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)"],
["obj1", "obj2"]],
- [["indexedDB", "http://test1.example.org", "idb2"],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)"],
["obj3"]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
[1, 2, 3]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj2"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"],
[1]],
- [["indexedDB", "http://test1.example.org", "idb2", "obj3"],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"],
[]],
[["indexedDB", "http://sectest1.example.org"],
[]],
[["indexedDB", "https://sectest1.example.org"],
- ["idb-s1", "idb-s2"]],
- [["indexedDB", "https://sectest1.example.org", "idb-s1"],
+ ["idb-s1 (default)", "idb-s2 (default)"]],
+ [["indexedDB", "https://sectest1.example.org", "idb-s1 (default)"],
["obj-s1"]],
- [["indexedDB", "https://sectest1.example.org", "idb-s2"],
+ [["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
["obj-s2"]],
- [["indexedDB", "https://sectest1.example.org", "idb-s1", "obj-s1"],
+ [["indexedDB", "https://sectest1.example.org", "idb-s1 (default)", "obj-s1"],
[6, 7]],
- [["indexedDB", "https://sectest1.example.org", "idb-s2", "obj-s2"],
+ [["indexedDB", "https://sectest1.example.org", "idb-s2 (default)", "obj-s2"],
[16]],
[["Cache", "http://test1.example.org", "plop"],
[MAIN_DOMAIN + "404_cached_file.js",
MAIN_DOMAIN + "browser_storage_basic.js"]],
];
/**
* Test that the desired number of tree items are present
--- a/devtools/client/storage/test/browser_storage_delete.js
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -12,17 +12,17 @@ const TEST_CASES = [
[["localStorage", "http://test1.example.org"],
"ls1", "name"],
[["sessionStorage", "http://test1.example.org"],
"ss1", "name"],
[
["cookies", "test1.example.org"],
getCookieId("c1", "test1.example.org", "/browser"), "name"
],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
1, "name"],
[["Cache", "http://test1.example.org", "plop"],
MAIN_DOMAIN + "404_cached_file.js", "url"],
];
add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
--- a/devtools/client/storage/test/browser_storage_delete_all.js
+++ b/devtools/client/storage/test/browser_storage_delete_all.js
@@ -24,29 +24,29 @@ add_task(function* () {
[["localStorage", "https://sectest1.example.org"],
["iframe-s-ls1"]],
[["sessionStorage", "http://test1.example.org"],
["ss1"]],
[["sessionStorage", "http://sectest1.example.org"],
["iframe-u-ss1", "iframe-u-ss2"]],
[["sessionStorage", "https://sectest1.example.org"],
["iframe-s-ss1"]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
[1, 2, 3]],
[["Cache", "http://test1.example.org", "plop"],
[MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
];
yield checkState(beforeState);
info("do the delete");
const deleteHosts = [
[["localStorage", "https://sectest1.example.org"], "iframe-s-ls1", "name"],
[["sessionStorage", "https://sectest1.example.org"], "iframe-s-ss1", "name"],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"], 1, "name"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], 1, "name"],
[["Cache", "http://test1.example.org", "plop"],
MAIN_DOMAIN + "404_cached_file.js", "url"],
];
for (let [store, rowName, cellToClick] of deleteHosts) {
let storeName = store.join(" > ");
yield selectTreeItem(store);
@@ -73,17 +73,17 @@ add_task(function* () {
[["localStorage", "https://sectest1.example.org"],
[]],
[["sessionStorage", "http://test1.example.org"],
["ss1"]],
[["sessionStorage", "http://sectest1.example.org"],
["iframe-u-ss1", "iframe-u-ss2"]],
[["sessionStorage", "https://sectest1.example.org"],
[]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
[]],
[["Cache", "http://test1.example.org", "plop"],
[]],
];
yield checkState(afterState);
yield finishTests();
--- a/devtools/client/storage/test/browser_storage_delete_tree.js
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -23,27 +23,27 @@ add_task(function* () {
getCookieId("c1", "test1.example.org", "/browser"),
getCookieId("cs2", ".example.org", "/"),
getCookieId("c3", "test1.example.org", "/"),
getCookieId("uc1", ".example.org", "/")
]
],
[["localStorage", "http://test1.example.org"], ["ls1", "ls2"]],
[["sessionStorage", "http://test1.example.org"], ["ss1"]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"], [1, 2, 3]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], [1, 2, 3]],
[["Cache", "http://test1.example.org", "plop"],
[MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
]);
info("do the delete");
const deleteHosts = [
["cookies", "test1.example.org"],
["localStorage", "http://test1.example.org"],
["sessionStorage", "http://test1.example.org"],
- ["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
["Cache", "http://test1.example.org", "plop"],
];
for (let store of deleteHosts) {
let storeName = store.join(" > ");
yield selectTreeItem(store);
@@ -62,14 +62,14 @@ add_task(function* () {
yield eventWait;
}
info("test state after delete");
yield checkState([
[["cookies", "test1.example.org"], []],
[["localStorage", "http://test1.example.org"], []],
[["sessionStorage", "http://test1.example.org"], []],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"], []],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], []],
[["Cache", "http://test1.example.org", "plop"], []],
]);
yield finishTests();
});
--- a/devtools/client/storage/test/browser_storage_empty_objectstores.js
+++ b/devtools/client/storage/test/browser_storage_empty_objectstores.js
@@ -16,24 +16,24 @@
// - The value of the first (unique) column for each row in the table
// corresponding to the tree item selected.
// ]
// These entries are formed by the cookies, local storage, session storage and
// indexedDB entries created in storage-listings.html,
// storage-secured-iframe.html and storage-unsecured-iframe.html
const storeItems = [
[["indexedDB", "http://test1.example.org"],
- ["idb1", "idb2"]],
- [["indexedDB", "http://test1.example.org", "idb1"],
+ ["idb1 (default)", "idb2 (default)"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)"],
["obj1", "obj2"]],
- [["indexedDB", "http://test1.example.org", "idb2"],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)"],
[]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
[1, 2, 3]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj2"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"],
[1]]
];
/**
* Test that the desired number of tree items are present
*/
function testTree() {
let doc = gPanelWindow.document;
--- a/devtools/client/storage/test/browser_storage_indexeddb_delete.js
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
@@ -11,21 +11,21 @@
add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html");
let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup");
let menuDeleteDb = contextMenu.querySelector("#storage-tree-popup-delete");
info("test state before delete");
yield checkState([
- [["indexedDB", "http://test1.example.org"], ["idb1", "idb2"]],
+ [["indexedDB", "http://test1.example.org"], ["idb1 (default)", "idb2 (default)"]],
]);
info("do the delete");
- const deletedDb = ["indexedDB", "http://test1.example.org", "idb1"];
+ const deletedDb = ["indexedDB", "http://test1.example.org", "idb1 (default)"];
yield selectTreeItem(deletedDb);
// Wait once for update and another time for value fetching
let eventWait = gUI.once("store-objects-updated").then(
() => gUI.once("store-objects-updated"));
let selector = `[data-id='${JSON.stringify(deletedDb)}'] > .tree-widget-item`;
@@ -35,13 +35,13 @@ add_task(function* () {
info(`Opened tree context menu in ${deletedDb.join(" > ")}`);
menuDeleteDb.click();
});
yield eventWait;
info("test state after delete");
yield checkState([
- [["indexedDB", "http://test1.example.org"], ["idb2"]],
+ [["indexedDB", "http://test1.example.org"], ["idb2 (default)"]],
]);
yield finishTests();
});
--- a/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
@@ -8,29 +8,29 @@
// Test what happens when deleting indexedDB database is blocked
add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-idb-delete-blocked.html");
info("test state before delete");
yield checkState([
- [["indexedDB", "http://test1.example.org"], ["idb"]]
+ [["indexedDB", "http://test1.example.org"], ["idb (default)"]]
]);
info("do the delete");
yield selectTreeItem(["indexedDB", "http://test1.example.org"]);
let actor = gUI.getCurrentActor();
- let result = yield actor.removeDatabase("http://test1.example.org", "idb");
+ let result = yield actor.removeDatabase("http://test1.example.org", "idb (default)");
ok(result.blocked, "removeDatabase attempt is blocked");
info("test state after blocked delete");
yield checkState([
- [["indexedDB", "http://test1.example.org"], ["idb"]]
+ [["indexedDB", "http://test1.example.org"], ["idb (default)"]]
]);
let eventWait = gUI.once("store-objects-updated");
info("telling content to close the db");
yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
let win = content.wrappedJSObject;
yield win.closeDb();
@@ -42,17 +42,17 @@ add_task(function* () {
info("test state after real delete");
yield checkState([
[["indexedDB", "http://test1.example.org"], []]
]);
info("try to delete database from nonexistent host");
let errorThrown = false;
try {
- result = yield actor.removeDatabase("http://test2.example.org", "idb");
+ result = yield actor.removeDatabase("http://test2.example.org", "idb (default)");
} catch (ex) {
errorThrown = true;
}
ok(errorThrown, "error was reported when trying to delete");
yield finishTests();
});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js
@@ -0,0 +1,31 @@
+/* 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/. */
+
+// Test to verify that indexedDBs with duplicate names (different types / paths)
+// work as expected.
+
+"use strict";
+
+add_task(function* () {
+ const TESTPAGE = MAIN_DOMAIN + "storage-indexeddb-duplicate-names.html";
+
+ setPermission(TESTPAGE, "indexedDB");
+
+ yield openTabAndSetupStorage(TESTPAGE);
+
+ yield checkState([
+ [
+ ["indexedDB", "http://test1.example.org"], [
+ "idb1 (default)",
+ "idb1 (temporary)",
+ "idb1 (persistent)",
+ "idb2 (default)",
+ "idb2 (temporary)",
+ "idb2 (persistent)"
+ ]
+ ]
+ ]);
+
+ yield finishTests();
+});
--- a/devtools/client/storage/test/browser_storage_sidebar.js
+++ b/devtools/client/storage/test/browser_storage_sidebar.js
@@ -67,27 +67,27 @@ const testCases = [
sendEscape: true
},
{
location: ["indexedDB", "http://test1.example.org"],
sidebarHidden: true
},
{
- location: "idb2",
+ location: "idb2 (default)",
sidebarHidden: false
},
{
- location: ["indexedDB", "http://test1.example.org", "idb2", "obj3"],
+ location: ["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"],
sidebarHidden: true
},
{
- location: ["indexedDB", "https://sectest1.example.org", "idb-s2"],
+ location: ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
sidebarHidden: true
},
{
location: "obj-s2",
sidebarHidden: false
},
{
sendEscape: true
--- a/devtools/client/storage/test/browser_storage_values.js
+++ b/devtools/client/storage/test/browser_storage_values.js
@@ -119,26 +119,26 @@ const testCases = [
["ss5", [
{name: "ss5", value: "Array"},
{name: "ss5.0", value: LONG_WORD},
{name: "ss5.1", value: LONG_WORD},
{name: "ss5.2", value: LONG_WORD},
{name: "ss5.3", value: `${LONG_WORD}&${LONG_WORD}`},
{name: "ss5.4", value: `${LONG_WORD}&${LONG_WORD}`},
], true],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"]],
[1, [
{name: 1, value: JSON.stringify({id: 1, name: "foo", email: "foo@bar.com"})}
]],
[null, [
{name: "1.id", value: "1"},
{name: "1.name", value: "foo"},
{name: "1.email", value: "foo@bar.com"},
], true],
- [["indexedDB", "http://test1.example.org", "idb1", "obj2"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"]],
[1, [
{name: 1, value: JSON.stringify({
id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"
})}
]],
[null, [
{name: "1.id2", value: "1"},
{name: "1.name", value: "foo"},
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -875,8 +875,24 @@ var focusSearchBoxUsingShortcut = Task.a
if (callback) {
callback();
}
});
function getCookieId(name, domain, path) {
return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
}
+
+function setPermission(url, permission) {
+ const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
+
+ let uri = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService)
+ .newURI(url, null, null);
+ let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+ let principal = ssm.createCodebasePrincipal(uri, {});
+
+ Components.classes["@mozilla.org/permissionmanager;1"]
+ .getService(nsIPermissionManager)
+ .addFromPrincipal(principal, permission,
+ nsIPermissionManager.ALLOW_ACTION);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/storage-indexeddb-duplicate-names.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ <title>Storage inspector IndexedDBs with duplicate names</title>
+
+ <script type="application/javascript;version=1.7">
+ "use strict";
+
+ function createIndexedDBs() {
+ createIndexedDB("idb1", "temporary");
+ createIndexedDB("idb1", "default");
+ createIndexedDB("idb1", "persistent");
+ createIndexedDB("idb2", "temporary");
+ createIndexedDB("idb2", "default");
+ createIndexedDB("idb2", "persistent");
+ }
+
+ function createIndexedDB(name, storage) {
+ let open = indexedDB.open(name, {storage: storage});
+
+ open.onsuccess = function () {
+ let db = open.result;
+ db.close();
+ };
+ }
+
+ function deleteDB(dbName, storage) {
+ return new Promise(resolve => {
+ dump(`removing database ${dbName} (${storage}) from ${document.location}\n`);
+ indexedDB.deleteDatabase(dbName, { storage: storage }).onsuccess = resolve;
+ });
+ }
+
+ window.clear = function* () {
+ yield deleteDB("idb1", "temporary");
+ yield deleteDB("idb1", "default");
+ yield deleteDB("idb1", "persistent");
+ yield deleteDB("idb2", "temporary");
+ yield deleteDB("idb2", "default");
+ yield deleteDB("idb2", "persistent");
+
+ dump(`removed indexedDB data from ${document.location}\n`);
+ };
+ </script>
+</head>
+<body onload="createIndexedDBs()">
+ <h1>storage-indexeddb-duplicate-names.html</h1>
+</body>
+</html>
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -804,17 +804,20 @@ StorageUI.prototype = {
if (f.private) {
privateFields.push(f.name);
}
columns[f.name] = f.name;
let columnName;
try {
- columnName = L10N.getStr("table.headers." + type + "." + f.name);
+ // Path key names for l10n in the case of a string change.
+ let name = f.name === "keyPath" ? "keyPath2" : f.name;
+
+ columnName = L10N.getStr("table.headers." + type + "." + name);
} catch (e) {
columnName = COOKIE_KEY_MAP[f.name];
}
if (!columnName) {
console.error("Unable to localize table header type:" + type + " key:" + f.name);
} else {
columns[f.name] = columnName;
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -1,12 +1,14 @@
/* 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/. */
+/* globals StopIteration */
+
"use strict";
const {Cc, Ci, Cu, CC} = require("chrome");
const events = require("sdk/event/core");
const protocol = require("devtools/shared/protocol");
const {LongStringActor} = require("devtools/server/actors/string");
const {DebuggerServer} = require("devtools/server/main");
const Services = require("Services");
@@ -88,17 +90,17 @@ var StorageActors = {};
* Creates a default object with the common methods required by all storage
* actors.
*
* This default object is missing a couple of required methods that should be
* implemented seperately for each actor. They are namely:
* - 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.
* - populateStoresForHost : Given a host, populate the map of all store
* objects for it
* - getFields: Given a subType(optional), get an array of objects containing
* column field info. The info includes,
* "name" is name of colume key.
@@ -136,16 +138,19 @@ StorageActors.defaults = function (typeN
get windows() {
return this.storageActor.windows;
},
/**
* Converts the window.location object into host.
*/
getHostName(location) {
+ if (location.protocol === "chrome:") {
+ return location.href;
+ }
return location.hostname || location.href;
},
initialize(storageActor) {
protocol.Actor.prototype.initialize.call(this, null);
this.storageActor = storageActor;
@@ -746,16 +751,17 @@ var cookieHelpers = {
let {field, oldValue, newValue} = data;
let origName = field === "name" ? oldValue : data.items.name;
let origHost = field === "host" ? oldValue : data.items.host;
let origPath = field === "path" ? oldValue : data.items.path;
let cookie = null;
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 &&
nsiCookie.path === origPath) {
cookie = {
host: nsiCookie.host,
path: nsiCookie.path,
@@ -1043,16 +1049,19 @@ function getObjectForLocalOrSessionStora
value: storage.getItem(key)
}));
},
getHostName(location) {
if (!location.host) {
return location.href;
}
+ if (location.protocol === "chrome:") {
+ return location.href;
+ }
return location.protocol + "//" + location.host;
},
populateStoresForHost(host, window) {
try {
this.hostVsStores.set(host, window[type]);
} catch (ex) {
console.warn(`Failed to enumerate ${type} for host ${host}: ${ex}`);
@@ -1253,16 +1262,19 @@ StorageActors.createActor({
{ name: "status", editable: false }
];
}),
getHostName(location) {
if (!location.host) {
return location.href;
}
+ if (location.protocol === "chrome:") {
+ return location.href;
+ }
return location.protocol + "//" + location.host;
},
populateStoresForHost: Task.async(function* (host) {
let storeMap = new Map();
let caches = yield this.getCachesForHost(host);
try {
for (let name of (yield caches.keys())) {
@@ -1425,22 +1437,25 @@ ObjectStoreMetadata.prototype = {
/**
* Meta data object for a particular indexed db in a host.
*
* @param {string} origin
* 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");
for (let i = 0; i < transaction.objectStoreNames.length; i++) {
let objectStore =
transaction.objectStore(transaction.objectStoreNames[i]);
this._objectStores.push([transaction.objectStoreNames[i],
@@ -1450,17 +1465,17 @@ function DatabaseMetadata(origin, db) {
}
DatabaseMetadata.prototype = {
get objectStores() {
return this._objectStores;
},
toObject() {
return {
- name: this._name,
+ name: `${this._name} (${this.storage})`,
origin: this._origin,
version: this._version,
objectStores: this._objectStores.size
};
}
};
StorageActors.createActor({
@@ -1530,16 +1545,19 @@ StorageActors.createActor({
let principal = win.document.nodePrincipal;
this.removeDBRecord(host, principal, db, store, id);
}),
getHostName(location) {
if (!location.host) {
return location.href;
}
+ if (location.protocol === "chrome:") {
+ 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
* do the same task asynchronously.
*/
@@ -1622,25 +1640,27 @@ StorageActors.createActor({
for (let host of this.hosts) {
yield this.populateStoresForHost(host);
}
}),
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);
}
}
this.hostVsStores.set(host, storeMap);
}),
/**
* Returns the over-the-wire implementation of the indexed db entity.
@@ -1663,20 +1683,32 @@ StorageActors.createActor({
// DB meta data
return {
db: item.name,
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)
};
},
form(form, detail) {
if (detail === "actorid") {
return this.actorID;
}
@@ -1702,38 +1734,41 @@ StorageActors.createActor({
this.storageActor.update(action, "indexedDB", {
[host]: [ JSON.stringify(path) ]
});
},
maybeSetupChildProcess() {
if (!DebuggerServer.isInChildProcess) {
this.backToChild = (func, rv) => rv;
+ this.clearDBStore = indexedDBHelpers.clearDBStore;
+ this.gatherFilesOrFolders = indexedDBHelpers.gatherFilesOrFolders;
this.getDBMetaData = indexedDBHelpers.getDBMetaData;
- this.openWithPrincipal = indexedDBHelpers.openWithPrincipal;
this.getDBNamesForHost = indexedDBHelpers.getDBNamesForHost;
+ this.getNameFromDatabaseFile = indexedDBHelpers.getNameFromDatabaseFile;
+ this.getObjectStoreData = indexedDBHelpers.getObjectStoreData;
this.getSanitizedHost = indexedDBHelpers.getSanitizedHost;
- this.getNameFromDatabaseFile = indexedDBHelpers.getNameFromDatabaseFile;
this.getValuesForHost = indexedDBHelpers.getValuesForHost;
- this.getObjectStoreData = indexedDBHelpers.getObjectStoreData;
+ this.openWithPrincipal = indexedDBHelpers.openWithPrincipal;
this.removeDB = indexedDBHelpers.removeDB;
this.removeDBRecord = indexedDBHelpers.removeDBRecord;
- this.clearDBStore = indexedDBHelpers.clearDBStore;
+ this.splitNameAndStorage = indexedDBHelpers.splitNameAndStorage;
return;
}
const { sendAsyncMessage, addMessageListener } =
this.conn.parentMessageManager;
this.conn.setupInParent({
module: "devtools/server/actors/storage",
setupParent: "setupParentProcessForIndexedDB"
});
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");
this.removeDBRecord = callParentProcessAsync.bind(null, "removeDBRecord");
this.clearDBStore = callParentProcessAsync.bind(null, "clearDBStore");
addMessageListener("debug:storage-indexedDB-request-child", msg => {
switch (msg.json.method) {
@@ -1819,51 +1854,66 @@ var indexedDBHelpers = {
});
},
/**
* Fetches and stores all the metadata information for the given database
* `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));
};
request.onerror = ({target}) => {
console.error(
`Error opening indexeddb database ${name} for host ${host}`, target.error);
success.resolve(this.backToChild("getDBMetaData", null));
};
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 = () => {
console.warn(`Deleting indexedDB database ${name} for host ${host} is blocked`);
resolve({ blocked: true });
};
request.onerror = () => {
@@ -1879,20 +1929,21 @@ var indexedDBHelpers = {
setTimeout(() => resolve({ blocked: true }), 3000);
});
return this.backToChild("removeDB", yield result);
}),
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);
});
let transaction = db.transaction(storeName, "readwrite");
let store = transaction.objectStore(storeName);
yield new promise((resolve, reject) => {
@@ -1911,20 +1962,21 @@ var indexedDBHelpers = {
db.close();
}
return this.backToChild("removeDBRecord", null);
}),
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);
});
let transaction = db.transaction(storeName, "readwrite");
let store = transaction.objectStore(storeName);
yield new promise((resolve, reject) => {
@@ -1946,77 +1998,141 @@ var indexedDBHelpers = {
return this.backToChild("clearDBStore", null);
}),
/**
* Fetches all the databases and their metadata for the given `host`.
*/
getDBNamesForHost: Task.async(function* (host) {
let sanitizedHost = this.getSanitizedHost(host);
- let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
- "default", sanitizedHost, "idb");
+ let profileDir = OS.Constants.Path.profileDir;
+ let files = [];
+ let names = [];
+ let storagePath = OS.Path.join(profileDir, "storage");
- 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: []});
+ // 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.gatherFilesOrFolders(storagePath, path => {
+ if (path.endsWith(".sqlite")) {
+ let { components } = OS.Path.split(path);
+ let isIDB = components[components.length - 2] === "idb";
+
+ return isIDB;
+ }
+ return false;
+ });
+
+ for (let file of sqliteFiles) {
+ let splitPath = OS.Path.split(file).components;
+ let idbIndex = splitPath.indexOf("idb");
+ let name = splitPath[idbIndex - 1];
+ let storage = splitPath[idbIndex - 2];
+ let relative = file.substr(profileDir.length + 1);
+
+ if (name.startsWith(sanitizedHost)) {
+ 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;
- }
-
- // Skip any non-sqlite files.
- if (!file.name.endsWith(".sqlite")) {
- 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.getNameFromDatabaseFile(file.path).then(name => {
- if (name) {
- names.push(name);
+ return this.backToChild("getDBNamesForHost", {names});
+ }),
+
+ /**
+ * Gather together all of the files in path and pass each path through a
+ * validation function.
+ *
+ * @param {String}
+ * Path in which to begin searching.
+ * @param {Function}
+ * Validation function, which checks each file path. If this function
+ * Returns true the file path is kept.
+ *
+ * @returns {Array}
+ * An array of file paths.
+ */
+ gatherFilesOrFolders: Task.async(function* (path, validationFunc) {
+ let files = [];
+ let iterator;
+ let paths = [path];
+
+ while (paths.length > 0) {
+ try {
+ iterator = new OS.File.DirectoryIterator(paths.pop());
+
+ for (let child in iterator) {
+ child = yield child;
+
+ path = child.path;
+
+ if (child.isDir) {
+ paths.push(path);
+ } else if (validationFunc(path)) {
+ files.push(path);
}
- return null;
- });
- });
- } finally {
- dirIterator.close();
+ }
+ } catch (ex) {
+ // Ignore StopIteration to prevent exiting the loop.
+ if (ex != StopIteration) {
+ throw ex;
+ }
+ }
}
- return this.backToChild("getDBNamesForHost", {names: names});
+ iterator.close();
+
+ return files;
}),
/**
* Removes any illegal characters from the host name to make it a valid file
* name.
*/
getSanitizedHost(host) {
+ if (host.startsWith("about:")) {
+ host = "moz-safe-" + host;
+ }
return host.replace(ILLEGAL_CHAR_REGEX, "+");
},
/**
* Retrieves the proper indexed db database name from the provided .sqlite
* file location.
*/
getNameFromDatabaseFile: Task.async(function* (path) {
let connection = null;
let retryCount = 0;
// 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 });
} catch (ex) {
// Continuously retrying is overkill. Waiting for 100ms before next try
yield sleep(100);
}
}
@@ -2067,48 +2183,58 @@ var indexedDBHelpers = {
for (let objectStore2 of objectStores2) {
objectStores.push(objectStore2[1].toObject());
}
}
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});
}),
/**
* Returns all or requested entries from a particular objectStore from the db
* in the given host.
*
* @param {string} host
* The given host.
* @param {nsIPrincipal} principal
* 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;
if (!size || size > MAX_STORE_OBJECT_COUNT) {
size = MAX_STORE_OBJECT_COUNT;
}
request.onsuccess = event => {
@@ -2200,31 +2326,35 @@ var indexedDBHelpers = {
return md;
},
handleChildRequest(msg) {
let args = msg.data.args;
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;
return indexedDBHelpers.getDBNamesForHost(host);
}
case "getValuesForHost": {
let [host, name, options, hostVsStores, principal] = args;
return indexedDBHelpers.getValuesForHost(host, name, options,
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;
return indexedDBHelpers.removeDBRecord(host, principal, db, store, id);
}
case "clearDBStore": {
let [host, principal, db, store] = args;
return indexedDBHelpers.clearDBStore(host, principal, db, store);
--- a/devtools/server/tests/browser/browser_storage_listings.js
+++ b/devtools/server/tests/browser/browser_storage_listings.js
@@ -116,59 +116,59 @@ const storeMap = {
}
]
}
};
const IDBValues = {
listStoresResponse: {
"http://test1.example.org": [
- ["idb1", "obj1"], ["idb1", "obj2"], ["idb2", "obj3"]
+ ["idb1 (default)", "obj1"], ["idb1 (default)", "obj2"], ["idb2 (default)", "obj3"]
],
"http://sectest1.example.org": [
],
"https://sectest1.example.org": [
- ["idb-s1", "obj-s1"], ["idb-s2", "obj-s2"]
+ ["idb-s1 (default)", "obj-s1"], ["idb-s2 (default)", "obj-s2"]
]
},
- dbDetails : {
+ dbDetails: {
"http://test1.example.org": [
{
- db: "idb1",
+ db: "idb1 (default)",
origin: "http://test1.example.org",
version: 1,
objectStores: 2
},
{
- db: "idb2",
+ db: "idb2 (default)",
origin: "http://test1.example.org",
version: 1,
objectStores: 1
},
],
"http://sectest1.example.org": [
],
"https://sectest1.example.org": [
{
- db: "idb-s1",
+ db: "idb-s1 (default)",
origin: "https://sectest1.example.org",
version: 1,
objectStores: 1
},
{
- db: "idb-s2",
+ db: "idb-s2 (default)",
origin: "https://sectest1.example.org",
version: 1,
objectStores: 1
},
]
},
objectStoreDetails: {
"http://test1.example.org": {
- idb1: [
+ "idb1 (default)": [
{
objectStore: "obj1",
keyPath: "id",
autoIncrement: false,
indexes: [
{
name: "name",
keyPath: "name",
@@ -185,17 +185,17 @@ const IDBValues = {
},
{
objectStore: "obj2",
keyPath: "id2",
autoIncrement: false,
indexes: []
}
],
- idb2: [
+ "idb2 (default)": [
{
objectStore: "obj3",
keyPath: "id3",
autoIncrement: false,
indexes: [
{
name: "name2",
keyPath: "name2",
@@ -203,25 +203,25 @@ const IDBValues = {
multiEntry: false,
}
]
},
]
},
"http://sectest1.example.org" : {},
"https://sectest1.example.org": {
- "idb-s1": [
+ "idb-s1 (default)": [
{
objectStore: "obj-s1",
keyPath: "id",
autoIncrement: false,
indexes: []
},
],
- "idb-s2": [
+ "idb-s2 (default)": [
{
objectStore: "obj-s2",
keyPath: "id3",
autoIncrement: true,
indexes: [
{
name: "name2",
keyPath: "name2",
@@ -231,17 +231,17 @@ const IDBValues = {
]
},
]
}
},
entries: {
"http://test1.example.org": {
- "idb1#obj1": [
+ "idb1 (default)#obj1": [
{
name: 1,
value: {
id: 1,
name: "foo",
email: "foo@bar.com",
}
},
@@ -257,32 +257,32 @@ const IDBValues = {
name: 3,
value: {
id: 3,
name: "foo2",
email: "foo3@bar.com",
}
}
],
- "idb1#obj2": [
+ "idb1 (default)#obj2": [
{
name: 1,
value: {
id2: 1,
name: "foo",
email: "foo@bar.com",
extra: "baz"
}
}
],
- "idb2#obj3": []
+ "idb2 (default)#obj3": []
},
"http://sectest1.example.org" : {},
"https://sectest1.example.org": {
- "idb-s1#obj-s1": [
+ "idb-s1 (default)#obj-s1": [
{
name: 6,
value: {
id: 6,
name: "foo",
email: "foo@bar.com",
}
},
@@ -290,17 +290,17 @@ const IDBValues = {
name: 7,
value: {
id: 7,
name: "foo2",
email: "foo2@bar.com",
}
}
],
- "idb-s2#obj-s2": [
+ "idb-s2 (default)#obj-s2": [
{
name: 13,
value: {
id2: 13,
name2: "foo",
email: "foo@bar.com",
}
}
--- a/devtools/shared/specs/storage.js
+++ b/devtools/shared/specs/storage.js
@@ -56,17 +56,17 @@ types.addDictType("cookieobject", {
types.addDictType("cookiestoreobject", {
total: "number",
offset: "number",
data: "array:nullable:cookieobject"
});
// Common methods for edit/remove
const editRemoveMethods = {
- getEditableFields: {
+ getFields: {
request: {},
response: {
value: RetVal("json")
}
},
editItem: {
request: {
data: Arg(0, "json"),