diff options
Diffstat (limited to 'toolkit/components/extensions/ExtensionStorageSync.sys.mjs')
-rw-r--r-- | toolkit/components/extensions/ExtensionStorageSync.sys.mjs | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ExtensionStorageSync.sys.mjs b/toolkit/components/extensions/ExtensionStorageSync.sys.mjs new file mode 100644 index 0000000000..d41cf5af12 --- /dev/null +++ b/toolkit/components/extensions/ExtensionStorageSync.sys.mjs @@ -0,0 +1,201 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* 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/. */ + +const STORAGE_SYNC_ENABLED_PREF = "webextensions.storage.sync.enabled"; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const NS_ERROR_DOM_QUOTA_EXCEEDED_ERR = 0x80530016; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs", + ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", + // We might end up falling back to kinto... + extensionStorageSyncKinto: + "resource://gre/modules/ExtensionStorageSyncKinto.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "prefPermitsStorageSync", + STORAGE_SYNC_ENABLED_PREF, + true +); + +// This xpcom service implements a "bridge" from the JS world to the Rust world. +// It sets up the database and implements a callback-based version of the +// browser.storage API. +ChromeUtils.defineLazyGetter(lazy, "storageSvc", () => + Cc["@mozilla.org/extensions/storage/sync;1"] + .getService(Ci.nsIInterfaceRequestor) + .getInterface(Ci.mozIExtensionStorageArea) +); + +// The interfaces which define the callbacks used by the bridge. There's a +// callback for success, failure, and to record data changes. +function ExtensionStorageApiCallback(resolve, reject, changeCallback) { + this.resolve = resolve; + this.reject = reject; + this.changeCallback = changeCallback; +} + +ExtensionStorageApiCallback.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "mozIExtensionStorageListener", + "mozIExtensionStorageCallback", + ]), + + handleSuccess(result) { + this.resolve(result ? JSON.parse(result) : null); + }, + + handleError(code, message) { + let e = new Error(message); + e.code = code; + Cu.reportError(e); + this.reject(e); + }, + + onChanged(extId, json) { + if (this.changeCallback && json) { + try { + this.changeCallback(extId, JSON.parse(json)); + } catch (ex) { + Cu.reportError(ex); + } + } + }, +}; + +// The backing implementation of the browser.storage.sync web extension API. +export class ExtensionStorageSync { + constructor() { + this.listeners = new Map(); + // We are optimistic :) If we ever see the special nsresult which indicates + // migration failure, it will become false. In practice, this will only ever + // happen on the first operation. + this.migrationOk = true; + } + + // The main entry-point to our bridge. It performs some important roles: + // * Ensures the API is allowed to be used. + // * Works out what "extension id" to use. + // * Turns the callback API into a promise API. + async _promisify(fnName, extension, context, ...args) { + let extId = extension.id; + if (lazy.prefPermitsStorageSync !== true) { + throw new lazy.ExtensionUtils.ExtensionError( + `Please set ${STORAGE_SYNC_ENABLED_PREF} to true in about:config` + ); + } + + if (this.migrationOk) { + // We can call ours. + try { + return await new Promise((resolve, reject) => { + let callback = new ExtensionStorageApiCallback( + resolve, + reject, + (extId, changes) => this.notifyListeners(extId, changes) + ); + let sargs = args.map(val => JSON.stringify(val)); + lazy.storageSvc[fnName](extId, ...sargs, callback); + }); + } catch (ex) { + if (ex.code != Cr.NS_ERROR_CANNOT_CONVERT_DATA) { + // Some non-migration related error we want to sanitize and propagate. + // The only "public" exception here is for quota failure - all others + // are sanitized. + let sanitized = + ex.code == NS_ERROR_DOM_QUOTA_EXCEEDED_ERR + ? // The same message as the local IDB implementation + `QuotaExceededError: storage.sync API call exceeded its quota limitations.` + : // The standard, generic extension error. + "An unexpected error occurred"; + throw new lazy.ExtensionUtils.ExtensionError(sanitized); + } + // This means "migrate failed" so we must fall back to kinto. + Cu.reportError( + "migration of extension-storage failed - will fall back to kinto" + ); + this.migrationOk = false; + } + } + // We've detected failure to migrate, so we want to use kinto. + return lazy.extensionStorageSyncKinto[fnName](extension, ...args, context); + } + + set(extension, items, context) { + return this._promisify("set", extension, context, items); + } + + remove(extension, keys, context) { + return this._promisify("remove", extension, context, keys); + } + + clear(extension, context) { + return this._promisify("clear", extension, context); + } + + clearOnUninstall(extensionId) { + if (!this.migrationOk) { + // If the rust-based backend isn't being used, + // no need to clear it. + return; + } + // Resolve the returned promise once the request has been either resolved + // or rejected (and report the error on the browser console in case of + // unexpected clear failures on addon uninstall). + return new Promise(resolve => { + const callback = new ExtensionStorageApiCallback( + resolve, + err => { + Cu.reportError(err); + resolve(); + }, + // empty changeCallback (no need to notify the extension + // while clearing the extension on uninstall). + () => {} + ); + lazy.storageSvc.clear(extensionId, callback); + }); + } + + get(extension, spec, context) { + return this._promisify("get", extension, context, spec); + } + + getBytesInUse(extension, keys, context) { + return this._promisify("getBytesInUse", extension, context, keys); + } + + addOnChangedListener(extension, listener, context) { + let listeners = this.listeners.get(extension.id) || new Set(); + listeners.add(listener); + this.listeners.set(extension.id, listeners); + } + + removeOnChangedListener(extension, listener) { + let listeners = this.listeners.get(extension.id); + listeners.delete(listener); + if (listeners.size == 0) { + this.listeners.delete(extension.id); + } + } + + notifyListeners(extId, changes) { + let listeners = this.listeners.get(extId) || new Set(); + if (listeners) { + for (let listener of listeners) { + lazy.ExtensionCommon.runSafeSyncWithoutClone(listener, changes); + } + } + } +} + +export var extensionStorageSync = new ExtensionStorageSync(); |