Bug 1573201 - Add limited write support for extension storage.local data in addon debugger r=miker,rpl

* Update the extensionStorage actor to enable some writing to extension storage.local through the Storage panel client.
* All values in the client are displayed as strings, so the actor must stringify them before sending them to the client and parse them when receiving changes from the client. For this reason, there is currently limited write support.
  * Item values that are JSONifiable (numbers, strings, booleans, object literals, arrays and null) can be edited from the Storage panel.
    * Object literals and arrays are only editable if their values are JSONifiable, with a maximum nested depth of 2 (e.g. an object with a nested object is editable, provided the nested object contains only primitive values). Object literals' keys must also be strings to be editable.
  * Non-JSONifiable values cannot be edited, and will be represented by "{}" in most cases in the panel, though some non-JSONifiable values (undefined, Date, and BigInt) will be displayed as more readable strings.
  * Some modifications are a little more complex, requiring an IndexedDB transaction. This will be handled in a separate patch.
    * Item names cannot be edited from the Storage panel.
    * New items cannot be added from the Storage panel.
  * Any item can be removed.
  * All items can be removed at once.
* In-line comments referencing Bugs 1542038 and 1542039 indicate places where the implementation may differ for local storage versus the other storage areas in the actor.
* The parseItemValue method used in the client was moved to a shared directory, so that the actor could parse string values from the client in its editItem method.

Differential Revision: https://phabricator.services.mozilla.com/D34416

--HG--
rename : devtools/client/shared/vendor/JSON5_LICENSE => devtools/shared/storage/vendor/JSON5_LICENSE
rename : devtools/client/shared/vendor/JSON5_UPGRADING.md => devtools/shared/storage/vendor/JSON5_UPGRADING.md
rename : devtools/client/shared/vendor/json5.js => devtools/shared/storage/vendor/json5.js
rename : devtools/client/shared/vendor/stringvalidator/UPDATING.md => devtools/shared/storage/vendor/stringvalidator/UPDATING.md
rename : devtools/client/shared/vendor/stringvalidator/moz.build => devtools/shared/storage/vendor/stringvalidator/moz.build
rename : devtools/client/shared/vendor/stringvalidator/tests/unit/head_stringvalidator.js => devtools/shared/storage/vendor/stringvalidator/tests/unit/head_stringvalidator.js
rename : devtools/client/shared/vendor/stringvalidator/tests/unit/test_sanitizers.js => devtools/shared/storage/vendor/stringvalidator/tests/unit/test_sanitizers.js
rename : devtools/client/shared/vendor/stringvalidator/tests/unit/test_validators.js => devtools/shared/storage/vendor/stringvalidator/tests/unit/test_validators.js
rename : devtools/client/shared/vendor/stringvalidator/tests/unit/xpcshell.ini => devtools/shared/storage/vendor/stringvalidator/tests/unit/xpcshell.ini
rename : devtools/client/shared/vendor/stringvalidator/util/assert.js => devtools/shared/storage/vendor/stringvalidator/util/assert.js
rename : devtools/client/shared/vendor/stringvalidator/util/moz.build => devtools/shared/storage/vendor/stringvalidator/util/moz.build
rename : devtools/client/shared/vendor/stringvalidator/validator.js => devtools/shared/storage/vendor/stringvalidator/validator.js
extra : moz-landing-system : lando
This commit is contained in:
Bianca Danforth 2019-11-22 20:09:59 +00:00
parent 02c08eadfc
commit c71a7e9564
26 changed files with 1196 additions and 220 deletions

View File

@ -106,6 +106,7 @@ devtools/shared/node-properties/
devtools/shared/pretty-fast/
devtools/shared/sourcemap/
devtools/shared/sprintfjs/
devtools/shared/storage/vendor/*
devtools/shared/qrcode/decoder/
devtools/shared/qrcode/encoder/
devtools/client/inspector/markup/test/lib_*

View File

@ -4,14 +4,9 @@
# 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/.
DIRS += [
'stringvalidator',
]
DevToolsModules(
'fluent-react.js',
'immutable.js',
'json5.js',
'jszip.js',
'lodash.js',
'react-dom-factories.js',

View File

@ -614,12 +614,14 @@ TableWidget.prototype = {
if (this._editableFieldsEngine) {
this._editableFieldsEngine.selectors = selectors;
this._editableFieldsEngine.items = this.items;
} else {
this._editableFieldsEngine = new EditableFieldsEngine({
root: this.tbody,
onTab: this.onEditorTab,
onTriggerEvent: "dblclick",
selectors: selectors,
items: this.items,
});
this._editableFieldsEngine.on("change", this.onChange);
@ -1750,6 +1752,7 @@ function EditableFieldsEngine(options) {
this.selectors = options.selectors;
this.onTab = options.onTab;
this.onTriggerEvent = options.onTriggerEvent || "dblclick";
this.items = options.items;
this.edit = this.edit.bind(this);
this.cancelEdit = this.cancelEdit.bind(this);
@ -1838,6 +1841,14 @@ EditableFieldsEngine.prototype = {
return;
}
// Some item names and values are not parsable by the client or server so should not be
// editable.
const name = target.getAttribute("data-id");
const item = this.items.get(name);
if ("isValueEditable" in item && !item.isValueEditable) {
return;
}
target.scrollIntoView(false);
target.focus();

View File

@ -90,3 +90,4 @@ fail-if = fission
fail-if = fission
[browser_storage_sidebar_update.js]
[browser_storage_values.js]
[browser_storage_webext_storage_local.js]

View File

@ -0,0 +1,245 @@
/* 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/ */
/* globals browser BigInt */
"use strict";
loader.lazyRequireGetter(
this,
"DebuggerServer",
"devtools/server/debugger-server",
true
);
loader.lazyRequireGetter(
this,
"DebuggerClient",
"devtools/shared/client/debugger-client",
true
);
const { Toolbox } = require("devtools/client/framework/toolbox");
/**
* Initialize and connect a DebuggerServer and DebuggerClient. Note: This test
* does not use TargetFactory, so it has to set up the DebuggerServer and
* DebuggerClient on its own.
* @return {Promise} Resolves with an instance of the DebuggerClient class
*/
async function setupLocalDebuggerServerAndClient() {
DebuggerServer.init();
DebuggerServer.registerAllActors();
const client = new DebuggerClient(DebuggerServer.connectPipe());
await client.connect();
return client;
}
/**
* Set up and optionally open the `about:debugging` toolbox for a given extension.
* @param {String} id - The id for the extension to be targeted by the toolbox.
* @param {Object} options - Configuration options with various optional fields:
* - {Boolean} openToolbox - If true, open the toolbox
* @return {Promise} Resolves with a web extension actor target object and the toolbox
* and storage objects when the toolbox has been setup
*/
async function setupExtensionDebuggingToolbox(id, options = {}) {
const { openToolbox = false } = options;
const client = await setupLocalDebuggerServerAndClient();
const front = await client.mainRoot.getAddon({ id });
const target = await front.getTarget();
target.shouldCloseClient = true;
let toolbox;
let storage;
if (openToolbox) {
const res = await openStoragePanel(null, target, Toolbox.HostType.WINDOW);
({ toolbox, storage } = res);
}
return { target, toolbox, storage };
}
add_task(async function set_enable_extensionStorage_pref() {
await SpecialPowers.pushPrefEnv({
set: [["devtools.storage.extensionStorage.enabled", true]],
});
});
/**
* Since storage item values are represented in the client as strings in textboxes, not all
* JavaScript object types supported by the WE storage local API and its IndexedDB backend
* can be successfully stringified for display in the table much less parsed correctly when
* the user tries to edit a value in the panel. This test is expected to change over time
* as more and more value types are supported.
*/
add_task(
async function test_extension_toolbox_only_supported_values_editable() {
async function background() {
browser.test.onMessage.addListener(async (msg, ...args) => {
switch (msg) {
case "storage-local-set":
await browser.storage.local.set(args[0]);
break;
case "storage-local-get": {
const items = await browser.storage.local.get(args[0]);
for (const [key, val] of Object.entries(items)) {
browser.test.assertTrue(
val === args[1],
`New value ${val} is set for key ${key}.`
);
}
break;
}
case "storage-local-fireOnChanged": {
const listener = () => {
browser.storage.onChanged.removeListener(listener);
browser.test.sendMessage("storage-local-onChanged");
};
browser.storage.onChanged.addListener(listener);
// Call an API method implemented in the parent process
// to ensure that the listener has been registered
// in the main process before the test proceeds.
await browser.runtime.getPlatformInfo();
break;
}
default:
browser.test.fail(`Unexpected test message: ${msg}`);
}
browser.test.sendMessage(`${msg}:done`);
});
browser.test.sendMessage("extension-origin", window.location.origin);
}
const extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["storage"],
},
background,
useAddonManager: "temporary",
});
await extension.startup();
const host = await extension.awaitMessage("extension-origin");
const itemsSupported = {
arr: [1, 2],
bool: true,
null: null,
num: 4,
obj: { a: 123 },
str: "hi",
// Nested objects or arrays at most 2 levels deep should be editable
nestedArr: [
{
a: "b",
},
"c",
],
nestedObj: {
a: [1, 2],
b: 3,
},
};
const itemsUnsupported = {
arrBuffer: new ArrayBuffer(8),
bigint: BigInt(1),
blob: new Blob(
[
JSON.stringify(
{
hello: "world",
},
null,
2
),
],
{
type: "application/json",
}
),
date: new Date(0),
map: new Map().set("a", "b"),
regexp: /regexp/,
set: new Set().add(1).add("a"),
undef: undefined,
// Arrays and object literals with non-JSONifiable values should not be editable
arrWithMap: [1, new Map().set("a", 1)],
objWithArrayBuffer: { a: new ArrayBuffer(8) },
// Nested objects or arrays more than 2 levels deep should not be editable
deepNestedArr: [[{ a: "b" }, 3], 4],
deepNestedObj: {
a: {
b: [1, 2],
},
},
};
info("Add storage items from the extension");
const allItems = { ...itemsSupported, ...itemsUnsupported };
extension.sendMessage("storage-local-fireOnChanged");
await extension.awaitMessage("storage-local-fireOnChanged:done");
extension.sendMessage("storage-local-set", allItems);
info(
"Wait for the extension to add storage items and receive the 'onChanged' event"
);
await extension.awaitMessage("storage-local-set:done");
await extension.awaitMessage("storage-local-onChanged");
info("Open the addon toolbox storage panel");
const { target } = await setupExtensionDebuggingToolbox(extension.id, {
openToolbox: true,
});
await selectTreeItem(["extensionStorage", host]);
info("Verify that value types supported by the storage actor are editable");
let validate = true;
const newValue = "anotherValue";
const supportedIds = Object.keys(itemsSupported);
for (const id of supportedIds) {
startCellEdit(id, "value", newValue);
await editCell(id, "value", newValue, validate);
}
info("Verify that associated values have been changed in the extension");
extension.sendMessage(
"storage-local-get",
Object.keys(itemsSupported),
newValue
);
await extension.awaitMessage("storage-local-get:done");
info(
"Verify that value types not supported by the storage actor are uneditable"
);
const expectedValStrings = {
arrBuffer: "{}",
bigint: "1n",
blob: "{}",
date: "1970-01-01T00:00:00.000Z",
map: "{}",
regexp: "{}",
set: "{}",
undef: "undefined",
arrWithMap: "[1,{}]",
objWithArrayBuffer: '{"a":{}}',
deepNestedArr: '[[{"a":"b"},3],4]',
deepNestedObj: '{"a":{"b":[1,2]}}',
};
validate = false;
for (const id of Object.keys(itemsUnsupported)) {
startCellEdit(id, "value", validate);
checkCellUneditable(id, "value");
checkCell(id, "value", expectedValStrings[id]);
}
info("Shut down the test");
await gDevTools.closeToolbox(target);
await extension.unload();
await target.destroy();
}
);

View File

@ -131,12 +131,16 @@ async function openTabAndSetupStorage(url, options = {}) {
*
* @param cb {Function} Optional callback, if you don't want to use the returned
* promise
* @param target {Object} Optional, the target for the toolbox; defaults to a tab target
* @param hostType {Toolbox.HostType} Optional, type of host that will host the toolbox
*
* @return {Promise} a promise that resolves when the storage inspector is ready
*/
var openStoragePanel = async function(cb) {
var openStoragePanel = async function(cb, target, hostType) {
info("Opening the storage inspector");
const target = await TargetFactory.forTab(gBrowser.selectedTab);
if (!target) {
target = await TargetFactory.forTab(gBrowser.selectedTab);
}
let storage, toolbox;
@ -163,7 +167,7 @@ var openStoragePanel = async function(cb) {
}
info("Opening the toolbox");
toolbox = await gDevTools.showToolbox(target, "storage");
toolbox = await gDevTools.showToolbox(target, "storage", hostType);
storage = toolbox.getPanel("storage");
gPanelWindow = storage.panelWindow;
gUI = storage.UI;
@ -814,6 +818,28 @@ function checkCell(id, column, expected) {
);
}
/**
* Check that a cell is not in edit mode.
*
* @param {String} id
* The uniqueId of the row.
* @param {String} column
* The id of the column
*/
function checkCellUneditable(id, column) {
const row = getRowCells(id, true);
const cell = row[column];
const editableFieldsEngine = gUI.table._editableFieldsEngine;
const textbox = editableFieldsEngine.textbox;
// When a field is being edited, the cell is hidden, and the textbox is made visible.
ok(
!cell.hidden && textbox.hidden,
`The cell located in column ${column} and row ${id} is not editable.`
);
}
/**
* Show or hide a column.
*
@ -884,10 +910,10 @@ async function typeWithTerminator(str, terminator, validate = true) {
}
info("Typing " + str);
EventUtils.sendString(str);
EventUtils.sendString(str, gPanelWindow);
info("Pressing " + terminator);
EventUtils.synthesizeKey(terminator);
EventUtils.synthesizeKey(terminator, null, gPanelWindow);
if (validate) {
info("Validating results... waiting for ROW_EDIT event.");

View File

@ -7,6 +7,7 @@
const EventEmitter = require("devtools/shared/event-emitter");
const { LocalizationHelper, ELLIPSIS } = require("devtools/shared/l10n");
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
const { parseItemValue } = require("devtools/shared/storage/utils");
const { KeyCodes } = require("devtools/client/shared/keycodes");
const { getUnicodeHostname } = require("devtools/client/shared/unicode-url");
@ -33,12 +34,6 @@ loader.lazyImporter(
"VariablesView",
"resource://devtools/client/shared/widgets/VariablesView.jsm"
);
loader.lazyRequireGetter(
this,
"validator",
"devtools/client/shared/vendor/stringvalidator/validator"
);
loader.lazyRequireGetter(this, "JSON5", "devtools/client/shared/vendor/json5");
/**
* Localization convenience methods.
@ -76,7 +71,6 @@ const COOKIE_KEY_MAP = {
};
const SAFE_HOSTS_PREFIXES_REGEX = /^(about:|https?:|file:|moz-extension:)/;
const MATH_REGEX = /(?:(?:^|[-+_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-]?\d+)?\s*))+$/;
// Maximum length of item name to show in context menu label - will be
// trimmed with ellipsis if it's longer.
@ -834,6 +828,7 @@ class StorageUI {
* Populates the selected entry from the table in the sidebar for a more
* detailed view.
*/
/* eslint-disable-next-line */
async updateObjectSidebar() {
const item = this.table.selectedRow;
let value;
@ -872,7 +867,10 @@ class StorageUI {
itemVar.setGrip(value);
// May be the item value is a json or a key value pair itself
this.parseItemValue(item.name, value);
const obj = parseItemValue(value);
if (typeof obj === "object") {
this.populateSidebar(item.name, obj);
}
// By default the item name and value are shown. If this is the only
// information available, then nothing else is to be displayed.
@ -906,7 +904,10 @@ class StorageUI {
}
mainScope.addItem(key, {}, true).setGrip(item[key]);
this.parseItemValue(key, item[key]);
const obj = parseItemValue(item[key]);
if (typeof obj === "object") {
this.populateSidebar(item.name, obj);
}
}
}
@ -943,48 +944,12 @@ class StorageUI {
}
/**
* Tries to parse a string value into either a json or a key-value separated
* object and populates the sidebar with the parsed value. The value can also
* be a key separated array.
* Populates the sidebar with a parsed object.
*
* @param {string} name
* The key corresponding to the `value` string in the object
* @param {string} originalValue
* The string to be parsed into an object
* @param {object} obj - Either a json or a key-value separated object or a
* key separated array
*/
parseItemValue(name, originalValue) {
// Find if value is URLEncoded ie
let decodedValue = "";
try {
decodedValue = decodeURIComponent(originalValue);
} catch (e) {
// Unable to decode, nothing to do
}
const value =
decodedValue && decodedValue !== originalValue
? decodedValue
: originalValue;
if (!this._shouldParse(value)) {
return;
}
let obj = null;
try {
obj = JSON5.parse(value);
} catch (ex) {
obj = null;
}
if (!obj && value) {
obj = this._extractKeyValPairs(value);
}
// return if obj is null, or same as value, or just a string.
if (!obj || obj === value || typeof obj === "string") {
return;
}
populateSidebar(name, obj) {
const jsonObject = Object.create(null);
const view = this.view;
jsonObject[name] = obj;
@ -1000,101 +965,6 @@ class StorageUI {
jsonVar.populate(jsonObject, { expanded: true });
}
/**
* Tries to parse a string into an object on the basis of key-value pairs,
* separated by various separators. If failed, tries to parse for single
* separator separated values to form an array.
*
* @param {string} value
* The string to be parsed into an object or array
*/
_extractKeyValPairs(value) {
const makeObject = (keySep, pairSep) => {
const object = {};
for (const pair of value.split(pairSep)) {
const [key, val] = pair.split(keySep);
object[key] = val;
}
return object;
};
// Possible separators.
const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."];
// Testing for object
for (let i = 0; i < separators.length; i++) {
const kv = separators[i];
for (let j = 0; j < separators.length; j++) {
if (i == j) {
continue;
}
const p = separators[j];
const word = `[^${kv}${p}]*`;
const keyValue = `${word}${kv}${word}`;
const keyValueList = `${keyValue}(${p}${keyValue})*`;
const regex = new RegExp(`^${keyValueList}$`);
if (
value.match &&
value.match(regex) &&
value.includes(kv) &&
(value.includes(p) || value.split(kv).length == 2)
) {
return makeObject(kv, p);
}
}
}
// Testing for array
for (const p of separators) {
const word = `[^${p}]*`;
const wordList = `(${word}${p})+${word}`;
const regex = new RegExp(`^${wordList}$`);
if (regex.test(value)) {
const pNoBackslash = p.replace(/\\*/g, "");
return value.split(pNoBackslash);
}
}
return null;
}
/**
* Check whether the value string represents something that should be
* displayed as text. If so then it shouldn't be parsed into a tree.
*
* @param {String} value
* The value to be parsed.
*/
_shouldParse(value) {
const validators = [
"isBase64",
"isBoolean",
"isCurrency",
"isDataURI",
"isEmail",
"isFQDN",
"isHexColor",
"isIP",
"isISO8601",
"isMACAddress",
"isSemVer",
"isURL",
];
// Check for minus calculations e.g. 8-3 because otherwise 5 will be displayed.
if (MATH_REGEX.test(value)) {
return false;
}
// Check for any other types that shouldn't be parsed.
for (const test of validators) {
if (validator[test](value)) {
return false;
}
}
// Seems like this is data that should be parsed.
return true;
}
/**
* Select handler for the storage tree. Fetches details of the selected item
* from the storage details and populates the storage tree.

View File

@ -12,6 +12,7 @@ const Services = require("Services");
const defer = require("devtools/shared/defer");
const { isWindowIncluded } = require("devtools/shared/layout/utils");
const specs = require("devtools/shared/specs/storage");
const { parseItemValue } = require("devtools/shared/storage/utils");
loader.lazyGetter(this, "ExtensionProcessScript", () => {
return require("resource://gre/modules/ExtensionProcessScript.jsm")
.ExtensionProcessScript;
@ -1405,6 +1406,120 @@ const extensionStorageHelpers = {
// a separate extensionStorage actor targeting that addon. The addonId is passed into the listener,
// so that changes propagate only if the storage actor has a matching addonId.
onChangedChildListeners: new Set(),
/**
* Editing is supported only for serializable types. Examples of unserializable
* types include Map, Set and ArrayBuffer.
*/
isEditable(value) {
// Bug 1542038: the managed storage area is never editable
for (const { test } of Object.values(this.supportedTypes)) {
if (test(value)) {
return true;
}
}
return false;
},
isPrimitive(value) {
const primitiveValueTypes = ["string", "number", "boolean"];
return primitiveValueTypes.includes(typeof value) || value === null;
},
isObjectLiteral(value) {
return (
value &&
typeof value === "object" &&
Cu.getClassName(value, true) === "Object"
);
},
// Nested arrays or object literals are only editable 2 levels deep
isArrayOrObjectLiteralEditable(obj) {
const topLevelValuesArr = Array.isArray(obj) ? obj : Object.values(obj);
if (
topLevelValuesArr.some(
value =>
!this.isPrimitive(value) &&
!Array.isArray(value) &&
!this.isObjectLiteral(value)
)
) {
// At least one value is too complex to parse
return false;
}
const arrayOrObjects = topLevelValuesArr.filter(
value => Array.isArray(value) || this.isObjectLiteral(value)
);
if (arrayOrObjects.length === 0) {
// All top level values are primitives
return true;
}
// One or more top level values was an array or object literal.
// All of these top level values must themselves have only primitive values
// for the object to be editable
for (const nestedObj of arrayOrObjects) {
const secondLevelValuesArr = Array.isArray(nestedObj)
? nestedObj
: Object.values(nestedObj);
if (secondLevelValuesArr.some(value => !this.isPrimitive(value))) {
return false;
}
}
return true;
},
typesFromString: {
// Helper methods to parse string values in editItem
jsonifiable: {
test(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
},
parse(str) {
return JSON.parse(str);
},
},
},
supportedTypes: {
// Helper methods to determine the value type of an item in isEditable
array: {
test(value) {
if (Array.isArray(value)) {
return extensionStorageHelpers.isArrayOrObjectLiteralEditable(value);
}
return false;
},
},
boolean: {
test(value) {
return typeof value === "boolean";
},
},
null: {
test(value) {
return value === null;
},
},
number: {
test(value) {
return typeof value === "number";
},
},
object: {
test(value) {
if (extensionStorageHelpers.isObjectLiteral(value)) {
return extensionStorageHelpers.isArrayOrObjectLiteralEditable(value);
}
return false;
},
},
string: {
test(value) {
return typeof value === "string";
},
},
},
// Sets the parent process message manager
setPpmm(ppmm) {
@ -1774,11 +1889,14 @@ if (Services.prefs.getBoolPref(EXTENSION_STORAGE_ENABLED_PREF, false)) {
storeMap.set(key, value);
}
// Show the storage actor in the add-on storage inspector even when there
// is no extension page currently open
const storageData = {};
storageData[host] = this.getNamesForHost(host);
this.storageActor.update("added", this.typeName, storageData);
if (this.storageActor.parentActor.fallbackWindow) {
// Show the storage actor in the add-on storage inspector even when there
// is no extension page currently open
// This strategy may need to change depending on the outcome of Bug 1597900
const storageData = {};
storageData[host] = this.getNamesForHost(host);
this.storageActor.update("added", this.typeName, storageData);
}
},
async getStoragePrincipal(addonId) {
@ -1815,7 +1933,9 @@ if (Services.prefs.getBoolPref(EXTENSION_STORAGE_ENABLED_PREF, false)) {
/**
* Converts a storage item to an "extensionobject" as defined in
* devtools/shared/specs/storage.js
* devtools/shared/specs/storage.js. Behavior largely mirrors the "indexedDB" storage actor,
* except where it would throw an unhandled error (i.e. for a `BigInt` or `undefined`
* `item.value`).
* @param {Object} item - The storage item to convert
* @param {String} item.name - The storage item key
* @param {*} item.value - The storage item value
@ -1826,24 +1946,28 @@ if (Services.prefs.getBoolPref(EXTENSION_STORAGE_ENABLED_PREF, false)) {
return null;
}
const { name, value } = item;
let { name, value } = item;
let isValueEditable = extensionStorageHelpers.isEditable(value);
let newValue;
if (typeof value === "string") {
newValue = value;
} else {
try {
newValue = JSON.stringify(value) || String(value);
} catch (error) {
// throws for bigint
newValue = String(value);
}
// JavaScript objects that are not JSON stringifiable will be represented
// by the string "Object"
if (newValue === "{}") {
newValue = "Object";
}
// `JSON.stringify()` throws for `BigInt`, adds extra quotes to strings and `Date` strings,
// and doesn't modify `undefined`.
switch (typeof value) {
case "bigint":
value = `${value.toString()}n`;
break;
case "string":
break;
case "undefined":
value = "undefined";
break;
default:
value = JSON.stringify(value);
if (
// can't use `instanceof` across frame boundaries
Object.prototype.toString.call(item.value) === "[object Date]"
) {
value = JSON.parse(value);
}
}
// FIXME: Bug 1318029 - Due to a bug that is thrown whenever a
@ -1851,24 +1975,94 @@ if (Services.prefs.getBoolPref(EXTENSION_STORAGE_ENABLED_PREF, false)) {
// to trim the value. When the bug is fixed we should stop trimming the
// string here.
const maxLength = DebuggerServer.LONG_STRING_LENGTH - 1;
if (newValue.length > maxLength) {
newValue = newValue.substr(0, maxLength);
if (value.length > maxLength) {
value = value.substr(0, maxLength);
isValueEditable = false;
}
return {
name,
value: new LongStringActor(this.conn, newValue || ""),
value: new LongStringActor(this.conn, value),
area: "local", // Bug 1542038, 1542039: set the correct storage area
isValueEditable,
};
},
getFields() {
return [
{ name: "name", editable: false },
{ name: "value", editable: false },
{ name: "value", editable: true },
{ name: "area", editable: false },
{ name: "isValueEditable", editable: false, private: true },
];
},
onItemUpdated(action, host, names) {
this.storageActor.update(action, this.typeName, {
[host]: names,
});
},
async editItem({ host, field, items, oldValue }) {
const db = this.dbConnectionForHost.get(host);
if (!db) {
return;
}
const { name, value } = items;
let parsedValue = parseItemValue(value);
if (parsedValue === value) {
const { typesFromString } = extensionStorageHelpers;
for (const { test, parse } of Object.values(typesFromString)) {
if (test(value)) {
parsedValue = parse(value);
break;
}
}
}
const changes = await db.set({ [name]: parsedValue });
this.fireOnChangedExtensionEvent(host, changes);
this.onItemUpdated("changed", host, [name]);
},
async removeItem(host, name) {
const db = this.dbConnectionForHost.get(host);
if (!db) {
return;
}
const changes = await db.remove(name);
this.fireOnChangedExtensionEvent(host, changes);
this.onItemUpdated("deleted", host, [name]);
},
async removeAll(host) {
const db = this.dbConnectionForHost.get(host);
if (!db) {
return;
}
const changes = await db.clear();
this.fireOnChangedExtensionEvent(host, changes);
this.onItemUpdated("cleared", host, []);
},
/**
* Let the extension know that storage data has been changed by the user from
* the storage inspector.
*/
fireOnChangedExtensionEvent(host, changes) {
// Bug 1542038, 1542039: Which message to send depends on the storage area
const uuid = new URL(host).host;
Services.cpmm.sendAsyncMessage(
`Extension:StorageLocalOnChanged:${uuid}`,
changes
);
},
}
);
}

View File

@ -97,7 +97,7 @@ async function openAddonStoragePanel(id) {
const stores = await storageFront.listStores();
const extensionStorage = stores.extensionStorage || null;
return { target, extensionStorage };
return { target, extensionStorage, storageFront };
}
/**
@ -141,13 +141,13 @@ async function extensionScriptWithMessageListener() {
});
browser.test.onMessage.addListener(async (msg, ...args) => {
let value = null;
let item = null;
switch (msg) {
case "storage-local-set":
await browser.storage.local.set(args[0]);
break;
case "storage-local-get":
value = (await browser.storage.local.get(args[0]))[args[0]];
item = await browser.storage.local.get(args[0]);
break;
case "storage-local-remove":
await browser.storage.local.remove(args[0]);
@ -166,7 +166,7 @@ async function extensionScriptWithMessageListener() {
browser.test.fail(`Unexpected test message: ${msg}`);
}
browser.test.sendMessage(`${msg}:done`, value);
browser.test.sendMessage(`${msg}:done`, item);
});
browser.test.sendMessage("extension-origin", window.location.origin);
}
@ -331,6 +331,7 @@ add_task(async function test_panel_live_updates() {
area: "local",
name,
value: { str: String(value) },
isValueEditable: true,
});
}
data = (await extensionStorage.getStoreObjects(host)).data;
@ -338,12 +339,42 @@ add_task(async function test_panel_live_updates() {
data,
[
...bulkStorageObjects,
{ area: "local", name: "a", value: { str: "123" } },
{ area: "local", name: "b", value: { str: "[4,5]" } },
{ area: "local", name: "c", value: { str: '{"d":678}' } },
{ area: "local", name: "d", value: { str: "true" } },
{ area: "local", name: "e", value: { str: "hi" } },
{ area: "local", name: "f", value: { str: "null" } },
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
{
area: "local",
name: "b",
value: { str: "[4,5]" },
isValueEditable: true,
},
{
area: "local",
name: "c",
value: { str: '{"d":678}' },
isValueEditable: true,
},
{
area: "local",
name: "d",
value: { str: "true" },
isValueEditable: true,
},
{
area: "local",
name: "e",
value: { str: "hi" },
isValueEditable: true,
},
{
area: "local",
name: "f",
value: { str: "null" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -366,12 +397,42 @@ add_task(async function test_panel_live_updates() {
data,
[
...bulkStorageObjects,
{ area: "local", name: "a", value: { str: '["c","d"]' } },
{ area: "local", name: "b", value: { str: "456" } },
{ area: "local", name: "c", value: { str: "false" } },
{ area: "local", name: "d", value: { str: "true" } },
{ area: "local", name: "e", value: { str: "hi" } },
{ area: "local", name: "f", value: { str: "null" } },
{
area: "local",
name: "a",
value: { str: '["c","d"]' },
isValueEditable: true,
},
{
area: "local",
name: "b",
value: { str: "456" },
isValueEditable: true,
},
{
area: "local",
name: "c",
value: { str: "false" },
isValueEditable: true,
},
{
area: "local",
name: "d",
value: { str: "true" },
isValueEditable: true,
},
{
area: "local",
name: "e",
value: { str: "hi" },
isValueEditable: true,
},
{
area: "local",
name: "f",
value: { str: "null" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -390,9 +451,24 @@ add_task(async function test_panel_live_updates() {
data,
[
...bulkStorageObjects,
{ area: "local", name: "a", value: { str: '["c","d"]' } },
{ area: "local", name: "b", value: { str: "456" } },
{ area: "local", name: "c", value: { str: "false" } },
{
area: "local",
name: "a",
value: { str: '["c","d"]' },
isValueEditable: true,
},
{
area: "local",
name: "b",
value: { str: "456" },
isValueEditable: true,
},
{
area: "local",
name: "c",
value: { str: "false" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -446,7 +522,14 @@ add_task(
const { data } = await extensionStorage.getStoreObjects(host);
Assert.deepEqual(
data,
[{ area: "local", name: "a", value: { str: "123" } }],
[
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -488,7 +571,14 @@ add_task(async function test_panel_data_matches_extension_with_no_pages_open() {
const { data } = await extensionStorage.getStoreObjects(host);
Assert.deepEqual(
data,
[{ area: "local", name: "a", value: { str: "123" } }],
[
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -501,11 +591,10 @@ add_task(async function test_panel_data_matches_extension_with_no_pages_open() {
* - Open the add-on storage panel.
* - With the storage panel still open, open an extension page in a new tab that adds an
* item.
* - Assert:
* - The data in the storage panel should live update to match the item added by the
* extension.
* - If an extension page adds the same data again, the data in the storage panel should
* not change.
* - The data in the storage panel should live update to match the item added by the
* extension.
* - If an extension page adds the same data again, the data in the storage panel should
* not change.
*/
add_task(
async function test_panel_data_live_updates_for_extension_without_bg_page() {
@ -541,7 +630,14 @@ add_task(
data = (await extensionStorage.getStoreObjects(host)).data;
Assert.deepEqual(
data,
[{ area: "local", name: "a", value: { str: "123" } }],
[
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -553,7 +649,14 @@ add_task(
data = (await extensionStorage.getStoreObjects(host)).data;
Assert.deepEqual(
data,
[{ area: "local", name: "a", value: { str: "123" } }],
[
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
],
"The results are unchanged when an extension page adds duplicate items"
);
@ -562,6 +665,246 @@ add_task(
}
);
/**
* Test case: Bg page adds item while storage panel is open. Panel edits item's value.
* - Load extension with background page.
* - Open the add-on storage panel.
* - With the storage panel still open, add item from the background page.
* - Edit the value of the item in the storage panel
* - The data in the storage panel should match the item added by the extension.
* - The storage actor is correctly parsing and setting the string representation of
* the value in the storage local database when the item's value is edited in the
* storage panel
*/
add_task(
async function test_editing_items_in_panel_parses_supported_values_correctly() {
const extension = await startupExtension(
getExtensionConfig({ background: extensionScriptWithMessageListener })
);
const host = await extension.awaitMessage("extension-origin");
const { target, extensionStorage } = await openAddonStoragePanel(
extension.id
);
const oldItem = { a: 123 };
const key = Object.keys(oldItem)[0];
const oldValue = oldItem[key];
// A tuple representing information for a new value entered into the panel for oldItem:
// [
// value,
// editItem string representation of value,
// toStoreObject string representation of value,
// ]
const valueInfo = [
[true, "true", "true"],
["hi", "hi", "hi"],
[456, "456", "456"],
[{ b: 789 }, "{b: 789}", '{"b":789}'],
[[1, 2, 3], "[1, 2, 3]", "[1,2,3]"],
[null, "null", "null"],
];
for (const [value, editItemValueStr, toStoreObjectValueStr] of valueInfo) {
info("Setting a storage item through the extension");
extension.sendMessage("storage-local-fireOnChanged");
extension.sendMessage("storage-local-set", oldItem);
await extension.awaitMessage("storage-local-set:done");
await extension.awaitMessage("storage-local-onChanged");
info(
"Editing the storage item in the panel with a new value of a different type"
);
// When the user edits an item in the panel, they are entering a string into a
// textbox. This string is parsed by the storage actor's editItem method.
await extensionStorage.editItem({
host,
field: "value",
items: { name: key, value: editItemValueStr },
oldValue,
});
info(
"Verifying item in the storage actor matches the item edited in the panel"
);
const { data } = await extensionStorage.getStoreObjects(host);
Assert.deepEqual(
data,
[
{
area: "local",
name: key,
value: { str: toStoreObjectValueStr },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
// The view layer is separate from the database layer; therefore while values are
// stringified (via toStoreObject) for display in the client, the value (and its type)
// in the database is unchanged.
info(
"Verifying the expected new value matches the value fetched in the extension"
);
extension.sendMessage("storage-local-get", key);
const extItem = await extension.awaitMessage("storage-local-get:done");
Assert.deepEqual(
value,
extItem[key],
`The string value ${editItemValueStr} was correctly parsed to ${value}`
);
}
await shutdown(extension, target);
}
);
/**
* Test case: Modifying storage items from the panel update extension storage local data.
* - Load extension with background page.
* - Open the add-on storage panel. From the panel:
* - Edit the value of a storage item,
* - Remove a storage item,
* - Remove all of the storage items,
* - For each modification, the storage data retrieved by the extension should match the
* data in the panel.
*/
add_task(
async function test_modifying_items_in_panel_updates_extension_storage_data() {
const extension = await startupExtension(
getExtensionConfig({ background: extensionScriptWithMessageListener })
);
const host = await extension.awaitMessage("extension-origin");
const {
target,
extensionStorage,
storageFront,
} = await openAddonStoragePanel(extension.id);
const DEFAULT_VALUE = "value"; // global in devtools/server/actors/storage.js
let items = {
guid_1: DEFAULT_VALUE,
guid_2: DEFAULT_VALUE,
guid_3: DEFAULT_VALUE,
};
info("Adding storage items from the extension");
let storesUpdate = storageFront.once("stores-update");
extension.sendMessage("storage-local-set", items);
await extension.awaitMessage("storage-local-set:done");
info("Waiting for the storage actor to emit a 'stores-update' event");
let data = await storesUpdate;
Assert.deepEqual(
{
added: {
extensionStorage: {
[host]: ["guid_1", "guid_2", "guid_3"],
},
},
},
data,
"The change data from the storage actor's 'stores-update' event matches the changes made in the client."
);
info("Waiting for panel to edit some items");
storesUpdate = storageFront.once("stores-update");
await extensionStorage.editItem({
host,
field: "value",
items: { name: "guid_1", value: "anotherValue" },
DEFAULT_VALUE,
});
info("Waiting for the storage actor to emit a 'stores-update' event");
data = await storesUpdate;
Assert.deepEqual(
{
changed: {
extensionStorage: {
[host]: ["guid_1"],
},
},
},
data,
"The change data from the storage actor's 'stores-update' event matches the changes made in the client."
);
items = {
guid_1: "anotherValue",
guid_2: DEFAULT_VALUE,
guid_3: DEFAULT_VALUE,
};
extension.sendMessage("storage-local-get", Object.keys(items));
let extItems = await extension.awaitMessage("storage-local-get:done");
Assert.deepEqual(
items,
extItems,
`The storage items in the extension match the items in the panel`
);
info("Waiting for panel to remove an item");
storesUpdate = storageFront.once("stores-update");
await extensionStorage.removeItem(host, "guid_3");
info("Waiting for the storage actor to emit a 'stores-update' event");
data = await storesUpdate;
Assert.deepEqual(
{
deleted: {
extensionStorage: {
[host]: ["guid_3"],
},
},
},
data,
"The change data from the storage actor's 'stores-update' event matches the changes made in the client."
);
items = {
guid_1: "anotherValue",
guid_2: DEFAULT_VALUE,
};
extension.sendMessage("storage-local-get", Object.keys(items));
extItems = await extension.awaitMessage("storage-local-get:done");
Assert.deepEqual(
items,
extItems,
`The storage items in the extension match the items in the panel`
);
info("Waiting for panel to remove all items");
const storesCleared = storageFront.once("stores-cleared");
await extensionStorage.removeAll(host);
info("Waiting for the storage actor to emit a 'stores-cleared' event");
data = await storesCleared;
Assert.deepEqual(
{
extensionStorage: {
[host]: [],
},
},
data,
"The change data from the storage actor's 'stores-cleared' event matches the changes made in the client."
);
items = {};
extension.sendMessage("storage-local-get", Object.keys(items));
extItems = await extension.awaitMessage("storage-local-get:done");
Assert.deepEqual(
items,
extItems,
`The storage items in the extension match the items in the panel`
);
await shutdown(extension, target);
}
);
/**
* Test case: Storage panel shows extension storage data added prior to extension startup
* - Load extension that adds a storage item
@ -612,7 +955,14 @@ add_task(
let { data } = await extensionStorage.getStoreObjects(host);
Assert.deepEqual(
data,
[{ area: "local", name: "a", value: { str: "123" } }],
[
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -626,8 +976,18 @@ add_task(
Assert.deepEqual(
data,
[
{ area: "local", name: "a", value: { str: "123" } },
{ area: "local", name: "b", value: { str: "456" } },
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
{
area: "local",
name: "b",
value: { str: "456" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -701,7 +1061,14 @@ add_task(async function test_panel_live_reload() {
const { data } = await extensionStorage.getStoreObjects(host);
Assert.deepEqual(
data,
[{ area: "local", name: "a", value: { str: "123" } }],
[
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -772,7 +1139,14 @@ add_task(async function test_panel_live_reload_for_extension_without_bg_page() {
const { data } = await extensionStorage.getStoreObjects(host);
Assert.deepEqual(
data,
[{ area: "local", name: "a", value: { str: "123" } }],
[
{
area: "local",
name: "a",
value: { str: "123" },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
@ -835,8 +1209,74 @@ add_task(
Assert.deepEqual(
data,
[
{ area: "local", name: "a", value: { str: '{"b":123}' } },
{ area: "local", name: "c", value: { str: '{"d":456}' } },
{
area: "local",
name: "a",
value: { str: '{"b":123}' },
isValueEditable: true,
},
{
area: "local",
name: "c",
value: { str: '{"d":456}' },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);
await shutdown(extension, target);
}
);
/**
* Test case: Bg page adds one storage.local item and one storage.sync item.
* - Load extension with background page that automatically adds two storage items on startup.
* - Open the add-on storage panel.
* - Assert that only the storage.local item is shown in the panel.
*/
add_task(
async function test_panel_data_only_updates_for_storage_local_changes() {
async function background() {
await browser.storage.local.set({ a: { b: 123 } });
await browser.storage.sync.set({ c: { d: 456 } });
browser.test.sendMessage("extension-origin", window.location.origin);
}
// Using the storage.sync API requires a non-temporary extension ID, see Bug 1323228.
const EXTENSION_ID =
"test_panel_data_only_updates_for_storage_local_changes@xpcshell.mozilla.org";
const manifest = {
applications: {
gecko: {
id: EXTENSION_ID,
},
},
};
info("Loading and starting extension");
const extension = await startupExtension(
getExtensionConfig({ manifest, background })
);
info("Waiting for message from test extension");
const host = await extension.awaitMessage("extension-origin");
info("Opening storage panel");
const { target, extensionStorage } = await openAddonStoragePanel(
extension.id
);
const { data } = await extensionStorage.getStoreObjects(host);
Assert.deepEqual(
data,
[
{
area: "local",
name: "a",
value: { str: '{"b":123}' },
isValueEditable: true,
},
],
"Got the expected results on populated storage.local"
);

View File

@ -29,6 +29,7 @@ DIRS += [
'security',
'sprintfjs',
'specs',
'storage',
'transport',
'webconsole',
'worker',

View File

@ -170,6 +170,8 @@ createStorageSpec({
types.addDictType("extensionobject", {
name: "nullable:string",
value: "nullable:longstring",
area: "string",
isValueEditable: "boolean",
});
types.addDictType("extensionstoreobject", {
@ -181,7 +183,15 @@ types.addDictType("extensionstoreobject", {
createStorageSpec({
typeName: "extensionStorage",
storeObjectType: "extensionstoreobject",
methods: {},
// Same as storageMethods except for addItem
methods: Object.assign({}, editRemoveMethods, {
removeAll: {
request: {
host: Arg(0, "string"),
},
response: {},
},
}),
});
types.addDictType("cacheobject", {

View File

@ -0,0 +1,13 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DIRS += [
'vendor'
]
DevToolsModules(
'utils.js'
)

View File

@ -0,0 +1,156 @@
/* 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";
loader.lazyRequireGetter(
this,
"validator",
"devtools/shared/storage/vendor/stringvalidator/validator"
);
loader.lazyRequireGetter(this, "JSON5", "devtools/shared/storage/vendor/json5");
const MATH_REGEX = /(?:(?:^|[-+_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-]?\d+)?\s*))+$/;
/**
* Tries to parse a string into an object on the basis of key-value pairs,
* separated by various separators. If failed, tries to parse for single
* separator separated values to form an array.
*
* @param {string} value
* The string to be parsed into an object or array
*/
function _extractKeyValPairs(value) {
const makeObject = (keySep, pairSep) => {
const object = {};
for (const pair of value.split(pairSep)) {
const [key, val] = pair.split(keySep);
object[key] = val;
}
return object;
};
// Possible separators.
const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."];
// Testing for object
for (let i = 0; i < separators.length; i++) {
const kv = separators[i];
for (let j = 0; j < separators.length; j++) {
if (i == j) {
continue;
}
const p = separators[j];
const word = `[^${kv}${p}]*`;
const keyValue = `${word}${kv}${word}`;
const keyValueList = `${keyValue}(${p}${keyValue})*`;
const regex = new RegExp(`^${keyValueList}$`);
if (
value.match &&
value.match(regex) &&
value.includes(kv) &&
(value.includes(p) || value.split(kv).length == 2)
) {
return makeObject(kv, p);
}
}
}
// Testing for array
for (const p of separators) {
const word = `[^${p}]*`;
const wordList = `(${word}${p})+${word}`;
const regex = new RegExp(`^${wordList}$`);
if (regex.test(value)) {
const pNoBackslash = p.replace(/\\*/g, "");
return value.split(pNoBackslash);
}
}
return null;
}
/**
* Check whether the value string represents something that should be
* displayed as text. If so then it shouldn't be parsed into a tree.
*
* @param {String} value
* The value to be parsed.
*/
function _shouldParse(value) {
const validators = [
"isBase64",
"isBoolean",
"isCurrency",
"isDataURI",
"isEmail",
"isFQDN",
"isHexColor",
"isIP",
"isISO8601",
"isMACAddress",
"isSemVer",
"isURL",
];
// Check for minus calculations e.g. 8-3 because otherwise 5 will be displayed.
if (MATH_REGEX.test(value)) {
return false;
}
// Check for any other types that shouldn't be parsed.
for (const test of validators) {
if (validator[test](value)) {
return false;
}
}
// Seems like this is data that should be parsed.
return true;
}
/**
* Tries to parse a string value into either a json or a key-value separated
* object. The value can also be a key separated array.
*
* @param {string} originalValue
* The string to be parsed into an object
*/
function parseItemValue(originalValue) {
// Find if value is URLEncoded ie
let decodedValue = "";
try {
decodedValue = decodeURIComponent(originalValue);
} catch (e) {
// Unable to decode, nothing to do
}
const value =
decodedValue && decodedValue !== originalValue
? decodedValue
: originalValue;
if (!_shouldParse(value)) {
return value;
}
let obj = null;
try {
obj = JSON5.parse(value);
} catch (ex) {
obj = null;
}
if (!obj && value) {
obj = _extractKeyValPairs(value);
}
// return if obj is null, or same as value, or just a string.
if (!obj || obj === value || typeof obj === "string") {
return value;
}
// If we got this far, originalValue is an object literal or array,
// and we have successfully parsed it
return obj;
}
exports.parseItemValue = parseItemValue;

View File

@ -17,7 +17,7 @@ git checkout v2.1.0 # checkout the right version tag
```bash
npm install
npm run build
cp dist/index.js <gecko-dev>/devtools/client/shared/vendor/json5.js
cp dist/index.js <gecko-dev>/devtools/shared/storage/vendor/json5.js
```
## Patching json5

View File

@ -0,0 +1,13 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DIRS += [
'stringvalidator',
]
DevToolsModules(
'json5.js',
)

View File

@ -2,7 +2,7 @@
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
this.validator = require("devtools/client/shared/vendor/stringvalidator/validator");
this.validator = require("devtools/shared/storage/vendor/stringvalidator/validator");
function describe(suite, testFunc) {
info(`\n Test suite: ${suite}`.toUpperCase());

View File

@ -6,7 +6,7 @@
"use strict";
var assert = require('devtools/client/shared/vendor/stringvalidator/util/assert').assert;
var assert = require('devtools/shared/storage/vendor/stringvalidator/util/assert').assert;
function test(options) {
var args = options.args || [];