Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
"use strict";
const { Actor } = require("resource://devtools/shared/protocol.js");
loader.lazyRequireGetter(
this,
"naturalSortCaseInsensitive",
true
);
// Maximum number of cookies/local storage key-value-pairs that can be sent
// over the wire to the client in one request.
const MAX_STORE_OBJECT_COUNT = 50;
exports.MAX_STORE_OBJECT_COUNT = MAX_STORE_OBJECT_COUNT;
const DEFAULT_VALUE = "value";
exports.DEFAULT_VALUE = DEFAULT_VALUE;
// 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}";
exports.SEPARATOR_GUID = SEPARATOR_GUID;
class BaseStorageActor extends Actor {
/**
* Base class with the common methods required by all storage actors.
*
* This base class 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 notification of the watched
* topic.
* - getNamesForHost : Given a host, get list of all known store names.
* - 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.
* "editable" is 1 means editable field; 0 means uneditable.
*
* @param {string} typeName
* The typeName of the actor.
*/
constructor(storageActor, typeName) {
super(storageActor.conn, specs.childSpecs[typeName]);
this.storageActor = storageActor;
// Map keyed by host name whose values are nested Maps.
// Nested maps are keyed by store names and values are store values.
// Store values are specific to each sub class.
// Map(host name => stores <Map(name => values )>)
// Map(string => stores <Map(string => any )>)
this.hostVsStores = new Map();
this.onWindowReady = this.onWindowReady.bind(this);
this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
this.storageActor.on("window-ready", this.onWindowReady);
this.storageActor.on("window-destroyed", this.onWindowDestroyed);
}
destroy() {
if (!this.storageActor) {
return;
}
this.storageActor.off("window-ready", this.onWindowReady);
this.storageActor.off("window-destroyed", this.onWindowDestroyed);
this.hostVsStores.clear();
super.destroy();
this.storageActor = null;
}
/**
* Returns a list of currently known hosts for the target window. This list
* contains unique hosts from the window + all inner windows. If
* this._internalHosts is defined then these will also be added to the list.
*/
get hosts() {
const hosts = new Set();
for (const { location } of this.storageActor.windows) {
const host = this.getHostName(location);
if (host) {
hosts.add(host);
}
}
if (this._internalHosts) {
for (const host of this._internalHosts) {
hosts.add(host);
}
}
return hosts;
}
/**
* Returns all the windows present on the page. Includes main window + inner
* iframe windows.
*/
get windows() {
return this.storageActor.windows;
}
/**
* Converts the window.location object into a URL (e.g. http://domain.com).
*/
getHostName(location) {
if (!location) {
// Debugging a legacy Firefox extension... no hostname available and no
// storage possible.
return null;
}
if (this.storageActor.getHostName) {
return this.storageActor.getHostName(location);
}
switch (location.protocol) {
case "about:":
return `${location.protocol}${location.pathname}`;
case "chrome:":
// chrome: URLs do not support storage of any type.
return null;
case "data:":
// data: URLs do not support storage of any type.
return null;
case "file:":
return `${location.protocol}//${location.pathname}`;
case "javascript:":
return location.href;
case "moz-extension:":
return location.origin;
case "resource:":
return `${location.origin}${location.pathname}`;
default:
// http: or unknown protocol.
return `${location.protocol}//${location.host}`;
}
}
/**
* Populates a map of known hosts vs a map of stores vs value.
*/
async populateStoresForHosts() {
for (const host of this.hosts) {
await this.populateStoresForHost(host);
}
}
getNamesForHost(host) {
return [...this.hostVsStores.get(host).keys()];
}
getValuesForHost(host, name) {
if (name) {
return [this.hostVsStores.get(host).get(name)];
}
return [...this.hostVsStores.get(host).values()];
}
getObjectsSize(host, names) {
return names.length;
}
/**
* When a new window is added to the page. This generally means that a new
* iframe is created, or the current window is completely reloaded.
*
* @param {window} window
* The window which was added.
*/
async onWindowReady(window) {
if (!this.hostVsStores) {
return;
}
const host = this.getHostName(window.location);
if (host && !this.hostVsStores.has(host)) {
await this.populateStoresForHost(host, window);
if (!this.storageActor) {
// The actor might be destroyed during populateStoresForHost.
return;
}
const data = {};
data[host] = this.getNamesForHost(host);
this.storageActor.update("added", this.typeName, data);
}
}
/**
* When a window is removed from the page. This generally means that an
* iframe was removed, or the current window reload is triggered.
*
* @param {window} window
* The window which was removed.
* @param {Object} options
* @param {Boolean} options.dontCheckHost
* If set to true, the function won't check if the host still is in this.hosts.
* This is helpful in the case of the StorageActorMock, as the `hosts` getter
* uses its `windows` getter, and at this point in time the window which is
* going to be destroyed still exists.
*/
onWindowDestroyed(window, { dontCheckHost } = {}) {
if (!this.hostVsStores) {
return;
}
if (!window.location) {
// Nothing can be done if location object is null
return;
}
const host = this.getHostName(window.location);
if (host && (!this.hosts.has(host) || dontCheckHost)) {
this.hostVsStores.delete(host);
const data = {};
data[host] = [];
this.storageActor.update("deleted", this.typeName, data);
}
}
form() {
const hosts = {};
for (const host of this.hosts) {
hosts[host] = [];
}
return {
actor: this.actorID,
hosts,
traits: this._getTraits(),
};
}
// Share getTraits for child classes overriding form()
_getTraits() {
return {
// The supportsXXX traits are not related to backward compatibility
// Different storage actor types implement different APIs, the traits
// help the client to know what is supported or not.
supportsAddItem: typeof this.addItem === "function",
// Note: supportsRemoveItem and supportsRemoveAll are always defined
// for all actors. See Bug 1655001.
supportsRemoveItem: typeof this.removeItem === "function",
supportsRemoveAll: typeof this.removeAll === "function",
supportsRemoveAllSessionCookies:
typeof this.removeAllSessionCookies === "function",
};
}
/**
* Returns a list of requested store objects. Maximum values returned are
* MAX_STORE_OBJECT_COUNT. This method returns paginated values whose
* starting index and total size can be controlled via the options object
*
* @param {string} host
* The host name for which the store values are required.
* @param {array:string} names
* Array containing the names of required store objects. Empty if all
* items are required.
* @param {object} options
* Additional options for the request containing following
* properties:
* - offset {number} : The begin index of the returned array amongst
* the total values
* - size {number} : The number of values required.
* - sortOn {string} : The values should be sorted on this property.
* - index {string} : In case of indexed db, the IDBIndex to be used
* for fetching the values.
* - sessionString {string} : Client-side value of storage-expires-session
* l10n string. Since this function can be called from both
* the client and the server, and given that client and
* server might have different locales, we can't compute
* the localized string directly from here.
* @return {object} An object containing following properties:
* - offset - The actual offset of the returned array. This might
* be different from the requested offset if that was
* invalid
* - total - The total number of entries possible.
* - data - The requested values.
*/
async getStoreObjects(host, names, options = {}) {
const offset = options.offset || 0;
let size = options.size || MAX_STORE_OBJECT_COUNT;
if (size > MAX_STORE_OBJECT_COUNT) {
size = MAX_STORE_OBJECT_COUNT;
}
const sortOn = options.sortOn || "name";
const toReturn = {
offset,
total: 0,
data: [],
};
let principal = null;
if (this.typeName === "indexedDB") {
// We only acquire principal when the type of the storage is indexedDB
// because the principal only matters the indexedDB.
const win = this.storageActor.getWindowFromHost(host);
principal = this.getPrincipal(win);
}
if (names) {
for (const name of names) {
const values = await this.getValuesForHost(
host,
name,
options,
this.hostVsStores,
principal
);
const { result, objectStores } = values;
if (result && typeof result.objectsSize !== "undefined") {
for (const { key, count } of result.objectsSize) {
this.objectsSize[key] = count;
}
}
if (result) {
toReturn.data.push(...result.data);
} else if (objectStores) {
toReturn.data.push(...objectStores);
} else {
toReturn.data.push(...values);
}
}
if (this.typeName === "Cache") {
// Cache storage contains several items per name but misses a custom
// `getObjectsSize` implementation, as implemented for IndexedDB.
// See Bug 1745242.
toReturn.total = toReturn.data.length;
} else {
toReturn.total = this.getObjectsSize(host, names, options);
}
} else {
let obj = await this.getValuesForHost(
host,
undefined,
undefined,
this.hostVsStores,
principal
);
if (obj.dbs) {
obj = obj.dbs;
}
toReturn.total = obj.length;
toReturn.data = obj;
}
if (offset > toReturn.total) {
// In this case, toReturn.data is an empty array.
toReturn.offset = toReturn.total;
toReturn.data = [];
} else {
// We need to use natural sort before slicing.
const sorted = toReturn.data.sort((a, b) => {
return naturalSortCaseInsensitive(
a[sortOn],
b[sortOn],
options.sessionString
);
});
let sliced;
if (this.typeName === "indexedDB") {
// indexedDB's getValuesForHost never returns *all* values available but only
// a slice, starting at the expected offset. Therefore the result is already
// sliced as expected.
sliced = sorted;
} else {
sliced = sorted.slice(offset, offset + size);
}
toReturn.data = sliced.map(a => this.toStoreObject(a));
}
return toReturn;
}
getPrincipal(win) {
if (win) {
return win.document.effectiveStoragePrincipal;
}
// We are running in the browser toolbox and viewing system DBs so we
// need to use system principal.
return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
}
}
exports.BaseStorageActor = BaseStorageActor;