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();
 | 
