diff options
Diffstat (limited to 'devtools/server')
8 files changed, 570 insertions, 213 deletions
diff --git a/devtools/server/actors/storage.js b/devtools/server/actors/storage.js index 572cd6b68..015d849ee 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. @@ -135,6 +146,9 @@ StorageActors.defaults = function (typeName, observationTopic) { * Converts the window.location object into host. */ getHostName(location) { + if (location.protocol === "chrome:") { + return location.href; + } return location.hostname || location.href; }, @@ -315,15 +329,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 +352,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)); } } @@ -460,11 +482,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 +512,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 +548,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 +561,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 +576,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 +602,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 +629,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 +649,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 +671,8 @@ StorageActors.createActor({ cookieHelpers.removeCookie.bind(cookieHelpers); this.removeAllCookies = cookieHelpers.removeAllCookies.bind(cookieHelpers); + this.removeAllSessionCookies = + cookieHelpers.removeAllSessionCookies.bind(cookieHelpers); return; } @@ -642,6 +696,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); @@ -696,7 +752,7 @@ var cookieHelpers = { * { * host: "http://www.mozilla.org", * field: "value", - * key: "name", + * editCookie: "name", * oldValue: "%7BHello%7D", * newValue: "%7BHelloo%7D", * items: { @@ -720,10 +776,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 +803,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 +857,15 @@ 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]; + } + function hostMatches(cookieHost, matchHost) { if (cookieHost == null) { return matchHost == null; @@ -807,12 +876,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 +907,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 +975,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 +993,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"); @@ -1004,6 +1093,9 @@ function getObjectForLocalOrSessionStorage(type) { if (!location.host) { return location.href; } + if (location.protocol === "chrome:") { + return location.href; + } return location.protocol + "//" + location.host; }, @@ -1024,11 +1116,19 @@ function getObjectForLocalOrSessionStorage(type) { 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 +1243,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,8 +1310,8 @@ StorageActors.createActor({ getFields: Task.async(function* () { return [ - { name: "url", editable: 0 }, - { name: "status", editable: 0 } + { name: "url", editable: false }, + { name: "status", editable: false } ]; }), @@ -1214,6 +1319,9 @@ StorageActors.createActor({ if (!location.host) { return location.href; } + if (location.protocol === "chrome:") { + return location.href; + } return location.protocol + "//" + location.host; }, @@ -1386,12 +1494,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 +1522,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 @@ -1487,6 +1600,9 @@ StorageActors.createActor({ if (!location.host) { return location.href; } + if (location.protocol === "chrome:") { + return location.href; + } return location.protocol + "//" + location.host; }, @@ -1579,15 +1695,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 +1732,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 +1791,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 +1817,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 +1862,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 +1915,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 +1935,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 +1991,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 +2024,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 +2060,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 this.backToChild("getDBNamesForHost", {names: names}); + 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 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 +2162,9 @@ var indexedDBHelpers = { * name. */ getSanitizedHost(host) { + if (host.startsWith("about:")) { + host = "moz-safe-" + host; + } return host.replace(ILLEGAL_CHAR_REGEX, "+"); }, @@ -1963,7 +2178,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 +2239,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 +2260,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 +2382,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 +2399,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; @@ -2480,6 +2709,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/tests/browser/browser.ini b/devtools/server/tests/browser/browser.ini index c05933230..b7929e2b0 100644 --- a/devtools/server/tests/browser/browser.ini +++ b/devtools/server/tests/browser/browser.ini @@ -11,6 +11,7 @@ support-files = doc_perf.html navigate-first.html navigate-second.html + storage-cookies-same-name.html storage-dynamic-windows.html storage-listings.html storage-unsecured-iframe.html @@ -80,6 +81,7 @@ skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still di #[browser_perf-front-profiler-01.js] bug 1077464 #[browser_perf-front-profiler-05.js] bug 1077464 #[browser_perf-front-profiler-06.js] +[browser_storage_cookies-duplicate-names.js] [browser_storage_dynamic_windows.js] [browser_storage_listings.js] [browser_storage_updates.js] diff --git a/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js new file mode 100644 index 000000000..c1cf0aa72 --- /dev/null +++ b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js @@ -0,0 +1,105 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the storage panel is able to display multiple cookies with the same +// name (and different paths). + +const {StorageFront} = require("devtools/shared/fronts/storage"); +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this); + +const TESTDATA = { + "test1.example.org": [ + { + name: "name", + value: "value1", + expires: 0, + path: "/", + host: "test1.example.org", + isDomain: false, + isSecure: false, + }, + { + name: "name", + value: "value2", + expires: 0, + path: "/path2/", + host: "test1.example.org", + isDomain: false, + isSecure: false, + }, + { + name: "name", + value: "value3", + expires: 0, + path: "/path3/", + host: "test1.example.org", + isDomain: false, + isSecure: false, + } + ] +}; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies-same-name.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = StorageFront(client, form); + let data = yield front.listStores(); + + ok(data.cookies, "Cookies storage actor is present"); + + yield testCookies(data.cookies); + yield clearStorage(); + + // Forcing GC/CC to get rid of docshells and windows created by this test. + forceCollections(); + yield client.close(); + forceCollections(); + DebuggerServer.destroy(); + forceCollections(); +}); + +function testCookies(cookiesActor) { + let numHosts = Object.keys(cookiesActor.hosts).length; + is(numHosts, 1, "Correct number of host entries for cookies"); + return testCookiesObjects(0, cookiesActor.hosts, cookiesActor); +} + +var testCookiesObjects = Task.async(function* (index, hosts, cookiesActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + is(data.total, TESTDATA[host].length, + "Number of cookies in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of TESTDATA[host]) { + if (item.name === toMatch.name && + item.host === toMatch.host && + item.path === toMatch.path) { + found = true; + ok(true, "Found cookie " + item.name + " in response"); + is(item.value.str, toMatch.value, "The value matches."); + is(item.expires, toMatch.expires, "The expiry time matches."); + is(item.path, toMatch.path, "The path matches."); + is(item.host, toMatch.host, "The host matches."); + is(item.isSecure, toMatch.isSecure, "The isSecure value matches."); + is(item.isDomain, toMatch.isDomain, "The isDomain value matches."); + break; + } + } + ok(found, "cookie " + item.name + " should exist in response"); + } + }; + + ok(!!TESTDATA[host], "Host is present in the list : " + host); + matchItems(yield cookiesActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testCookiesObjects(++index, hosts, cookiesActor); +}); diff --git a/devtools/server/tests/browser/browser_storage_dynamic_windows.js b/devtools/server/tests/browser/browser_storage_dynamic_windows.js index 440c91222..91b4155d9 100644 --- a/devtools/server/tests/browser/browser_storage_dynamic_windows.js +++ b/devtools/server/tests/browser/browser_storage_dynamic_windows.js @@ -66,6 +66,7 @@ function markOutMatched(toBeEmptied, data, deleted) { info("Testing for " + storageType); for (let host in data[storageType]) { ok(toBeEmptied[storageType][host], "Host " + host + " found"); + if (!deleted) { for (let item of data[storageType][host]) { let index = toBeEmptied[storageType][host].indexOf(item); @@ -87,50 +88,6 @@ function markOutMatched(toBeEmptied, data, deleted) { } } -// function testReload(front) { -// info("Testing if reload works properly"); - -// let shouldBeEmptyFirst = Cu.cloneInto(beforeReload, {}); -// let shouldBeEmptyLast = Cu.cloneInto(beforeReload, {}); -// return new Promise(resolve => { - -// let onStoresUpdate = data => { -// info("in stores update of testReload"); -// // This might be second time stores update is happening, in which case, -// // data.deleted will be null. -// // OR.. This might be the first time on a super slow machine where both -// // data.deleted and data.added is missing in the first update. -// if (data.deleted) { -// markOutMatched(shouldBeEmptyFirst, data.deleted, true); -// } - -// if (!Object.keys(shouldBeEmptyFirst).length) { -// info("shouldBeEmptyFirst is empty now"); -// } - -// // stores-update call might not have data.added for the first time on -// // slow machines, in which case, data.added will be null -// if (data.added) { -// markOutMatched(shouldBeEmptyLast, data.added); -// } - -// if (!Object.keys(shouldBeEmptyLast).length) { -// info("Everything to be received is received."); -// endTestReloaded(); -// } -// }; - -// let endTestReloaded = () => { -// front.off("stores-update", onStoresUpdate); -// resolve(); -// }; - -// front.on("stores-update", onStoresUpdate); - -// content.location.reload(); -// }); -// } - function testAddIframe(front) { info("Testing if new iframe addition works properly"); return new Promise(resolve => { @@ -142,7 +99,10 @@ function testAddIframe(front) { "https://sectest1.example.org": ["iframe-s-ss1"] }, cookies: { - "sectest1.example.org": ["sc1"] + "sectest1.example.org": [ + getCookieId("sc1", "sectest1.example.org", + "/browser/devtools/server/tests/browser/") + ] }, indexedDB: { // empty because indexed db creation happens after the page load, so at @@ -150,7 +110,7 @@ function testAddIframe(front) { "https://sectest1.example.org": [] }, Cache: { - "https://sectest1.example.org":[] + "https://sectest1.example.org": [] } }; diff --git a/devtools/server/tests/browser/browser_storage_listings.js b/devtools/server/tests/browser/browser_storage_listings.js index 4ff3c3fc1..e47a320b8 100644 --- a/devtools/server/tests/browser/browser_storage_listings.js +++ b/devtools/server/tests/browser/browser_storage_listings.js @@ -20,15 +20,6 @@ const storeMap = { isSecure: false, }, { - name: "cs2", - value: "sessionCookie", - path: "/", - host: ".example.org", - expires: 0, - isDomain: true, - isSecure: false, - }, - { name: "c3", value: "foobar-2", expires: 2000000001000, @@ -130,24 +121,24 @@ 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 @@ -157,13 +148,13 @@ const IDBValues = { ], "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 @@ -172,7 +163,7 @@ const IDBValues = { }, objectStoreDetails: { "http://test1.example.org": { - idb1: [ + "idb1 (default)": [ { objectStore: "obj1", keyPath: "id", @@ -199,7 +190,7 @@ const IDBValues = { indexes: [] } ], - idb2: [ + "idb2 (default)": [ { objectStore: "obj3", keyPath: "id3", @@ -217,7 +208,7 @@ const IDBValues = { }, "http://sectest1.example.org" : {}, "https://sectest1.example.org": { - "idb-s1": [ + "idb-s1 (default)": [ { objectStore: "obj-s1", keyPath: "id", @@ -225,7 +216,7 @@ const IDBValues = { indexes: [] }, ], - "idb-s2": [ + "idb-s2 (default)": [ { objectStore: "obj-s2", keyPath: "id3", @@ -245,7 +236,7 @@ const IDBValues = { }, entries: { "http://test1.example.org": { - "idb1#obj1": [ + "idb1 (default)#obj1": [ { name: 1, value: { @@ -271,7 +262,7 @@ const IDBValues = { } } ], - "idb1#obj2": [ + "idb1 (default)#obj2": [ { name: 1, value: { @@ -282,11 +273,11 @@ const IDBValues = { } } ], - "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: { @@ -304,7 +295,7 @@ const IDBValues = { } } ], - "idb-s2#obj-s2": [ + "idb-s2 (default)#obj-s2": [ { name: 13, value: { @@ -337,7 +328,8 @@ function* testStores(data) { } function testCookies(cookiesActor) { - is(Object.keys(cookiesActor.hosts).length, 2, "Correct number of host entries for cookies"); + is(Object.keys(cookiesActor.hosts).length, 2, + "Correct number of host entries for cookies"); return testCookiesObjects(0, cookiesActor.hosts, cookiesActor); } @@ -346,9 +338,9 @@ var testCookiesObjects = Task.async(function* (index, hosts, cookiesActor) { let matchItems = data => { let cookiesLength = 0; for (let secureCookie of storeMap.cookies[host]) { - if (secureCookie.isSecure) { - ++cookiesLength; - } + if (secureCookie.isSecure) { + ++cookiesLength; + } } // Any secure cookies did not get stored in the database. is(data.total, storeMap.cookies[host].length - cookiesLength, @@ -478,17 +470,17 @@ var testIndexedDBs = Task.async(function* (index, hosts, indexedDBActor) { for (let item of data.data) { let found = false; for (let toMatch of IDBValues.dbDetails[host]) { - if (item.db == toMatch.db) { + if (item.uniqueKey == toMatch.db) { found = true; - ok(true, "Found indexed db " + item.db + " in response"); + ok(true, "Found indexed db " + item.uniqueKey + " in response"); is(item.origin, toMatch.origin, "The origin matches."); is(item.version, toMatch.version, "The version matches."); is(item.objectStores, toMatch.objectStores, - "The numebr of object stores matches."); + "The number of object stores matches."); break; } } - ok(found, "indexed db " + item.name + " should exist in response"); + ok(found, "indexed db " + item.uniqueKey + " should exist in response"); } }; diff --git a/devtools/server/tests/browser/browser_storage_updates.js b/devtools/server/tests/browser/browser_storage_updates.js index 28b2e509f..01a35cefc 100644 --- a/devtools/server/tests/browser/browser_storage_updates.js +++ b/devtools/server/tests/browser/browser_storage_updates.js @@ -27,7 +27,12 @@ const TESTS = [ expected: { added: { cookies: { - "test1.example.org": ["c1", "c2"] + "test1.example.org": [ + getCookieId("c1", "test1.example.org", + "/browser/devtools/server/tests/browser/"), + getCookieId("c2", "test1.example.org", + "/browser/devtools/server/tests/browser/") + ] }, localStorage: { "http://test1.example.org": ["l1"] @@ -48,7 +53,10 @@ const TESTS = [ expected: { changed: { cookies: { - "test1.example.org": ["c1"] + "test1.example.org": [ + getCookieId("c1", "test1.example.org", + "/browser/devtools/server/tests/browser/"), + ] } }, added: { @@ -74,7 +82,10 @@ const TESTS = [ expected: { deleted: { cookies: { - "test1.example.org": ["c2"] + "test1.example.org": [ + getCookieId("c2", "test1.example.org", + "/browser/devtools/server/tests/browser/"), + ] }, localStorage: { "http://test1.example.org": ["l1"] @@ -112,7 +123,10 @@ const TESTS = [ expected: { added: { cookies: { - "test1.example.org": ["c3"] + "test1.example.org": [ + getCookieId("c3", "test1.example.org", + "/browser/devtools/server/tests/browser/"), + ] }, sessionStorage: { "http://test1.example.org": ["s1", "s2"] @@ -125,7 +139,10 @@ const TESTS = [ }, deleted: { cookies: { - "test1.example.org": ["c1"] + "test1.example.org": [ + getCookieId("c1", "test1.example.org", + "/browser/devtools/server/tests/browser/"), + ] }, localStorage: { "http://test1.example.org": ["l2"] @@ -158,7 +175,10 @@ const TESTS = [ expected: { deleted: { cookies: { - "test1.example.org": ["c3"] + "test1.example.org": [ + getCookieId("c3", "test1.example.org", + "/browser/devtools/server/tests/browser/"), + ] } } } diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js index 1e7f09d95..5cf98c2b0 100644 --- a/devtools/server/tests/browser/head.js +++ b/devtools/server/tests/browser/head.js @@ -2,6 +2,10 @@ * 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/. */ +"use strict"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; @@ -19,6 +23,11 @@ const MAIN_DOMAIN = "http://test1.example.org/" + PATH; const ALT_DOMAIN = "http://sectest1.example.org/" + PATH; const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH; +// GUID to be used as a separator in compound keys. This must match the same +// constant in devtools/server/actors/storage.js, +// devtools/client/storage/ui.js and devtools/client/storage/test/head.js +const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"; + // All tests are asynchronous. waitForExplicitFinish(); @@ -94,7 +103,6 @@ function once(target, eventName, useCapture = false) { info("Waiting for event: '" + eventName + "' on " + target + "."); return new Promise(resolve => { - for (let [add, remove] of [ ["addEventListener", "removeEventListener"], ["addListener", "removeListener"], @@ -137,6 +145,8 @@ function getMockTabActor(win) { } registerCleanupFunction(function tearDown() { + Services.cookies.removeAll(); + while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } @@ -148,8 +158,11 @@ function idleWait(time) { function busyWait(time) { let start = Date.now(); + // eslint-disable-next-line let stack; - while (Date.now() - start < time) { stack = Components.stack; } + while (Date.now() - start < time) { + stack = Components.stack; + } } /** @@ -172,11 +185,12 @@ function waitUntil(predicate, interval = 10) { } function waitForMarkerType(front, types, predicate, - unpackFun = (name, data) => data.markers, - eventName = "timeline-data") -{ + unpackFun = (name, data) => data.markers, + eventName = "timeline-data") { types = [].concat(types); - predicate = predicate || function () { return true; }; + predicate = predicate || function () { + return true; + }; let filteredMarkers = []; let { promise, resolve } = defer(); @@ -190,9 +204,11 @@ function waitForMarkerType(front, types, predicate, let markers = unpackFun(name, data); info("Got markers: " + JSON.stringify(markers, null, 2)); - filteredMarkers = filteredMarkers.concat(markers.filter(m => types.indexOf(m.name) !== -1)); + filteredMarkers = filteredMarkers.concat( + markers.filter(m => types.indexOf(m.name) !== -1)); - if (types.every(t => filteredMarkers.some(m => m.name === t)) && predicate(filteredMarkers)) { + if (types.every(t => filteredMarkers.some(m => m.name === t)) && + predicate(filteredMarkers)) { front.off(eventName, handler); resolve(filteredMarkers); } @@ -201,3 +217,7 @@ function waitForMarkerType(front, types, predicate, return promise; } + +function getCookieId(name, domain, path) { + return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`; +} diff --git a/devtools/server/tests/browser/storage-cookies-same-name.html b/devtools/server/tests/browser/storage-cookies-same-name.html new file mode 100644 index 000000000..e3e092ec3 --- /dev/null +++ b/devtools/server/tests/browser/storage-cookies-same-name.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Storage inspector cookies with duplicate names</title> +</head> +<body onload="createCookies()"> +<script type="application/javascript;version=1.7"> +"use strict"; +function createCookies() { + document.cookie = "name=value1;path=/;"; + document.cookie = "name=value2;path=/path2/;"; + document.cookie = "name=value3;path=/path3/;"; +} + +window.removeCookie = function (name) { + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; +}; + +window.clearCookies = function () { + let cookies = document.cookie; + for (let cookie of cookies.split(";")) { + removeCookie(cookie.split("=")[0]); + } +}; +</script> +</body> +</html> |