308 lines
9.3 KiB
JavaScript
308 lines
9.3 KiB
JavaScript
/* 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/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
import {
|
|
BridgedEngine,
|
|
BridgeWrapperXPCOM,
|
|
LogAdapter,
|
|
} from "resource://services-sync/bridged_engine.sys.mjs";
|
|
import { SyncEngine, Tracker } from "resource://services-sync/engines.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
MULTI_DEVICE_THRESHOLD: "resource://services-sync/constants.sys.mjs",
|
|
Observers: "resource://services-common/observers.sys.mjs",
|
|
SCORE_INCREMENT_MEDIUM: "resource://services-sync/constants.sys.mjs",
|
|
Svc: "resource://services-sync/util.sys.mjs",
|
|
extensionStorageSync: "resource://gre/modules/ExtensionStorageSync.sys.mjs",
|
|
|
|
extensionStorageSyncKinto:
|
|
"resource://gre/modules/ExtensionStorageSyncKinto.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"StorageSyncService",
|
|
"@mozilla.org/extensions/storage/sync;1",
|
|
"nsIInterfaceRequestor"
|
|
);
|
|
|
|
const PREF_FORCE_ENABLE = "engine.extension-storage.force";
|
|
|
|
// A helper to indicate whether extension-storage is enabled - it's based on
|
|
// the "addons" pref. The same logic is shared between both engine impls.
|
|
function getEngineEnabled() {
|
|
// By default, we sync extension storage if we sync addons. This
|
|
// lets us simplify the UX since users probably don't consider
|
|
// "extension preferences" a separate category of syncing.
|
|
// However, we also respect engine.extension-storage.force, which
|
|
// can be set to true or false, if a power user wants to customize
|
|
// the behavior despite the lack of UI.
|
|
if (
|
|
lazy.Svc.PrefBranch.getPrefType(PREF_FORCE_ENABLE) !=
|
|
Ci.nsIPrefBranch.PREF_INVALID
|
|
) {
|
|
return lazy.Svc.PrefBranch.getBoolPref(PREF_FORCE_ENABLE);
|
|
}
|
|
return lazy.Svc.PrefBranch.getBoolPref("engine.addons", false);
|
|
}
|
|
|
|
function setEngineEnabled(enabled) {
|
|
// This will be called by the engine manager when declined on another device.
|
|
// Things will go a bit pear-shaped if the engine manager tries to end up
|
|
// with 'addons' and 'extension-storage' in different states - however, this
|
|
// *can* happen given we support the `engine.extension-storage.force`
|
|
// preference. So if that pref exists, we set it to this value. If that pref
|
|
// doesn't exist, we just ignore it and hope that the 'addons' engine is also
|
|
// going to be set to the same state.
|
|
if (
|
|
lazy.Svc.PrefBranch.getPrefType(PREF_FORCE_ENABLE) !=
|
|
Ci.nsIPrefBranch.PREF_INVALID
|
|
) {
|
|
lazy.Svc.PrefBranch.setBoolPref(PREF_FORCE_ENABLE, enabled);
|
|
}
|
|
}
|
|
|
|
// A "bridged engine" to our webext-storage component.
|
|
export function ExtensionStorageEngineBridge(service) {
|
|
this.component = lazy.StorageSyncService.getInterface(
|
|
Ci.mozIBridgedSyncEngine
|
|
);
|
|
BridgedEngine.call(this, "Extension-Storage", service);
|
|
this._bridge = new BridgeWrapperXPCOM(this.component);
|
|
|
|
let app_services_logger = Cc["@mozilla.org/appservices/logger;1"].getService(
|
|
Ci.mozIAppServicesLogger
|
|
);
|
|
let logger_target = "app-services:webext_storage:sync";
|
|
app_services_logger.register(logger_target, new LogAdapter(this._log));
|
|
}
|
|
|
|
ExtensionStorageEngineBridge.prototype = {
|
|
syncPriority: 10,
|
|
|
|
// Used to override the engine name in telemetry, so that we can distinguish .
|
|
overrideTelemetryName: "rust-webext-storage",
|
|
|
|
_notifyPendingChanges() {
|
|
return new Promise(resolve => {
|
|
this.component
|
|
.QueryInterface(Ci.mozISyncedExtensionStorageArea)
|
|
.fetchPendingSyncChanges({
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"mozIExtensionStorageListener",
|
|
"mozIExtensionStorageCallback",
|
|
]),
|
|
onChanged: (extId, json) => {
|
|
try {
|
|
lazy.extensionStorageSync.notifyListeners(
|
|
extId,
|
|
JSON.parse(json)
|
|
);
|
|
} catch (ex) {
|
|
this._log.warn(
|
|
`Error notifying change listeners for ${extId}`,
|
|
ex
|
|
);
|
|
}
|
|
},
|
|
handleSuccess: resolve,
|
|
handleError: (code, message) => {
|
|
this._log.warn(
|
|
"Error fetching pending synced changes",
|
|
message,
|
|
code
|
|
);
|
|
resolve();
|
|
},
|
|
});
|
|
});
|
|
},
|
|
|
|
_takeMigrationInfo() {
|
|
return new Promise(resolve => {
|
|
this.component
|
|
.QueryInterface(Ci.mozIExtensionStorageArea)
|
|
.takeMigrationInfo({
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"mozIExtensionStorageCallback",
|
|
]),
|
|
handleSuccess: result => {
|
|
resolve(result ? JSON.parse(result) : null);
|
|
},
|
|
handleError: (code, message) => {
|
|
this._log.warn("Error fetching migration info", message, code);
|
|
// `takeMigrationInfo` doesn't actually perform the migration,
|
|
// just reads (and clears) any data stored in the DB from the
|
|
// previous migration.
|
|
//
|
|
// Any errors here are very likely occurring a good while
|
|
// after the migration ran, so we just warn and pretend
|
|
// nothing was there.
|
|
resolve(null);
|
|
},
|
|
});
|
|
});
|
|
},
|
|
|
|
async _syncStartup() {
|
|
let result = await super._syncStartup();
|
|
let info = await this._takeMigrationInfo();
|
|
if (info) {
|
|
lazy.Observers.notify(
|
|
"weave:telemetry:migration",
|
|
info,
|
|
"webext-storage"
|
|
);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
async _processIncoming() {
|
|
await super._processIncoming();
|
|
try {
|
|
await this._notifyPendingChanges();
|
|
} catch (ex) {
|
|
// Failing to notify `storage.onChanged` observers is bad, but shouldn't
|
|
// interrupt syncing.
|
|
this._log.warn("Error notifying about synced changes", ex);
|
|
}
|
|
},
|
|
|
|
get enabled() {
|
|
return getEngineEnabled();
|
|
},
|
|
set enabled(enabled) {
|
|
setEngineEnabled(enabled);
|
|
},
|
|
};
|
|
Object.setPrototypeOf(
|
|
ExtensionStorageEngineBridge.prototype,
|
|
BridgedEngine.prototype
|
|
);
|
|
|
|
/**
|
|
*****************************************************************************
|
|
*
|
|
* Deprecated support for Kinto
|
|
*
|
|
*****************************************************************************
|
|
*/
|
|
|
|
/**
|
|
* The Engine that manages syncing for the web extension "storage"
|
|
* API, and in particular ext.storage.sync.
|
|
*
|
|
* ext.storage.sync is implemented using Kinto, so it has mechanisms
|
|
* for syncing that we do not need to integrate in the IceCat Sync
|
|
* framework, so this is something of a stub.
|
|
*/
|
|
export function ExtensionStorageEngineKinto(service) {
|
|
SyncEngine.call(this, "Extension-Storage", service);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"_skipPercentageChance",
|
|
"services.sync.extension-storage.skipPercentageChance",
|
|
0
|
|
);
|
|
}
|
|
|
|
ExtensionStorageEngineKinto.prototype = {
|
|
_trackerObj: ExtensionStorageTracker,
|
|
// we don't need these since we implement our own sync logic
|
|
_storeObj: undefined,
|
|
_recordObj: undefined,
|
|
|
|
syncPriority: 10,
|
|
allowSkippedRecord: false,
|
|
|
|
async _sync() {
|
|
return lazy.extensionStorageSyncKinto.syncAll();
|
|
},
|
|
|
|
get enabled() {
|
|
return getEngineEnabled();
|
|
},
|
|
// We only need the enabled setter for the edge-case where info/collections
|
|
// has `extension-storage` - which could happen if the pref to flip the new
|
|
// engine on was once set but no longer is.
|
|
set enabled(enabled) {
|
|
setEngineEnabled(enabled);
|
|
},
|
|
|
|
_wipeClient() {
|
|
return lazy.extensionStorageSyncKinto.clearAll();
|
|
},
|
|
|
|
shouldSkipSync(syncReason) {
|
|
if (syncReason == "user" || syncReason == "startup") {
|
|
this._log.info(
|
|
`Not skipping extension storage sync: reason == ${syncReason}`
|
|
);
|
|
// Always sync if a user clicks the button, or if we're starting up.
|
|
return false;
|
|
}
|
|
// Ensure this wouldn't cause a resync...
|
|
if (this._tracker.score >= lazy.MULTI_DEVICE_THRESHOLD) {
|
|
this._log.info(
|
|
"Not skipping extension storage sync: Would trigger resync anyway"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
let probability = this._skipPercentageChance / 100.0;
|
|
// Math.random() returns a value in the interval [0, 1), so `>` is correct:
|
|
// if `probability` is 1 skip every time, and if it's 0, never skip.
|
|
let shouldSkip = probability > Math.random();
|
|
|
|
this._log.info(
|
|
`Skipping extension-storage sync with a chance of ${probability}: ${shouldSkip}`
|
|
);
|
|
return shouldSkip;
|
|
},
|
|
};
|
|
Object.setPrototypeOf(
|
|
ExtensionStorageEngineKinto.prototype,
|
|
SyncEngine.prototype
|
|
);
|
|
|
|
function ExtensionStorageTracker(name, engine) {
|
|
Tracker.call(this, name, engine);
|
|
this._ignoreAll = false;
|
|
}
|
|
ExtensionStorageTracker.prototype = {
|
|
get ignoreAll() {
|
|
return this._ignoreAll;
|
|
},
|
|
|
|
set ignoreAll(value) {
|
|
this._ignoreAll = value;
|
|
},
|
|
|
|
onStart() {
|
|
lazy.Svc.Obs.add("ext.storage.sync-changed", this.asyncObserver);
|
|
},
|
|
|
|
onStop() {
|
|
lazy.Svc.Obs.remove("ext.storage.sync-changed", this.asyncObserver);
|
|
},
|
|
|
|
async observe(subject, topic) {
|
|
if (this.ignoreAll) {
|
|
return;
|
|
}
|
|
|
|
if (topic !== "ext.storage.sync-changed") {
|
|
return;
|
|
}
|
|
|
|
// Single adds, removes and changes are not so important on their
|
|
// own, so let's just increment score a bit.
|
|
this.score += lazy.SCORE_INCREMENT_MEDIUM;
|
|
},
|
|
};
|
|
Object.setPrototypeOf(ExtensionStorageTracker.prototype, Tracker.prototype);
|