forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			201 lines
		
	
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			201 lines
		
	
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- 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();
 |