diff --git a/.eslintignore b/.eslintignore index e894ad7938f2..137ddad03d3a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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_* diff --git a/devtools/client/shared/vendor/moz.build b/devtools/client/shared/vendor/moz.build index 59fd377d63c0..a7e6ec5b1a83 100644 --- a/devtools/client/shared/vendor/moz.build +++ b/devtools/client/shared/vendor/moz.build @@ -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', diff --git a/devtools/client/shared/widgets/TableWidget.js b/devtools/client/shared/widgets/TableWidget.js index e37d1e9903a9..ca11f724ff73 100644 --- a/devtools/client/shared/widgets/TableWidget.js +++ b/devtools/client/shared/widgets/TableWidget.js @@ -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(); diff --git a/devtools/client/storage/test/browser.ini b/devtools/client/storage/test/browser.ini index 7d744009bd27..c02e0ddcd2da 100644 --- a/devtools/client/storage/test/browser.ini +++ b/devtools/client/storage/test/browser.ini @@ -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] diff --git a/devtools/client/storage/test/browser_storage_webext_storage_local.js b/devtools/client/storage/test/browser_storage_webext_storage_local.js new file mode 100644 index 000000000000..0076f822e299 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_webext_storage_local.js @@ -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(); + } +); diff --git a/devtools/client/storage/test/head.js b/devtools/client/storage/test/head.js index 2fce5f4a60d5..331f6971b039 100644 --- a/devtools/client/storage/test/head.js +++ b/devtools/client/storage/test/head.js @@ -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."); diff --git a/devtools/client/storage/ui.js b/devtools/client/storage/ui.js index 44ea3138f8a5..82f2e0812137 100644 --- a/devtools/client/storage/ui.js +++ b/devtools/client/storage/ui.js @@ -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. diff --git a/devtools/server/actors/storage.js b/devtools/server/actors/storage.js index f816e291bf2e..6cc9b35ab8b5 100644 --- a/devtools/server/actors/storage.js +++ b/devtools/server/actors/storage.js @@ -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 + ); + }, } ); } diff --git a/devtools/server/tests/unit/test_extension_storage_actor.js b/devtools/server/tests/unit/test_extension_storage_actor.js index 28010e98dfd1..5168c204e847 100644 --- a/devtools/server/tests/unit/test_extension_storage_actor.js +++ b/devtools/server/tests/unit/test_extension_storage_actor.js @@ -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" ); diff --git a/devtools/shared/moz.build b/devtools/shared/moz.build index 3bf70d668075..928a78f35547 100644 --- a/devtools/shared/moz.build +++ b/devtools/shared/moz.build @@ -29,6 +29,7 @@ DIRS += [ 'security', 'sprintfjs', 'specs', + 'storage', 'transport', 'webconsole', 'worker', diff --git a/devtools/shared/specs/storage.js b/devtools/shared/specs/storage.js index 600c5cd2db44..448230cd7061 100644 --- a/devtools/shared/specs/storage.js +++ b/devtools/shared/specs/storage.js @@ -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", { diff --git a/devtools/shared/storage/moz.build b/devtools/shared/storage/moz.build new file mode 100644 index 000000000000..35bb70f2fe04 --- /dev/null +++ b/devtools/shared/storage/moz.build @@ -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' +) diff --git a/devtools/shared/storage/utils.js b/devtools/shared/storage/utils.js new file mode 100644 index 000000000000..2ea7d5b40ed2 --- /dev/null +++ b/devtools/shared/storage/utils.js @@ -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; diff --git a/devtools/client/shared/vendor/JSON5_LICENSE b/devtools/shared/storage/vendor/JSON5_LICENSE similarity index 100% rename from devtools/client/shared/vendor/JSON5_LICENSE rename to devtools/shared/storage/vendor/JSON5_LICENSE diff --git a/devtools/client/shared/vendor/JSON5_UPGRADING.md b/devtools/shared/storage/vendor/JSON5_UPGRADING.md similarity index 91% rename from devtools/client/shared/vendor/JSON5_UPGRADING.md rename to devtools/shared/storage/vendor/JSON5_UPGRADING.md index d8d275fa9d0e..038421f9453b 100644 --- a/devtools/client/shared/vendor/JSON5_UPGRADING.md +++ b/devtools/shared/storage/vendor/JSON5_UPGRADING.md @@ -17,7 +17,7 @@ git checkout v2.1.0 # checkout the right version tag ```bash npm install npm run build -cp dist/index.js /devtools/client/shared/vendor/json5.js +cp dist/index.js /devtools/shared/storage/vendor/json5.js ``` ## Patching json5 diff --git a/devtools/client/shared/vendor/json5.js b/devtools/shared/storage/vendor/json5.js similarity index 100% rename from devtools/client/shared/vendor/json5.js rename to devtools/shared/storage/vendor/json5.js diff --git a/devtools/shared/storage/vendor/moz.build b/devtools/shared/storage/vendor/moz.build new file mode 100644 index 000000000000..5e8a82199a3c --- /dev/null +++ b/devtools/shared/storage/vendor/moz.build @@ -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', +) \ No newline at end of file diff --git a/devtools/client/shared/vendor/stringvalidator/UPDATING.md b/devtools/shared/storage/vendor/stringvalidator/UPDATING.md similarity index 100% rename from devtools/client/shared/vendor/stringvalidator/UPDATING.md rename to devtools/shared/storage/vendor/stringvalidator/UPDATING.md diff --git a/devtools/client/shared/vendor/stringvalidator/moz.build b/devtools/shared/storage/vendor/stringvalidator/moz.build similarity index 100% rename from devtools/client/shared/vendor/stringvalidator/moz.build rename to devtools/shared/storage/vendor/stringvalidator/moz.build diff --git a/devtools/client/shared/vendor/stringvalidator/tests/unit/head_stringvalidator.js b/devtools/shared/storage/vendor/stringvalidator/tests/unit/head_stringvalidator.js similarity index 80% rename from devtools/client/shared/vendor/stringvalidator/tests/unit/head_stringvalidator.js rename to devtools/shared/storage/vendor/stringvalidator/tests/unit/head_stringvalidator.js index 171d2a8e3f5a..b68cdd3ed1ed 100644 --- a/devtools/client/shared/vendor/stringvalidator/tests/unit/head_stringvalidator.js +++ b/devtools/shared/storage/vendor/stringvalidator/tests/unit/head_stringvalidator.js @@ -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()); diff --git a/devtools/client/shared/vendor/stringvalidator/tests/unit/test_sanitizers.js b/devtools/shared/storage/vendor/stringvalidator/tests/unit/test_sanitizers.js similarity index 100% rename from devtools/client/shared/vendor/stringvalidator/tests/unit/test_sanitizers.js rename to devtools/shared/storage/vendor/stringvalidator/tests/unit/test_sanitizers.js diff --git a/devtools/client/shared/vendor/stringvalidator/tests/unit/test_validators.js b/devtools/shared/storage/vendor/stringvalidator/tests/unit/test_validators.js similarity index 99% rename from devtools/client/shared/vendor/stringvalidator/tests/unit/test_validators.js rename to devtools/shared/storage/vendor/stringvalidator/tests/unit/test_validators.js index 26a5ead8cb35..2f7d2a3b4e04 100644 --- a/devtools/client/shared/vendor/stringvalidator/tests/unit/test_validators.js +++ b/devtools/shared/storage/vendor/stringvalidator/tests/unit/test_validators.js @@ -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 || []; diff --git a/devtools/client/shared/vendor/stringvalidator/tests/unit/xpcshell.ini b/devtools/shared/storage/vendor/stringvalidator/tests/unit/xpcshell.ini similarity index 100% rename from devtools/client/shared/vendor/stringvalidator/tests/unit/xpcshell.ini rename to devtools/shared/storage/vendor/stringvalidator/tests/unit/xpcshell.ini diff --git a/devtools/client/shared/vendor/stringvalidator/util/assert.js b/devtools/shared/storage/vendor/stringvalidator/util/assert.js similarity index 100% rename from devtools/client/shared/vendor/stringvalidator/util/assert.js rename to devtools/shared/storage/vendor/stringvalidator/util/assert.js diff --git a/devtools/client/shared/vendor/stringvalidator/util/moz.build b/devtools/shared/storage/vendor/stringvalidator/util/moz.build similarity index 100% rename from devtools/client/shared/vendor/stringvalidator/util/moz.build rename to devtools/shared/storage/vendor/stringvalidator/util/moz.build diff --git a/devtools/client/shared/vendor/stringvalidator/validator.js b/devtools/shared/storage/vendor/stringvalidator/validator.js similarity index 100% rename from devtools/client/shared/vendor/stringvalidator/validator.js rename to devtools/shared/storage/vendor/stringvalidator/validator.js