summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-02 09:21:33 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-02 09:21:33 -0500
commit02a742bd76b4aae01636185b6107ff0e50cfee92 (patch)
tree9ac98ca9a764666bd0edd4cfd59ae970705b98a3
parentfe0327462877532fae7e02e8f191b1b63d104ce5 (diff)
downloaduxp-02a742bd76b4aae01636185b6107ff0e50cfee92.tar.gz
Remove kinto client, Firefox kinto storage adapter, blocklist update client and integration with sync, OneCRL and the custom time check for derives system time.
-rw-r--r--browser/base/content/content.js43
-rw-r--r--browser/locales/en-US/chrome/overrides/netError.dtd2
-rw-r--r--modules/libpref/init/all.js25
-rw-r--r--security/manager/ssl/CertBlocklist.cpp23
-rw-r--r--security/manager/ssl/CertBlocklist.h2
-rw-r--r--services/common/blocklist-clients.js310
-rw-r--r--services/common/blocklist-updater.js117
-rw-r--r--services/common/kinto-http-client.js1891
-rw-r--r--services/common/kinto-offline-client.js4286
-rw-r--r--services/common/moz.build4
-rw-r--r--services/common/tests/unit/test_blocklist_certificates.js224
-rw-r--r--services/common/tests/unit/test_blocklist_clients.js412
-rw-r--r--services/common/tests/unit/test_blocklist_signatures.js510
-rw-r--r--services/common/tests/unit/test_blocklist_updater.js173
-rw-r--r--services/common/tests/unit/test_kinto.js412
-rw-r--r--services/common/tests/unit/xpcshell.ini8
-rw-r--r--services/sync/modules/engines/extension-storage.js277
-rw-r--r--services/sync/modules/service.js1
-rw-r--r--services/sync/moz.build1
-rw-r--r--testing/profiles/prefs_general.js2
-rw-r--r--toolkit/components/extensions/ExtensionStorageSync.jsm848
-rw-r--r--toolkit/components/extensions/ext-storage.js22
-rw-r--r--toolkit/components/extensions/moz.build1
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js1073
-rw-r--r--toolkit/components/extensions/test/xpcshell/xpcshell.ini3
-rw-r--r--toolkit/mozapps/extensions/internal/AddonTestUtils.jsm1
-rw-r--r--toolkit/mozapps/extensions/nsBlocklistService.js11
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js6
28 files changed, 24 insertions, 10664 deletions
diff --git a/browser/base/content/content.js b/browser/base/content/content.js
index 658d2014da..8d6f0745e9 100644
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -315,29 +315,26 @@ var AboutNetAndCertErrorListener = {
case MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE:
case MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE:
- // use blocklist stats if available
- if (Services.prefs.getPrefType(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS)) {
- let difference = Services.prefs.getIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS);
-
- // if the difference is more than a day
- if (Math.abs(difference) > 60 * 60 * 24) {
- let formatter = new Intl.DateTimeFormat();
- let systemDate = formatter.format(new Date());
- // negative difference means local time is behind server time
- let actualDate = formatter.format(new Date(Date.now() - difference * 1000));
-
- content.document.getElementById("wrongSystemTime_URL")
- .textContent = content.document.location.hostname;
- content.document.getElementById("wrongSystemTime_systemDate")
- .textContent = systemDate;
- content.document.getElementById("wrongSystemTime_actualDate")
- .textContent = actualDate;
-
- content.document.getElementById("errorShortDesc")
- .style.display = "none";
- content.document.getElementById("wrongSystemTimePanel")
- .style.display = "block";
- }
+ let appBuildId = Services.appinfo.appBuildID;
+ let year = parseInt(appBuildID.substr(0, 4), 10);
+ let month = parseInt(appBuildID.substr(4, 2), 10) - 1;
+ let day = parseInt(appBuildID.substr(6, 2), 10);
+ let buildDate = new Date(year, month, day);
+ let systemDate = new Date();
+
+ // if the difference is more than a day
+ if (buildDate > systemDate) {
+ let formatter = new Intl.DateTimeFormat();
+
+ content.document.getElementById("wrongSystemTime_URL")
+ .textContent = content.document.location.hostname;
+ content.document.getElementById("wrongSystemTime_systemDate")
+ .textContent = formatter.format(systemDate);
+
+ content.document.getElementById("errorShortDesc")
+ .style.display = "none";
+ content.document.getElementById("wrongSystemTimePanel")
+ .style.display = "block";
}
learnMoreLink.href = baseURL + "time-errors";
break;
diff --git a/browser/locales/en-US/chrome/overrides/netError.dtd b/browser/locales/en-US/chrome/overrides/netError.dtd
index 30dd2346ab..92db8ee3ac 100644
--- a/browser/locales/en-US/chrome/overrides/netError.dtd
+++ b/browser/locales/en-US/chrome/overrides/netError.dtd
@@ -199,7 +199,7 @@ was trying to connect. -->
<!-- LOCALIZATION NOTE (certerror.wrongSystemTime) - The <span id='..' /> tags will be injected with actual values,
please leave them unchanged. -->
-<!ENTITY certerror.wrongSystemTime "<p>A secure connection to <span id='wrongSystemTime_URL'/> isn’t possible because your clock appears to show the wrong time.</p> <p>Your computer thinks it is <span id='wrongSystemTime_systemDate'/>, when it should be <span id='wrongSystemTime_actualDate'/>. To fix this problem, change your date and time settings to match the correct time.</p>">
+<!ENTITY certerror.wrongSystemTime "<p>&brandShortName; did not connect to <span id='wrongSystemTimeWithoutReference_URL'/> because your computer’s clock appears to show the wrong time and this is preventing a secure connection.</p> <p>Your computer is set to <span id='wrongSystemTimeWithoutReference_systemDate'/>. To fix this problem, change your date and time settings to match the correct time.</p>">
<!ENTITY certerror.pagetitle1 "Insecure Connection">
<!ENTITY certerror.whatShouldIDo.badStsCertExplanation "This site uses HTTP
diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js
index 1cb9e19214..72eb8524ee 100644
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2175,9 +2175,6 @@ pref("security.cert_pinning.process_headers_from_non_builtin_roots", false);
// their protocol with the inner URI of the view-source URI
pref("security.view-source.reachable-from-inner-protocol", false);
-// Services security settings
-pref("services.settings.server", "https://firefox.settings.services.mozilla.com/v1");
-
// Blocklist preferences
pref("extensions.blocklist.enabled", true);
// OneCRL freshness checking depends on this value, so if you change it,
@@ -2192,28 +2189,6 @@ pref("extensions.blocklist.itemURL", "https://blocklist.addons.mozilla.org/%LOCA
// Controls what level the blocklist switches from warning about items to forcibly
// blocking them.
pref("extensions.blocklist.level", 2);
-// Blocklist via settings server (Kinto)
-pref("services.blocklist.changes.path", "/buckets/monitor/collections/changes/records");
-pref("services.blocklist.bucket", "blocklists");
-pref("services.blocklist.onecrl.collection", "certificates");
-pref("services.blocklist.onecrl.checked", 0);
-pref("services.blocklist.addons.collection", "addons");
-pref("services.blocklist.addons.checked", 0);
-pref("services.blocklist.plugins.collection", "plugins");
-pref("services.blocklist.plugins.checked", 0);
-pref("services.blocklist.gfx.collection", "gfx");
-pref("services.blocklist.gfx.checked", 0);
-
-// Controls whether signing should be enforced on signature-capable blocklist
-// collections.
-pref("services.blocklist.signing.enforced", true);
-
-// Enable blocklists via the services settings mechanism
-pref("services.blocklist.update_enabled", true);
-
-// Enable certificate blocklist updates via services settings
-pref("security.onecrl.via.amo", false);
-
// Modifier key prefs: default to Windows settings,
// menu access key = alt, accelerator key = control.
diff --git a/security/manager/ssl/CertBlocklist.cpp b/security/manager/ssl/CertBlocklist.cpp
index 56473eca3e..c5e66b0d9c 100644
--- a/security/manager/ssl/CertBlocklist.cpp
+++ b/security/manager/ssl/CertBlocklist.cpp
@@ -34,14 +34,11 @@ using namespace mozilla::pkix;
#define PREF_BACKGROUND_UPDATE_TIMER "app.update.lastUpdateTime.blocklist-background-update-timer"
#define PREF_BLOCKLIST_ONECRL_CHECKED "services.blocklist.onecrl.checked"
#define PREF_MAX_STALENESS_IN_SECONDS "security.onecrl.maximum_staleness_in_seconds"
-#define PREF_ONECRL_VIA_AMO "security.onecrl.via.amo"
static LazyLogModule gCertBlockPRLog("CertBlock");
uint32_t CertBlocklist::sLastBlocklistUpdate = 0U;
-uint32_t CertBlocklist::sLastKintoUpdate = 0U;
uint32_t CertBlocklist::sMaxStaleness = 0U;
-bool CertBlocklist::sUseAMO = true;
CertBlocklistItem::CertBlocklistItem(const uint8_t* DNData,
size_t DNLength,
@@ -143,9 +140,6 @@ CertBlocklist::~CertBlocklist()
PREF_MAX_STALENESS_IN_SECONDS,
this);
Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged,
- PREF_ONECRL_VIA_AMO,
- this);
- Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged,
PREF_BLOCKLIST_ONECRL_CHECKED,
this);
}
@@ -177,12 +171,6 @@ CertBlocklist::Init()
return rv;
}
rv = Preferences::RegisterCallbackAndCall(CertBlocklist::PreferenceChanged,
- PREF_ONECRL_VIA_AMO,
- this);
- if (NS_FAILED(rv)) {
- return rv;
- }
- rv = Preferences::RegisterCallbackAndCall(CertBlocklist::PreferenceChanged,
PREF_BLOCKLIST_ONECRL_CHECKED,
this);
if (NS_FAILED(rv)) {
@@ -628,10 +616,10 @@ CertBlocklist::IsBlocklistFresh(bool* _retval)
*_retval = false;
uint32_t now = uint32_t(PR_Now() / PR_USEC_PER_SEC);
- uint32_t lastUpdate = sUseAMO ? sLastBlocklistUpdate : sLastKintoUpdate;
+ uint32_t lastUpdate = sLastBlocklistUpdate;
MOZ_LOG(gCertBlockPRLog, LogLevel::Warning,
- ("CertBlocklist::IsBlocklistFresh using AMO? %i lastUpdate is %i",
- sUseAMO, lastUpdate));
+ ("CertBlocklist::IsBlocklistFresh lastUpdate is %i",
+ lastUpdate));
if (now > lastUpdate) {
int64_t interval = now - lastUpdate;
@@ -659,13 +647,8 @@ CertBlocklist::PreferenceChanged(const char* aPref, void* aClosure)
if (strcmp(aPref, PREF_BACKGROUND_UPDATE_TIMER) == 0) {
sLastBlocklistUpdate = Preferences::GetUint(PREF_BACKGROUND_UPDATE_TIMER,
uint32_t(0));
- } else if (strcmp(aPref, PREF_BLOCKLIST_ONECRL_CHECKED) == 0) {
- sLastKintoUpdate = Preferences::GetUint(PREF_BLOCKLIST_ONECRL_CHECKED,
- uint32_t(0));
} else if (strcmp(aPref, PREF_MAX_STALENESS_IN_SECONDS) == 0) {
sMaxStaleness = Preferences::GetUint(PREF_MAX_STALENESS_IN_SECONDS,
uint32_t(0));
- } else if (strcmp(aPref, PREF_ONECRL_VIA_AMO) == 0) {
- sUseAMO = Preferences::GetBool(PREF_ONECRL_VIA_AMO, true);
}
}
diff --git a/security/manager/ssl/CertBlocklist.h b/security/manager/ssl/CertBlocklist.h
index 60f675cd8f..2cad45eef4 100644
--- a/security/manager/ssl/CertBlocklist.h
+++ b/security/manager/ssl/CertBlocklist.h
@@ -80,9 +80,7 @@ private:
protected:
static void PreferenceChanged(const char* aPref, void* aClosure);
static uint32_t sLastBlocklistUpdate;
- static uint32_t sLastKintoUpdate;
static uint32_t sMaxStaleness;
- static bool sUseAMO;
virtual ~CertBlocklist();
};
diff --git a/services/common/blocklist-clients.js b/services/common/blocklist-clients.js
deleted file mode 100644
index fc51aaca43..0000000000
--- a/services/common/blocklist-clients.js
+++ /dev/null
@@ -1,310 +0,0 @@
-/* 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";
-
-this.EXPORTED_SYMBOLS = ["AddonBlocklistClient",
- "GfxBlocklistClient",
- "OneCRLBlocklistClient",
- "PluginBlocklistClient",
- "FILENAME_ADDONS_JSON",
- "FILENAME_GFX_JSON",
- "FILENAME_PLUGINS_JSON"];
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-const { Task } = Cu.import("resource://gre/modules/Task.jsm");
-const { OS } = Cu.import("resource://gre/modules/osfile.jsm");
-Cu.importGlobalProperties(["fetch"]);
-
-const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
-const { KintoHttpClient } = Cu.import("resource://services-common/kinto-http-client.js");
-const { CanonicalJSON } = Components.utils.import("resource://gre/modules/CanonicalJSON.jsm");
-
-const PREF_SETTINGS_SERVER = "services.settings.server";
-const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
-const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
-const PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS = "services.blocklist.onecrl.checked";
-const PREF_BLOCKLIST_ADDONS_COLLECTION = "services.blocklist.addons.collection";
-const PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS = "services.blocklist.addons.checked";
-const PREF_BLOCKLIST_PLUGINS_COLLECTION = "services.blocklist.plugins.collection";
-const PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS = "services.blocklist.plugins.checked";
-const PREF_BLOCKLIST_GFX_COLLECTION = "services.blocklist.gfx.collection";
-const PREF_BLOCKLIST_GFX_CHECKED_SECONDS = "services.blocklist.gfx.checked";
-const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced";
-
-const INVALID_SIGNATURE = "Invalid content/signature";
-
-this.FILENAME_ADDONS_JSON = "blocklist-addons.json";
-this.FILENAME_GFX_JSON = "blocklist-gfx.json";
-this.FILENAME_PLUGINS_JSON = "blocklist-plugins.json";
-
-function mergeChanges(localRecords, changes) {
- // Kinto.js adds attributes to local records that aren't present on server.
- // (e.g. _status)
- const stripPrivateProps = (obj) => {
- return Object.keys(obj).reduce((current, key) => {
- if (!key.startsWith("_")) {
- current[key] = obj[key];
- }
- return current;
- }, {});
- };
-
- const records = {};
- // Local records by id.
- localRecords.forEach((record) => records[record.id] = stripPrivateProps(record));
- // All existing records are replaced by the version from the server.
- changes.forEach((record) => records[record.id] = record);
-
- return Object.values(records)
- // Filter out deleted records.
- .filter((record) => record.deleted != true)
- // Sort list by record id.
- .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
-}
-
-
-function fetchCollectionMetadata(collection) {
- const client = new KintoHttpClient(collection.api.remote);
- return client.bucket(collection.bucket).collection(collection.name).getData()
- .then(result => {
- return result.signature;
- });
-}
-
-function fetchRemoteCollection(collection) {
- const client = new KintoHttpClient(collection.api.remote);
- return client.bucket(collection.bucket)
- .collection(collection.name)
- .listRecords({sort: "id"});
-}
-
-/**
- * Helper to instantiate a Kinto client based on preferences for remote server
- * URL and bucket name. It uses the `FirefoxAdapter` which relies on SQLite to
- * persist the local DB.
- */
-function kintoClient() {
- let base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
- let bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
-
- let Kinto = loadKinto();
-
- let FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
-
- let config = {
- remote: base,
- bucket: bucket,
- adapter: FirefoxAdapter,
- };
-
- return new Kinto(config);
-}
-
-
-class BlocklistClient {
-
- constructor(collectionName, lastCheckTimePref, processCallback, signerName) {
- this.collectionName = collectionName;
- this.lastCheckTimePref = lastCheckTimePref;
- this.processCallback = processCallback;
- this.signerName = signerName;
- }
-
- validateCollectionSignature(payload, collection, ignoreLocal) {
- return Task.spawn((function* () {
- // this is a content-signature field from an autograph response.
- const {x5u, signature} = yield fetchCollectionMetadata(collection);
- const certChain = yield fetch(x5u).then((res) => res.text());
-
- const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
- .createInstance(Ci.nsIContentSignatureVerifier);
-
- let toSerialize;
- if (ignoreLocal) {
- toSerialize = {
- last_modified: `${payload.last_modified}`,
- data: payload.data
- };
- } else {
- const localRecords = (yield collection.list()).data;
- const records = mergeChanges(localRecords, payload.changes);
- toSerialize = {
- last_modified: `${payload.lastModified}`,
- data: records
- };
- }
-
- const serialized = CanonicalJSON.stringify(toSerialize);
-
- if (verifier.verifyContentSignature(serialized, "p384ecdsa=" + signature,
- certChain,
- this.signerName)) {
- // In case the hash is valid, apply the changes locally.
- return payload;
- }
- throw new Error(INVALID_SIGNATURE);
- }).bind(this));
- }
-
- /**
- * Synchronize from Kinto server, if necessary.
- *
- * @param {int} lastModified the lastModified date (on the server) for
- the remote collection.
- * @param {Date} serverTime the current date return by the server.
- * @return {Promise} which rejects on sync or process failure.
- */
- maybeSync(lastModified, serverTime) {
- let db = kintoClient();
- let opts = {};
- let enforceCollectionSigning =
- Services.prefs.getBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING);
-
- // if there is a signerName and collection signing is enforced, add a
- // hook for incoming changes that validates the signature
- if (this.signerName && enforceCollectionSigning) {
- opts.hooks = {
- "incoming-changes": [this.validateCollectionSignature.bind(this)]
- }
- }
-
- let collection = db.collection(this.collectionName, opts);
-
- return Task.spawn((function* syncCollection() {
- try {
- yield collection.db.open();
-
- let collectionLastModified = yield collection.db.getLastModified();
- // If the data is up to date, there's no need to sync. We still need
- // to record the fact that a check happened.
- if (lastModified <= collectionLastModified) {
- this.updateLastCheck(serverTime);
- return;
- }
- // Fetch changes from server.
- try {
- let syncResult = yield collection.sync();
- if (!syncResult.ok) {
- throw new Error("Sync failed");
- }
- } catch (e) {
- if (e.message == INVALID_SIGNATURE) {
- // if sync fails with a signature error, it's likely that our
- // local data has been modified in some way.
- // We will attempt to fix this by retrieving the whole
- // remote collection.
- let payload = yield fetchRemoteCollection(collection);
- yield this.validateCollectionSignature(payload, collection, true);
- // if the signature is good (we haven't thrown), and the remote
- // last_modified is newer than the local last_modified, replace the
- // local data
- const localLastModified = yield collection.db.getLastModified();
- if (payload.last_modified >= localLastModified) {
- yield collection.clear();
- yield collection.loadDump(payload.data);
- }
- } else {
- throw e;
- }
- }
- // Read local collection of records.
- let list = yield collection.list();
-
- yield this.processCallback(list.data);
-
- // Track last update.
- this.updateLastCheck(serverTime);
- } finally {
- collection.db.close();
- }
- }).bind(this));
- }
-
- /**
- * Save last time server was checked in users prefs.
- *
- * @param {Date} serverTime the current date return by server.
- */
- updateLastCheck(serverTime) {
- let checkedServerTimeInSeconds = Math.round(serverTime / 1000);
- Services.prefs.setIntPref(this.lastCheckTimePref, checkedServerTimeInSeconds);
- }
-}
-
-/**
- * Revoke the appropriate certificates based on the records from the blocklist.
- *
- * @param {Object} records current records in the local db.
- */
-function* updateCertBlocklist(records) {
- let certList = Cc["@mozilla.org/security/certblocklist;1"]
- .getService(Ci.nsICertBlocklist);
- for (let item of records) {
- try {
- if (item.issuerName && item.serialNumber) {
- certList.revokeCertByIssuerAndSerial(item.issuerName,
- item.serialNumber);
- } else if (item.subject && item.pubKeyHash) {
- certList.revokeCertBySubjectAndPubKey(item.subject,
- item.pubKeyHash);
- }
- } catch (e) {
- // prevent errors relating to individual blocklist entries from
- // causing sync to fail. At some point in the future, we may want to
- // accumulate telemetry on these failures.
- Cu.reportError(e);
- }
- }
- certList.saveEntries();
-}
-
-/**
- * Write list of records into JSON file, and notify nsBlocklistService.
- *
- * @param {String} filename path relative to profile dir.
- * @param {Object} records current records in the local db.
- */
-function* updateJSONBlocklist(filename, records) {
- // Write JSON dump for synchronous load at startup.
- const path = OS.Path.join(OS.Constants.Path.profileDir, filename);
- const serialized = JSON.stringify({data: records}, null, 2);
- try {
- yield OS.File.writeAtomic(path, serialized, {tmpPath: path + ".tmp"});
-
- // Notify change to `nsBlocklistService`
- const eventData = {filename: filename};
- Services.cpmm.sendAsyncMessage("Blocklist:reload-from-disk", eventData);
- } catch(e) {
- Cu.reportError(e);
- }
-}
-
-
-this.OneCRLBlocklistClient = new BlocklistClient(
- Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION),
- PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS,
- updateCertBlocklist,
- "onecrl.content-signature.mozilla.org"
-);
-
-this.AddonBlocklistClient = new BlocklistClient(
- Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_COLLECTION),
- PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS,
- updateJSONBlocklist.bind(undefined, FILENAME_ADDONS_JSON)
-);
-
-this.GfxBlocklistClient = new BlocklistClient(
- Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_COLLECTION),
- PREF_BLOCKLIST_GFX_CHECKED_SECONDS,
- updateJSONBlocklist.bind(undefined, FILENAME_GFX_JSON)
-);
-
-this.PluginBlocklistClient = new BlocklistClient(
- Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_COLLECTION),
- PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS,
- updateJSONBlocklist.bind(undefined, FILENAME_PLUGINS_JSON)
-);
diff --git a/services/common/blocklist-updater.js b/services/common/blocklist-updater.js
deleted file mode 100644
index 3b39b95524..0000000000
--- a/services/common/blocklist-updater.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/* 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/. */
-
-this.EXPORTED_SYMBOLS = ["checkVersions", "addTestBlocklistClient"];
-
-const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.importGlobalProperties(['fetch']);
-const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js", {});
-
-const PREF_SETTINGS_SERVER = "services.settings.server";
-const PREF_BLOCKLIST_CHANGES_PATH = "services.blocklist.changes.path";
-const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
-const PREF_BLOCKLIST_LAST_UPDATE = "services.blocklist.last_update_seconds";
-const PREF_BLOCKLIST_LAST_ETAG = "services.blocklist.last_etag";
-const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
-
-
-const gBlocklistClients = {
- [BlocklistClients.OneCRLBlocklistClient.collectionName]: BlocklistClients.OneCRLBlocklistClient,
- [BlocklistClients.AddonBlocklistClient.collectionName]: BlocklistClients.AddonBlocklistClient,
- [BlocklistClients.GfxBlocklistClient.collectionName]: BlocklistClients.GfxBlocklistClient,
- [BlocklistClients.PluginBlocklistClient.collectionName]: BlocklistClients.PluginBlocklistClient
-};
-
-// Add a blocklist client for testing purposes. Do not use for any other purpose
-this.addTestBlocklistClient = (name, client) => { gBlocklistClients[name] = client; }
-
-// This is called by the ping mechanism.
-// returns a promise that rejects if something goes wrong
-this.checkVersions = function() {
- return Task.spawn(function* syncClients() {
- // Fetch a versionInfo object that looks like:
- // {"data":[
- // {
- // "host":"kinto-ota.dev.mozaws.net",
- // "last_modified":1450717104423,
- // "bucket":"blocklists",
- // "collection":"certificates"
- // }]}
- // Right now, we only use the collection name and the last modified info
- let kintoBase = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
- let changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_BLOCKLIST_CHANGES_PATH);
- let blocklistsBucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
-
- // Use ETag to obtain a `304 Not modified` when no change occurred.
- const headers = {};
- if (Services.prefs.prefHasUserValue(PREF_BLOCKLIST_LAST_ETAG)) {
- const lastEtag = Services.prefs.getCharPref(PREF_BLOCKLIST_LAST_ETAG);
- if (lastEtag) {
- headers["If-None-Match"] = lastEtag;
- }
- }
-
- let response = yield fetch(changesEndpoint, {headers});
-
- let versionInfo;
- // No changes since last time. Go on with empty list of changes.
- if (response.status == 304) {
- versionInfo = {data: []};
- } else {
- versionInfo = yield response.json();
- }
-
- // If the server is failing, the JSON response might not contain the
- // expected data (e.g. error response - Bug 1259145)
- if (!versionInfo.hasOwnProperty("data")) {
- throw new Error("Polling for changes failed.");
- }
-
- // Record new update time and the difference between local and server time
- let serverTimeMillis = Date.parse(response.headers.get("Date"));
-
- // negative clockDifference means local time is behind server time
- // by the absolute of that value in seconds (positive means it's ahead)
- let clockDifference = Math.floor((Date.now() - serverTimeMillis) / 1000);
- Services.prefs.setIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, clockDifference);
- Services.prefs.setIntPref(PREF_BLOCKLIST_LAST_UPDATE, serverTimeMillis / 1000);
-
- let firstError;
- for (let collectionInfo of versionInfo.data) {
- // Skip changes that don't concern configured blocklist bucket.
- if (collectionInfo.bucket != blocklistsBucket) {
- continue;
- }
-
- let collection = collectionInfo.collection;
- let client = gBlocklistClients[collection];
- if (client && client.maybeSync) {
- let lastModified = 0;
- if (collectionInfo.last_modified) {
- lastModified = collectionInfo.last_modified;
- }
- try {
- yield client.maybeSync(lastModified, serverTimeMillis);
- } catch (e) {
- if (!firstError) {
- firstError = e;
- }
- }
- }
- }
- if (firstError) {
- // cause the promise to reject by throwing the first observed error
- throw firstError;
- }
-
- // Save current Etag for next poll.
- if (response.headers.has("ETag")) {
- const currentEtag = response.headers.get("ETag");
- Services.prefs.setCharPref(PREF_BLOCKLIST_LAST_ETAG, currentEtag);
- }
- });
-};
diff --git a/services/common/kinto-http-client.js b/services/common/kinto-http-client.js
deleted file mode 100644
index 57f6946d10..0000000000
--- a/services/common/kinto-http-client.js
+++ /dev/null
@@ -1,1891 +0,0 @@
-/*
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * This file is generated from kinto-http.js - do not modify directly.
- */
-
-this.EXPORTED_SYMBOLS = ["KintoHttpClient"];
-
-/*
- * Version 2.0.0 - 61435f3
- */
-
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.KintoHttpClient = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-/*
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.default = undefined;
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-var _base = require("../src/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/Timer.jsm");
-Cu.importGlobalProperties(['fetch']);
-const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
-
-let KintoHttpClient = class KintoHttpClient extends _base2.default {
- constructor(remote, options = {}) {
- const events = {};
- EventEmitter.decorate(events);
- super(remote, _extends({ events }, options));
- }
-};
-
-// This fixes compatibility with CommonJS required by browserify.
-// See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495
-
-exports.default = KintoHttpClient;
-if (typeof module === "object") {
- module.exports = KintoHttpClient;
-}
-
-},{"../src/base":2}],2:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.default = exports.SUPPORTED_PROTOCOL_VERSION = undefined;
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _desc, _value, _class;
-
-var _utils = require("./utils");
-
-var _http = require("./http");
-
-var _http2 = _interopRequireDefault(_http);
-
-var _endpoint = require("./endpoint");
-
-var _endpoint2 = _interopRequireDefault(_endpoint);
-
-var _requests = require("./requests");
-
-var requests = _interopRequireWildcard(_requests);
-
-var _batch = require("./batch");
-
-var _bucket = require("./bucket");
-
-var _bucket2 = _interopRequireDefault(_bucket);
-
-function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
- var desc = {};
- Object['ke' + 'ys'](descriptor).forEach(function (key) {
- desc[key] = descriptor[key];
- });
- desc.enumerable = !!desc.enumerable;
- desc.configurable = !!desc.configurable;
-
- if ('value' in desc || desc.initializer) {
- desc.writable = true;
- }
-
- desc = decorators.slice().reverse().reduce(function (desc, decorator) {
- return decorator(target, property, desc) || desc;
- }, desc);
-
- if (context && desc.initializer !== void 0) {
- desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
- desc.initializer = undefined;
- }
-
- if (desc.initializer === void 0) {
- Object['define' + 'Property'](target, property, desc);
- desc = null;
- }
-
- return desc;
-}
-
-/**
- * Currently supported protocol version.
- * @type {String}
- */
-const SUPPORTED_PROTOCOL_VERSION = exports.SUPPORTED_PROTOCOL_VERSION = "v1";
-
-/**
- * High level HTTP client for the Kinto API.
- *
- * @example
- * const client = new KintoClient("https://kinto.dev.mozaws.net/v1");
- * client.bucket("default")
-* .collection("my-blog")
-* .createRecord({title: "First article"})
- * .then(console.log.bind(console))
- * .catch(console.error.bind(console));
- */
-let KintoClientBase = (_dec = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec2 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec3 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec4 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec5 = (0, _utils.nobatch)("Can't use batch within a batch!"), _dec6 = (0, _utils.support)("1.4", "2.0"), (_class = class KintoClientBase {
- /**
- * Constructor.
- *
- * @param {String} remote The remote URL.
- * @param {Object} [options={}] The options object.
- * @param {Boolean} [options.safe=true] Adds concurrency headers to every requests.
- * @param {EventEmitter} [options.events=EventEmitter] The events handler instance.
- * @param {Object} [options.headers={}] The key-value headers to pass to each request.
- * @param {String} [options.bucket="default"] The default bucket to use.
- * @param {String} [options.requestMode="cors"] The HTTP request mode (from ES6 fetch spec).
- * @param {Number} [options.timeout=5000] The requests timeout in ms.
- */
- constructor(remote, options = {}) {
- if (typeof remote !== "string" || !remote.length) {
- throw new Error("Invalid remote URL: " + remote);
- }
- if (remote[remote.length - 1] === "/") {
- remote = remote.slice(0, -1);
- }
- this._backoffReleaseTime = null;
-
- /**
- * Default request options container.
- * @private
- * @type {Object}
- */
- this.defaultReqOptions = {
- bucket: options.bucket || "default",
- headers: options.headers || {},
- safe: !!options.safe
- };
-
- this._options = options;
- this._requests = [];
- this._isBatch = !!options.batch;
-
- // public properties
- /**
- * The remote server base URL.
- * @type {String}
- */
- this.remote = remote;
- /**
- * Current server information.
- * @ignore
- * @type {Object|null}
- */
- this.serverInfo = null;
- /**
- * The event emitter instance. Should comply with the `EventEmitter`
- * interface.
- * @ignore
- * @type {Class}
- */
- this.events = options.events;
-
- const { requestMode, timeout } = options;
- /**
- * The HTTP instance.
- * @ignore
- * @type {HTTP}
- */
- this.http = new _http2.default(this.events, { requestMode, timeout });
- this._registerHTTPEvents();
- }
-
- /**
- * The remote endpoint base URL. Setting the value will also extract and
- * validate the version.
- * @type {String}
- */
- get remote() {
- return this._remote;
- }
-
- /**
- * @ignore
- */
- set remote(url) {
- let version;
- try {
- version = url.match(/\/(v\d+)\/?$/)[1];
- } catch (err) {
- throw new Error("The remote URL must contain the version: " + url);
- }
- if (version !== SUPPORTED_PROTOCOL_VERSION) {
- throw new Error(`Unsupported protocol version: ${ version }`);
- }
- this._remote = url;
- this._version = version;
- }
-
- /**
- * The current server protocol version, eg. `v1`.
- * @type {String}
- */
- get version() {
- return this._version;
- }
-
- /**
- * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is
- * ongoing.
- *
- * @type {Number}
- */
- get backoff() {
- const currentTime = new Date().getTime();
- if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) {
- return this._backoffReleaseTime - currentTime;
- }
- return 0;
- }
-
- /**
- * Registers HTTP events.
- * @private
- */
- _registerHTTPEvents() {
- // Prevent registering event from a batch client instance
- if (!this._isBatch) {
- this.events.on("backoff", backoffMs => {
- this._backoffReleaseTime = backoffMs;
- });
- }
- }
-
- /**
- * Retrieve a bucket object to perform operations on it.
- *
- * @param {String} name The bucket name.
- * @param {Object} [options={}] The request options.
- * @param {Boolean} [options.safe] The resulting safe option.
- * @param {String} [options.bucket] The resulting bucket name option.
- * @param {Object} [options.headers] The extended headers object option.
- * @return {Bucket}
- */
- bucket(name, options = {}) {
- const bucketOptions = (0, _utils.omit)(this._getRequestOptions(options), "bucket");
- return new _bucket2.default(this, name, bucketOptions);
- }
-
- /**
- * Generates a request options object, deeply merging the client configured
- * defaults with the ones provided as argument.
- *
- * Note: Headers won't be overriden but merged with instance default ones.
- *
- * @private
- * @param {Object} [options={}] The request options.
- * @property {Boolean} [options.safe] The resulting safe option.
- * @property {String} [options.bucket] The resulting bucket name option.
- * @property {Object} [options.headers] The extended headers object option.
- * @return {Object}
- */
- _getRequestOptions(options = {}) {
- return _extends({}, this.defaultReqOptions, options, {
- batch: this._isBatch,
- // Note: headers should never be overriden but extended
- headers: _extends({}, this.defaultReqOptions.headers, options.headers)
- });
- }
-
- /**
- * Retrieves server information and persist them locally. This operation is
- * usually performed a single time during the instance lifecycle.
- *
- * @param {Object} [options={}] The request options.
- * @return {Promise<Object, Error>}
- */
- fetchServerInfo(options = {}) {
- if (this.serverInfo) {
- return Promise.resolve(this.serverInfo);
- }
- return this.http.request(this.remote + (0, _endpoint2.default)("root"), {
- headers: _extends({}, this.defaultReqOptions.headers, options.headers)
- }).then(({ json }) => {
- this.serverInfo = json;
- return this.serverInfo;
- });
- }
-
- /**
- * Retrieves Kinto server settings.
- *
- * @param {Object} [options={}] The request options.
- * @return {Promise<Object, Error>}
- */
-
- fetchServerSettings(options = {}) {
- return this.fetchServerInfo(options).then(({ settings }) => settings);
- }
-
- /**
- * Retrieve server capabilities information.
- *
- * @param {Object} [options={}] The request options.
- * @return {Promise<Object, Error>}
- */
-
- fetchServerCapabilities(options = {}) {
- return this.fetchServerInfo(options).then(({ capabilities }) => capabilities);
- }
-
- /**
- * Retrieve authenticated user information.
- *
- * @param {Object} [options={}] The request options.
- * @return {Promise<Object, Error>}
- */
-
- fetchUser(options = {}) {
- return this.fetchServerInfo(options).then(({ user }) => user);
- }
-
- /**
- * Retrieve authenticated user information.
- *
- * @param {Object} [options={}] The request options.
- * @return {Promise<Object, Error>}
- */
-
- fetchHTTPApiVersion(options = {}) {
- return this.fetchServerInfo(options).then(({ http_api_version }) => {
- return http_api_version;
- });
- }
-
- /**
- * Process batch requests, chunking them according to the batch_max_requests
- * server setting when needed.
- *
- * @param {Array} requests The list of batch subrequests to perform.
- * @param {Object} [options={}] The options object.
- * @return {Promise<Object, Error>}
- */
- _batchRequests(requests, options = {}) {
- const headers = _extends({}, this.defaultReqOptions.headers, options.headers);
- if (!requests.length) {
- return Promise.resolve([]);
- }
- return this.fetchServerSettings().then(serverSettings => {
- const maxRequests = serverSettings["batch_max_requests"];
- if (maxRequests && requests.length > maxRequests) {
- const chunks = (0, _utils.partition)(requests, maxRequests);
- return (0, _utils.pMap)(chunks, chunk => this._batchRequests(chunk, options));
- }
- return this.execute({
- path: (0, _endpoint2.default)("batch"),
- method: "POST",
- headers: headers,
- body: {
- defaults: { headers },
- requests: requests
- }
- })
- // we only care about the responses
- .then(({ responses }) => responses);
- });
- }
-
- /**
- * Sends batch requests to the remote server.
- *
- * Note: Reserved for internal use only.
- *
- * @ignore
- * @param {Function} fn The function to use for describing batch ops.
- * @param {Object} [options={}] The options object.
- * @param {Boolean} [options.safe] The safe option.
- * @param {String} [options.bucket] The bucket name option.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.aggregate=false] Produces an aggregated result object.
- * @return {Promise<Object, Error>}
- */
-
- batch(fn, options = {}) {
- const rootBatch = new KintoClientBase(this.remote, _extends({}, this._options, this._getRequestOptions(options), {
- batch: true
- }));
- let bucketBatch, collBatch;
- if (options.bucket) {
- bucketBatch = rootBatch.bucket(options.bucket);
- if (options.collection) {
- collBatch = bucketBatch.collection(options.collection);
- }
- }
- const batchClient = collBatch || bucketBatch || rootBatch;
- try {
- fn(batchClient);
- } catch (err) {
- return Promise.reject(err);
- }
- return this._batchRequests(rootBatch._requests, options).then(responses => {
- if (options.aggregate) {
- return (0, _batch.aggregate)(responses, rootBatch._requests);
- }
- return responses;
- });
- }
-
- /**
- * Executes an atomic HTTP request.
- *
- * @private
- * @param {Object} request The request object.
- * @param {Object} [options={}] The options object.
- * @param {Boolean} [options.raw=false] If true, resolve with full response object, including json body and headers instead of just json.
- * @return {Promise<Object, Error>}
- */
- execute(request, options = { raw: false }) {
- // If we're within a batch, add the request to the stack to send at once.
- if (this._isBatch) {
- this._requests.push(request);
- // Resolve with a message in case people attempt at consuming the result
- // from within a batch operation.
- const msg = "This result is generated from within a batch " + "operation and should not be consumed.";
- return Promise.resolve(options.raw ? { json: msg } : msg);
- }
- const promise = this.fetchServerSettings().then(_ => {
- return this.http.request(this.remote + request.path, _extends({}, request, {
- body: JSON.stringify(request.body)
- }));
- });
- return options.raw ? promise : promise.then(({ json }) => json);
- }
-
- /**
- * Retrieves the list of buckets.
- *
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object[], Error>}
- */
- listBuckets(options = {}) {
- return this.execute({
- path: (0, _endpoint2.default)("bucket"),
- headers: _extends({}, this.defaultReqOptions.headers, options.headers)
- });
- }
-
- /**
- * Creates a new bucket on the server.
- *
- * @param {String} id The bucket name.
- * @param {Object} [options={}] The options object.
- * @param {Boolean} [options.data] The bucket data option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object, Error>}
- */
- createBucket(id, options = {}) {
- if (!id) {
- throw new Error("A bucket id is required.");
- }
- // Note that we simply ignore any "bucket" option passed here, as the one
- // we're interested in is the one provided as a required argument.
- const reqOptions = this._getRequestOptions(options);
- const { data = {}, permissions } = reqOptions;
- data.id = id;
- const path = (0, _endpoint2.default)("bucket", id);
- return this.execute(requests.createRequest(path, { data, permissions }, reqOptions));
- }
-
- /**
- * Deletes a bucket from the server.
- *
- * @ignore
- * @param {Object|String} bucket The bucket to delete.
- * @param {Object} [options={}] The options object.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Object} [options.headers] The headers object option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- deleteBucket(bucket, options = {}) {
- const bucketObj = (0, _utils.toDataBody)(bucket);
- if (!bucketObj.id) {
- throw new Error("A bucket id is required.");
- }
- const path = (0, _endpoint2.default)("bucket", bucketObj.id);
- const { last_modified } = { bucketObj };
- const reqOptions = this._getRequestOptions(_extends({ last_modified }, options));
- return this.execute(requests.deleteRequest(path, reqOptions));
- }
-
- /**
- * Deletes all buckets on the server.
- *
- * @ignore
- * @param {Object} [options={}] The options object.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Object} [options.headers] The headers object option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
-
- deleteBuckets(options = {}) {
- const reqOptions = this._getRequestOptions(options);
- const path = (0, _endpoint2.default)("bucket");
- return this.execute(requests.deleteRequest(path, reqOptions));
- }
-}, (_applyDecoratedDescriptor(_class.prototype, "fetchServerSettings", [_dec], Object.getOwnPropertyDescriptor(_class.prototype, "fetchServerSettings"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchServerCapabilities", [_dec2], Object.getOwnPropertyDescriptor(_class.prototype, "fetchServerCapabilities"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchUser", [_dec3], Object.getOwnPropertyDescriptor(_class.prototype, "fetchUser"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchHTTPApiVersion", [_dec4], Object.getOwnPropertyDescriptor(_class.prototype, "fetchHTTPApiVersion"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "batch", [_dec5], Object.getOwnPropertyDescriptor(_class.prototype, "batch"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "deleteBuckets", [_dec6], Object.getOwnPropertyDescriptor(_class.prototype, "deleteBuckets"), _class.prototype)), _class));
-exports.default = KintoClientBase;
-
-},{"./batch":3,"./bucket":4,"./endpoint":6,"./http":8,"./requests":9,"./utils":10}],3:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.aggregate = aggregate;
-/**
- * Exports batch responses as a result object.
- *
- * @private
- * @param {Array} responses The batch subrequest responses.
- * @param {Array} requests The initial issued requests.
- * @return {Object}
- */
-function aggregate(responses = [], requests = []) {
- if (responses.length !== requests.length) {
- throw new Error("Responses length should match requests one.");
- }
- const results = {
- errors: [],
- published: [],
- conflicts: [],
- skipped: []
- };
- return responses.reduce((acc, response, index) => {
- const { status } = response;
- if (status >= 200 && status < 400) {
- acc.published.push(response.body);
- } else if (status === 404) {
- acc.skipped.push(response.body);
- } else if (status === 412) {
- acc.conflicts.push({
- // XXX: specifying the type is probably superfluous
- type: "outgoing",
- local: requests[index].body,
- remote: response.body.details && response.body.details.existing || null
- });
- } else {
- acc.errors.push({
- path: response.path,
- sent: requests[index],
- error: response.body
- });
- }
- return acc;
- }, results);
-}
-
-},{}],4:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.default = undefined;
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-var _utils = require("./utils");
-
-var _collection = require("./collection");
-
-var _collection2 = _interopRequireDefault(_collection);
-
-var _requests = require("./requests");
-
-var requests = _interopRequireWildcard(_requests);
-
-var _endpoint = require("./endpoint");
-
-var _endpoint2 = _interopRequireDefault(_endpoint);
-
-function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-/**
- * Abstract representation of a selected bucket.
- *
- */
-let Bucket = class Bucket {
- /**
- * Constructor.
- *
- * @param {KintoClient} client The client instance.
- * @param {String} name The bucket name.
- * @param {Object} [options={}] The headers object option.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- */
- constructor(client, name, options = {}) {
- /**
- * @ignore
- */
- this.client = client;
- /**
- * The bucket name.
- * @type {String}
- */
- this.name = name;
- /**
- * The default options object.
- * @ignore
- * @type {Object}
- */
- this.options = options;
- /**
- * @ignore
- */
- this._isBatch = !!options.batch;
- }
-
- /**
- * Merges passed request options with default bucket ones, if any.
- *
- * @private
- * @param {Object} [options={}] The options to merge.
- * @return {Object} The merged options.
- */
- _bucketOptions(options = {}) {
- const headers = _extends({}, this.options && this.options.headers, options.headers);
- return _extends({}, this.options, options, {
- headers,
- bucket: this.name,
- batch: this._isBatch
- });
- }
-
- /**
- * Selects a collection.
- *
- * @param {String} name The collection name.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @return {Collection}
- */
- collection(name, options = {}) {
- return new _collection2.default(this.client, this, name, this._bucketOptions(options));
- }
-
- /**
- * Retrieves bucket data.
- *
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object, Error>}
- */
- getData(options = {}) {
- return this.client.execute({
- path: (0, _endpoint2.default)("bucket", this.name),
- headers: _extends({}, this.options.headers, options.headers)
- }).then(res => res.data);
- }
-
- /**
- * Set bucket data.
- * @param {Object} data The bucket data object.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Boolean} [options.patch] The patch option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- setData(data, options = {}) {
- if (!(0, _utils.isObject)(data)) {
- throw new Error("A bucket object is required.");
- }
-
- const bucket = _extends({}, data, { id: this.name });
-
- // For default bucket, we need to drop the id from the data object.
- // Bug in Kinto < 3.1.1
- const bucketId = bucket.id;
- if (bucket.id === "default") {
- delete bucket.id;
- }
-
- const path = (0, _endpoint2.default)("bucket", bucketId);
- const { permissions } = options;
- const reqOptions = _extends({}, this._bucketOptions(options));
- const request = requests.updateRequest(path, { data: bucket, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Retrieves the list of collections in the current bucket.
- *
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Array<Object>, Error>}
- */
- listCollections(options = {}) {
- return this.client.execute({
- path: (0, _endpoint2.default)("collection", this.name),
- headers: _extends({}, this.options.headers, options.headers)
- });
- }
-
- /**
- * Creates a new collection in current bucket.
- *
- * @param {String|undefined} id The collection id.
- * @param {Object} [options={}] The options object.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Object} [options.headers] The headers object option.
- * @param {Object} [options.permissions] The permissions object.
- * @param {Object} [options.data] The data object.
- * @return {Promise<Object, Error>}
- */
- createCollection(id, options = {}) {
- const reqOptions = this._bucketOptions(options);
- const { permissions, data = {} } = reqOptions;
- data.id = id;
- const path = (0, _endpoint2.default)("collection", this.name, id);
- const request = requests.createRequest(path, { data, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Deletes a collection from the current bucket.
- *
- * @param {Object|String} collection The collection to delete.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- deleteCollection(collection, options = {}) {
- const collectionObj = (0, _utils.toDataBody)(collection);
- if (!collectionObj.id) {
- throw new Error("A collection id is required.");
- }
- const { id, last_modified } = collectionObj;
- const reqOptions = this._bucketOptions(_extends({ last_modified }, options));
- const path = (0, _endpoint2.default)("collection", this.name, id);
- const request = requests.deleteRequest(path, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Retrieves the list of groups in the current bucket.
- *
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Array<Object>, Error>}
- */
- listGroups(options = {}) {
- return this.client.execute({
- path: (0, _endpoint2.default)("group", this.name),
- headers: _extends({}, this.options.headers, options.headers)
- });
- }
-
- /**
- * Creates a new group in current bucket.
- *
- * @param {String} id The group id.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object, Error>}
- */
- getGroup(id, options = {}) {
- return this.client.execute({
- path: (0, _endpoint2.default)("group", this.name, id),
- headers: _extends({}, this.options.headers, options.headers)
- });
- }
-
- /**
- * Creates a new group in current bucket.
- *
- * @param {String|undefined} id The group id.
- * @param {Array<String>} [members=[]] The list of principals.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.data] The data object.
- * @param {Object} [options.permissions] The permissions object.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object, Error>}
- */
- createGroup(id, members = [], options = {}) {
- const reqOptions = this._bucketOptions(options);
- const data = _extends({}, options.data, {
- id,
- members
- });
- const path = (0, _endpoint2.default)("group", this.name, id);
- const { permissions } = options;
- const request = requests.createRequest(path, { data, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Updates an existing group in current bucket.
- *
- * @param {Object} group The group object.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.data] The data object.
- * @param {Object} [options.permissions] The permissions object.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Object} [options.headers] The headers object option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- updateGroup(group, options = {}) {
- if (!(0, _utils.isObject)(group)) {
- throw new Error("A group object is required.");
- }
- if (!group.id) {
- throw new Error("A group id is required.");
- }
- const reqOptions = this._bucketOptions(options);
- const data = _extends({}, options.data, group);
- const path = (0, _endpoint2.default)("group", this.name, group.id);
- const { permissions } = options;
- const request = requests.updateRequest(path, { data, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Deletes a group from the current bucket.
- *
- * @param {Object|String} group The group to delete.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- deleteGroup(group, options = {}) {
- const groupObj = (0, _utils.toDataBody)(group);
- const { id, last_modified } = groupObj;
- const reqOptions = this._bucketOptions(_extends({ last_modified }, options));
- const path = (0, _endpoint2.default)("group", this.name, id);
- const request = requests.deleteRequest(path, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Retrieves the list of permissions for this bucket.
- *
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object, Error>}
- */
- getPermissions(options = {}) {
- return this.client.execute({
- path: (0, _endpoint2.default)("bucket", this.name),
- headers: _extends({}, this.options.headers, options.headers)
- }).then(res => res.permissions);
- }
-
- /**
- * Replaces all existing bucket permissions with the ones provided.
- *
- * @param {Object} permissions The permissions object.
- * @param {Object} [options={}] The options object
- * @param {Boolean} [options.safe] The safe option.
- * @param {Object} [options.headers] The headers object option.
- * @param {Object} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- setPermissions(permissions, options = {}) {
- if (!(0, _utils.isObject)(permissions)) {
- throw new Error("A permissions object is required.");
- }
- const path = (0, _endpoint2.default)("bucket", this.name);
- const reqOptions = _extends({}, this._bucketOptions(options));
- const { last_modified } = options;
- const data = { last_modified };
- const request = requests.updateRequest(path, { data, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Performs batch operations at the current bucket level.
- *
- * @param {Function} fn The batch operation function.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Boolean} [options.aggregate] Produces a grouped result object.
- * @return {Promise<Object, Error>}
- */
- batch(fn, options = {}) {
- return this.client.batch(fn, this._bucketOptions(options));
- }
-};
-exports.default = Bucket;
-
-},{"./collection":5,"./endpoint":6,"./requests":9,"./utils":10}],5:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.default = undefined;
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-var _utils = require("./utils");
-
-var _requests = require("./requests");
-
-var requests = _interopRequireWildcard(_requests);
-
-var _endpoint = require("./endpoint");
-
-var _endpoint2 = _interopRequireDefault(_endpoint);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
-
-/**
- * Abstract representation of a selected collection.
- *
- */
-let Collection = class Collection {
- /**
- * Constructor.
- *
- * @param {KintoClient} client The client instance.
- * @param {Bucket} bucket The bucket instance.
- * @param {String} name The collection name.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- */
- constructor(client, bucket, name, options = {}) {
- /**
- * @ignore
- */
- this.client = client;
- /**
- * @ignore
- */
- this.bucket = bucket;
- /**
- * The collection name.
- * @type {String}
- */
- this.name = name;
-
- /**
- * The default collection options object, embedding the default bucket ones.
- * @ignore
- * @type {Object}
- */
- this.options = _extends({}, this.bucket.options, options, {
- headers: _extends({}, this.bucket.options && this.bucket.options.headers, options.headers)
- });
- /**
- * @ignore
- */
- this._isBatch = !!options.batch;
- }
-
- /**
- * Merges passed request options with default bucket and collection ones, if
- * any.
- *
- * @private
- * @param {Object} [options={}] The options to merge.
- * @return {Object} The merged options.
- */
- _collOptions(options = {}) {
- const headers = _extends({}, this.options && this.options.headers, options.headers);
- return _extends({}, this.options, options, {
- headers
- });
- }
-
- /**
- * Retrieves collection data.
- *
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object, Error>}
- */
- getData(options = {}) {
- const { headers } = this._collOptions(options);
- return this.client.execute({
- path: (0, _endpoint2.default)("collection", this.bucket.name, this.name),
- headers
- }).then(res => res.data);
- }
-
- /**
- * Set collection data.
- * @param {Object} data The collection data object.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Boolean} [options.patch] The patch option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- setData(data, options = {}) {
- if (!(0, _utils.isObject)(data)) {
- throw new Error("A collection object is required.");
- }
- const reqOptions = this._collOptions(options);
- const { permissions } = reqOptions;
-
- const path = (0, _endpoint2.default)("collection", this.bucket.name, this.name);
- const request = requests.updateRequest(path, { data, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Retrieves the list of permissions for this collection.
- *
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object, Error>}
- */
- getPermissions(options = {}) {
- const { headers } = this._collOptions(options);
- return this.client.execute({
- path: (0, _endpoint2.default)("collection", this.bucket.name, this.name),
- headers
- }).then(res => res.permissions);
- }
-
- /**
- * Replaces all existing collection permissions with the ones provided.
- *
- * @param {Object} permissions The permissions object.
- * @param {Object} [options={}] The options object
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- setPermissions(permissions, options = {}) {
- if (!(0, _utils.isObject)(permissions)) {
- throw new Error("A permissions object is required.");
- }
- const reqOptions = this._collOptions(options);
- const path = (0, _endpoint2.default)("collection", this.bucket.name, this.name);
- const data = { last_modified: options.last_modified };
- const request = requests.updateRequest(path, { data, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Creates a record in current collection.
- *
- * @param {Object} record The record to create.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @return {Promise<Object, Error>}
- */
- createRecord(record, options = {}) {
- const reqOptions = this._collOptions(options);
- const { permissions } = reqOptions;
- const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, record.id);
- const request = requests.createRequest(path, { data: record, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Updates a record in current collection.
- *
- * @param {Object} record The record to update.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- updateRecord(record, options = {}) {
- if (!(0, _utils.isObject)(record)) {
- throw new Error("A record object is required.");
- }
- if (!record.id) {
- throw new Error("A record id is required.");
- }
- const reqOptions = this._collOptions(options);
- const { permissions } = reqOptions;
- const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, record.id);
- const request = requests.updateRequest(path, { data: record, permissions }, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Deletes a record from the current collection.
- *
- * @param {Object|String} record The record to delete.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Number} [options.last_modified] The last_modified option.
- * @return {Promise<Object, Error>}
- */
- deleteRecord(record, options = {}) {
- const recordObj = (0, _utils.toDataBody)(record);
- if (!recordObj.id) {
- throw new Error("A record id is required.");
- }
- const { id, last_modified } = recordObj;
- const reqOptions = this._collOptions(_extends({ last_modified }, options));
- const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, id);
- const request = requests.deleteRequest(path, reqOptions);
- return this.client.execute(request);
- }
-
- /**
- * Retrieves a record from the current collection.
- *
- * @param {String} id The record id to retrieve.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @return {Promise<Object, Error>}
- */
- getRecord(id, options = {}) {
- return this.client.execute(_extends({
- path: (0, _endpoint2.default)("record", this.bucket.name, this.name, id)
- }, this._collOptions(options)));
- }
-
- /**
- * Lists records from the current collection.
- *
- * Sorting is done by passing a `sort` string option:
- *
- * - The field to order the results by, prefixed with `-` for descending.
- * Default: `-last_modified`.
- *
- * @see http://kinto.readthedocs.io/en/stable/core/api/resource.html#sorting
- *
- * Filtering is done by passing a `filters` option object:
- *
- * - `{fieldname: "value"}`
- * - `{min_fieldname: 4000}`
- * - `{in_fieldname: "1,2,3"}`
- * - `{not_fieldname: 0}`
- * - `{exclude_fieldname: "0,1"}`
- *
- * @see http://kinto.readthedocs.io/en/stable/core/api/resource.html#filtering
- *
- * Paginating is done by passing a `limit` option, then calling the `next()`
- * method from the resolved result object to fetch the next page, if any.
- *
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Object} [options.filters=[]] The filters object.
- * @param {String} [options.sort="-last_modified"] The sort field.
- * @param {String} [options.limit=null] The limit field.
- * @param {String} [options.pages=1] The number of result pages to aggregate.
- * @param {Number} [options.since=null] Only retrieve records modified since the provided timestamp.
- * @return {Promise<Object, Error>}
- */
- listRecords(options = {}) {
- const { http } = this.client;
- const { sort, filters, limit, pages, since } = _extends({
- sort: "-last_modified"
- }, options);
- // Safety/Consistency check on ETag value.
- if (since && typeof since !== "string") {
- throw new Error(`Invalid value for since (${ since }), should be ETag value.`);
- }
- const collHeaders = this.options.headers;
- const path = (0, _endpoint2.default)("record", this.bucket.name, this.name);
- const querystring = (0, _utils.qsify)(_extends({}, filters, {
- _sort: sort,
- _limit: limit,
- _since: since
- }));
- let results = [],
- current = 0;
-
- const next = function (nextPage) {
- if (!nextPage) {
- throw new Error("Pagination exhausted.");
- }
- return processNextPage(nextPage);
- };
-
- const processNextPage = nextPage => {
- return http.request(nextPage, { headers: collHeaders }).then(handleResponse);
- };
-
- const pageResults = (results, nextPage, etag) => {
- // ETag string is supposed to be opaque and stored «as-is».
- // ETag header values are quoted (because of * and W/"foo").
- return {
- last_modified: etag ? etag.replace(/"/g, "") : etag,
- data: results,
- next: next.bind(null, nextPage)
- };
- };
-
- const handleResponse = ({ headers, json }) => {
- const nextPage = headers.get("Next-Page");
- const etag = headers.get("ETag");
- if (!pages) {
- return pageResults(json.data, nextPage, etag);
- }
- // Aggregate new results with previous ones
- results = results.concat(json.data);
- current += 1;
- if (current >= pages || !nextPage) {
- // Pagination exhausted
- return pageResults(results, nextPage, etag);
- }
- // Follow next page
- return processNextPage(nextPage);
- };
-
- return this.client.execute(_extends({
- path: path + "?" + querystring
- }, this._collOptions(options)), { raw: true }).then(handleResponse);
- }
-
- /**
- * Performs batch operations at the current collection level.
- *
- * @param {Function} fn The batch operation function.
- * @param {Object} [options={}] The options object.
- * @param {Object} [options.headers] The headers object option.
- * @param {Boolean} [options.safe] The safe option.
- * @param {Boolean} [options.aggregate] Produces a grouped result object.
- * @return {Promise<Object, Error>}
- */
- batch(fn, options = {}) {
- const reqOptions = this._collOptions(options);
- return this.client.batch(fn, _extends({}, reqOptions, {
- bucket: this.bucket.name,
- collection: this.name
- }));
- }
-};
-exports.default = Collection;
-
-},{"./endpoint":6,"./requests":9,"./utils":10}],6:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.default = endpoint;
-/**
- * Endpoints templates.
- * @type {Object}
- */
-const ENDPOINTS = {
- root: () => "/",
- batch: () => "/batch",
- bucket: bucket => "/buckets" + (bucket ? `/${ bucket }` : ""),
- collection: (bucket, coll) => `${ ENDPOINTS.bucket(bucket) }/collections` + (coll ? `/${ coll }` : ""),
- group: (bucket, group) => `${ ENDPOINTS.bucket(bucket) }/groups` + (group ? `/${ group }` : ""),
- record: (bucket, coll, id) => `${ ENDPOINTS.collection(bucket, coll) }/records` + (id ? `/${ id }` : "")
-};
-
-/**
- * Retrieves a server enpoint by its name.
- *
- * @private
- * @param {String} name The endpoint name.
- * @param {...string} args The endpoint parameters.
- * @return {String}
- */
-function endpoint(name, ...args) {
- return ENDPOINTS[name](...args);
-}
-
-},{}],7:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-/**
- * Kinto server error code descriptors.
- * @type {Object}
- */
-exports.default = {
- 104: "Missing Authorization Token",
- 105: "Invalid Authorization Token",
- 106: "Request body was not valid JSON",
- 107: "Invalid request parameter",
- 108: "Missing request parameter",
- 109: "Invalid posted data",
- 110: "Invalid Token / id",
- 111: "Missing Token / id",
- 112: "Content-Length header was not provided",
- 113: "Request body too large",
- 114: "Resource was modified meanwhile",
- 115: "Method not allowed on this end point (hint: server may be readonly)",
- 116: "Requested version not available on this server",
- 117: "Client has sent too many requests",
- 121: "Resource access is forbidden for this user",
- 122: "Another resource violates constraint",
- 201: "Service Temporary unavailable due to high load",
- 202: "Service deprecated",
- 999: "Internal Server Error"
-};
-
-},{}],8:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.default = undefined;
-
-var _errors = require("./errors");
-
-var _errors2 = _interopRequireDefault(_errors);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-/**
- * Enhanced HTTP client for the Kinto protocol.
- * @private
- */
-let HTTP = class HTTP {
- /**
- * Default HTTP request headers applied to each outgoing request.
- *
- * @type {Object}
- */
- static get DEFAULT_REQUEST_HEADERS() {
- return {
- "Accept": "application/json",
- "Content-Type": "application/json"
- };
- }
-
- /**
- * Default options.
- *
- * @type {Object}
- */
- static get defaultOptions() {
- return { timeout: 5000, requestMode: "cors" };
- }
-
- /**
- * Constructor.
- *
- * @param {EventEmitter} events The event handler.
- * @param {Object} [options={}} The options object.
- * @param {Number} [options.timeout=5000] The request timeout in ms (default: `5000`).
- * @param {String} [options.requestMode="cors"] The HTTP request mode (default: `"cors"`).
- */
- constructor(events, options = {}) {
- // public properties
- /**
- * The event emitter instance.
- * @type {EventEmitter}
- */
- if (!events) {
- throw new Error("No events handler provided");
- }
- this.events = events;
-
- /**
- * The request mode.
- * @see https://fetch.spec.whatwg.org/#requestmode
- * @type {String}
- */
- this.requestMode = options.requestMode || HTTP.defaultOptions.requestMode;
-
- /**
- * The request timeout.
- * @type {Number}
- */
- this.timeout = options.timeout || HTTP.defaultOptions.timeout;
- }
-
- /**
- * Performs an HTTP request to the Kinto server.
- *
- * Resolves with an objet containing the following HTTP response properties:
- * - `{Number} status` The HTTP status code.
- * - `{Object} json` The JSON response body.
- * - `{Headers} headers` The response headers object; see the ES6 fetch() spec.
- *
- * @param {String} url The URL.
- * @param {Object} [options={}] The fetch() options object.
- * @param {Object} [options.headers] The request headers object (default: {})
- * @return {Promise}
- */
- request(url, options = { headers: {} }) {
- let response, status, statusText, headers, hasTimedout;
- // Ensure default request headers are always set
- options.headers = Object.assign({}, HTTP.DEFAULT_REQUEST_HEADERS, options.headers);
- options.mode = this.requestMode;
- return new Promise((resolve, reject) => {
- const _timeoutId = setTimeout(() => {
- hasTimedout = true;
- reject(new Error("Request timeout."));
- }, this.timeout);
- fetch(url, options).then(res => {
- if (!hasTimedout) {
- clearTimeout(_timeoutId);
- resolve(res);
- }
- }).catch(err => {
- if (!hasTimedout) {
- clearTimeout(_timeoutId);
- reject(err);
- }
- });
- }).then(res => {
- response = res;
- headers = res.headers;
- status = res.status;
- statusText = res.statusText;
- this._checkForDeprecationHeader(headers);
- this._checkForBackoffHeader(status, headers);
- this._checkForRetryAfterHeader(status, headers);
- return res.text();
- })
- // Check if we have a body; if so parse it as JSON.
- .then(text => {
- if (text.length === 0) {
- return null;
- }
- // Note: we can't consume the response body twice.
- return JSON.parse(text);
- }).catch(err => {
- const error = new Error(`HTTP ${ status || 0 }; ${ err }`);
- error.response = response;
- error.stack = err.stack;
- throw error;
- }).then(json => {
- if (json && status >= 400) {
- let message = `HTTP ${ status } ${ json.error || "" }: `;
- if (json.errno && json.errno in _errors2.default) {
- const errnoMsg = _errors2.default[json.errno];
- message += errnoMsg;
- if (json.message && json.message !== errnoMsg) {
- message += ` (${ json.message })`;
- }
- } else {
- message += statusText || "";
- }
- const error = new Error(message.trim());
- error.response = response;
- error.data = json;
- throw error;
- }
- return { status, json, headers };
- });
- }
-
- _checkForDeprecationHeader(headers) {
- const alertHeader = headers.get("Alert");
- if (!alertHeader) {
- return;
- }
- let alert;
- try {
- alert = JSON.parse(alertHeader);
- } catch (err) {
- console.warn("Unable to parse Alert header message", alertHeader);
- return;
- }
- console.warn(alert.message, alert.url);
- this.events.emit("deprecated", alert);
- }
-
- _checkForBackoffHeader(status, headers) {
- let backoffMs;
- const backoffSeconds = parseInt(headers.get("Backoff"), 10);
- if (backoffSeconds > 0) {
- backoffMs = new Date().getTime() + backoffSeconds * 1000;
- } else {
- backoffMs = 0;
- }
- this.events.emit("backoff", backoffMs);
- }
-
- _checkForRetryAfterHeader(status, headers) {
- let retryAfter = headers.get("Retry-After");
- if (!retryAfter) {
- return;
- }
- retryAfter = new Date().getTime() + parseInt(retryAfter, 10) * 1000;
- this.events.emit("retry-after", retryAfter);
- }
-};
-exports.default = HTTP;
-
-},{"./errors":7}],9:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
-exports.createRequest = createRequest;
-exports.updateRequest = updateRequest;
-exports.deleteRequest = deleteRequest;
-
-var _utils = require("./utils");
-
-const requestDefaults = {
- safe: false,
- // check if we should set default content type here
- headers: {},
- permissions: undefined,
- data: undefined,
- patch: false
-};
-
-/**
- * @private
- */
-function safeHeader(safe, last_modified) {
- if (!safe) {
- return {};
- }
- if (last_modified) {
- return { "If-Match": `"${ last_modified }"` };
- }
- return { "If-None-Match": "*" };
-}
-
-/**
- * @private
- */
-function createRequest(path, { data, permissions }, options = {}) {
- const { headers, safe } = _extends({}, requestDefaults, options);
- return {
- method: data && data.id ? "PUT" : "POST",
- path,
- headers: _extends({}, headers, safeHeader(safe)),
- body: {
- data,
- permissions
- }
- };
-}
-
-/**
- * @private
- */
-function updateRequest(path, { data, permissions }, options = {}) {
- const {
- headers,
- safe,
- patch
- } = _extends({}, requestDefaults, options);
- const { last_modified } = _extends({}, data, options);
-
- if (Object.keys((0, _utils.omit)(data, "id", "last_modified")).length === 0) {
- data = undefined;
- }
-
- return {
- method: patch ? "PATCH" : "PUT",
- path,
- headers: _extends({}, headers, safeHeader(safe, last_modified)),
- body: {
- data,
- permissions
- }
- };
-}
-
-/**
- * @private
- */
-function deleteRequest(path, options = {}) {
- const { headers, safe, last_modified } = _extends({}, requestDefaults, options);
- if (safe && !last_modified) {
- throw new Error("Safe concurrency check requires a last_modified value.");
- }
- return {
- method: "DELETE",
- path,
- headers: _extends({}, headers, safeHeader(safe, last_modified))
- };
-}
-
-},{"./utils":10}],10:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.partition = partition;
-exports.pMap = pMap;
-exports.omit = omit;
-exports.toDataBody = toDataBody;
-exports.qsify = qsify;
-exports.checkVersion = checkVersion;
-exports.support = support;
-exports.capable = capable;
-exports.nobatch = nobatch;
-exports.isObject = isObject;
-/**
- * Chunks an array into n pieces.
- *
- * @private
- * @param {Array} array
- * @param {Number} n
- * @return {Array}
- */
-function partition(array, n) {
- if (n <= 0) {
- return array;
- }
- return array.reduce((acc, x, i) => {
- if (i === 0 || i % n === 0) {
- acc.push([x]);
- } else {
- acc[acc.length - 1].push(x);
- }
- return acc;
- }, []);
-}
-
-/**
- * Maps a list to promises using the provided mapping function, executes them
- * sequentially then returns a Promise resolving with ordered results obtained.
- * Think of this as a sequential Promise.all.
- *
- * @private
- * @param {Array} list The list to map.
- * @param {Function} fn The mapping function.
- * @return {Promise}
- */
-function pMap(list, fn) {
- let results = [];
- return list.reduce((promise, entry) => {
- return promise.then(() => {
- return Promise.resolve(fn(entry)).then(result => results = results.concat(result));
- });
- }, Promise.resolve()).then(() => results);
-}
-
-/**
- * Takes an object and returns a copy of it with the provided keys omitted.
- *
- * @private
- * @param {Object} obj The source object.
- * @param {...String} keys The keys to omit.
- * @return {Object}
- */
-function omit(obj, ...keys) {
- return Object.keys(obj).reduce((acc, key) => {
- if (keys.indexOf(key) === -1) {
- acc[key] = obj[key];
- }
- return acc;
- }, {});
-}
-
-/**
- * Always returns a resource data object from the provided argument.
- *
- * @private
- * @param {Object|String} resource
- * @return {Object}
- */
-function toDataBody(resource) {
- if (isObject(resource)) {
- return resource;
- }
- if (typeof resource === "string") {
- return { id: resource };
- }
- throw new Error("Invalid argument.");
-}
-
-/**
- * Transforms an object into an URL query string, stripping out any undefined
- * values.
- *
- * @param {Object} obj
- * @return {String}
- */
-function qsify(obj) {
- const sep = "&";
- const encode = v => encodeURIComponent(typeof v === "boolean" ? String(v) : v);
- const stripUndefined = o => JSON.parse(JSON.stringify(o));
- const stripped = stripUndefined(obj);
- return Object.keys(stripped).map(k => {
- const ks = encode(k) + "=";
- if (Array.isArray(stripped[k])) {
- return stripped[k].map(v => ks + encode(v)).join(sep);
- } else {
- return ks + encode(stripped[k]);
- }
- }).join(sep);
-}
-
-/**
- * Checks if a version is within the provided range.
- *
- * @param {String} version The version to check.
- * @param {String} minVersion The minimum supported version (inclusive).
- * @param {String} maxVersion The minimum supported version (exclusive).
- * @throws {Error} If the version is outside of the provided range.
- */
-function checkVersion(version, minVersion, maxVersion) {
- const extract = str => str.split(".").map(x => parseInt(x, 10));
- const [verMajor, verMinor] = extract(version);
- const [minMajor, minMinor] = extract(minVersion);
- const [maxMajor, maxMinor] = extract(maxVersion);
- const checks = [verMajor < minMajor, verMajor === minMajor && verMinor < minMinor, verMajor > maxMajor, verMajor === maxMajor && verMinor >= maxMinor];
- if (checks.some(x => x)) {
- throw new Error(`Version ${ version } doesn't satisfy ` + `${ minVersion } <= x < ${ maxVersion }`);
- }
-}
-
-/**
- * Generates a decorator function ensuring a version check is performed against
- * the provided requirements before executing it.
- *
- * @param {String} min The required min version (inclusive).
- * @param {String} max The required max version (inclusive).
- * @return {Function}
- */
-function support(min, max) {
- return function (target, key, descriptor) {
- const fn = descriptor.value;
- return {
- configurable: true,
- get() {
- const wrappedMethod = (...args) => {
- // "this" is the current instance which its method is decorated.
- const client = "client" in this ? this.client : this;
- return client.fetchHTTPApiVersion().then(version => checkVersion(version, min, max)).then(Promise.resolve(fn.apply(this, args)));
- };
- Object.defineProperty(this, key, {
- value: wrappedMethod,
- configurable: true,
- writable: true
- });
- return wrappedMethod;
- }
- };
- };
-}
-
-/**
- * Generates a decorator function ensuring that the specified capabilities are
- * available on the server before executing it.
- *
- * @param {Array<String>} capabilities The required capabilities.
- * @return {Function}
- */
-function capable(capabilities) {
- return function (target, key, descriptor) {
- const fn = descriptor.value;
- return {
- configurable: true,
- get() {
- const wrappedMethod = (...args) => {
- // "this" is the current instance which its method is decorated.
- const client = "client" in this ? this.client : this;
- return client.fetchServerCapabilities().then(available => {
- const missing = capabilities.filter(c => available.indexOf(c) < 0);
- if (missing.length > 0) {
- throw new Error(`Required capabilities ${ missing.join(", ") } ` + "not present on server");
- }
- }).then(Promise.resolve(fn.apply(this, args)));
- };
- Object.defineProperty(this, key, {
- value: wrappedMethod,
- configurable: true,
- writable: true
- });
- return wrappedMethod;
- }
- };
- };
-}
-
-/**
- * Generates a decorator function ensuring an operation is not performed from
- * within a batch request.
- *
- * @param {String} message The error message to throw.
- * @return {Function}
- */
-function nobatch(message) {
- return function (target, key, descriptor) {
- const fn = descriptor.value;
- return {
- configurable: true,
- get() {
- const wrappedMethod = (...args) => {
- // "this" is the current instance which its method is decorated.
- if (this._isBatch) {
- throw new Error(message);
- }
- return fn.apply(this, args);
- };
- Object.defineProperty(this, key, {
- value: wrappedMethod,
- configurable: true,
- writable: true
- });
- return wrappedMethod;
- }
- };
- };
-}
-
-/**
- * Returns true if the specified value is an object (i.e. not an array nor null).
- * @param {Object} thing The value to inspect.
- * @return {bool}
- */
-function isObject(thing) {
- return typeof thing === "object" && thing !== null && !Array.isArray(thing);
-}
-
-},{}]},{},[1])(1)
-}); \ No newline at end of file
diff --git a/services/common/kinto-offline-client.js b/services/common/kinto-offline-client.js
deleted file mode 100644
index 4d0dbd0f3d..0000000000
--- a/services/common/kinto-offline-client.js
+++ /dev/null
@@ -1,4286 +0,0 @@
-/*
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * This file is generated from kinto.js - do not modify directly.
- */
-
-this.EXPORTED_SYMBOLS = ["loadKinto"];
-
-/*
- * Version 5.1.0 - 8beb61d
- */
-
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.loadKinto = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _extends2 = require("babel-runtime/helpers/extends");
-
-var _extends3 = _interopRequireDefault(_extends2);
-
-var _stringify = require("babel-runtime/core-js/json/stringify");
-
-var _stringify2 = _interopRequireDefault(_stringify);
-
-var _promise = require("babel-runtime/core-js/promise");
-
-var _promise2 = _interopRequireDefault(_promise);
-
-exports.reduceRecords = reduceRecords;
-
-var _base = require("../src/adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-var _utils = require("../src/utils");
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-Components.utils.import("resource://gre/modules/Sqlite.jsm");
-Components.utils.import("resource://gre/modules/Task.jsm");
-
-const SQLITE_PATH = "kinto.sqlite";
-
-const statements = {
- "createCollectionData": `
- CREATE TABLE collection_data (
- collection_name TEXT,
- record_id TEXT,
- record TEXT
- );`,
-
- "createCollectionMetadata": `
- CREATE TABLE collection_metadata (
- collection_name TEXT PRIMARY KEY,
- last_modified INTEGER
- ) WITHOUT ROWID;`,
-
- "createCollectionDataRecordIdIndex": `
- CREATE UNIQUE INDEX unique_collection_record
- ON collection_data(collection_name, record_id);`,
-
- "clearData": `
- DELETE FROM collection_data
- WHERE collection_name = :collection_name;`,
-
- "createData": `
- INSERT INTO collection_data (collection_name, record_id, record)
- VALUES (:collection_name, :record_id, :record);`,
-
- "updateData": `
- INSERT OR REPLACE INTO collection_data (collection_name, record_id, record)
- VALUES (:collection_name, :record_id, :record);`,
-
- "deleteData": `
- DELETE FROM collection_data
- WHERE collection_name = :collection_name
- AND record_id = :record_id;`,
-
- "saveLastModified": `
- REPLACE INTO collection_metadata (collection_name, last_modified)
- VALUES (:collection_name, :last_modified);`,
-
- "getLastModified": `
- SELECT last_modified
- FROM collection_metadata
- WHERE collection_name = :collection_name;`,
-
- "getRecord": `
- SELECT record
- FROM collection_data
- WHERE collection_name = :collection_name
- AND record_id = :record_id;`,
-
- "listRecords": `
- SELECT record
- FROM collection_data
- WHERE collection_name = :collection_name;`,
-
- // N.B. we have to have a dynamic number of placeholders, which you
- // can't do without building your own statement. See `execute` for details
- "listRecordsById": `
- SELECT record_id, record
- FROM collection_data
- WHERE collection_name = ?
- AND record_id IN `,
-
- "importData": `
- REPLACE INTO collection_data (collection_name, record_id, record)
- VALUES (:collection_name, :record_id, :record);`,
-
- "scanAllRecords": `SELECT * FROM collection_data;`,
-
- "clearCollectionMetadata": `DELETE FROM collection_metadata;`
-};
-
-const createStatements = ["createCollectionData", "createCollectionMetadata", "createCollectionDataRecordIdIndex"];
-
-const currentSchemaVersion = 1;
-
-/**
- * Firefox adapter.
- *
- * Uses Sqlite as a backing store.
- *
- * Options:
- * - path: the filename/path for the Sqlite database. If absent, use SQLITE_PATH.
- */
-class FirefoxAdapter extends _base2.default {
- constructor(collection, options = {}) {
- super();
- const { sqliteHandle = null } = options;
- this.collection = collection;
- this._connection = sqliteHandle;
- this._options = options;
- }
-
- // We need to be capable of calling this from "outside" the adapter
- // so that someone can initialize a connection and pass it to us in
- // adapterOptions.
- static _init(connection) {
- return Task.spawn(function* () {
- yield connection.executeTransaction(function* doSetup() {
- const schema = yield connection.getSchemaVersion();
-
- if (schema == 0) {
-
- for (let statementName of createStatements) {
- yield connection.execute(statements[statementName]);
- }
-
- yield connection.setSchemaVersion(currentSchemaVersion);
- } else if (schema != 1) {
- throw new Error("Unknown database schema: " + schema);
- }
- });
- return connection;
- });
- }
-
- _executeStatement(statement, params) {
- if (!this._connection) {
- throw new Error("The storage adapter is not open");
- }
- return this._connection.executeCached(statement, params);
- }
-
- open() {
- const self = this;
- return Task.spawn(function* () {
- if (!self._connection) {
- const path = self._options.path || SQLITE_PATH;
- const opts = { path, sharedMemoryCache: false };
- self._connection = yield Sqlite.openConnection(opts).then(FirefoxAdapter._init);
- }
- });
- }
-
- close() {
- if (this._connection) {
- const promise = this._connection.close();
- this._connection = null;
- return promise;
- }
- return _promise2.default.resolve();
- }
-
- clear() {
- const params = { collection_name: this.collection };
- return this._executeStatement(statements.clearData, params);
- }
-
- execute(callback, options = { preload: [] }) {
- if (!this._connection) {
- throw new Error("The storage adapter is not open");
- }
-
- let result;
- const conn = this._connection;
- const collection = this.collection;
-
- return conn.executeTransaction(function* doExecuteTransaction() {
- // Preload specified records from DB, within transaction.
- const parameters = [collection, ...options.preload];
- const placeholders = options.preload.map(_ => "?");
- const stmt = statements.listRecordsById + "(" + placeholders.join(",") + ");";
- const rows = yield conn.execute(stmt, parameters);
-
- const preloaded = rows.reduce((acc, row) => {
- const record = JSON.parse(row.getResultByName("record"));
- acc[row.getResultByName("record_id")] = record;
- return acc;
- }, {});
-
- const proxy = transactionProxy(collection, preloaded);
- result = callback(proxy);
-
- for (let { statement, params } of proxy.operations) {
- yield conn.executeCached(statement, params);
- }
- }, conn.TRANSACTION_EXCLUSIVE).then(_ => result);
- }
-
- get(id) {
- const params = {
- collection_name: this.collection,
- record_id: id
- };
- return this._executeStatement(statements.getRecord, params).then(result => {
- if (result.length == 0) {
- return;
- }
- return JSON.parse(result[0].getResultByName("record"));
- });
- }
-
- list(params = { filters: {}, order: "" }) {
- const parameters = {
- collection_name: this.collection
- };
- return this._executeStatement(statements.listRecords, parameters).then(result => {
- const records = [];
- for (let k = 0; k < result.length; k++) {
- const row = result[k];
- records.push(JSON.parse(row.getResultByName("record")));
- }
- return records;
- }).then(results => {
- // The resulting list of records is filtered and sorted.
- // XXX: with some efforts, this could be implemented using SQL.
- return reduceRecords(params.filters, params.order, results);
- });
- }
-
- /**
- * Load a list of records into the local database.
- *
- * Note: The adapter is not in charge of filtering the already imported
- * records. This is done in `Collection#loadDump()`, as a common behaviour
- * between every adapters.
- *
- * @param {Array} records.
- * @return {Array} imported records.
- */
- loadDump(records) {
- const connection = this._connection;
- const collection_name = this.collection;
- return Task.spawn(function* () {
- yield connection.executeTransaction(function* doImport() {
- for (let record of records) {
- const params = {
- collection_name: collection_name,
- record_id: record.id,
- record: (0, _stringify2.default)(record)
- };
- yield connection.execute(statements.importData, params);
- }
- const lastModified = Math.max(...records.map(record => record.last_modified));
- const params = {
- collection_name: collection_name
- };
- const previousLastModified = yield connection.execute(statements.getLastModified, params).then(result => {
- return result.length > 0 ? result[0].getResultByName("last_modified") : -1;
- });
- if (lastModified > previousLastModified) {
- const params = {
- collection_name: collection_name,
- last_modified: lastModified
- };
- yield connection.execute(statements.saveLastModified, params);
- }
- });
- return records;
- });
- }
-
- saveLastModified(lastModified) {
- const parsedLastModified = parseInt(lastModified, 10) || null;
- const params = {
- collection_name: this.collection,
- last_modified: parsedLastModified
- };
- return this._executeStatement(statements.saveLastModified, params).then(() => parsedLastModified);
- }
-
- getLastModified() {
- const params = {
- collection_name: this.collection
- };
- return this._executeStatement(statements.getLastModified, params).then(result => {
- if (result.length == 0) {
- return 0;
- }
- return result[0].getResultByName("last_modified");
- });
- }
-
- /**
- * Reset the sync status of every record and collection we have
- * access to.
- */
- resetSyncStatus() {
- // We're going to use execute instead of executeCached, so build
- // in our own sanity check
- if (!this._connection) {
- throw new Error("The storage adapter is not open");
- }
-
- return this._connection.executeTransaction(function* (conn) {
- const promises = [];
- yield conn.execute(statements.scanAllRecords, null, function (row) {
- const record = JSON.parse(row.getResultByName("record"));
- const record_id = row.getResultByName("record_id");
- const collection_name = row.getResultByName("collection_name");
- if (record._status === "deleted") {
- // Garbage collect deleted records.
- promises.push(conn.execute(statements.deleteData, { collection_name, record_id }));
- } else {
- const newRecord = (0, _extends3.default)({}, record, {
- _status: "created",
- last_modified: undefined
- });
- promises.push(conn.execute(statements.updateData, { record: (0, _stringify2.default)(newRecord), record_id, collection_name }));
- }
- });
- yield _promise2.default.all(promises);
- yield conn.execute(statements.clearCollectionMetadata);
- });
- }
-}
-
-exports.default = FirefoxAdapter;
-function transactionProxy(collection, preloaded) {
- const _operations = [];
-
- return {
- get operations() {
- return _operations;
- },
-
- create(record) {
- _operations.push({
- statement: statements.createData,
- params: {
- collection_name: collection,
- record_id: record.id,
- record: (0, _stringify2.default)(record)
- }
- });
- },
-
- update(record) {
- _operations.push({
- statement: statements.updateData,
- params: {
- collection_name: collection,
- record_id: record.id,
- record: (0, _stringify2.default)(record)
- }
- });
- },
-
- delete(id) {
- _operations.push({
- statement: statements.deleteData,
- params: {
- collection_name: collection,
- record_id: id
- }
- });
- },
-
- get(id) {
- // Gecko JS engine outputs undesired warnings if id is not in preloaded.
- return id in preloaded ? preloaded[id] : undefined;
- }
- };
-}
-
-/**
- * Filter and sort list against provided filters and order.
- *
- * @param {Object} filters The filters to apply.
- * @param {String} order The order to apply.
- * @param {Array} list The list to reduce.
- * @return {Array}
- */
-function reduceRecords(filters, order, list) {
- const filtered = filters ? (0, _utils.filterObjects)(filters, list) : list;
- return order ? (0, _utils.sortObjects)(order, filtered) : filtered;
-}
-
-},{"../src/adapters/base":85,"../src/utils":87,"babel-runtime/core-js/json/stringify":3,"babel-runtime/core-js/promise":6,"babel-runtime/helpers/extends":8}],2:[function(require,module,exports){
-/*
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _extends2 = require("babel-runtime/helpers/extends");
-
-var _extends3 = _interopRequireDefault(_extends2);
-
-exports.default = loadKinto;
-
-var _base = require("../src/adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-var _KintoBase = require("../src/KintoBase");
-
-var _KintoBase2 = _interopRequireDefault(_KintoBase);
-
-var _FirefoxStorage = require("./FirefoxStorage");
-
-var _FirefoxStorage2 = _interopRequireDefault(_FirefoxStorage);
-
-var _utils = require("../src/utils");
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-function loadKinto() {
- const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
- const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-
- // Use standalone kinto-http module landed in FFx.
- const { KintoHttpClient } = Cu.import("resource://services-common/kinto-http-client.js");
-
- Cu.import("resource://gre/modules/Timer.jsm");
- Cu.importGlobalProperties(['fetch']);
-
- // Leverage Gecko service to generate UUIDs.
- function makeIDSchema() {
- return {
- validate: _utils.RE_UUID.test.bind(_utils.RE_UUID),
- generate: function () {
- return generateUUID().toString().replace(/[{}]/g, "");
- }
- };
- }
-
- class KintoFX extends _KintoBase2.default {
- static get adapters() {
- return {
- BaseAdapter: _base2.default,
- FirefoxAdapter: _FirefoxStorage2.default
- };
- }
-
- constructor(options = {}) {
- const emitter = {};
- EventEmitter.decorate(emitter);
-
- const defaults = {
- events: emitter,
- ApiClass: KintoHttpClient,
- adapter: _FirefoxStorage2.default
- };
-
- const expandedOptions = (0, _extends3.default)({}, defaults, options);
- super(expandedOptions);
- }
-
- collection(collName, options = {}) {
- const idSchema = makeIDSchema();
- const expandedOptions = (0, _extends3.default)({ idSchema }, options);
- return super.collection(collName, expandedOptions);
- }
- }
-
- return KintoFX;
-}
-
-// This fixes compatibility with CommonJS required by browserify.
-// See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495
-if (typeof module === "object") {
- module.exports = loadKinto;
-}
-
-},{"../src/KintoBase":83,"../src/adapters/base":85,"../src/utils":87,"./FirefoxStorage":1,"babel-runtime/helpers/extends":8}],3:[function(require,module,exports){
-module.exports = { "default": require("core-js/library/fn/json/stringify"), __esModule: true };
-},{"core-js/library/fn/json/stringify":10}],4:[function(require,module,exports){
-module.exports = { "default": require("core-js/library/fn/object/assign"), __esModule: true };
-},{"core-js/library/fn/object/assign":11}],5:[function(require,module,exports){
-module.exports = { "default": require("core-js/library/fn/object/keys"), __esModule: true };
-},{"core-js/library/fn/object/keys":12}],6:[function(require,module,exports){
-module.exports = { "default": require("core-js/library/fn/promise"), __esModule: true };
-},{"core-js/library/fn/promise":13}],7:[function(require,module,exports){
-"use strict";
-
-exports.__esModule = true;
-
-var _promise = require("../core-js/promise");
-
-var _promise2 = _interopRequireDefault(_promise);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-exports.default = function (fn) {
- return function () {
- var gen = fn.apply(this, arguments);
- return new _promise2.default(function (resolve, reject) {
- function step(key, arg) {
- try {
- var info = gen[key](arg);
- var value = info.value;
- } catch (error) {
- reject(error);
- return;
- }
-
- if (info.done) {
- resolve(value);
- } else {
- return _promise2.default.resolve(value).then(function (value) {
- return step("next", value);
- }, function (err) {
- return step("throw", err);
- });
- }
- }
-
- return step("next");
- });
- };
-};
-},{"../core-js/promise":6}],8:[function(require,module,exports){
-"use strict";
-
-exports.__esModule = true;
-
-var _assign = require("../core-js/object/assign");
-
-var _assign2 = _interopRequireDefault(_assign);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-exports.default = _assign2.default || function (target) {
- for (var i = 1; i < arguments.length; i++) {
- var source = arguments[i];
-
- for (var key in source) {
- if (Object.prototype.hasOwnProperty.call(source, key)) {
- target[key] = source[key];
- }
- }
- }
-
- return target;
-};
-},{"../core-js/object/assign":4}],9:[function(require,module,exports){
-
-},{}],10:[function(require,module,exports){
-var core = require('../../modules/_core')
- , $JSON = core.JSON || (core.JSON = {stringify: JSON.stringify});
-module.exports = function stringify(it){ // eslint-disable-line no-unused-vars
- return $JSON.stringify.apply($JSON, arguments);
-};
-},{"../../modules/_core":21}],11:[function(require,module,exports){
-require('../../modules/es6.object.assign');
-module.exports = require('../../modules/_core').Object.assign;
-},{"../../modules/_core":21,"../../modules/es6.object.assign":77}],12:[function(require,module,exports){
-require('../../modules/es6.object.keys');
-module.exports = require('../../modules/_core').Object.keys;
-},{"../../modules/_core":21,"../../modules/es6.object.keys":78}],13:[function(require,module,exports){
-require('../modules/es6.object.to-string');
-require('../modules/es6.string.iterator');
-require('../modules/web.dom.iterable');
-require('../modules/es6.promise');
-module.exports = require('../modules/_core').Promise;
-},{"../modules/_core":21,"../modules/es6.object.to-string":79,"../modules/es6.promise":80,"../modules/es6.string.iterator":81,"../modules/web.dom.iterable":82}],14:[function(require,module,exports){
-module.exports = function(it){
- if(typeof it != 'function')throw TypeError(it + ' is not a function!');
- return it;
-};
-},{}],15:[function(require,module,exports){
-module.exports = function(){ /* empty */ };
-},{}],16:[function(require,module,exports){
-module.exports = function(it, Constructor, name, forbiddenField){
- if(!(it instanceof Constructor) || (forbiddenField !== undefined && forbiddenField in it)){
- throw TypeError(name + ': incorrect invocation!');
- } return it;
-};
-},{}],17:[function(require,module,exports){
-var isObject = require('./_is-object');
-module.exports = function(it){
- if(!isObject(it))throw TypeError(it + ' is not an object!');
- return it;
-};
-},{"./_is-object":38}],18:[function(require,module,exports){
-// false -> Array#indexOf
-// true -> Array#includes
-var toIObject = require('./_to-iobject')
- , toLength = require('./_to-length')
- , toIndex = require('./_to-index');
-module.exports = function(IS_INCLUDES){
- return function($this, el, fromIndex){
- var O = toIObject($this)
- , length = toLength(O.length)
- , index = toIndex(fromIndex, length)
- , value;
- // Array#includes uses SameValueZero equality algorithm
- if(IS_INCLUDES && el != el)while(length > index){
- value = O[index++];
- if(value != value)return true;
- // Array#toIndex ignores holes, Array#includes - not
- } else for(;length > index; index++)if(IS_INCLUDES || index in O){
- if(O[index] === el)return IS_INCLUDES || index || 0;
- } return !IS_INCLUDES && -1;
- };
-};
-},{"./_to-index":67,"./_to-iobject":69,"./_to-length":70}],19:[function(require,module,exports){
-// getting tag from 19.1.3.6 Object.prototype.toString()
-var cof = require('./_cof')
- , TAG = require('./_wks')('toStringTag')
- // ES3 wrong here
- , ARG = cof(function(){ return arguments; }()) == 'Arguments';
-
-// fallback for IE11 Script Access Denied error
-var tryGet = function(it, key){
- try {
- return it[key];
- } catch(e){ /* empty */ }
-};
-
-module.exports = function(it){
- var O, T, B;
- return it === undefined ? 'Undefined' : it === null ? 'Null'
- // @@toStringTag case
- : typeof (T = tryGet(O = Object(it), TAG)) == 'string' ? T
- // builtinTag case
- : ARG ? cof(O)
- // ES3 arguments fallback
- : (B = cof(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : B;
-};
-},{"./_cof":20,"./_wks":74}],20:[function(require,module,exports){
-var toString = {}.toString;
-
-module.exports = function(it){
- return toString.call(it).slice(8, -1);
-};
-},{}],21:[function(require,module,exports){
-var core = module.exports = {version: '2.4.0'};
-if(typeof __e == 'number')__e = core; // eslint-disable-line no-undef
-},{}],22:[function(require,module,exports){
-// optional / simple context binding
-var aFunction = require('./_a-function');
-module.exports = function(fn, that, length){
- aFunction(fn);
- if(that === undefined)return fn;
- switch(length){
- case 1: return function(a){
- return fn.call(that, a);
- };
- case 2: return function(a, b){
- return fn.call(that, a, b);
- };
- case 3: return function(a, b, c){
- return fn.call(that, a, b, c);
- };
- }
- return function(/* ...args */){
- return fn.apply(that, arguments);
- };
-};
-},{"./_a-function":14}],23:[function(require,module,exports){
-// 7.2.1 RequireObjectCoercible(argument)
-module.exports = function(it){
- if(it == undefined)throw TypeError("Can't call method on " + it);
- return it;
-};
-},{}],24:[function(require,module,exports){
-// Thank's IE8 for his funny defineProperty
-module.exports = !require('./_fails')(function(){
- return Object.defineProperty({}, 'a', {get: function(){ return 7; }}).a != 7;
-});
-},{"./_fails":28}],25:[function(require,module,exports){
-var isObject = require('./_is-object')
- , document = require('./_global').document
- // in old IE typeof document.createElement is 'object'
- , is = isObject(document) && isObject(document.createElement);
-module.exports = function(it){
- return is ? document.createElement(it) : {};
-};
-},{"./_global":30,"./_is-object":38}],26:[function(require,module,exports){
-// IE 8- don't enum bug keys
-module.exports = (
- 'constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf'
-).split(',');
-},{}],27:[function(require,module,exports){
-var global = require('./_global')
- , core = require('./_core')
- , ctx = require('./_ctx')
- , hide = require('./_hide')
- , PROTOTYPE = 'prototype';
-
-var $export = function(type, name, source){
- var IS_FORCED = type & $export.F
- , IS_GLOBAL = type & $export.G
- , IS_STATIC = type & $export.S
- , IS_PROTO = type & $export.P
- , IS_BIND = type & $export.B
- , IS_WRAP = type & $export.W
- , exports = IS_GLOBAL ? core : core[name] || (core[name] = {})
- , expProto = exports[PROTOTYPE]
- , target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE]
- , key, own, out;
- if(IS_GLOBAL)source = name;
- for(key in source){
- // contains in native
- own = !IS_FORCED && target && target[key] !== undefined;
- if(own && key in exports)continue;
- // export native or passed
- out = own ? target[key] : source[key];
- // prevent global pollution for namespaces
- exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key]
- // bind timers to global for call from export context
- : IS_BIND && own ? ctx(out, global)
- // wrap global constructors for prevent change them in library
- : IS_WRAP && target[key] == out ? (function(C){
- var F = function(a, b, c){
- if(this instanceof C){
- switch(arguments.length){
- case 0: return new C;
- case 1: return new C(a);
- case 2: return new C(a, b);
- } return new C(a, b, c);
- } return C.apply(this, arguments);
- };
- F[PROTOTYPE] = C[PROTOTYPE];
- return F;
- // make static versions for prototype methods
- })(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
- // export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
- if(IS_PROTO){
- (exports.virtual || (exports.virtual = {}))[key] = out;
- // export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
- if(type & $export.R && expProto && !expProto[key])hide(expProto, key, out);
- }
- }
-};
-// type bitmap
-$export.F = 1; // forced
-$export.G = 2; // global
-$export.S = 4; // static
-$export.P = 8; // proto
-$export.B = 16; // bind
-$export.W = 32; // wrap
-$export.U = 64; // safe
-$export.R = 128; // real proto method for `library`
-module.exports = $export;
-},{"./_core":21,"./_ctx":22,"./_global":30,"./_hide":32}],28:[function(require,module,exports){
-module.exports = function(exec){
- try {
- return !!exec();
- } catch(e){
- return true;
- }
-};
-},{}],29:[function(require,module,exports){
-var ctx = require('./_ctx')
- , call = require('./_iter-call')
- , isArrayIter = require('./_is-array-iter')
- , anObject = require('./_an-object')
- , toLength = require('./_to-length')
- , getIterFn = require('./core.get-iterator-method')
- , BREAK = {}
- , RETURN = {};
-var exports = module.exports = function(iterable, entries, fn, that, ITERATOR){
- var iterFn = ITERATOR ? function(){ return iterable; } : getIterFn(iterable)
- , f = ctx(fn, that, entries ? 2 : 1)
- , index = 0
- , length, step, iterator, result;
- if(typeof iterFn != 'function')throw TypeError(iterable + ' is not iterable!');
- // fast case for arrays with default iterator
- if(isArrayIter(iterFn))for(length = toLength(iterable.length); length > index; index++){
- result = entries ? f(anObject(step = iterable[index])[0], step[1]) : f(iterable[index]);
- if(result === BREAK || result === RETURN)return result;
- } else for(iterator = iterFn.call(iterable); !(step = iterator.next()).done; ){
- result = call(iterator, f, step.value, entries);
- if(result === BREAK || result === RETURN)return result;
- }
-};
-exports.BREAK = BREAK;
-exports.RETURN = RETURN;
-},{"./_an-object":17,"./_ctx":22,"./_is-array-iter":37,"./_iter-call":39,"./_to-length":70,"./core.get-iterator-method":75}],30:[function(require,module,exports){
-// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
-var global = module.exports = typeof window != 'undefined' && window.Math == Math
- ? window : typeof self != 'undefined' && self.Math == Math ? self : Function('return this')();
-if(typeof __g == 'number')__g = global; // eslint-disable-line no-undef
-},{}],31:[function(require,module,exports){
-var hasOwnProperty = {}.hasOwnProperty;
-module.exports = function(it, key){
- return hasOwnProperty.call(it, key);
-};
-},{}],32:[function(require,module,exports){
-var dP = require('./_object-dp')
- , createDesc = require('./_property-desc');
-module.exports = require('./_descriptors') ? function(object, key, value){
- return dP.f(object, key, createDesc(1, value));
-} : function(object, key, value){
- object[key] = value;
- return object;
-};
-},{"./_descriptors":24,"./_object-dp":49,"./_property-desc":57}],33:[function(require,module,exports){
-module.exports = require('./_global').document && document.documentElement;
-},{"./_global":30}],34:[function(require,module,exports){
-module.exports = !require('./_descriptors') && !require('./_fails')(function(){
- return Object.defineProperty(require('./_dom-create')('div'), 'a', {get: function(){ return 7; }}).a != 7;
-});
-},{"./_descriptors":24,"./_dom-create":25,"./_fails":28}],35:[function(require,module,exports){
-// fast apply, http://jsperf.lnkit.com/fast-apply/5
-module.exports = function(fn, args, that){
- var un = that === undefined;
- switch(args.length){
- case 0: return un ? fn()
- : fn.call(that);
- case 1: return un ? fn(args[0])
- : fn.call(that, args[0]);
- case 2: return un ? fn(args[0], args[1])
- : fn.call(that, args[0], args[1]);
- case 3: return un ? fn(args[0], args[1], args[2])
- : fn.call(that, args[0], args[1], args[2]);
- case 4: return un ? fn(args[0], args[1], args[2], args[3])
- : fn.call(that, args[0], args[1], args[2], args[3]);
- } return fn.apply(that, args);
-};
-},{}],36:[function(require,module,exports){
-// fallback for non-array-like ES3 and non-enumerable old V8 strings
-var cof = require('./_cof');
-module.exports = Object('z').propertyIsEnumerable(0) ? Object : function(it){
- return cof(it) == 'String' ? it.split('') : Object(it);
-};
-},{"./_cof":20}],37:[function(require,module,exports){
-// check on default Array iterator
-var Iterators = require('./_iterators')
- , ITERATOR = require('./_wks')('iterator')
- , ArrayProto = Array.prototype;
-
-module.exports = function(it){
- return it !== undefined && (Iterators.Array === it || ArrayProto[ITERATOR] === it);
-};
-},{"./_iterators":44,"./_wks":74}],38:[function(require,module,exports){
-module.exports = function(it){
- return typeof it === 'object' ? it !== null : typeof it === 'function';
-};
-},{}],39:[function(require,module,exports){
-// call something on iterator step with safe closing on error
-var anObject = require('./_an-object');
-module.exports = function(iterator, fn, value, entries){
- try {
- return entries ? fn(anObject(value)[0], value[1]) : fn(value);
- // 7.4.6 IteratorClose(iterator, completion)
- } catch(e){
- var ret = iterator['return'];
- if(ret !== undefined)anObject(ret.call(iterator));
- throw e;
- }
-};
-},{"./_an-object":17}],40:[function(require,module,exports){
-'use strict';
-var create = require('./_object-create')
- , descriptor = require('./_property-desc')
- , setToStringTag = require('./_set-to-string-tag')
- , IteratorPrototype = {};
-
-// 25.1.2.1.1 %IteratorPrototype%[@@iterator]()
-require('./_hide')(IteratorPrototype, require('./_wks')('iterator'), function(){ return this; });
-
-module.exports = function(Constructor, NAME, next){
- Constructor.prototype = create(IteratorPrototype, {next: descriptor(1, next)});
- setToStringTag(Constructor, NAME + ' Iterator');
-};
-},{"./_hide":32,"./_object-create":48,"./_property-desc":57,"./_set-to-string-tag":61,"./_wks":74}],41:[function(require,module,exports){
-'use strict';
-var LIBRARY = require('./_library')
- , $export = require('./_export')
- , redefine = require('./_redefine')
- , hide = require('./_hide')
- , has = require('./_has')
- , Iterators = require('./_iterators')
- , $iterCreate = require('./_iter-create')
- , setToStringTag = require('./_set-to-string-tag')
- , getPrototypeOf = require('./_object-gpo')
- , ITERATOR = require('./_wks')('iterator')
- , BUGGY = !([].keys && 'next' in [].keys()) // Safari has buggy iterators w/o `next`
- , FF_ITERATOR = '@@iterator'
- , KEYS = 'keys'
- , VALUES = 'values';
-
-var returnThis = function(){ return this; };
-
-module.exports = function(Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCED){
- $iterCreate(Constructor, NAME, next);
- var getMethod = function(kind){
- if(!BUGGY && kind in proto)return proto[kind];
- switch(kind){
- case KEYS: return function keys(){ return new Constructor(this, kind); };
- case VALUES: return function values(){ return new Constructor(this, kind); };
- } return function entries(){ return new Constructor(this, kind); };
- };
- var TAG = NAME + ' Iterator'
- , DEF_VALUES = DEFAULT == VALUES
- , VALUES_BUG = false
- , proto = Base.prototype
- , $native = proto[ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT]
- , $default = $native || getMethod(DEFAULT)
- , $entries = DEFAULT ? !DEF_VALUES ? $default : getMethod('entries') : undefined
- , $anyNative = NAME == 'Array' ? proto.entries || $native : $native
- , methods, key, IteratorPrototype;
- // Fix native
- if($anyNative){
- IteratorPrototype = getPrototypeOf($anyNative.call(new Base));
- if(IteratorPrototype !== Object.prototype){
- // Set @@toStringTag to native iterators
- setToStringTag(IteratorPrototype, TAG, true);
- // fix for some old engines
- if(!LIBRARY && !has(IteratorPrototype, ITERATOR))hide(IteratorPrototype, ITERATOR, returnThis);
- }
- }
- // fix Array#{values, @@iterator}.name in V8 / FF
- if(DEF_VALUES && $native && $native.name !== VALUES){
- VALUES_BUG = true;
- $default = function values(){ return $native.call(this); };
- }
- // Define iterator
- if((!LIBRARY || FORCED) && (BUGGY || VALUES_BUG || !proto[ITERATOR])){
- hide(proto, ITERATOR, $default);
- }
- // Plug for library
- Iterators[NAME] = $default;
- Iterators[TAG] = returnThis;
- if(DEFAULT){
- methods = {
- values: DEF_VALUES ? $default : getMethod(VALUES),
- keys: IS_SET ? $default : getMethod(KEYS),
- entries: $entries
- };
- if(FORCED)for(key in methods){
- if(!(key in proto))redefine(proto, key, methods[key]);
- } else $export($export.P + $export.F * (BUGGY || VALUES_BUG), NAME, methods);
- }
- return methods;
-};
-},{"./_export":27,"./_has":31,"./_hide":32,"./_iter-create":40,"./_iterators":44,"./_library":45,"./_object-gpo":52,"./_redefine":59,"./_set-to-string-tag":61,"./_wks":74}],42:[function(require,module,exports){
-var ITERATOR = require('./_wks')('iterator')
- , SAFE_CLOSING = false;
-
-try {
- var riter = [7][ITERATOR]();
- riter['return'] = function(){ SAFE_CLOSING = true; };
- Array.from(riter, function(){ throw 2; });
-} catch(e){ /* empty */ }
-
-module.exports = function(exec, skipClosing){
- if(!skipClosing && !SAFE_CLOSING)return false;
- var safe = false;
- try {
- var arr = [7]
- , iter = arr[ITERATOR]();
- iter.next = function(){ return {done: safe = true}; };
- arr[ITERATOR] = function(){ return iter; };
- exec(arr);
- } catch(e){ /* empty */ }
- return safe;
-};
-},{"./_wks":74}],43:[function(require,module,exports){
-module.exports = function(done, value){
- return {value: value, done: !!done};
-};
-},{}],44:[function(require,module,exports){
-module.exports = {};
-},{}],45:[function(require,module,exports){
-module.exports = true;
-},{}],46:[function(require,module,exports){
-var global = require('./_global')
- , macrotask = require('./_task').set
- , Observer = global.MutationObserver || global.WebKitMutationObserver
- , process = global.process
- , Promise = global.Promise
- , isNode = require('./_cof')(process) == 'process';
-
-module.exports = function(){
- var head, last, notify;
-
- var flush = function(){
- var parent, fn;
- if(isNode && (parent = process.domain))parent.exit();
- while(head){
- fn = head.fn;
- head = head.next;
- try {
- fn();
- } catch(e){
- if(head)notify();
- else last = undefined;
- throw e;
- }
- } last = undefined;
- if(parent)parent.enter();
- };
-
- // Node.js
- if(isNode){
- notify = function(){
- process.nextTick(flush);
- };
- // browsers with MutationObserver
- } else if(Observer){
- var toggle = true
- , node = document.createTextNode('');
- new Observer(flush).observe(node, {characterData: true}); // eslint-disable-line no-new
- notify = function(){
- node.data = toggle = !toggle;
- };
- // environments with maybe non-completely correct, but existent Promise
- } else if(Promise && Promise.resolve){
- var promise = Promise.resolve();
- notify = function(){
- promise.then(flush);
- };
- // for other environments - macrotask based on:
- // - setImmediate
- // - MessageChannel
- // - window.postMessag
- // - onreadystatechange
- // - setTimeout
- } else {
- notify = function(){
- // strange IE + webpack dev server bug - use .call(global)
- macrotask.call(global, flush);
- };
- }
-
- return function(fn){
- var task = {fn: fn, next: undefined};
- if(last)last.next = task;
- if(!head){
- head = task;
- notify();
- } last = task;
- };
-};
-},{"./_cof":20,"./_global":30,"./_task":66}],47:[function(require,module,exports){
-'use strict';
-// 19.1.2.1 Object.assign(target, source, ...)
-var getKeys = require('./_object-keys')
- , gOPS = require('./_object-gops')
- , pIE = require('./_object-pie')
- , toObject = require('./_to-object')
- , IObject = require('./_iobject')
- , $assign = Object.assign;
-
-// should work with symbols and should have deterministic property order (V8 bug)
-module.exports = !$assign || require('./_fails')(function(){
- var A = {}
- , B = {}
- , S = Symbol()
- , K = 'abcdefghijklmnopqrst';
- A[S] = 7;
- K.split('').forEach(function(k){ B[k] = k; });
- return $assign({}, A)[S] != 7 || Object.keys($assign({}, B)).join('') != K;
-}) ? function assign(target, source){ // eslint-disable-line no-unused-vars
- var T = toObject(target)
- , aLen = arguments.length
- , index = 1
- , getSymbols = gOPS.f
- , isEnum = pIE.f;
- while(aLen > index){
- var S = IObject(arguments[index++])
- , keys = getSymbols ? getKeys(S).concat(getSymbols(S)) : getKeys(S)
- , length = keys.length
- , j = 0
- , key;
- while(length > j)if(isEnum.call(S, key = keys[j++]))T[key] = S[key];
- } return T;
-} : $assign;
-},{"./_fails":28,"./_iobject":36,"./_object-gops":51,"./_object-keys":54,"./_object-pie":55,"./_to-object":71}],48:[function(require,module,exports){
-// 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties])
-var anObject = require('./_an-object')
- , dPs = require('./_object-dps')
- , enumBugKeys = require('./_enum-bug-keys')
- , IE_PROTO = require('./_shared-key')('IE_PROTO')
- , Empty = function(){ /* empty */ }
- , PROTOTYPE = 'prototype';
-
-// Create object with fake `null` prototype: use iframe Object with cleared prototype
-var createDict = function(){
- // Thrash, waste and sodomy: IE GC bug
- var iframe = require('./_dom-create')('iframe')
- , i = enumBugKeys.length
- , lt = '<'
- , gt = '>'
- , iframeDocument;
- iframe.style.display = 'none';
- require('./_html').appendChild(iframe);
- iframe.src = 'javascript:'; // eslint-disable-line no-script-url
- // createDict = iframe.contentWindow.Object;
- // html.removeChild(iframe);
- iframeDocument = iframe.contentWindow.document;
- iframeDocument.open();
- iframeDocument.write(lt + 'script' + gt + 'document.F=Object' + lt + '/script' + gt);
- iframeDocument.close();
- createDict = iframeDocument.F;
- while(i--)delete createDict[PROTOTYPE][enumBugKeys[i]];
- return createDict();
-};
-
-module.exports = Object.create || function create(O, Properties){
- var result;
- if(O !== null){
- Empty[PROTOTYPE] = anObject(O);
- result = new Empty;
- Empty[PROTOTYPE] = null;
- // add "__proto__" for Object.getPrototypeOf polyfill
- result[IE_PROTO] = O;
- } else result = createDict();
- return Properties === undefined ? result : dPs(result, Properties);
-};
-
-},{"./_an-object":17,"./_dom-create":25,"./_enum-bug-keys":26,"./_html":33,"./_object-dps":50,"./_shared-key":62}],49:[function(require,module,exports){
-var anObject = require('./_an-object')
- , IE8_DOM_DEFINE = require('./_ie8-dom-define')
- , toPrimitive = require('./_to-primitive')
- , dP = Object.defineProperty;
-
-exports.f = require('./_descriptors') ? Object.defineProperty : function defineProperty(O, P, Attributes){
- anObject(O);
- P = toPrimitive(P, true);
- anObject(Attributes);
- if(IE8_DOM_DEFINE)try {
- return dP(O, P, Attributes);
- } catch(e){ /* empty */ }
- if('get' in Attributes || 'set' in Attributes)throw TypeError('Accessors not supported!');
- if('value' in Attributes)O[P] = Attributes.value;
- return O;
-};
-},{"./_an-object":17,"./_descriptors":24,"./_ie8-dom-define":34,"./_to-primitive":72}],50:[function(require,module,exports){
-var dP = require('./_object-dp')
- , anObject = require('./_an-object')
- , getKeys = require('./_object-keys');
-
-module.exports = require('./_descriptors') ? Object.defineProperties : function defineProperties(O, Properties){
- anObject(O);
- var keys = getKeys(Properties)
- , length = keys.length
- , i = 0
- , P;
- while(length > i)dP.f(O, P = keys[i++], Properties[P]);
- return O;
-};
-},{"./_an-object":17,"./_descriptors":24,"./_object-dp":49,"./_object-keys":54}],51:[function(require,module,exports){
-exports.f = Object.getOwnPropertySymbols;
-},{}],52:[function(require,module,exports){
-// 19.1.2.9 / 15.2.3.2 Object.getPrototypeOf(O)
-var has = require('./_has')
- , toObject = require('./_to-object')
- , IE_PROTO = require('./_shared-key')('IE_PROTO')
- , ObjectProto = Object.prototype;
-
-module.exports = Object.getPrototypeOf || function(O){
- O = toObject(O);
- if(has(O, IE_PROTO))return O[IE_PROTO];
- if(typeof O.constructor == 'function' && O instanceof O.constructor){
- return O.constructor.prototype;
- } return O instanceof Object ? ObjectProto : null;
-};
-},{"./_has":31,"./_shared-key":62,"./_to-object":71}],53:[function(require,module,exports){
-var has = require('./_has')
- , toIObject = require('./_to-iobject')
- , arrayIndexOf = require('./_array-includes')(false)
- , IE_PROTO = require('./_shared-key')('IE_PROTO');
-
-module.exports = function(object, names){
- var O = toIObject(object)
- , i = 0
- , result = []
- , key;
- for(key in O)if(key != IE_PROTO)has(O, key) && result.push(key);
- // Don't enum bug & hidden keys
- while(names.length > i)if(has(O, key = names[i++])){
- ~arrayIndexOf(result, key) || result.push(key);
- }
- return result;
-};
-},{"./_array-includes":18,"./_has":31,"./_shared-key":62,"./_to-iobject":69}],54:[function(require,module,exports){
-// 19.1.2.14 / 15.2.3.14 Object.keys(O)
-var $keys = require('./_object-keys-internal')
- , enumBugKeys = require('./_enum-bug-keys');
-
-module.exports = Object.keys || function keys(O){
- return $keys(O, enumBugKeys);
-};
-},{"./_enum-bug-keys":26,"./_object-keys-internal":53}],55:[function(require,module,exports){
-exports.f = {}.propertyIsEnumerable;
-},{}],56:[function(require,module,exports){
-// most Object methods by ES6 should accept primitives
-var $export = require('./_export')
- , core = require('./_core')
- , fails = require('./_fails');
-module.exports = function(KEY, exec){
- var fn = (core.Object || {})[KEY] || Object[KEY]
- , exp = {};
- exp[KEY] = exec(fn);
- $export($export.S + $export.F * fails(function(){ fn(1); }), 'Object', exp);
-};
-},{"./_core":21,"./_export":27,"./_fails":28}],57:[function(require,module,exports){
-module.exports = function(bitmap, value){
- return {
- enumerable : !(bitmap & 1),
- configurable: !(bitmap & 2),
- writable : !(bitmap & 4),
- value : value
- };
-};
-},{}],58:[function(require,module,exports){
-var hide = require('./_hide');
-module.exports = function(target, src, safe){
- for(var key in src){
- if(safe && target[key])target[key] = src[key];
- else hide(target, key, src[key]);
- } return target;
-};
-},{"./_hide":32}],59:[function(require,module,exports){
-module.exports = require('./_hide');
-},{"./_hide":32}],60:[function(require,module,exports){
-'use strict';
-var global = require('./_global')
- , core = require('./_core')
- , dP = require('./_object-dp')
- , DESCRIPTORS = require('./_descriptors')
- , SPECIES = require('./_wks')('species');
-
-module.exports = function(KEY){
- var C = typeof core[KEY] == 'function' ? core[KEY] : global[KEY];
- if(DESCRIPTORS && C && !C[SPECIES])dP.f(C, SPECIES, {
- configurable: true,
- get: function(){ return this; }
- });
-};
-},{"./_core":21,"./_descriptors":24,"./_global":30,"./_object-dp":49,"./_wks":74}],61:[function(require,module,exports){
-var def = require('./_object-dp').f
- , has = require('./_has')
- , TAG = require('./_wks')('toStringTag');
-
-module.exports = function(it, tag, stat){
- if(it && !has(it = stat ? it : it.prototype, TAG))def(it, TAG, {configurable: true, value: tag});
-};
-},{"./_has":31,"./_object-dp":49,"./_wks":74}],62:[function(require,module,exports){
-var shared = require('./_shared')('keys')
- , uid = require('./_uid');
-module.exports = function(key){
- return shared[key] || (shared[key] = uid(key));
-};
-},{"./_shared":63,"./_uid":73}],63:[function(require,module,exports){
-var global = require('./_global')
- , SHARED = '__core-js_shared__'
- , store = global[SHARED] || (global[SHARED] = {});
-module.exports = function(key){
- return store[key] || (store[key] = {});
-};
-},{"./_global":30}],64:[function(require,module,exports){
-// 7.3.20 SpeciesConstructor(O, defaultConstructor)
-var anObject = require('./_an-object')
- , aFunction = require('./_a-function')
- , SPECIES = require('./_wks')('species');
-module.exports = function(O, D){
- var C = anObject(O).constructor, S;
- return C === undefined || (S = anObject(C)[SPECIES]) == undefined ? D : aFunction(S);
-};
-},{"./_a-function":14,"./_an-object":17,"./_wks":74}],65:[function(require,module,exports){
-var toInteger = require('./_to-integer')
- , defined = require('./_defined');
-// true -> String#at
-// false -> String#codePointAt
-module.exports = function(TO_STRING){
- return function(that, pos){
- var s = String(defined(that))
- , i = toInteger(pos)
- , l = s.length
- , a, b;
- if(i < 0 || i >= l)return TO_STRING ? '' : undefined;
- a = s.charCodeAt(i);
- return a < 0xd800 || a > 0xdbff || i + 1 === l || (b = s.charCodeAt(i + 1)) < 0xdc00 || b > 0xdfff
- ? TO_STRING ? s.charAt(i) : a
- : TO_STRING ? s.slice(i, i + 2) : (a - 0xd800 << 10) + (b - 0xdc00) + 0x10000;
- };
-};
-},{"./_defined":23,"./_to-integer":68}],66:[function(require,module,exports){
-var ctx = require('./_ctx')
- , invoke = require('./_invoke')
- , html = require('./_html')
- , cel = require('./_dom-create')
- , global = require('./_global')
- , process = global.process
- , setTask = global.setImmediate
- , clearTask = global.clearImmediate
- , MessageChannel = global.MessageChannel
- , counter = 0
- , queue = {}
- , ONREADYSTATECHANGE = 'onreadystatechange'
- , defer, channel, port;
-var run = function(){
- var id = +this;
- if(queue.hasOwnProperty(id)){
- var fn = queue[id];
- delete queue[id];
- fn();
- }
-};
-var listener = function(event){
- run.call(event.data);
-};
-// Node.js 0.9+ & IE10+ has setImmediate, otherwise:
-if(!setTask || !clearTask){
- setTask = function setImmediate(fn){
- var args = [], i = 1;
- while(arguments.length > i)args.push(arguments[i++]);
- queue[++counter] = function(){
- invoke(typeof fn == 'function' ? fn : Function(fn), args);
- };
- defer(counter);
- return counter;
- };
- clearTask = function clearImmediate(id){
- delete queue[id];
- };
- // Node.js 0.8-
- if(require('./_cof')(process) == 'process'){
- defer = function(id){
- process.nextTick(ctx(run, id, 1));
- };
- // Browsers with MessageChannel, includes WebWorkers
- } else if(MessageChannel){
- channel = new MessageChannel;
- port = channel.port2;
- channel.port1.onmessage = listener;
- defer = ctx(port.postMessage, port, 1);
- // Browsers with postMessage, skip WebWorkers
- // IE8 has postMessage, but it's sync & typeof its postMessage is 'object'
- } else if(global.addEventListener && typeof postMessage == 'function' && !global.importScripts){
- defer = function(id){
- global.postMessage(id + '', '*');
- };
- global.addEventListener('message', listener, false);
- // IE8-
- } else if(ONREADYSTATECHANGE in cel('script')){
- defer = function(id){
- html.appendChild(cel('script'))[ONREADYSTATECHANGE] = function(){
- html.removeChild(this);
- run.call(id);
- };
- };
- // Rest old browsers
- } else {
- defer = function(id){
- setTimeout(ctx(run, id, 1), 0);
- };
- }
-}
-module.exports = {
- set: setTask,
- clear: clearTask
-};
-},{"./_cof":20,"./_ctx":22,"./_dom-create":25,"./_global":30,"./_html":33,"./_invoke":35}],67:[function(require,module,exports){
-var toInteger = require('./_to-integer')
- , max = Math.max
- , min = Math.min;
-module.exports = function(index, length){
- index = toInteger(index);
- return index < 0 ? max(index + length, 0) : min(index, length);
-};
-},{"./_to-integer":68}],68:[function(require,module,exports){
-// 7.1.4 ToInteger
-var ceil = Math.ceil
- , floor = Math.floor;
-module.exports = function(it){
- return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it);
-};
-},{}],69:[function(require,module,exports){
-// to indexed object, toObject with fallback for non-array-like ES3 strings
-var IObject = require('./_iobject')
- , defined = require('./_defined');
-module.exports = function(it){
- return IObject(defined(it));
-};
-},{"./_defined":23,"./_iobject":36}],70:[function(require,module,exports){
-// 7.1.15 ToLength
-var toInteger = require('./_to-integer')
- , min = Math.min;
-module.exports = function(it){
- return it > 0 ? min(toInteger(it), 0x1fffffffffffff) : 0; // pow(2, 53) - 1 == 9007199254740991
-};
-},{"./_to-integer":68}],71:[function(require,module,exports){
-// 7.1.13 ToObject(argument)
-var defined = require('./_defined');
-module.exports = function(it){
- return Object(defined(it));
-};
-},{"./_defined":23}],72:[function(require,module,exports){
-// 7.1.1 ToPrimitive(input [, PreferredType])
-var isObject = require('./_is-object');
-// instead of the ES6 spec version, we didn't implement @@toPrimitive case
-// and the second argument - flag - preferred type is a string
-module.exports = function(it, S){
- if(!isObject(it))return it;
- var fn, val;
- if(S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it)))return val;
- if(typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it)))return val;
- if(!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it)))return val;
- throw TypeError("Can't convert object to primitive value");
-};
-},{"./_is-object":38}],73:[function(require,module,exports){
-var id = 0
- , px = Math.random();
-module.exports = function(key){
- return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36));
-};
-},{}],74:[function(require,module,exports){
-var store = require('./_shared')('wks')
- , uid = require('./_uid')
- , Symbol = require('./_global').Symbol
- , USE_SYMBOL = typeof Symbol == 'function';
-
-var $exports = module.exports = function(name){
- return store[name] || (store[name] =
- USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)('Symbol.' + name));
-};
-
-$exports.store = store;
-},{"./_global":30,"./_shared":63,"./_uid":73}],75:[function(require,module,exports){
-var classof = require('./_classof')
- , ITERATOR = require('./_wks')('iterator')
- , Iterators = require('./_iterators');
-module.exports = require('./_core').getIteratorMethod = function(it){
- if(it != undefined)return it[ITERATOR]
- || it['@@iterator']
- || Iterators[classof(it)];
-};
-},{"./_classof":19,"./_core":21,"./_iterators":44,"./_wks":74}],76:[function(require,module,exports){
-'use strict';
-var addToUnscopables = require('./_add-to-unscopables')
- , step = require('./_iter-step')
- , Iterators = require('./_iterators')
- , toIObject = require('./_to-iobject');
-
-// 22.1.3.4 Array.prototype.entries()
-// 22.1.3.13 Array.prototype.keys()
-// 22.1.3.29 Array.prototype.values()
-// 22.1.3.30 Array.prototype[@@iterator]()
-module.exports = require('./_iter-define')(Array, 'Array', function(iterated, kind){
- this._t = toIObject(iterated); // target
- this._i = 0; // next index
- this._k = kind; // kind
-// 22.1.5.2.1 %ArrayIteratorPrototype%.next()
-}, function(){
- var O = this._t
- , kind = this._k
- , index = this._i++;
- if(!O || index >= O.length){
- this._t = undefined;
- return step(1);
- }
- if(kind == 'keys' )return step(0, index);
- if(kind == 'values')return step(0, O[index]);
- return step(0, [index, O[index]]);
-}, 'values');
-
-// argumentsList[@@iterator] is %ArrayProto_values% (9.4.4.6, 9.4.4.7)
-Iterators.Arguments = Iterators.Array;
-
-addToUnscopables('keys');
-addToUnscopables('values');
-addToUnscopables('entries');
-},{"./_add-to-unscopables":15,"./_iter-define":41,"./_iter-step":43,"./_iterators":44,"./_to-iobject":69}],77:[function(require,module,exports){
-// 19.1.3.1 Object.assign(target, source)
-var $export = require('./_export');
-
-$export($export.S + $export.F, 'Object', {assign: require('./_object-assign')});
-},{"./_export":27,"./_object-assign":47}],78:[function(require,module,exports){
-// 19.1.2.14 Object.keys(O)
-var toObject = require('./_to-object')
- , $keys = require('./_object-keys');
-
-require('./_object-sap')('keys', function(){
- return function keys(it){
- return $keys(toObject(it));
- };
-});
-},{"./_object-keys":54,"./_object-sap":56,"./_to-object":71}],79:[function(require,module,exports){
-arguments[4][9][0].apply(exports,arguments)
-},{"dup":9}],80:[function(require,module,exports){
-'use strict';
-var LIBRARY = require('./_library')
- , global = require('./_global')
- , ctx = require('./_ctx')
- , classof = require('./_classof')
- , $export = require('./_export')
- , isObject = require('./_is-object')
- , aFunction = require('./_a-function')
- , anInstance = require('./_an-instance')
- , forOf = require('./_for-of')
- , speciesConstructor = require('./_species-constructor')
- , task = require('./_task').set
- , microtask = require('./_microtask')()
- , PROMISE = 'Promise'
- , TypeError = global.TypeError
- , process = global.process
- , $Promise = global[PROMISE]
- , process = global.process
- , isNode = classof(process) == 'process'
- , empty = function(){ /* empty */ }
- , Internal, GenericPromiseCapability, Wrapper;
-
-var USE_NATIVE = !!function(){
- try {
- // correct subclassing with @@species support
- var promise = $Promise.resolve(1)
- , FakePromise = (promise.constructor = {})[require('./_wks')('species')] = function(exec){ exec(empty, empty); };
- // unhandled rejections tracking support, NodeJS Promise without it fails @@species test
- return (isNode || typeof PromiseRejectionEvent == 'function') && promise.then(empty) instanceof FakePromise;
- } catch(e){ /* empty */ }
-}();
-
-// helpers
-var sameConstructor = function(a, b){
- // with library wrapper special case
- return a === b || a === $Promise && b === Wrapper;
-};
-var isThenable = function(it){
- var then;
- return isObject(it) && typeof (then = it.then) == 'function' ? then : false;
-};
-var newPromiseCapability = function(C){
- return sameConstructor($Promise, C)
- ? new PromiseCapability(C)
- : new GenericPromiseCapability(C);
-};
-var PromiseCapability = GenericPromiseCapability = function(C){
- var resolve, reject;
- this.promise = new C(function($$resolve, $$reject){
- if(resolve !== undefined || reject !== undefined)throw TypeError('Bad Promise constructor');
- resolve = $$resolve;
- reject = $$reject;
- });
- this.resolve = aFunction(resolve);
- this.reject = aFunction(reject);
-};
-var perform = function(exec){
- try {
- exec();
- } catch(e){
- return {error: e};
- }
-};
-var notify = function(promise, isReject){
- if(promise._n)return;
- promise._n = true;
- var chain = promise._c;
- microtask(function(){
- var value = promise._v
- , ok = promise._s == 1
- , i = 0;
- var run = function(reaction){
- var handler = ok ? reaction.ok : reaction.fail
- , resolve = reaction.resolve
- , reject = reaction.reject
- , domain = reaction.domain
- , result, then;
- try {
- if(handler){
- if(!ok){
- if(promise._h == 2)onHandleUnhandled(promise);
- promise._h = 1;
- }
- if(handler === true)result = value;
- else {
- if(domain)domain.enter();
- result = handler(value);
- if(domain)domain.exit();
- }
- if(result === reaction.promise){
- reject(TypeError('Promise-chain cycle'));
- } else if(then = isThenable(result)){
- then.call(result, resolve, reject);
- } else resolve(result);
- } else reject(value);
- } catch(e){
- reject(e);
- }
- };
- while(chain.length > i)run(chain[i++]); // variable length - can't use forEach
- promise._c = [];
- promise._n = false;
- if(isReject && !promise._h)onUnhandled(promise);
- });
-};
-var onUnhandled = function(promise){
- task.call(global, function(){
- var value = promise._v
- , abrupt, handler, console;
- if(isUnhandled(promise)){
- abrupt = perform(function(){
- if(isNode){
- process.emit('unhandledRejection', value, promise);
- } else if(handler = global.onunhandledrejection){
- handler({promise: promise, reason: value});
- } else if((console = global.console) && console.error){
- console.error('Unhandled promise rejection', value);
- }
- });
- // Browsers should not trigger `rejectionHandled` event if it was handled here, NodeJS - should
- promise._h = isNode || isUnhandled(promise) ? 2 : 1;
- } promise._a = undefined;
- if(abrupt)throw abrupt.error;
- });
-};
-var isUnhandled = function(promise){
- if(promise._h == 1)return false;
- var chain = promise._a || promise._c
- , i = 0
- , reaction;
- while(chain.length > i){
- reaction = chain[i++];
- if(reaction.fail || !isUnhandled(reaction.promise))return false;
- } return true;
-};
-var onHandleUnhandled = function(promise){
- task.call(global, function(){
- var handler;
- if(isNode){
- process.emit('rejectionHandled', promise);
- } else if(handler = global.onrejectionhandled){
- handler({promise: promise, reason: promise._v});
- }
- });
-};
-var $reject = function(value){
- var promise = this;
- if(promise._d)return;
- promise._d = true;
- promise = promise._w || promise; // unwrap
- promise._v = value;
- promise._s = 2;
- if(!promise._a)promise._a = promise._c.slice();
- notify(promise, true);
-};
-var $resolve = function(value){
- var promise = this
- , then;
- if(promise._d)return;
- promise._d = true;
- promise = promise._w || promise; // unwrap
- try {
- if(promise === value)throw TypeError("Promise can't be resolved itself");
- if(then = isThenable(value)){
- microtask(function(){
- var wrapper = {_w: promise, _d: false}; // wrap
- try {
- then.call(value, ctx($resolve, wrapper, 1), ctx($reject, wrapper, 1));
- } catch(e){
- $reject.call(wrapper, e);
- }
- });
- } else {
- promise._v = value;
- promise._s = 1;
- notify(promise, false);
- }
- } catch(e){
- $reject.call({_w: promise, _d: false}, e); // wrap
- }
-};
-
-// constructor polyfill
-if(!USE_NATIVE){
- // 25.4.3.1 Promise(executor)
- $Promise = function Promise(executor){
- anInstance(this, $Promise, PROMISE, '_h');
- aFunction(executor);
- Internal.call(this);
- try {
- executor(ctx($resolve, this, 1), ctx($reject, this, 1));
- } catch(err){
- $reject.call(this, err);
- }
- };
- Internal = function Promise(executor){
- this._c = []; // <- awaiting reactions
- this._a = undefined; // <- checked in isUnhandled reactions
- this._s = 0; // <- state
- this._d = false; // <- done
- this._v = undefined; // <- value
- this._h = 0; // <- rejection state, 0 - default, 1 - handled, 2 - unhandled
- this._n = false; // <- notify
- };
- Internal.prototype = require('./_redefine-all')($Promise.prototype, {
- // 25.4.5.3 Promise.prototype.then(onFulfilled, onRejected)
- then: function then(onFulfilled, onRejected){
- var reaction = newPromiseCapability(speciesConstructor(this, $Promise));
- reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true;
- reaction.fail = typeof onRejected == 'function' && onRejected;
- reaction.domain = isNode ? process.domain : undefined;
- this._c.push(reaction);
- if(this._a)this._a.push(reaction);
- if(this._s)notify(this, false);
- return reaction.promise;
- },
- // 25.4.5.1 Promise.prototype.catch(onRejected)
- 'catch': function(onRejected){
- return this.then(undefined, onRejected);
- }
- });
- PromiseCapability = function(){
- var promise = new Internal;
- this.promise = promise;
- this.resolve = ctx($resolve, promise, 1);
- this.reject = ctx($reject, promise, 1);
- };
-}
-
-$export($export.G + $export.W + $export.F * !USE_NATIVE, {Promise: $Promise});
-require('./_set-to-string-tag')($Promise, PROMISE);
-require('./_set-species')(PROMISE);
-Wrapper = require('./_core')[PROMISE];
-
-// statics
-$export($export.S + $export.F * !USE_NATIVE, PROMISE, {
- // 25.4.4.5 Promise.reject(r)
- reject: function reject(r){
- var capability = newPromiseCapability(this)
- , $$reject = capability.reject;
- $$reject(r);
- return capability.promise;
- }
-});
-$export($export.S + $export.F * (LIBRARY || !USE_NATIVE), PROMISE, {
- // 25.4.4.6 Promise.resolve(x)
- resolve: function resolve(x){
- // instanceof instead of internal slot check because we should fix it without replacement native Promise core
- if(x instanceof $Promise && sameConstructor(x.constructor, this))return x;
- var capability = newPromiseCapability(this)
- , $$resolve = capability.resolve;
- $$resolve(x);
- return capability.promise;
- }
-});
-$export($export.S + $export.F * !(USE_NATIVE && require('./_iter-detect')(function(iter){
- $Promise.all(iter)['catch'](empty);
-})), PROMISE, {
- // 25.4.4.1 Promise.all(iterable)
- all: function all(iterable){
- var C = this
- , capability = newPromiseCapability(C)
- , resolve = capability.resolve
- , reject = capability.reject;
- var abrupt = perform(function(){
- var values = []
- , index = 0
- , remaining = 1;
- forOf(iterable, false, function(promise){
- var $index = index++
- , alreadyCalled = false;
- values.push(undefined);
- remaining++;
- C.resolve(promise).then(function(value){
- if(alreadyCalled)return;
- alreadyCalled = true;
- values[$index] = value;
- --remaining || resolve(values);
- }, reject);
- });
- --remaining || resolve(values);
- });
- if(abrupt)reject(abrupt.error);
- return capability.promise;
- },
- // 25.4.4.4 Promise.race(iterable)
- race: function race(iterable){
- var C = this
- , capability = newPromiseCapability(C)
- , reject = capability.reject;
- var abrupt = perform(function(){
- forOf(iterable, false, function(promise){
- C.resolve(promise).then(capability.resolve, reject);
- });
- });
- if(abrupt)reject(abrupt.error);
- return capability.promise;
- }
-});
-},{"./_a-function":14,"./_an-instance":16,"./_classof":19,"./_core":21,"./_ctx":22,"./_export":27,"./_for-of":29,"./_global":30,"./_is-object":38,"./_iter-detect":42,"./_library":45,"./_microtask":46,"./_redefine-all":58,"./_set-species":60,"./_set-to-string-tag":61,"./_species-constructor":64,"./_task":66,"./_wks":74}],81:[function(require,module,exports){
-'use strict';
-var $at = require('./_string-at')(true);
-
-// 21.1.3.27 String.prototype[@@iterator]()
-require('./_iter-define')(String, 'String', function(iterated){
- this._t = String(iterated); // target
- this._i = 0; // next index
-// 21.1.5.2.1 %StringIteratorPrototype%.next()
-}, function(){
- var O = this._t
- , index = this._i
- , point;
- if(index >= O.length)return {value: undefined, done: true};
- point = $at(O, index);
- this._i += point.length;
- return {value: point, done: false};
-});
-},{"./_iter-define":41,"./_string-at":65}],82:[function(require,module,exports){
-require('./es6.array.iterator');
-var global = require('./_global')
- , hide = require('./_hide')
- , Iterators = require('./_iterators')
- , TO_STRING_TAG = require('./_wks')('toStringTag');
-
-for(var collections = ['NodeList', 'DOMTokenList', 'MediaList', 'StyleSheetList', 'CSSRuleList'], i = 0; i < 5; i++){
- var NAME = collections[i]
- , Collection = global[NAME]
- , proto = Collection && Collection.prototype;
- if(proto && !proto[TO_STRING_TAG])hide(proto, TO_STRING_TAG, NAME);
- Iterators[NAME] = Iterators.Array;
-}
-},{"./_global":30,"./_hide":32,"./_iterators":44,"./_wks":74,"./es6.array.iterator":76}],83:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _extends2 = require("babel-runtime/helpers/extends");
-
-var _extends3 = _interopRequireDefault(_extends2);
-
-var _collection = require("./collection");
-
-var _collection2 = _interopRequireDefault(_collection);
-
-var _base = require("./adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const DEFAULT_BUCKET_NAME = "default";
-const DEFAULT_REMOTE = "http://localhost:8888/v1";
-
-/**
- * KintoBase class.
- */
-class KintoBase {
- /**
- * Provides a public access to the base adapter class. Users can create a
- * custom DB adapter by extending {@link BaseAdapter}.
- *
- * @type {Object}
- */
- static get adapters() {
- return {
- BaseAdapter: _base2.default
- };
- }
-
- /**
- * Synchronization strategies. Available strategies are:
- *
- * - `MANUAL`: Conflicts will be reported in a dedicated array.
- * - `SERVER_WINS`: Conflicts are resolved using remote data.
- * - `CLIENT_WINS`: Conflicts are resolved using local data.
- *
- * @type {Object}
- */
- static get syncStrategy() {
- return _collection2.default.strategy;
- }
-
- /**
- * Constructor.
- *
- * Options:
- * - `{String}` `remote` The server URL to use.
- * - `{String}` `bucket` The collection bucket name.
- * - `{EventEmitter}` `events` Events handler.
- * - `{BaseAdapter}` `adapter` The base DB adapter class.
- * - `{Object}` `adapterOptions` Options given to the adapter.
- * - `{String}` `dbPrefix` The DB name prefix.
- * - `{Object}` `headers` The HTTP headers to use.
- * - `{String}` `requestMode` The HTTP CORS mode to use.
- * - `{Number}` `timeout` The requests timeout in ms (default: `5000`).
- *
- * @param {Object} options The options object.
- */
- constructor(options = {}) {
- const defaults = {
- bucket: DEFAULT_BUCKET_NAME,
- remote: DEFAULT_REMOTE
- };
- this._options = (0, _extends3.default)({}, defaults, options);
- if (!this._options.adapter) {
- throw new Error("No adapter provided");
- }
-
- const { remote, events, headers, requestMode, timeout, ApiClass } = this._options;
-
- // public properties
-
- /**
- * The kinto HTTP client instance.
- * @type {KintoClient}
- */
- this.api = new ApiClass(remote, { events, headers, requestMode, timeout });
- /**
- * The event emitter instance.
- * @type {EventEmitter}
- */
- this.events = this._options.events;
- }
-
- /**
- * Creates a {@link Collection} instance. The second (optional) parameter
- * will set collection-level options like e.g. `remoteTransformers`.
- *
- * @param {String} collName The collection name.
- * @param {Object} options May contain the following fields:
- * remoteTransformers: Array<RemoteTransformer>
- * @return {Collection}
- */
- collection(collName, options = {}) {
- if (!collName) {
- throw new Error("missing collection name");
- }
-
- const bucket = this._options.bucket;
- return new _collection2.default(bucket, collName, this.api, {
- events: this._options.events,
- adapter: this._options.adapter,
- adapterOptions: this._options.adapterOptions,
- dbPrefix: this._options.dbPrefix,
- idSchema: options.idSchema,
- remoteTransformers: options.remoteTransformers,
- hooks: options.hooks
- });
- }
-}
-exports.default = KintoBase;
-
-},{"./adapters/base":85,"./collection":86,"babel-runtime/helpers/extends":8}],84:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _asyncToGenerator2 = require("babel-runtime/helpers/asyncToGenerator");
-
-var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);
-
-var _promise = require("babel-runtime/core-js/promise");
-
-var _promise2 = _interopRequireDefault(_promise);
-
-var _keys = require("babel-runtime/core-js/object/keys");
-
-var _keys2 = _interopRequireDefault(_keys);
-
-var _base = require("./base.js");
-
-var _base2 = _interopRequireDefault(_base);
-
-var _utils = require("../utils");
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const INDEXED_FIELDS = ["id", "_status", "last_modified"];
-
-/**
- * IDB cursor handlers.
- * @type {Object}
- */
-const cursorHandlers = {
- all(filters, done) {
- const results = [];
- return function (event) {
- const cursor = event.target.result;
- if (cursor) {
- if ((0, _utils.filterObject)(filters, cursor.value)) {
- results.push(cursor.value);
- }
- cursor.continue();
- } else {
- done(results);
- }
- };
- },
-
- in(values, done) {
- if (values.length === 0) {
- return done([]);
- }
- const sortedValues = [].slice.call(values).sort();
- const results = [];
- return function (event) {
- const cursor = event.target.result;
- if (!cursor) {
- done(results);
- return;
- }
- const { key, value } = cursor;
- let i = 0;
- while (key > sortedValues[i]) {
- // The cursor has passed beyond this key. Check next.
- ++i;
- if (i === sortedValues.length) {
- done(results); // There is no next. Stop searching.
- return;
- }
- }
- if (key === sortedValues[i]) {
- results.push(value);
- cursor.continue();
- } else {
- cursor.continue(sortedValues[i]);
- }
- };
- }
-};
-
-/**
- * Extract from filters definition the first indexed field. Since indexes were
- * created on single-columns, extracting a single one makes sense.
- *
- * @param {Object} filters The filters object.
- * @return {String|undefined}
- */
-function findIndexedField(filters) {
- const filteredFields = (0, _keys2.default)(filters);
- const indexedFields = filteredFields.filter(field => {
- return INDEXED_FIELDS.indexOf(field) !== -1;
- });
- return indexedFields[0];
-}
-
-/**
- * Creates an IDB request and attach it the appropriate cursor event handler to
- * perform a list query.
- *
- * Multiple matching values are handled by passing an array.
- *
- * @param {IDBStore} store The IDB store.
- * @param {String|undefined} indexField The indexed field to query, if any.
- * @param {Any} value The value to filter, if any.
- * @param {Object} filters More filters.
- * @param {Function} done The operation completion handler.
- * @return {IDBRequest}
- */
-function createListRequest(store, indexField, value, filters, done) {
- if (!indexField) {
- // Get all records.
- const request = store.openCursor();
- request.onsuccess = cursorHandlers.all(filters, done);
- return request;
- }
-
- // WHERE IN equivalent clause
- if (Array.isArray(value)) {
- const request = store.index(indexField).openCursor();
- request.onsuccess = cursorHandlers.in(value, done);
- return request;
- }
-
- // WHERE field = value clause
- const request = store.index(indexField).openCursor(IDBKeyRange.only(value));
- request.onsuccess = cursorHandlers.all(filters, done);
- return request;
-}
-
-/**
- * IndexedDB adapter.
- *
- * This adapter doesn't support any options.
- */
-class IDB extends _base2.default {
- /**
- * Constructor.
- *
- * @param {String} dbname The database nale.
- */
- constructor(dbname) {
- super();
- this._db = null;
- // public properties
- /**
- * The database name.
- * @type {String}
- */
- this.dbname = dbname;
- }
-
- _handleError(method, err) {
- const error = new Error(method + "() " + err.message);
- error.stack = err.stack;
- throw error;
- }
-
- /**
- * Ensures a connection to the IndexedDB database has been opened.
- *
- * @override
- * @return {Promise}
- */
- open() {
- if (this._db) {
- return _promise2.default.resolve(this);
- }
- return new _promise2.default((resolve, reject) => {
- const request = indexedDB.open(this.dbname, 1);
- request.onupgradeneeded = event => {
- // DB object
- const db = event.target.result;
- // Main collection store
- const collStore = db.createObjectStore(this.dbname, {
- keyPath: "id"
- });
- // Primary key (generated by IdSchema, UUID by default)
- collStore.createIndex("id", "id", { unique: true });
- // Local record status ("synced", "created", "updated", "deleted")
- collStore.createIndex("_status", "_status");
- // Last modified field
- collStore.createIndex("last_modified", "last_modified");
-
- // Metadata store
- const metaStore = db.createObjectStore("__meta__", {
- keyPath: "name"
- });
- metaStore.createIndex("name", "name", { unique: true });
- };
- request.onerror = event => reject(event.target.error);
- request.onsuccess = event => {
- this._db = event.target.result;
- resolve(this);
- };
- });
- }
-
- /**
- * Closes current connection to the database.
- *
- * @override
- * @return {Promise}
- */
- close() {
- if (this._db) {
- this._db.close(); // indexedDB.close is synchronous
- this._db = null;
- }
- return super.close();
- }
-
- /**
- * Returns a transaction and a store objects for this collection.
- *
- * To determine if a transaction has completed successfully, we should rather
- * listen to the transaction’s complete event rather than the IDBObjectStore
- * request’s success event, because the transaction may still fail after the
- * success event fires.
- *
- * @param {String} mode Transaction mode ("readwrite" or undefined)
- * @param {String|null} name Store name (defaults to coll name)
- * @return {Object}
- */
- prepare(mode = undefined, name = null) {
- const storeName = name || this.dbname;
- // On Safari, calling IDBDatabase.transaction with mode == undefined raises
- // a TypeError.
- const transaction = mode ? this._db.transaction([storeName], mode) : this._db.transaction([storeName]);
- const store = transaction.objectStore(storeName);
- return { transaction, store };
- }
-
- /**
- * Deletes every records in the current collection.
- *
- * @override
- * @return {Promise}
- */
- clear() {
- var _this = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- try {
- yield _this.open();
- return new _promise2.default(function (resolve, reject) {
- const { transaction, store } = _this.prepare("readwrite");
- store.clear();
- transaction.onerror = function (event) {
- return reject(new Error(event.target.error));
- };
- transaction.oncomplete = function () {
- return resolve();
- };
- });
- } catch (e) {
- _this._handleError("clear", e);
- }
- })();
- }
-
- /**
- * Executes the set of synchronous CRUD operations described in the provided
- * callback within an IndexedDB transaction, for current db store.
- *
- * The callback will be provided an object exposing the following synchronous
- * CRUD operation methods: get, create, update, delete.
- *
- * Important note: because limitations in IndexedDB implementations, no
- * asynchronous code should be performed within the provided callback; the
- * promise will therefore be rejected if the callback returns a Promise.
- *
- * Options:
- * - {Array} preload: The list of record IDs to fetch and make available to
- * the transaction object get() method (default: [])
- *
- * @example
- * const db = new IDB("example");
- * db.execute(transaction => {
- * transaction.create({id: 1, title: "foo"});
- * transaction.update({id: 2, title: "bar"});
- * transaction.delete(3);
- * return "foo";
- * })
- * .catch(console.error.bind(console));
- * .then(console.log.bind(console)); // => "foo"
- *
- * @param {Function} callback The operation description callback.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- execute(callback, options = { preload: [] }) {
- var _this2 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- // Transactions in IndexedDB are autocommited when a callback does not
- // perform any additional operation.
- // The way Promises are implemented in Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=1193394)
- // prevents using within an opened transaction.
- // To avoid managing asynchronocity in the specified `callback`, we preload
- // a list of record in order to execute the `callback` synchronously.
- // See also:
- // - http://stackoverflow.com/a/28388805/330911
- // - http://stackoverflow.com/a/10405196
- // - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- yield _this2.open();
- return new _promise2.default(function (resolve, reject) {
- // Start transaction.
- const { transaction, store } = _this2.prepare("readwrite");
- // Preload specified records using index.
- const ids = options.preload;
- store.index("id").openCursor().onsuccess = cursorHandlers.in(ids, function (records) {
- // Store obtained records by id.
- const preloaded = records.reduce(function (acc, record) {
- acc[record.id] = record;
- return acc;
- }, {});
- // Expose a consistent API for every adapter instead of raw store methods.
- const proxy = transactionProxy(store, preloaded);
- // The callback is executed synchronously within the same transaction.
- let result;
- try {
- result = callback(proxy);
- } catch (e) {
- transaction.abort();
- reject(e);
- }
- if (result instanceof _promise2.default) {
- // XXX: investigate how to provide documentation details in error.
- reject(new Error("execute() callback should not return a Promise."));
- }
- // XXX unsure if we should manually abort the transaction on error
- transaction.onerror = function (event) {
- return reject(new Error(event.target.error));
- };
- transaction.oncomplete = function (event) {
- return resolve(result);
- };
- });
- });
- })();
- }
-
- /**
- * Retrieve a record by its primary key from the IndexedDB database.
- *
- * @override
- * @param {String} id The record id.
- * @return {Promise}
- */
- get(id) {
- var _this3 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- try {
- yield _this3.open();
- return new _promise2.default(function (resolve, reject) {
- const { transaction, store } = _this3.prepare();
- const request = store.get(id);
- transaction.onerror = function (event) {
- return reject(new Error(event.target.error));
- };
- transaction.oncomplete = function () {
- return resolve(request.result);
- };
- });
- } catch (e) {
- _this3._handleError("get", e);
- }
- })();
- }
-
- /**
- * Lists all records from the IndexedDB database.
- *
- * @override
- * @return {Promise}
- */
- list(params = { filters: {} }) {
- var _this4 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- const { filters } = params;
- const indexField = findIndexedField(filters);
- const value = filters[indexField];
- try {
- yield _this4.open();
- const results = yield new _promise2.default(function (resolve, reject) {
- let results = [];
- // If `indexField` was used already, don't filter again.
- const remainingFilters = (0, _utils.omitKeys)(filters, indexField);
-
- const { transaction, store } = _this4.prepare();
- createListRequest(store, indexField, value, remainingFilters, function (_results) {
- // we have received all requested records, parking them within
- // current scope
- results = _results;
- });
- transaction.onerror = function (event) {
- return reject(new Error(event.target.error));
- };
- transaction.oncomplete = function (event) {
- return resolve(results);
- };
- });
-
- // The resulting list of records is sorted.
- // XXX: with some efforts, this could be fully implemented using IDB API.
- return params.order ? (0, _utils.sortObjects)(params.order, results) : results;
- } catch (e) {
- _this4._handleError("list", e);
- }
- })();
- }
-
- /**
- * Store the lastModified value into metadata store.
- *
- * @override
- * @param {Number} lastModified
- * @return {Promise}
- */
- saveLastModified(lastModified) {
- var _this5 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- const value = parseInt(lastModified, 10) || null;
- yield _this5.open();
- return new _promise2.default(function (resolve, reject) {
- const { transaction, store } = _this5.prepare("readwrite", "__meta__");
- store.put({ name: "lastModified", value: value });
- transaction.onerror = function (event) {
- return reject(event.target.error);
- };
- transaction.oncomplete = function (event) {
- return resolve(value);
- };
- });
- })();
- }
-
- /**
- * Retrieve saved lastModified value.
- *
- * @override
- * @return {Promise}
- */
- getLastModified() {
- var _this6 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- yield _this6.open();
- return new _promise2.default(function (resolve, reject) {
- const { transaction, store } = _this6.prepare(undefined, "__meta__");
- const request = store.get("lastModified");
- transaction.onerror = function (event) {
- return reject(event.target.error);
- };
- transaction.oncomplete = function (event) {
- resolve(request.result && request.result.value || null);
- };
- });
- })();
- }
-
- /**
- * Load a dump of records exported from a server.
- *
- * @abstract
- * @return {Promise}
- */
- loadDump(records) {
- var _this7 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- try {
- yield _this7.execute(function (transaction) {
- records.forEach(function (record) {
- return transaction.update(record);
- });
- });
- const previousLastModified = yield _this7.getLastModified();
- const lastModified = Math.max(...records.map(function (record) {
- return record.last_modified;
- }));
- if (lastModified > previousLastModified) {
- yield _this7.saveLastModified(lastModified);
- }
- return records;
- } catch (e) {
- _this7._handleError("loadDump", e);
- }
- })();
- }
-}
-
-exports.default = IDB; /**
- * IDB transaction proxy.
- *
- * @param {IDBStore} store The IndexedDB database store.
- * @param {Array} preloaded The list of records to make available to
- * get() (default: []).
- * @return {Object}
- */
-
-function transactionProxy(store, preloaded = []) {
- return {
- create(record) {
- store.add(record);
- },
-
- update(record) {
- store.put(record);
- },
-
- delete(id) {
- store.delete(id);
- },
-
- get(id) {
- return preloaded[id];
- }
- };
-}
-
-},{"../utils":87,"./base.js":85,"babel-runtime/core-js/object/keys":5,"babel-runtime/core-js/promise":6,"babel-runtime/helpers/asyncToGenerator":7}],85:[function(require,module,exports){
-"use strict";
-
-/**
- * Base db adapter.
- *
- * @abstract
- */
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
-var _promise = require("babel-runtime/core-js/promise");
-
-var _promise2 = _interopRequireDefault(_promise);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-class BaseAdapter {
- /**
- * Opens a connection to the database.
- *
- * @abstract
- * @return {Promise}
- */
- open() {
- return _promise2.default.resolve();
- }
-
- /**
- * Closes current connection to the database.
- *
- * @abstract
- * @return {Promise}
- */
- close() {
- return _promise2.default.resolve();
- }
-
- /**
- * Deletes every records present in the database.
- *
- * @abstract
- * @return {Promise}
- */
- clear() {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Executes a batch of operations within a single transaction.
- *
- * @abstract
- * @param {Function} callback The operation callback.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- execute(callback, options = { preload: [] }) {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Retrieve a record by its primary key from the database.
- *
- * @abstract
- * @param {String} id The record id.
- * @return {Promise}
- */
- get(id) {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Lists all records from the database.
- *
- * @abstract
- * @param {Object} params The filters and order to apply to the results.
- * @return {Promise}
- */
- list(params = { filters: {}, order: "" }) {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Store the lastModified value.
- *
- * @abstract
- * @param {Number} lastModified
- * @return {Promise}
- */
- saveLastModified(lastModified) {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Retrieve saved lastModified value.
- *
- * @abstract
- * @return {Promise}
- */
- getLastModified() {
- throw new Error("Not Implemented.");
- }
-
- /**
- * Load a dump of records exported from a server.
- *
- * @abstract
- * @return {Promise}
- */
- loadDump(records) {
- throw new Error("Not Implemented.");
- }
-}
-exports.default = BaseAdapter;
-
-},{"babel-runtime/core-js/promise":6}],86:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.CollectionTransaction = exports.SyncResultObject = undefined;
-
-var _stringify = require("babel-runtime/core-js/json/stringify");
-
-var _stringify2 = _interopRequireDefault(_stringify);
-
-var _promise = require("babel-runtime/core-js/promise");
-
-var _promise2 = _interopRequireDefault(_promise);
-
-var _asyncToGenerator2 = require("babel-runtime/helpers/asyncToGenerator");
-
-var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);
-
-var _extends2 = require("babel-runtime/helpers/extends");
-
-var _extends3 = _interopRequireDefault(_extends2);
-
-var _assign = require("babel-runtime/core-js/object/assign");
-
-var _assign2 = _interopRequireDefault(_assign);
-
-exports.recordsEqual = recordsEqual;
-
-var _base = require("./adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-var _IDB = require("./adapters/IDB");
-
-var _IDB2 = _interopRequireDefault(_IDB);
-
-var _utils = require("./utils");
-
-var _uuid = require("uuid");
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const RECORD_FIELDS_TO_CLEAN = ["_status"];
-const AVAILABLE_HOOKS = ["incoming-changes"];
-
-/**
- * Compare two records omitting local fields and synchronization
- * attributes (like _status and last_modified)
- * @param {Object} a A record to compare.
- * @param {Object} b A record to compare.
- * @return {boolean}
- */
-function recordsEqual(a, b, localFields = []) {
- const fieldsToClean = RECORD_FIELDS_TO_CLEAN.concat(["last_modified"]).concat(localFields);
- const cleanLocal = r => (0, _utils.omitKeys)(r, fieldsToClean);
- return (0, _utils.deepEqual)(cleanLocal(a), cleanLocal(b));
-}
-
-/**
- * Synchronization result object.
- */
-class SyncResultObject {
- /**
- * Object default values.
- * @type {Object}
- */
- static get defaults() {
- return {
- ok: true,
- lastModified: null,
- errors: [],
- created: [],
- updated: [],
- deleted: [],
- published: [],
- conflicts: [],
- skipped: [],
- resolved: []
- };
- }
-
- /**
- * Public constructor.
- */
- constructor() {
- /**
- * Current synchronization result status; becomes `false` when conflicts or
- * errors are registered.
- * @type {Boolean}
- */
- this.ok = true;
- (0, _assign2.default)(this, SyncResultObject.defaults);
- }
-
- /**
- * Adds entries for a given result type.
- *
- * @param {String} type The result type.
- * @param {Array} entries The result entries.
- * @return {SyncResultObject}
- */
- add(type, entries) {
- if (!Array.isArray(this[type])) {
- return;
- }
- // Deduplicate entries by id. If the values don't have `id` attribute, just
- // keep all.
- const deduplicated = this[type].concat(entries).reduce((acc, cur) => {
- const existing = acc.filter(r => cur.id && r.id ? cur.id != r.id : true);
- return existing.concat(cur);
- }, []);
- this[type] = deduplicated;
- this.ok = this.errors.length + this.conflicts.length === 0;
- return this;
- }
-
- /**
- * Reinitializes result entries for a given result type.
- *
- * @param {String} type The result type.
- * @return {SyncResultObject}
- */
- reset(type) {
- this[type] = SyncResultObject.defaults[type];
- this.ok = this.errors.length + this.conflicts.length === 0;
- return this;
- }
-}
-
-exports.SyncResultObject = SyncResultObject;
-function createUUIDSchema() {
- return {
- generate() {
- return (0, _uuid.v4)();
- },
-
- validate(id) {
- return (0, _utils.isUUID)(id);
- }
- };
-}
-
-function markStatus(record, status) {
- return (0, _extends3.default)({}, record, { _status: status });
-}
-
-function markDeleted(record) {
- return markStatus(record, "deleted");
-}
-
-function markSynced(record) {
- return markStatus(record, "synced");
-}
-
-/**
- * Import a remote change into the local database.
- *
- * @param {IDBTransactionProxy} transaction The transaction handler.
- * @param {Object} remote The remote change object to import.
- * @param {Array<String>} localFields The list of fields that remain local.
- * @return {Object}
- */
-function importChange(transaction, remote, localFields) {
- const local = transaction.get(remote.id);
- if (!local) {
- // Not found locally but remote change is marked as deleted; skip to
- // avoid recreation.
- if (remote.deleted) {
- return { type: "skipped", data: remote };
- }
- const synced = markSynced(remote);
- transaction.create(synced);
- return { type: "created", data: synced };
- }
- // Compare local and remote, ignoring local fields.
- const isIdentical = recordsEqual(local, remote, localFields);
- // Apply remote changes on local record.
- const synced = (0, _extends3.default)({}, local, markSynced(remote));
- // Detect or ignore conflicts if record has also been modified locally.
- if (local._status !== "synced") {
- // Locally deleted, unsynced: scheduled for remote deletion.
- if (local._status === "deleted") {
- return { type: "skipped", data: local };
- }
- if (isIdentical) {
- // If records are identical, import anyway, so we bump the
- // local last_modified value from the server and set record
- // status to "synced".
- transaction.update(synced);
- return { type: "updated", data: { old: local, new: synced } };
- }
- if (local.last_modified !== undefined && local.last_modified === remote.last_modified) {
- // If our local version has the same last_modified as the remote
- // one, this represents an object that corresponds to a resolved
- // conflict. Our local version represents the final output, so
- // we keep that one. (No transaction operation to do.)
- // But if our last_modified is undefined,
- // that means we've created the same object locally as one on
- // the server, which *must* be a conflict.
- return { type: "void" };
- }
- return {
- type: "conflicts",
- data: { type: "incoming", local: local, remote: remote }
- };
- }
- // Local record was synced.
- if (remote.deleted) {
- transaction.delete(remote.id);
- return { type: "deleted", data: local };
- }
- // Import locally.
- transaction.update(synced);
- // if identical, simply exclude it from all SyncResultObject lists
- const type = isIdentical ? "void" : "updated";
- return { type, data: { old: local, new: synced } };
-}
-
-/**
- * Abstracts a collection of records stored in the local database, providing
- * CRUD operations and synchronization helpers.
- */
-class Collection {
- /**
- * Constructor.
- *
- * Options:
- * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`)
- * - `{String} dbPrefix` The DB name prefix (default: `""`)
- *
- * @param {String} bucket The bucket identifier.
- * @param {String} name The collection name.
- * @param {Api} api The Api instance.
- * @param {Object} options The options object.
- */
- constructor(bucket, name, api, options = {}) {
- this._bucket = bucket;
- this._name = name;
- this._lastModified = null;
-
- const DBAdapter = options.adapter || _IDB2.default;
- if (!DBAdapter) {
- throw new Error("No adapter provided");
- }
- const dbPrefix = options.dbPrefix || "";
- const db = new DBAdapter(`${ dbPrefix }${ bucket }/${ name }`, options.adapterOptions);
- if (!(db instanceof _base2.default)) {
- throw new Error("Unsupported adapter.");
- }
- // public properties
- /**
- * The db adapter instance
- * @type {BaseAdapter}
- */
- this.db = db;
- /**
- * The Api instance.
- * @type {KintoClient}
- */
- this.api = api;
- /**
- * The event emitter instance.
- * @type {EventEmitter}
- */
- this.events = options.events;
- /**
- * The IdSchema instance.
- * @type {Object}
- */
- this.idSchema = this._validateIdSchema(options.idSchema);
- /**
- * The list of remote transformers.
- * @type {Array}
- */
- this.remoteTransformers = this._validateRemoteTransformers(options.remoteTransformers);
- /**
- * The list of hooks.
- * @type {Object}
- */
- this.hooks = this._validateHooks(options.hooks);
- /**
- * The list of fields names that will remain local.
- * @type {Array}
- */
- this.localFields = options.localFields || [];
- }
-
- /**
- * The collection name.
- * @type {String}
- */
- get name() {
- return this._name;
- }
-
- /**
- * The bucket name.
- * @type {String}
- */
- get bucket() {
- return this._bucket;
- }
-
- /**
- * The last modified timestamp.
- * @type {Number}
- */
- get lastModified() {
- return this._lastModified;
- }
-
- /**
- * Synchronization strategies. Available strategies are:
- *
- * - `MANUAL`: Conflicts will be reported in a dedicated array.
- * - `SERVER_WINS`: Conflicts are resolved using remote data.
- * - `CLIENT_WINS`: Conflicts are resolved using local data.
- *
- * @type {Object}
- */
- static get strategy() {
- return {
- CLIENT_WINS: "client_wins",
- SERVER_WINS: "server_wins",
- MANUAL: "manual"
- };
- }
-
- /**
- * Validates an idSchema.
- *
- * @param {Object|undefined} idSchema
- * @return {Object}
- */
- _validateIdSchema(idSchema) {
- if (typeof idSchema === "undefined") {
- return createUUIDSchema();
- }
- if (typeof idSchema !== "object") {
- throw new Error("idSchema must be an object.");
- } else if (typeof idSchema.generate !== "function") {
- throw new Error("idSchema must provide a generate function.");
- } else if (typeof idSchema.validate !== "function") {
- throw new Error("idSchema must provide a validate function.");
- }
- return idSchema;
- }
-
- /**
- * Validates a list of remote transformers.
- *
- * @param {Array|undefined} remoteTransformers
- * @return {Array}
- */
- _validateRemoteTransformers(remoteTransformers) {
- if (typeof remoteTransformers === "undefined") {
- return [];
- }
- if (!Array.isArray(remoteTransformers)) {
- throw new Error("remoteTransformers should be an array.");
- }
- return remoteTransformers.map(transformer => {
- if (typeof transformer !== "object") {
- throw new Error("A transformer must be an object.");
- } else if (typeof transformer.encode !== "function") {
- throw new Error("A transformer must provide an encode function.");
- } else if (typeof transformer.decode !== "function") {
- throw new Error("A transformer must provide a decode function.");
- }
- return transformer;
- });
- }
-
- /**
- * Validate the passed hook is correct.
- *
- * @param {Array|undefined} hook.
- * @return {Array}
- **/
- _validateHook(hook) {
- if (!Array.isArray(hook)) {
- throw new Error("A hook definition should be an array of functions.");
- }
- return hook.map(fn => {
- if (typeof fn !== "function") {
- throw new Error("A hook definition should be an array of functions.");
- }
- return fn;
- });
- }
-
- /**
- * Validates a list of hooks.
- *
- * @param {Object|undefined} hooks
- * @return {Object}
- */
- _validateHooks(hooks) {
- if (typeof hooks === "undefined") {
- return {};
- }
- if (Array.isArray(hooks)) {
- throw new Error("hooks should be an object, not an array.");
- }
- if (typeof hooks !== "object") {
- throw new Error("hooks should be an object.");
- }
-
- const validatedHooks = {};
-
- for (let hook in hooks) {
- if (AVAILABLE_HOOKS.indexOf(hook) === -1) {
- throw new Error("The hook should be one of " + AVAILABLE_HOOKS.join(", "));
- }
- validatedHooks[hook] = this._validateHook(hooks[hook]);
- }
- return validatedHooks;
- }
-
- /**
- * Deletes every records in the current collection and marks the collection as
- * never synced.
- *
- * @return {Promise}
- */
- clear() {
- var _this = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- yield _this.db.clear();
- yield _this.db.saveLastModified(null);
- return { data: [], permissions: {} };
- })();
- }
-
- /**
- * Encodes a record.
- *
- * @param {String} type Either "remote" or "local".
- * @param {Object} record The record object to encode.
- * @return {Promise}
- */
- _encodeRecord(type, record) {
- if (!this[`${ type }Transformers`].length) {
- return _promise2.default.resolve(record);
- }
- return (0, _utils.waterfall)(this[`${ type }Transformers`].map(transformer => {
- return record => transformer.encode(record);
- }), record);
- }
-
- /**
- * Decodes a record.
- *
- * @param {String} type Either "remote" or "local".
- * @param {Object} record The record object to decode.
- * @return {Promise}
- */
- _decodeRecord(type, record) {
- if (!this[`${ type }Transformers`].length) {
- return _promise2.default.resolve(record);
- }
- return (0, _utils.waterfall)(this[`${ type }Transformers`].reverse().map(transformer => {
- return record => transformer.decode(record);
- }), record);
- }
-
- /**
- * Adds a record to the local database, asserting that none
- * already exist with this ID.
- *
- * Note: If either the `useRecordId` or `synced` options are true, then the
- * record object must contain the id field to be validated. If none of these
- * options are true, an id is generated using the current IdSchema; in this
- * case, the record passed must not have an id.
- *
- * Options:
- * - {Boolean} synced Sets record status to "synced" (default: `false`).
- * - {Boolean} useRecordId Forces the `id` field from the record to be used,
- * instead of one that is generated automatically
- * (default: `false`).
- *
- * @param {Object} record
- * @param {Object} options
- * @return {Promise}
- */
- create(record, options = { useRecordId: false, synced: false }) {
- // Validate the record and its ID (if any), even though this
- // validation is also done in the CollectionTransaction method,
- // because we need to pass the ID to preloadIds.
- const reject = msg => _promise2.default.reject(new Error(msg));
- if (typeof record !== "object") {
- return reject("Record is not an object.");
- }
- if ((options.synced || options.useRecordId) && !record.hasOwnProperty("id")) {
- return reject("Missing required Id; synced and useRecordId options require one");
- }
- if (!options.synced && !options.useRecordId && record.hasOwnProperty("id")) {
- return reject("Extraneous Id; can't create a record having one set.");
- }
- const newRecord = (0, _extends3.default)({}, record, {
- id: options.synced || options.useRecordId ? record.id : this.idSchema.generate(),
- _status: options.synced ? "synced" : "created"
- });
- if (!this.idSchema.validate(newRecord.id)) {
- return reject(`Invalid Id: ${ newRecord.id }`);
- }
- return this.execute(txn => txn.create(newRecord), { preloadIds: [newRecord.id] }).catch(err => {
- if (options.useRecordId) {
- throw new Error("Couldn't create record. It may have been virtually deleted.");
- }
- throw err;
- });
- }
-
- /**
- * Like {@link CollectionTransaction#update}, but wrapped in its own transaction.
- *
- * Options:
- * - {Boolean} synced: Sets record status to "synced" (default: false)
- * - {Boolean} patch: Extends the existing record instead of overwriting it
- * (default: false)
- *
- * @param {Object} record
- * @param {Object} options
- * @return {Promise}
- */
- update(record, options = { synced: false, patch: false }) {
- // Validate the record and its ID, even though this validation is
- // also done in the CollectionTransaction method, because we need
- // to pass the ID to preloadIds.
- if (typeof record !== "object") {
- return _promise2.default.reject(new Error("Record is not an object."));
- }
- if (!record.hasOwnProperty("id")) {
- return _promise2.default.reject(new Error("Cannot update a record missing id."));
- }
- if (!this.idSchema.validate(record.id)) {
- return _promise2.default.reject(new Error(`Invalid Id: ${ record.id }`));
- }
-
- return this.execute(txn => txn.update(record, options), { preloadIds: [record.id] });
- }
-
- /**
- * Like {@link CollectionTransaction#upsert}, but wrapped in its own transaction.
- *
- * @param {Object} record
- * @return {Promise}
- */
- upsert(record) {
- // Validate the record and its ID, even though this validation is
- // also done in the CollectionTransaction method, because we need
- // to pass the ID to preloadIds.
- if (typeof record !== "object") {
- return _promise2.default.reject(new Error("Record is not an object."));
- }
- if (!record.hasOwnProperty("id")) {
- return _promise2.default.reject(new Error("Cannot update a record missing id."));
- }
- if (!this.idSchema.validate(record.id)) {
- return _promise2.default.reject(new Error(`Invalid Id: ${ record.id }`));
- }
-
- return this.execute(txn => txn.upsert(record), { preloadIds: [record.id] });
- }
-
- /**
- * Like {@link CollectionTransaction#get}, but wrapped in its own transaction.
- *
- * Options:
- * - {Boolean} includeDeleted: Include virtually deleted records.
- *
- * @param {String} id
- * @param {Object} options
- * @return {Promise}
- */
- get(id, options = { includeDeleted: false }) {
- return this.execute(txn => txn.get(id, options), { preloadIds: [id] });
- }
-
- /**
- * Like {@link CollectionTransaction#getAny}, but wrapped in its own transaction.
- *
- * @param {String} id
- * @return {Promise}
- */
- getAny(id) {
- return this.execute(txn => txn.getAny(id), { preloadIds: [id] });
- }
-
- /**
- * Same as {@link Collection#delete}, but wrapped in its own transaction.
- *
- * Options:
- * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
- * update its `_status` attribute to `deleted` instead (default: true)
- *
- * @param {String} id The record's Id.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- delete(id, options = { virtual: true }) {
- return this.execute(transaction => {
- return transaction.delete(id, options);
- }, { preloadIds: [id] });
- }
-
- /**
- * The same as {@link CollectionTransaction#deleteAny}, but wrapped
- * in its own transaction.
- *
- * @param {String} id The record's Id.
- * @return {Promise}
- */
- deleteAny(id) {
- return this.execute(txn => txn.deleteAny(id), { preloadIds: [id] });
- }
-
- /**
- * Lists records from the local database.
- *
- * Params:
- * - {Object} filters Filter the results (default: `{}`).
- * - {String} order The order to apply (default: `-last_modified`).
- *
- * Options:
- * - {Boolean} includeDeleted: Include virtually deleted records.
- *
- * @param {Object} params The filters and order to apply to the results.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- list(params = {}, options = { includeDeleted: false }) {
- var _this2 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- params = (0, _extends3.default)({ order: "-last_modified", filters: {} }, params);
- const results = yield _this2.db.list(params);
- let data = results;
- if (!options.includeDeleted) {
- data = results.filter(function (record) {
- return record._status !== "deleted";
- });
- }
- return { data, permissions: {} };
- })();
- }
-
- /**
- * Imports remote changes into the local database.
- * This method is in charge of detecting the conflicts, and resolve them
- * according to the specified strategy.
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Array} decodedChanges The list of changes to import in the local database.
- * @param {String} strategy The {@link Collection.strategy} (default: MANUAL)
- * @return {Promise}
- */
- importChanges(syncResultObject, decodedChanges, strategy = Collection.strategy.MANUAL) {
- var _this3 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- // Retrieve records matching change ids.
- try {
- const { imports, resolved } = yield _this3.db.execute(function (transaction) {
- const imports = decodedChanges.map(function (remote) {
- // Store remote change into local database.
- return importChange(transaction, remote, _this3.localFields);
- });
- const conflicts = imports.filter(function (i) {
- return i.type === "conflicts";
- }).map(function (i) {
- return i.data;
- });
- const resolved = _this3._handleConflicts(transaction, conflicts, strategy);
- return { imports, resolved };
- }, { preload: decodedChanges.map(function (record) {
- return record.id;
- }) });
-
- // Lists of created/updated/deleted records
- imports.forEach(function ({ type, data }) {
- return syncResultObject.add(type, data);
- });
-
- // Automatically resolved conflicts (if not manual)
- if (resolved.length > 0) {
- syncResultObject.reset("conflicts").add("resolved", resolved);
- }
- } catch (err) {
- const data = {
- type: "incoming",
- message: err.message,
- stack: err.stack
- };
- // XXX one error of the whole transaction instead of per atomic op
- syncResultObject.add("errors", data);
- }
-
- return syncResultObject;
- })();
- }
-
- /**
- * Imports the responses of pushed changes into the local database.
- * Basically it stores the timestamp assigned by the server into the local
- * database.
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Array} toApplyLocally The list of changes to import in the local database.
- * @param {Array} conflicts The list of conflicts that have to be resolved.
- * @param {String} strategy The {@link Collection.strategy}.
- * @return {Promise}
- */
- _applyPushedResults(syncResultObject, toApplyLocally, conflicts, strategy = Collection.strategy.MANUAL) {
- var _this4 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- const toDeleteLocally = toApplyLocally.filter(function (r) {
- return r.deleted;
- });
- const toUpdateLocally = toApplyLocally.filter(function (r) {
- return !r.deleted;
- });
-
- const { published, resolved } = yield _this4.db.execute(function (transaction) {
- const updated = toUpdateLocally.map(function (record) {
- const synced = markSynced(record);
- transaction.update(synced);
- return synced;
- });
- const deleted = toDeleteLocally.map(function (record) {
- transaction.delete(record.id);
- // Amend result data with the deleted attribute set
- return { id: record.id, deleted: true };
- });
- const published = updated.concat(deleted);
- // Handle conflicts, if any
- const resolved = _this4._handleConflicts(transaction, conflicts, strategy);
- return { published, resolved };
- });
-
- syncResultObject.add("published", published);
-
- if (resolved.length > 0) {
- syncResultObject.reset("conflicts").reset("resolved").add("resolved", resolved);
- }
- return syncResultObject;
- })();
- }
-
- /**
- * Handles synchronization conflicts according to specified strategy.
- *
- * @param {SyncResultObject} result The sync result object.
- * @param {String} strategy The {@link Collection.strategy}.
- * @return {Promise}
- */
- _handleConflicts(transaction, conflicts, strategy) {
- if (strategy === Collection.strategy.MANUAL) {
- return [];
- }
- return conflicts.map(conflict => {
- const resolution = strategy === Collection.strategy.CLIENT_WINS ? conflict.local : conflict.remote;
- const updated = this._resolveRaw(conflict, resolution);
- transaction.update(updated);
- return updated;
- });
- }
-
- /**
- * Execute a bunch of operations in a transaction.
- *
- * This transaction should be atomic -- either all of its operations
- * will succeed, or none will.
- *
- * The argument to this function is itself a function which will be
- * called with a {@link CollectionTransaction}. Collection methods
- * are available on this transaction, but instead of returning
- * promises, they are synchronous. execute() returns a Promise whose
- * value will be the return value of the provided function.
- *
- * Most operations will require access to the record itself, which
- * must be preloaded by passing its ID in the preloadIds option.
- *
- * Options:
- * - {Array} preloadIds: list of IDs to fetch at the beginning of
- * the transaction
- *
- * @return {Promise} Resolves with the result of the given function
- * when the transaction commits.
- */
- execute(doOperations, { preloadIds = [] } = {}) {
- for (let id of preloadIds) {
- if (!this.idSchema.validate(id)) {
- return _promise2.default.reject(Error(`Invalid Id: ${ id }`));
- }
- }
-
- return this.db.execute(transaction => {
- const txn = new CollectionTransaction(this, transaction);
- const result = doOperations(txn);
- txn.emitEvents();
- return result;
- }, { preload: preloadIds });
- }
-
- /**
- * Resets the local records as if they were never synced; existing records are
- * marked as newly created, deleted records are dropped.
- *
- * A next call to {@link Collection.sync} will thus republish the whole
- * content of the local collection to the server.
- *
- * @return {Promise} Resolves with the number of processed records.
- */
- resetSyncStatus() {
- var _this5 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- const unsynced = yield _this5.list({ filters: { _status: ["deleted", "synced"] }, order: "" }, { includeDeleted: true });
- yield _this5.db.execute(function (transaction) {
- unsynced.data.forEach(function (record) {
- if (record._status === "deleted") {
- // Garbage collect deleted records.
- transaction.delete(record.id);
- } else {
- // Records that were synced become «created».
- transaction.update((0, _extends3.default)({}, record, {
- last_modified: undefined,
- _status: "created"
- }));
- }
- });
- });
- _this5._lastModified = null;
- yield _this5.db.saveLastModified(null);
- return unsynced.data.length;
- })();
- }
-
- /**
- * Returns an object containing two lists:
- *
- * - `toDelete`: unsynced deleted records we can safely delete;
- * - `toSync`: local updates to send to the server.
- *
- * @return {Promise}
- */
- gatherLocalChanges() {
- var _this6 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- const unsynced = yield _this6.list({ filters: { _status: ["created", "updated"] }, order: "" });
- const deleted = yield _this6.list({ filters: { _status: "deleted" }, order: "" }, { includeDeleted: true });
-
- const toSync = yield _promise2.default.all(unsynced.data.map(_this6._encodeRecord.bind(_this6, "remote")));
- const toDelete = yield _promise2.default.all(deleted.data.map(_this6._encodeRecord.bind(_this6, "remote")));
-
- return { toSync, toDelete };
- })();
- }
-
- /**
- * Fetch remote changes, import them to the local database, and handle
- * conflicts according to `options.strategy`. Then, updates the passed
- * {@link SyncResultObject} with import results.
- *
- * Options:
- * - {String} strategy: The selected sync strategy.
- *
- * @param {KintoClient.Collection} client Kinto client Collection instance.
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Object} options
- * @return {Promise}
- */
- pullChanges(client, syncResultObject, options = {}) {
- var _this7 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- if (!syncResultObject.ok) {
- return syncResultObject;
- }
-
- const since = _this7.lastModified ? _this7.lastModified : yield _this7.db.getLastModified();
-
- options = (0, _extends3.default)({
- strategy: Collection.strategy.MANUAL,
- lastModified: since,
- headers: {}
- }, options);
-
- // Optionally ignore some records when pulling for changes.
- // (avoid redownloading our own changes on last step of #sync())
- let filters;
- if (options.exclude) {
- // Limit the list of excluded records to the first 50 records in order
- // to remain under de-facto URL size limit (~2000 chars).
- // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184
- const exclude_id = options.exclude.slice(0, 50).map(function (r) {
- return r.id;
- }).join(",");
- filters = { exclude_id };
- }
- // First fetch remote changes from the server
- const { data, last_modified } = yield client.listRecords({
- // Since should be ETag (see https://github.com/Kinto/kinto.js/issues/356)
- since: options.lastModified ? `${ options.lastModified }` : undefined,
- headers: options.headers,
- filters
- });
- // last_modified is the ETag header value (string).
- // For retro-compatibility with first kinto.js versions
- // parse it to integer.
- const unquoted = last_modified ? parseInt(last_modified, 10) : undefined;
-
- // Check if server was flushed.
- // This is relevant for the Kinto demo server
- // (and thus for many new comers).
- const localSynced = options.lastModified;
- const serverChanged = unquoted > options.lastModified;
- const emptyCollection = data.length === 0;
- if (!options.exclude && localSynced && serverChanged && emptyCollection) {
- throw Error("Server has been flushed.");
- }
-
- syncResultObject.lastModified = unquoted;
-
- // Decode incoming changes.
- const decodedChanges = yield _promise2.default.all(data.map(function (change) {
- return _this7._decodeRecord("remote", change);
- }));
- // Hook receives decoded records.
- const payload = { lastModified: unquoted, changes: decodedChanges };
- const afterHooks = yield _this7.applyHook("incoming-changes", payload);
-
- // No change, nothing to import.
- if (afterHooks.changes.length > 0) {
- // Reflect these changes locally
- yield _this7.importChanges(syncResultObject, afterHooks.changes, options.strategy);
- }
- return syncResultObject;
- })();
- }
-
- applyHook(hookName, payload) {
- if (typeof this.hooks[hookName] == "undefined") {
- return _promise2.default.resolve(payload);
- }
- return (0, _utils.waterfall)(this.hooks[hookName].map(hook => {
- return record => {
- const result = hook(payload, this);
- const resultThenable = result && typeof result.then === "function";
- const resultChanges = result && result.hasOwnProperty("changes");
- if (!(resultThenable || resultChanges)) {
- throw new Error(`Invalid return value for hook: ${ (0, _stringify2.default)(result) } has no 'then()' or 'changes' properties`);
- }
- return result;
- };
- }), payload);
- }
-
- /**
- * Publish local changes to the remote server and updates the passed
- * {@link SyncResultObject} with publication results.
- *
- * @param {KintoClient.Collection} client Kinto client Collection instance.
- * @param {SyncResultObject} syncResultObject The sync result object.
- * @param {Object} changes The change object.
- * @param {Array} changes.toDelete The list of records to delete.
- * @param {Array} changes.toSync The list of records to create/update.
- * @param {Object} options The options object.
- * @return {Promise}
- */
- pushChanges(client, { toDelete = [], toSync }, syncResultObject, options = {}) {
- var _this8 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- if (!syncResultObject.ok) {
- return syncResultObject;
- }
- const safe = !options.strategy || options.strategy !== Collection.CLIENT_WINS;
-
- // Perform a batch request with every changes.
- const synced = yield client.batch(function (batch) {
- toDelete.forEach(function (r) {
- // never published locally deleted records should not be pusblished
- if (r.last_modified) {
- batch.deleteRecord(r);
- }
- });
- toSync.forEach(function (r) {
- // Clean local fields (like _status) before sending to server.
- const published = _this8.cleanLocalFields(r);
- if (r._status === "created") {
- batch.createRecord(published);
- } else {
- batch.updateRecord(published);
- }
- });
- }, { headers: options.headers, safe, aggregate: true });
-
- // Store outgoing errors into sync result object
- syncResultObject.add("errors", synced.errors.map(function (e) {
- return (0, _extends3.default)({}, e, { type: "outgoing" });
- }));
-
- // Store outgoing conflicts into sync result object
- const conflicts = [];
- for (let { type, local, remote } of synced.conflicts) {
- // Note: we ensure that local data are actually available, as they may
- // be missing in the case of a published deletion.
- const safeLocal = local && local.data || { id: remote.id };
- const realLocal = yield _this8._decodeRecord("remote", safeLocal);
- const realRemote = yield _this8._decodeRecord("remote", remote);
- const conflict = { type, local: realLocal, remote: realRemote };
- conflicts.push(conflict);
- }
- syncResultObject.add("conflicts", conflicts);
-
- // Records that must be deleted are either deletions that were pushed
- // to server (published) or deleted records that were never pushed (skipped).
- const missingRemotely = synced.skipped.map(function (r) {
- return (0, _extends3.default)({}, r, { deleted: true });
- });
-
- // For created and updated records, the last_modified coming from server
- // will be stored locally.
- // Reflect publication results locally using the response from
- // the batch request.
- const published = synced.published.map(function (c) {
- return c.data;
- });
- const toApplyLocally = published.concat(missingRemotely);
-
- // Apply the decode transformers, if any
- const decoded = yield _promise2.default.all(toApplyLocally.map(function (record) {
- return _this8._decodeRecord("remote", record);
- }));
-
- // We have to update the local records with the responses of the server
- // (eg. last_modified values etc.).
- if (decoded.length > 0 || conflicts.length > 0) {
- yield _this8._applyPushedResults(syncResultObject, decoded, conflicts, options.strategy);
- }
-
- return syncResultObject;
- })();
- }
-
- /**
- * Return a copy of the specified record without the local fields.
- *
- * @param {Object} record A record with potential local fields.
- * @return {Object}
- */
- cleanLocalFields(record) {
- const localKeys = RECORD_FIELDS_TO_CLEAN.concat(this.localFields);
- return (0, _utils.omitKeys)(record, localKeys);
- }
-
- /**
- * Resolves a conflict, updating local record according to proposed
- * resolution — keeping remote record `last_modified` value as a reference for
- * further batch sending.
- *
- * @param {Object} conflict The conflict object.
- * @param {Object} resolution The proposed record.
- * @return {Promise}
- */
- resolve(conflict, resolution) {
- return this.db.execute(transaction => {
- const updated = this._resolveRaw(conflict, resolution);
- transaction.update(updated);
- return { data: updated, permissions: {} };
- });
- }
-
- /**
- * @private
- */
- _resolveRaw(conflict, resolution) {
- const resolved = (0, _extends3.default)({}, resolution, {
- // Ensure local record has the latest authoritative timestamp
- last_modified: conflict.remote.last_modified
- });
- // If the resolution object is strictly equal to the
- // remote record, then we can mark it as synced locally.
- // Otherwise, mark it as updated (so that the resolution is pushed).
- const synced = (0, _utils.deepEqual)(resolved, conflict.remote);
- return markStatus(resolved, synced ? "synced" : "updated");
- }
-
- /**
- * Synchronize remote and local data. The promise will resolve with a
- * {@link SyncResultObject}, though will reject:
- *
- * - if the server is currently backed off;
- * - if the server has been detected flushed.
- *
- * Options:
- * - {Object} headers: HTTP headers to attach to outgoing requests.
- * - {Collection.strategy} strategy: See {@link Collection.strategy}.
- * - {Boolean} ignoreBackoff: Force synchronization even if server is currently
- * backed off.
- * - {String} bucket: The remove bucket id to use (default: null)
- * - {String} collection: The remove collection id to use (default: null)
- * - {String} remote The remote Kinto server endpoint to use (default: null).
- *
- * @param {Object} options Options.
- * @return {Promise}
- * @throws {Error} If an invalid remote option is passed.
- */
- sync(options = {
- strategy: Collection.strategy.MANUAL,
- headers: {},
- ignoreBackoff: false,
- bucket: null,
- collection: null,
- remote: null
- }) {
- var _this9 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- const previousRemote = _this9.api.remote;
- if (options.remote) {
- // Note: setting the remote ensures it's valid, throws when invalid.
- _this9.api.remote = options.remote;
- }
- if (!options.ignoreBackoff && _this9.api.backoff > 0) {
- const seconds = Math.ceil(_this9.api.backoff / 1000);
- return _promise2.default.reject(new Error(`Server is asking clients to back off; retry in ${ seconds }s or use the ignoreBackoff option.`));
- }
-
- const client = _this9.api.bucket(options.bucket || _this9.bucket).collection(options.collection || _this9.name);
-
- const result = new SyncResultObject();
- try {
- // Fetch last changes from the server.
- yield _this9.pullChanges(client, result, options);
- const { lastModified } = result;
-
- // Fetch local changes
- const { toDelete, toSync } = yield _this9.gatherLocalChanges();
-
- // Publish local changes and pull local resolutions
- yield _this9.pushChanges(client, { toDelete, toSync }, result, options);
-
- // Publish local resolution of push conflicts to server (on CLIENT_WINS)
- const resolvedUnsynced = result.resolved.filter(function (r) {
- return r._status !== "synced";
- });
- if (resolvedUnsynced.length > 0) {
- const resolvedEncoded = yield _promise2.default.all(resolvedUnsynced.map(_this9._encodeRecord.bind(_this9, "remote")));
- yield _this9.pushChanges(client, { toSync: resolvedEncoded }, result, options);
- }
- // Perform a last pull to catch changes that occured after the last pull,
- // while local changes were pushed. Do not do it nothing was pushed.
- if (result.published.length > 0) {
- // Avoid redownloading our own changes during the last pull.
- const pullOpts = (0, _extends3.default)({}, options, { lastModified, exclude: result.published });
- yield _this9.pullChanges(client, result, pullOpts);
- }
-
- // Don't persist lastModified value if any conflict or error occured
- if (result.ok) {
- // No conflict occured, persist collection's lastModified value
- _this9._lastModified = yield _this9.db.saveLastModified(result.lastModified);
- }
- } finally {
- // Ensure API default remote is reverted if a custom one's been used
- _this9.api.remote = previousRemote;
- }
- return result;
- })();
- }
-
- /**
- * Load a list of records already synced with the remote server.
- *
- * The local records which are unsynced or whose timestamp is either missing
- * or superior to those being loaded will be ignored.
- *
- * @param {Array} records The previously exported list of records to load.
- * @return {Promise} with the effectively imported records.
- */
- loadDump(records) {
- var _this10 = this;
-
- return (0, _asyncToGenerator3.default)(function* () {
- if (!Array.isArray(records)) {
- throw new Error("Records is not an array.");
- }
-
- for (let record of records) {
- if (!record.hasOwnProperty("id") || !_this10.idSchema.validate(record.id)) {
- throw new Error("Record has invalid ID: " + (0, _stringify2.default)(record));
- }
-
- if (!record.last_modified) {
- throw new Error("Record has no last_modified value: " + (0, _stringify2.default)(record));
- }
- }
-
- // Fetch all existing records from local database,
- // and skip those who are newer or not marked as synced.
-
- // XXX filter by status / ids in records
-
- const { data } = yield _this10.list({}, { includeDeleted: true });
- const existingById = data.reduce(function (acc, record) {
- acc[record.id] = record;
- return acc;
- }, {});
-
- const newRecords = records.filter(function (record) {
- const localRecord = existingById[record.id];
- const shouldKeep =
- // No local record with this id.
- localRecord === undefined ||
- // Or local record is synced
- localRecord._status === "synced" &&
- // And was synced from server
- localRecord.last_modified !== undefined &&
- // And is older than imported one.
- record.last_modified > localRecord.last_modified;
- return shouldKeep;
- });
-
- return yield _this10.db.loadDump(newRecords.map(markSynced));
- })();
- }
-}
-
-exports.default = Collection; /**
- * A Collection-oriented wrapper for an adapter's transaction.
- *
- * This defines the high-level functions available on a collection.
- * The collection itself offers functions of the same name. These will
- * perform just one operation in its own transaction.
- */
-
-class CollectionTransaction {
- constructor(collection, adapterTransaction) {
- this.collection = collection;
- this.adapterTransaction = adapterTransaction;
-
- this._events = [];
- }
-
- _queueEvent(action, payload) {
- this._events.push({ action, payload });
- }
-
- /**
- * Emit queued events, to be called once every transaction operations have
- * been executed successfully.
- */
- emitEvents() {
- for (let { action, payload } of this._events) {
- this.collection.events.emit(action, payload);
- }
- if (this._events.length > 0) {
- const targets = this._events.map(({ action, payload }) => (0, _extends3.default)({ action }, payload));
- this.collection.events.emit("change", { targets });
- }
- this._events = [];
- }
-
- /**
- * Retrieve a record by its id from the local database, or
- * undefined if none exists.
- *
- * This will also return virtually deleted records.
- *
- * @param {String} id
- * @return {Object}
- */
- getAny(id) {
- const record = this.adapterTransaction.get(id);
- return { data: record, permissions: {} };
- }
-
- /**
- * Retrieve a record by its id from the local database.
- *
- * Options:
- * - {Boolean} includeDeleted: Include virtually deleted records.
- *
- * @param {String} id
- * @param {Object} options
- * @return {Object}
- */
- get(id, options = { includeDeleted: false }) {
- const res = this.getAny(id);
- if (!res.data || !options.includeDeleted && res.data._status === "deleted") {
- throw new Error(`Record with id=${ id } not found.`);
- }
-
- return res;
- }
-
- /**
- * Deletes a record from the local database.
- *
- * Options:
- * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
- * update its `_status` attribute to `deleted` instead (default: true)
- *
- * @param {String} id The record's Id.
- * @param {Object} options The options object.
- * @return {Object}
- */
- delete(id, options = { virtual: true }) {
- // Ensure the record actually exists.
- const existing = this.adapterTransaction.get(id);
- const alreadyDeleted = existing && existing._status == "deleted";
- if (!existing || alreadyDeleted && options.virtual) {
- throw new Error(`Record with id=${ id } not found.`);
- }
- // Virtual updates status.
- if (options.virtual) {
- this.adapterTransaction.update(markDeleted(existing));
- } else {
- // Delete for real.
- this.adapterTransaction.delete(id);
- }
- this._queueEvent("delete", { data: existing });
- return { data: existing, permissions: {} };
- }
-
- /**
- * Deletes a record from the local database, if any exists.
- * Otherwise, do nothing.
- *
- * @param {String} id The record's Id.
- * @return {Object}
- */
- deleteAny(id) {
- const existing = this.adapterTransaction.get(id);
- if (existing) {
- this.adapterTransaction.update(markDeleted(existing));
- this._queueEvent("delete", { data: existing });
- }
- return { data: (0, _extends3.default)({ id }, existing), deleted: !!existing, permissions: {} };
- }
-
- /**
- * Adds a record to the local database, asserting that none
- * already exist with this ID.
- *
- * @param {Object} record, which must contain an ID
- * @return {Object}
- */
- create(record) {
- if (typeof record !== "object") {
- throw new Error("Record is not an object.");
- }
- if (!record.hasOwnProperty("id")) {
- throw new Error("Cannot create a record missing id");
- }
- if (!this.collection.idSchema.validate(record.id)) {
- throw new Error(`Invalid Id: ${ record.id }`);
- }
-
- this.adapterTransaction.create(record);
- this._queueEvent("create", { data: record });
- return { data: record, permissions: {} };
- }
-
- /**
- * Updates a record from the local database.
- *
- * Options:
- * - {Boolean} synced: Sets record status to "synced" (default: false)
- * - {Boolean} patch: Extends the existing record instead of overwriting it
- * (default: false)
- *
- * @param {Object} record
- * @param {Object} options
- * @return {Object}
- */
- update(record, options = { synced: false, patch: false }) {
- if (typeof record !== "object") {
- throw new Error("Record is not an object.");
- }
- if (!record.hasOwnProperty("id")) {
- throw new Error("Cannot update a record missing id.");
- }
- if (!this.collection.idSchema.validate(record.id)) {
- throw new Error(`Invalid Id: ${ record.id }`);
- }
-
- const oldRecord = this.adapterTransaction.get(record.id);
- if (!oldRecord) {
- throw new Error(`Record with id=${ record.id } not found.`);
- }
- const newRecord = options.patch ? (0, _extends3.default)({}, oldRecord, record) : record;
- const updated = this._updateRaw(oldRecord, newRecord, options);
- this.adapterTransaction.update(updated);
- this._queueEvent("update", { data: updated, oldRecord });
- return { data: updated, oldRecord, permissions: {} };
- }
-
- /**
- * Lower-level primitive for updating a record while respecting
- * _status and last_modified.
- *
- * @param {Object} oldRecord: the record retrieved from the DB
- * @param {Object} newRecord: the record to replace it with
- * @return {Object}
- */
- _updateRaw(oldRecord, newRecord, { synced = false } = {}) {
- const updated = (0, _extends3.default)({}, newRecord);
- // Make sure to never loose the existing timestamp.
- if (oldRecord && oldRecord.last_modified && !updated.last_modified) {
- updated.last_modified = oldRecord.last_modified;
- }
- // If only local fields have changed, then keep record as synced.
- // If status is created, keep record as created.
- // If status is deleted, mark as updated.
- const isIdentical = oldRecord && recordsEqual(oldRecord, updated, this.localFields);
- const keepSynced = isIdentical && oldRecord._status == "synced";
- const neverSynced = !oldRecord || oldRecord && oldRecord._status == "created";
- const newStatus = keepSynced || synced ? "synced" : neverSynced ? "created" : "updated";
- return markStatus(updated, newStatus);
- }
-
- /**
- * Upsert a record into the local database.
- *
- * This record must have an ID.
- *
- * If a record with this ID already exists, it will be replaced.
- * Otherwise, this record will be inserted.
- *
- * @param {Object} record
- * @return {Object}
- */
- upsert(record) {
- if (typeof record !== "object") {
- throw new Error("Record is not an object.");
- }
- if (!record.hasOwnProperty("id")) {
- throw new Error("Cannot update a record missing id.");
- }
- if (!this.collection.idSchema.validate(record.id)) {
- throw new Error(`Invalid Id: ${ record.id }`);
- }
- let oldRecord = this.adapterTransaction.get(record.id);
- const updated = this._updateRaw(oldRecord, record);
- this.adapterTransaction.update(updated);
- // Don't return deleted records -- pretend they are gone
- if (oldRecord && oldRecord._status == "deleted") {
- oldRecord = undefined;
- }
- if (oldRecord) {
- this._queueEvent("update", { data: updated, oldRecord });
- } else {
- this._queueEvent("create", { data: updated });
- }
- return { data: updated, oldRecord, permissions: {} };
- }
-}
-exports.CollectionTransaction = CollectionTransaction;
-
-},{"./adapters/IDB":84,"./adapters/base":85,"./utils":87,"babel-runtime/core-js/json/stringify":3,"babel-runtime/core-js/object/assign":4,"babel-runtime/core-js/promise":6,"babel-runtime/helpers/asyncToGenerator":7,"babel-runtime/helpers/extends":8,"uuid":9}],87:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.RE_UUID = undefined;
-
-var _promise = require("babel-runtime/core-js/promise");
-
-var _promise2 = _interopRequireDefault(_promise);
-
-var _keys = require("babel-runtime/core-js/object/keys");
-
-var _keys2 = _interopRequireDefault(_keys);
-
-exports.sortObjects = sortObjects;
-exports.filterObject = filterObject;
-exports.filterObjects = filterObjects;
-exports.isUUID = isUUID;
-exports.waterfall = waterfall;
-exports.deepEqual = deepEqual;
-exports.omitKeys = omitKeys;
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const RE_UUID = exports.RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
-
-/**
- * Checks if a value is undefined.
- * @param {Any} value
- * @return {Boolean}
- */
-function _isUndefined(value) {
- return typeof value === "undefined";
-}
-
-/**
- * Sorts records in a list according to a given ordering.
- *
- * @param {String} order The ordering, eg. `-last_modified`.
- * @param {Array} list The collection to order.
- * @return {Array}
- */
-function sortObjects(order, list) {
- const hasDash = order[0] === "-";
- const field = hasDash ? order.slice(1) : order;
- const direction = hasDash ? -1 : 1;
- return list.slice().sort((a, b) => {
- if (a[field] && _isUndefined(b[field])) {
- return direction;
- }
- if (b[field] && _isUndefined(a[field])) {
- return -direction;
- }
- if (_isUndefined(a[field]) && _isUndefined(b[field])) {
- return 0;
- }
- return a[field] > b[field] ? direction : -direction;
- });
-}
-
-/**
- * Test if a single object matches all given filters.
- *
- * @param {Object} filters The filters object.
- * @param {Object} entry The object to filter.
- * @return {Function}
- */
-function filterObject(filters, entry) {
- return (0, _keys2.default)(filters).every(filter => {
- const value = filters[filter];
- if (Array.isArray(value)) {
- return value.some(candidate => candidate === entry[filter]);
- }
- return entry[filter] === value;
- });
-}
-
-/**
- * Filters records in a list matching all given filters.
- *
- * @param {Object} filters The filters object.
- * @param {Array} list The collection to filter.
- * @return {Array}
- */
-function filterObjects(filters, list) {
- return list.filter(entry => {
- return filterObject(filters, entry);
- });
-}
-
-/**
- * Checks if a string is an UUID.
- *
- * @param {String} uuid The uuid to validate.
- * @return {Boolean}
- */
-function isUUID(uuid) {
- return RE_UUID.test(uuid);
-}
-
-/**
- * Resolves a list of functions sequentially, which can be sync or async; in
- * case of async, functions must return a promise.
- *
- * @param {Array} fns The list of functions.
- * @param {Any} init The initial value.
- * @return {Promise}
- */
-function waterfall(fns, init) {
- if (!fns.length) {
- return _promise2.default.resolve(init);
- }
- return fns.reduce((promise, nextFn) => {
- return promise.then(nextFn);
- }, _promise2.default.resolve(init));
-}
-
-/**
- * Simple deep object comparison function. This only supports comparison of
- * serializable JavaScript objects.
- *
- * @param {Object} a The source object.
- * @param {Object} b The compared object.
- * @return {Boolean}
- */
-function deepEqual(a, b) {
- if (a === b) {
- return true;
- }
- if (typeof a !== typeof b) {
- return false;
- }
- if (!(a && typeof a == "object") || !(b && typeof b == "object")) {
- return false;
- }
- if ((0, _keys2.default)(a).length !== (0, _keys2.default)(b).length) {
- return false;
- }
- for (let k in a) {
- if (!deepEqual(a[k], b[k])) {
- return false;
- }
- }
- return true;
-}
-
-/**
- * Return an object without the specified keys.
- *
- * @param {Object} obj The original object.
- * @param {Array} keys The list of keys to exclude.
- * @return {Object} A copy without the specified keys.
- */
-function omitKeys(obj, keys = []) {
- return (0, _keys2.default)(obj).reduce((acc, key) => {
- if (keys.indexOf(key) === -1) {
- acc[key] = obj[key];
- }
- return acc;
- }, {});
-}
-
-},{"babel-runtime/core-js/object/keys":5,"babel-runtime/core-js/promise":6}]},{},[2])(2)
-}); \ No newline at end of file
diff --git a/services/common/moz.build b/services/common/moz.build
index c09e6bed05..26a1bd9b46 100644
--- a/services/common/moz.build
+++ b/services/common/moz.build
@@ -15,10 +15,6 @@ EXTRA_COMPONENTS += [
EXTRA_JS_MODULES['services-common'] += [
'async.js',
- 'blocklist-clients.js',
- 'blocklist-updater.js',
- 'kinto-http-client.js',
- 'kinto-offline-client.js',
'logmanager.js',
'observers.js',
'rest.js',
diff --git a/services/common/tests/unit/test_blocklist_certificates.js b/services/common/tests/unit/test_blocklist_certificates.js
deleted file mode 100644
index e859703213..0000000000
--- a/services/common/tests/unit/test_blocklist_certificates.js
+++ /dev/null
@@ -1,224 +0,0 @@
-const { Constructor: CC } = Components;
-
-Cu.import("resource://testing-common/httpd.js");
-
-const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js");
-const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
-
-const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
- "nsIBinaryInputStream", "setInputStream");
-
-let server;
-
-// set up what we need to make storage adapters
-const Kinto = loadKinto();
-const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
-const kintoFilename = "kinto.sqlite";
-
-let kintoClient;
-
-function do_get_kinto_collection(collectionName) {
- if (!kintoClient) {
- let config = {
- // Set the remote to be some server that will cause test failure when
- // hit since we should never hit the server directly, only via maybeSync()
- remote: "https://firefox.settings.services.mozilla.com/v1/",
- // Set up the adapter and bucket as normal
- adapter: FirefoxAdapter,
- bucket: "blocklists"
- };
- kintoClient = new Kinto(config);
- }
- return kintoClient.collection(collectionName);
-}
-
-// Some simple tests to demonstrate that the logic inside maybeSync works
-// correctly and that simple kinto operations are working as expected. There
-// are more tests for core Kinto.js (and its storage adapter) in the
-// xpcshell tests under /services/common
-add_task(function* test_something(){
- const configPath = "/v1/";
- const recordsPath = "/v1/buckets/blocklists/collections/certificates/records";
-
- Services.prefs.setCharPref("services.settings.server",
- `http://localhost:${server.identity.primaryPort}/v1`);
-
- // register a handler
- function handleResponse (request, response) {
- try {
- const sample = getSampleResponse(request, server.identity.primaryPort);
- if (!sample) {
- do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
- }
-
- response.setStatusLine(null, sample.status.status,
- sample.status.statusText);
- // send the headers
- for (let headerLine of sample.sampleHeaders) {
- let headerElements = headerLine.split(':');
- response.setHeader(headerElements[0], headerElements[1].trimLeft());
- }
- response.setHeader("Date", (new Date()).toUTCString());
-
- response.write(sample.responseBody);
- } catch (e) {
- do_print(e);
- }
- }
- server.registerPathHandler(configPath, handleResponse);
- server.registerPathHandler(recordsPath, handleResponse);
-
- // Test an empty db populates
- let result = yield OneCRLBlocklistClient.maybeSync(2000, Date.now());
-
- // Open the collection, verify it's been populated:
- // Our test data has a single record; it should be in the local collection
- let collection = do_get_kinto_collection("certificates");
- yield collection.db.open();
- let list = yield collection.list();
- do_check_eq(list.data.length, 1);
- yield collection.db.close();
-
- // Test the db is updated when we call again with a later lastModified value
- result = yield OneCRLBlocklistClient.maybeSync(4000, Date.now());
-
- // Open the collection, verify it's been updated:
- // Our test data now has two records; both should be in the local collection
- collection = do_get_kinto_collection("certificates");
- yield collection.db.open();
- list = yield collection.list();
- do_check_eq(list.data.length, 3);
- yield collection.db.close();
-
- // Try to maybeSync with the current lastModified value - no connection
- // should be attempted.
- // Clear the kinto base pref so any connections will cause a test failure
- Services.prefs.clearUserPref("services.settings.server");
- yield OneCRLBlocklistClient.maybeSync(4000, Date.now());
-
- // Try again with a lastModified value at some point in the past
- yield OneCRLBlocklistClient.maybeSync(3000, Date.now());
-
- // Check the OneCRL check time pref is modified, even if the collection
- // hasn't changed
- Services.prefs.setIntPref("services.blocklist.onecrl.checked", 0);
- yield OneCRLBlocklistClient.maybeSync(3000, Date.now());
- let newValue = Services.prefs.getIntPref("services.blocklist.onecrl.checked");
- do_check_neq(newValue, 0);
-
- // Check that a sync completes even when there's bad data in the
- // collection. This will throw on fail, so just calling maybeSync is an
- // acceptible test.
- Services.prefs.setCharPref("services.settings.server",
- `http://localhost:${server.identity.primaryPort}/v1`);
- yield OneCRLBlocklistClient.maybeSync(5000, Date.now());
-});
-
-function run_test() {
- // Ensure that signature verification is disabled to prevent interference
- // with basic certificate sync tests
- Services.prefs.setBoolPref("services.blocklist.signing.enforced", false);
-
- // Set up an HTTP Server
- server = new HttpServer();
- server.start(-1);
-
- run_next_test();
-
- do_register_cleanup(function() {
- server.stop(() => { });
- });
-}
-
-// get a response for a given request from sample data
-function getSampleResponse(req, port) {
- const responses = {
- "OPTIONS": {
- "sampleHeaders": [
- "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
- "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
- "Access-Control-Allow-Origin: *",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress"
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": "null"
- },
- "GET:/v1/?": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress"
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
- },
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"3000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "issuerName": "MEQxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwx0aGF3dGUsIEluYy4xHjAcBgNVBAMTFXRoYXd0ZSBFViBTU0wgQ0EgLSBHMw==",
- "serialNumber":"CrTHPEE6AZSfI3jysin2bA==",
- "id":"78cf8900-fdea-4ce5-f8fb-b78710617718",
- "last_modified":3000
- }]})
- },
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"4000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "issuerName":"MFkxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKjAoBgNVBAMTIVN0YWF0IGRlciBOZWRlcmxhbmRlbiBPdmVyaGVpZCBDQQ",
- "serialNumber":"ATFpsA==",
- "id":"dabafde9-df4a-ddba-2548-748da04cc02c",
- "last_modified":4000
- },{
- "subject":"MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5",
- "pubKeyHash":"VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=",
- "id":"dabafde9-df4a-ddba-2548-748da04cc02d",
- "last_modified":4000
- }]})
- },
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"5000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "issuerName":"not a base64 encoded issuer",
- "serialNumber":"not a base64 encoded serial",
- "id":"dabafde9-df4a-ddba-2548-748da04cc02e",
- "last_modified":5000
- },{
- "subject":"not a base64 encoded subject",
- "pubKeyHash":"not a base64 encoded pubKeyHash",
- "id":"dabafde9-df4a-ddba-2548-748da04cc02f",
- "last_modified":5000
- },{
- "subject":"MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5",
- "pubKeyHash":"VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=",
- "id":"dabafde9-df4a-ddba-2548-748da04cc02g",
- "last_modified":5000
- }]})
- }
- };
- return responses[`${req.method}:${req.path}?${req.queryString}`] ||
- responses[req.method];
-
-}
diff --git a/services/common/tests/unit/test_blocklist_clients.js b/services/common/tests/unit/test_blocklist_clients.js
deleted file mode 100644
index 121fac926b..0000000000
--- a/services/common/tests/unit/test_blocklist_clients.js
+++ /dev/null
@@ -1,412 +0,0 @@
-const { Constructor: CC } = Components;
-
-const KEY_PROFILEDIR = "ProfD";
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://testing-common/httpd.js");
-Cu.import("resource://gre/modules/Timer.jsm");
-const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm");
-const { OS } = Cu.import("resource://gre/modules/osfile.jsm");
-
-const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
-const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js");
-
-const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
- "nsIBinaryInputStream", "setInputStream");
-
-const gBlocklistClients = [
- {client: BlocklistClients.AddonBlocklistClient, filename: BlocklistClients.FILENAME_ADDONS_JSON, testData: ["i808","i720", "i539"]},
- {client: BlocklistClients.PluginBlocklistClient, filename: BlocklistClients.FILENAME_PLUGINS_JSON, testData: ["p1044","p32","p28"]},
- {client: BlocklistClients.GfxBlocklistClient, filename: BlocklistClients.FILENAME_GFX_JSON, testData: ["g204","g200","g36"]},
-];
-
-
-let server;
-let kintoClient;
-
-function kintoCollection(collectionName) {
- if (!kintoClient) {
- const Kinto = loadKinto();
- const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
- const config = {
- // Set the remote to be some server that will cause test failure when
- // hit since we should never hit the server directly, only via maybeSync()
- remote: "https://firefox.settings.services.mozilla.com/v1/",
- adapter: FirefoxAdapter,
- bucket: "blocklists"
- };
- kintoClient = new Kinto(config);
- }
- return kintoClient.collection(collectionName);
-}
-
-function* readJSON(filepath) {
- const binaryData = yield OS.File.read(filepath);
- const textData = (new TextDecoder()).decode(binaryData);
- return Promise.resolve(JSON.parse(textData));
-}
-
-function* clear_state() {
- for (let {client} of gBlocklistClients) {
- // Remove last server times.
- Services.prefs.clearUserPref(client.lastCheckTimePref);
-
- // Clear local DB.
- const collection = kintoCollection(client.collectionName);
- try {
- yield collection.db.open();
- yield collection.clear();
- } finally {
- yield collection.db.close();
- }
- }
-
- // Remove profile data.
- for (let {filename} of gBlocklistClients) {
- const blocklist = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
- if (blocklist.exists()) {
- blocklist.remove(true);
- }
- }
-}
-
-
-function run_test() {
- // Set up an HTTP Server
- server = new HttpServer();
- server.start(-1);
-
- // Point the blocklist clients to use this local HTTP server.
- Services.prefs.setCharPref("services.settings.server",
- `http://localhost:${server.identity.primaryPort}/v1`);
-
- // Setup server fake responses.
- function handleResponse(request, response) {
- try {
- const sample = getSampleResponse(request, server.identity.primaryPort);
- if (!sample) {
- do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
- }
-
- response.setStatusLine(null, sample.status.status,
- sample.status.statusText);
- // send the headers
- for (let headerLine of sample.sampleHeaders) {
- let headerElements = headerLine.split(':');
- response.setHeader(headerElements[0], headerElements[1].trimLeft());
- }
- response.setHeader("Date", (new Date()).toUTCString());
-
- response.write(sample.responseBody);
- response.finish();
- } catch (e) {
- do_print(e);
- }
- }
- const configPath = "/v1/";
- const addonsRecordsPath = "/v1/buckets/blocklists/collections/addons/records";
- const gfxRecordsPath = "/v1/buckets/blocklists/collections/gfx/records";
- const pluginsRecordsPath = "/v1/buckets/blocklists/collections/plugins/records";
- server.registerPathHandler(configPath, handleResponse);
- server.registerPathHandler(addonsRecordsPath, handleResponse);
- server.registerPathHandler(gfxRecordsPath, handleResponse);
- server.registerPathHandler(pluginsRecordsPath, handleResponse);
-
-
- run_next_test();
-
- do_register_cleanup(function() {
- server.stop(() => { });
- });
-}
-
-add_task(function* test_records_obtained_from_server_are_stored_in_db(){
- for (let {client} of gBlocklistClients) {
- // Test an empty db populates
- let result = yield client.maybeSync(2000, Date.now());
-
- // Open the collection, verify it's been populated:
- // Our test data has a single record; it should be in the local collection
- let collection = kintoCollection(client.collectionName);
- yield collection.db.open();
- let list = yield collection.list();
- equal(list.data.length, 1);
- yield collection.db.close();
- }
-});
-add_task(clear_state);
-
-add_task(function* test_list_is_written_to_file_in_profile(){
- for (let {client, filename, testData} of gBlocklistClients) {
- const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
- strictEqual(profFile.exists(), false);
-
- let result = yield client.maybeSync(2000, Date.now());
-
- strictEqual(profFile.exists(), true);
- const content = yield readJSON(profFile.path);
- equal(content.data[0].blockID, testData[testData.length - 1]);
- }
-});
-add_task(clear_state);
-
-add_task(function* test_current_server_time_is_saved_in_pref(){
- for (let {client} of gBlocklistClients) {
- const before = Services.prefs.getIntPref(client.lastCheckTimePref);
- const serverTime = Date.now();
- yield client.maybeSync(2000, serverTime);
- const after = Services.prefs.getIntPref(client.lastCheckTimePref);
- equal(after, Math.round(serverTime / 1000));
- }
-});
-add_task(clear_state);
-
-add_task(function* test_update_json_file_when_addons_has_changes(){
- for (let {client, filename, testData} of gBlocklistClients) {
- yield client.maybeSync(2000, Date.now() - 1000);
- const before = Services.prefs.getIntPref(client.lastCheckTimePref);
- const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
- const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
- const serverTime = Date.now();
-
- yield client.maybeSync(3001, serverTime);
-
- // File was updated.
- notEqual(fileLastModified, profFile.lastModifiedTime);
- const content = yield readJSON(profFile.path);
- deepEqual(content.data.map((r) => r.blockID), testData);
- // Server time was updated.
- const after = Services.prefs.getIntPref(client.lastCheckTimePref);
- equal(after, Math.round(serverTime / 1000));
- }
-});
-add_task(clear_state);
-
-add_task(function* test_sends_reload_message_when_blocklist_has_changes(){
- for (let {client, filename} of gBlocklistClients) {
- let received = yield new Promise((resolve, reject) => {
- Services.ppmm.addMessageListener("Blocklist:reload-from-disk", {
- receiveMessage(aMsg) { resolve(aMsg) }
- });
-
- client.maybeSync(2000, Date.now() - 1000);
- });
-
- equal(received.data.filename, filename);
- }
-});
-add_task(clear_state);
-
-add_task(function* test_do_nothing_when_blocklist_is_up_to_date(){
- for (let {client, filename} of gBlocklistClients) {
- yield client.maybeSync(2000, Date.now() - 1000);
- const before = Services.prefs.getIntPref(client.lastCheckTimePref);
- const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
- const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
- const serverTime = Date.now();
-
- yield client.maybeSync(3000, serverTime);
-
- // File was not updated.
- equal(fileLastModified, profFile.lastModifiedTime);
- // Server time was updated.
- const after = Services.prefs.getIntPref(client.lastCheckTimePref);
- equal(after, Math.round(serverTime / 1000));
- }
-});
-add_task(clear_state);
-
-
-
-// get a response for a given request from sample data
-function getSampleResponse(req, port) {
- const responses = {
- "OPTIONS": {
- "sampleHeaders": [
- "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
- "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
- "Access-Control-Allow-Origin: *",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress"
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": "null"
- },
- "GET:/v1/?": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress"
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
- },
- "GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"3000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "prefs": [],
- "blockID": "i539",
- "last_modified": 3000,
- "versionRange": [{
- "targetApplication": [],
- "maxVersion": "*",
- "minVersion": "0",
- "severity": "1"
- }],
- "guid": "ScorpionSaver@jetpack",
- "id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
- }]})
- },
- "GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"3000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "matchFilename": "NPFFAddOn.dll",
- "blockID": "p28",
- "id": "7b1e0b3c-e390-a817-11b6-a6887f65f56e",
- "last_modified": 3000,
- "versionRange": []
- }]})
- },
- "GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"3000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "driverVersionComparator": "LESS_THAN_OR_EQUAL",
- "driverVersion": "8.17.12.5896",
- "vendor": "0x10de",
- "blockID": "g36",
- "feature": "DIRECT3D_9_LAYERS",
- "devices": ["0x0a6c"],
- "featureStatus": "BLOCKED_DRIVER_VERSION",
- "last_modified": 3000,
- "os": "WINNT 6.1",
- "id": "3f947f16-37c2-4e96-d356-78b26363729b"
- }]})
- },
- "GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified&_since=3000": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"4000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "prefs": [],
- "blockID": "i808",
- "last_modified": 4000,
- "versionRange": [{
- "targetApplication": [],
- "maxVersion": "*",
- "minVersion": "0",
- "severity": "3"
- }],
- "guid": "{c96d1ae6-c4cf-4984-b110-f5f561b33b5a}",
- "id": "9ccfac91-e463-c30c-f0bd-14143794a8dd"
- }, {
- "prefs": ["browser.startup.homepage"],
- "blockID": "i720",
- "last_modified": 3500,
- "versionRange": [{
- "targetApplication": [],
- "maxVersion": "*",
- "minVersion": "0",
- "severity": "1"
- }],
- "guid": "FXqG@xeeR.net",
- "id": "cf9b3129-a97e-dbd7-9525-a8575ac03c25"
- }]})
- },
- "GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified&_since=3000": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"4000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "infoURL": "https://get.adobe.com/flashplayer/",
- "blockID": "p1044",
- "matchFilename": "libflashplayer\\.so",
- "last_modified": 4000,
- "versionRange": [{
- "targetApplication": [],
- "minVersion": "11.2.202.509",
- "maxVersion": "11.2.202.539",
- "severity": "0",
- "vulnerabilityStatus": "1"
- }],
- "os": "Linux",
- "id": "aabad965-e556-ffe7-4191-074f5dee3df3"
- }, {
- "matchFilename": "npViewpoint.dll",
- "blockID": "p32",
- "id": "1f48af42-c508-b8ef-b8d5-609d48e4f6c9",
- "last_modified": 3500,
- "versionRange": [{
- "targetApplication": [{
- "minVersion": "3.0",
- "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
- "maxVersion": "*"
- }]
- }]
- }]})
- },
- "GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified&_since=3000": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"4000\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{
- "vendor": "0x8086",
- "blockID": "g204",
- "feature": "WEBGL_MSAA",
- "devices": [],
- "id": "c96bca82-e6bd-044d-14c4-9c1d67e9283a",
- "last_modified": 4000,
- "os": "Darwin 10",
- "featureStatus": "BLOCKED_DEVICE"
- }, {
- "vendor": "0x10de",
- "blockID": "g200",
- "feature": "WEBGL_MSAA",
- "devices": [],
- "id": "c3a15ba9-e0e2-421f-e399-c995e5b8d14e",
- "last_modified": 3500,
- "os": "Darwin 11",
- "featureStatus": "BLOCKED_DEVICE"
- }]})
- }
- };
- return responses[`${req.method}:${req.path}?${req.queryString}`] ||
- responses[req.method];
-
-}
diff --git a/services/common/tests/unit/test_blocklist_signatures.js b/services/common/tests/unit/test_blocklist_signatures.js
deleted file mode 100644
index b2ee1019a6..0000000000
--- a/services/common/tests/unit/test_blocklist_signatures.js
+++ /dev/null
@@ -1,510 +0,0 @@
-"use strict";
-
-Cu.import("resource://services-common/blocklist-updater.js");
-Cu.import("resource://testing-common/httpd.js");
-
-const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
-const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
-const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js");
-
-let server;
-
-const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
-const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced";
-const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
-const PREF_SETTINGS_SERVER = "services.settings.server";
-const PREF_SIGNATURE_ROOT = "security.content.signature.root_hash";
-
-
-const CERT_DIR = "test_blocklist_signatures/";
-const CHAIN_FILES =
- ["collection_signing_ee.pem",
- "collection_signing_int.pem",
- "collection_signing_root.pem"];
-
-function getFileData(file) {
- const stream = Cc["@mozilla.org/network/file-input-stream;1"]
- .createInstance(Ci.nsIFileInputStream);
- stream.init(file, -1, 0, 0);
- const data = NetUtil.readInputStreamToString(stream, stream.available());
- stream.close();
- return data;
-}
-
-function setRoot() {
- const filename = CERT_DIR + CHAIN_FILES[0];
-
- const certFile = do_get_file(filename, false);
- const b64cert = getFileData(certFile)
- .replace(/-----BEGIN CERTIFICATE-----/, "")
- .replace(/-----END CERTIFICATE-----/, "")
- .replace(/[\r\n]/g, "");
- const certdb = Cc["@mozilla.org/security/x509certdb;1"]
- .getService(Ci.nsIX509CertDB);
- const cert = certdb.constructX509FromBase64(b64cert);
- Services.prefs.setCharPref(PREF_SIGNATURE_ROOT, cert.sha256Fingerprint);
-}
-
-function getCertChain() {
- const chain = [];
- for (let file of CHAIN_FILES) {
- chain.push(getFileData(do_get_file(CERT_DIR + file)));
- }
- return chain.join("\n");
-}
-
-function* checkRecordCount(count) {
- // open the collection manually
- const base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
- const bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
- const collectionName =
- Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION);
-
- const Kinto = loadKinto();
-
- const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
-
- const config = {
- remote: base,
- bucket: bucket,
- adapter: FirefoxAdapter,
- };
-
- const db = new Kinto(config);
- const collection = db.collection(collectionName);
-
- yield collection.db.open();
-
- // Check we have the expected number of records
- let records = yield collection.list();
- do_check_eq(count, records.data.length);
-
- // Close the collection so the test can exit cleanly
- yield collection.db.close();
-}
-
-// Check to ensure maybeSync is called with correct values when a changes
-// document contains information on when a collection was last modified
-add_task(function* test_check_signatures(){
- const port = server.identity.primaryPort;
-
- // a response to give the client when the cert chain is expected
- function makeMetaResponseBody(lastModified, signature) {
- return {
- data: {
- id: "certificates",
- last_modified: lastModified,
- signature: {
- x5u: `http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem`,
- public_key: "fake",
- "content-signature": `x5u=http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem;p384ecdsa=${signature}`,
- signature_encoding: "rs_base64url",
- signature: signature,
- hash_algorithm: "sha384",
- ref: "1yryrnmzou5rf31ou80znpnq8n"
- }
- }
- };
- }
-
- function makeMetaResponse(eTag, body, comment) {
- return {
- comment: comment,
- sampleHeaders: [
- "Content-Type: application/json; charset=UTF-8",
- `ETag: \"${eTag}\"`
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: JSON.stringify(body)
- };
- }
-
- function registerHandlers(responses){
- function handleResponse (serverTimeMillis, request, response) {
- const key = `${request.method}:${request.path}?${request.queryString}`;
- const available = responses[key];
- const sampled = available.length > 1 ? available.shift() : available[0];
-
- if (!sampled) {
- do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
- }
-
- response.setStatusLine(null, sampled.status.status,
- sampled.status.statusText);
- // send the headers
- for (let headerLine of sampled.sampleHeaders) {
- let headerElements = headerLine.split(':');
- response.setHeader(headerElements[0], headerElements[1].trimLeft());
- }
-
- // set the server date
- response.setHeader("Date", (new Date(serverTimeMillis)).toUTCString());
-
- response.write(sampled.responseBody);
- }
-
- for (let key of Object.keys(responses)) {
- const keyParts = key.split(":");
- const method = keyParts[0];
- const valueParts = keyParts[1].split("?");
- const path = valueParts[0];
-
- server.registerPathHandler(path, handleResponse.bind(null, 2000));
- }
- }
-
- // First, perform a signature verification with known data and signature
- // to ensure things are working correctly
- let verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
- .createInstance(Ci.nsIContentSignatureVerifier);
-
- const emptyData = '[]';
- const emptySignature = "p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9";
- const name = "onecrl.content-signature.mozilla.org";
- ok(verifier.verifyContentSignature(emptyData, emptySignature,
- getCertChain(), name));
-
- verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
- .createInstance(Ci.nsIContentSignatureVerifier);
-
- const collectionData = '[{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:43:37Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"97fbf7c4-3ef2-f54f-0029-1ba6540c63ea","issuerName":"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==","last_modified":2000,"serialNumber":"BAAAAAABA/A35EU="},{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:48:11Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc","issuerName":"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB","last_modified":3000,"serialNumber":"BAAAAAABI54PryQ="}]';
- const collectionSignature = "p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p";
-
- ok(verifier.verifyContentSignature(collectionData, collectionSignature, getCertChain(), name));
-
- // set up prefs so the kinto updater talks to the test server
- Services.prefs.setCharPref(PREF_SETTINGS_SERVER,
- `http://localhost:${server.identity.primaryPort}/v1`);
-
- // Set up some data we need for our test
- let startTime = Date.now();
-
- // These are records we'll use in the test collections
- const RECORD1 = {
- details: {
- bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
- created: "2016-01-18T14:43:37Z",
- name: "GlobalSign certs",
- who: ".",
- why: "."
- },
- enabled: true,
- id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
- issuerName: "MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==",
- last_modified: 2000,
- serialNumber: "BAAAAAABA/A35EU="
- };
-
- const RECORD2 = {
- details: {
- bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
- created: "2016-01-18T14:48:11Z",
- name: "GlobalSign certs",
- who: ".",
- why: "."
- },
- enabled: true,
- id: "e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc",
- issuerName: "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
- last_modified: 3000,
- serialNumber: "BAAAAAABI54PryQ="
- };
-
- const RECORD3 = {
- details: {
- bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
- created: "2016-01-18T14:48:11Z",
- name: "GlobalSign certs",
- who: ".",
- why: "."
- },
- enabled: true,
- id: "c7c49b69-a4ab-418e-92a9-e1961459aa7f",
- issuerName: "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
- last_modified: 4000,
- serialNumber: "BAAAAAABI54PryQ="
- };
-
- const RECORD1_DELETION = {
- deleted: true,
- enabled: true,
- id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
- last_modified: 3500,
- };
-
- // Check that a signature on an empty collection is OK
- // We need to set up paths on the HTTP server to return specific data from
- // specific paths for each test. Here we prepare data for each response.
-
- // A cert chain response (this the cert chain that contains the signing
- // cert, the root and any intermediates in between). This is used in each
- // sync.
- const RESPONSE_CERT_CHAIN = {
- comment: "RESPONSE_CERT_CHAIN",
- sampleHeaders: [
- "Content-Type: text/plain; charset=UTF-8"
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: getCertChain()
- };
-
- // A server settings response. This is used in each sync.
- const RESPONSE_SERVER_SETTINGS = {
- comment: "RESPONSE_SERVER_SETTINGS",
- sampleHeaders: [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress"
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
- };
-
- // This is the initial, empty state of the collection. This is only used
- // for the first sync.
- const RESPONSE_EMPTY_INITIAL = {
- comment: "RESPONSE_EMPTY_INITIAL",
- sampleHeaders: [
- "Content-Type: application/json; charset=UTF-8",
- "ETag: \"1000\""
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: JSON.stringify({"data": []})
- };
-
- const RESPONSE_BODY_META_EMPTY_SIG = makeMetaResponseBody(1000,
- "vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u");
-
- // The collection metadata containing the signature for the empty
- // collection.
- const RESPONSE_META_EMPTY_SIG =
- makeMetaResponse(1000, RESPONSE_BODY_META_EMPTY_SIG,
- "RESPONSE_META_EMPTY_SIG");
-
- // Here, we map request method and path to the available responses
- const emptyCollectionResponses = {
- "GET:/test_blocklist_signatures/test_cert_chain.pem?":[RESPONSE_CERT_CHAIN],
- "GET:/v1/?": [RESPONSE_SERVER_SETTINGS],
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified":
- [RESPONSE_EMPTY_INITIAL],
- "GET:/v1/buckets/blocklists/collections/certificates?":
- [RESPONSE_META_EMPTY_SIG]
- };
-
- // .. and use this map to register handlers for each path
- registerHandlers(emptyCollectionResponses);
-
- // With all of this set up, we attempt a sync. This will resolve if all is
- // well and throw if something goes wrong.
- yield OneCRLBlocklistClient.maybeSync(1000, startTime);
-
- // Check that some additions (2 records) to the collection have a valid
- // signature.
-
- // This response adds two entries (RECORD1 and RECORD2) to the collection
- const RESPONSE_TWO_ADDED = {
- comment: "RESPONSE_TWO_ADDED",
- sampleHeaders: [
- "Content-Type: application/json; charset=UTF-8",
- "ETag: \"3000\""
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: JSON.stringify({"data": [RECORD2, RECORD1]})
- };
-
- const RESPONSE_BODY_META_TWO_ITEMS_SIG = makeMetaResponseBody(3000,
- "dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy");
-
- // A signature response for the collection containg RECORD1 and RECORD2
- const RESPONSE_META_TWO_ITEMS_SIG =
- makeMetaResponse(3000, RESPONSE_BODY_META_TWO_ITEMS_SIG,
- "RESPONSE_META_TWO_ITEMS_SIG");
-
- const twoItemsResponses = {
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=1000":
- [RESPONSE_TWO_ADDED],
- "GET:/v1/buckets/blocklists/collections/certificates?":
- [RESPONSE_META_TWO_ITEMS_SIG]
- };
- registerHandlers(twoItemsResponses);
- yield OneCRLBlocklistClient.maybeSync(3000, startTime);
-
- // Check the collection with one addition and one removal has a valid
- // signature
-
- // Remove RECORD1, add RECORD3
- const RESPONSE_ONE_ADDED_ONE_REMOVED = {
- comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ",
- sampleHeaders: [
- "Content-Type: application/json; charset=UTF-8",
- "ETag: \"4000\""
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: JSON.stringify({"data": [RECORD3, RECORD1_DELETION]})
- };
-
- const RESPONSE_BODY_META_THREE_ITEMS_SIG = makeMetaResponseBody(4000,
- "MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw");
-
- // signature response for the collection containing RECORD2 and RECORD3
- const RESPONSE_META_THREE_ITEMS_SIG =
- makeMetaResponse(4000, RESPONSE_BODY_META_THREE_ITEMS_SIG,
- "RESPONSE_META_THREE_ITEMS_SIG");
-
- const oneAddedOneRemovedResponses = {
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000":
- [RESPONSE_ONE_ADDED_ONE_REMOVED],
- "GET:/v1/buckets/blocklists/collections/certificates?":
- [RESPONSE_META_THREE_ITEMS_SIG]
- };
- registerHandlers(oneAddedOneRemovedResponses);
- yield OneCRLBlocklistClient.maybeSync(4000, startTime);
-
- // Check the signature is still valid with no operation (no changes)
-
- // Leave the collection unchanged
- const RESPONSE_EMPTY_NO_UPDATE = {
- comment: "RESPONSE_EMPTY_NO_UPDATE ",
- sampleHeaders: [
- "Content-Type: application/json; charset=UTF-8",
- "ETag: \"4000\""
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: JSON.stringify({"data": []})
- };
-
- const noOpResponses = {
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
- [RESPONSE_EMPTY_NO_UPDATE],
- "GET:/v1/buckets/blocklists/collections/certificates?":
- [RESPONSE_META_THREE_ITEMS_SIG]
- };
- registerHandlers(noOpResponses);
- yield OneCRLBlocklistClient.maybeSync(4100, startTime);
-
- // Check the collection is reset when the signature is invalid
-
- // Prepare a (deliberately) bad signature to check the collection state is
- // reset if something is inconsistent
- const RESPONSE_COMPLETE_INITIAL = {
- comment: "RESPONSE_COMPLETE_INITIAL ",
- sampleHeaders: [
- "Content-Type: application/json; charset=UTF-8",
- "ETag: \"4000\""
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: JSON.stringify({"data": [RECORD2, RECORD3]})
- };
-
- const RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID = {
- comment: "RESPONSE_COMPLETE_INITIAL ",
- sampleHeaders: [
- "Content-Type: application/json; charset=UTF-8",
- "ETag: \"4000\""
- ],
- status: {status: 200, statusText: "OK"},
- responseBody: JSON.stringify({"data": [RECORD3, RECORD2]})
- };
-
- const RESPONSE_BODY_META_BAD_SIG = makeMetaResponseBody(4000,
- "aW52YWxpZCBzaWduYXR1cmUK");
-
- const RESPONSE_META_BAD_SIG =
- makeMetaResponse(4000, RESPONSE_BODY_META_BAD_SIG, "RESPONSE_META_BAD_SIG");
-
- const badSigGoodSigResponses = {
- // In this test, we deliberately serve a bad signature initially. The
- // subsequent signature returned is a valid one for the three item
- // collection.
- "GET:/v1/buckets/blocklists/collections/certificates?":
- [RESPONSE_META_BAD_SIG, RESPONSE_META_THREE_ITEMS_SIG],
- // The first collection state is the three item collection (since
- // there's a sync with no updates) - but, since the signature is wrong,
- // another request will be made...
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
- [RESPONSE_EMPTY_NO_UPDATE],
- // The next request is for the full collection. This will be checked
- // against the valid signature - so the sync should succeed.
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified":
- [RESPONSE_COMPLETE_INITIAL],
- // The next request is for the full collection sorted by id. This will be
- // checked against the valid signature - so the sync should succeed.
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
- [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
- };
-
- registerHandlers(badSigGoodSigResponses);
- yield OneCRLBlocklistClient.maybeSync(5000, startTime);
-
- const badSigGoodOldResponses = {
- // In this test, we deliberately serve a bad signature initially. The
- // subsequent sitnature returned is a valid one for the three item
- // collection.
- "GET:/v1/buckets/blocklists/collections/certificates?":
- [RESPONSE_META_BAD_SIG, RESPONSE_META_EMPTY_SIG],
- // The first collection state is the current state (since there's no update
- // - but, since the signature is wrong, another request will be made)
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
- [RESPONSE_EMPTY_NO_UPDATE],
- // The next request is for the full collection sorted by id. This will be
- // checked against the valid signature and last_modified times will be
- // compared. Sync should fail, even though the signature is good,
- // because the local collection is newer.
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
- [RESPONSE_EMPTY_INITIAL],
- };
-
- // ensure our collection hasn't been replaced with an older, empty one
- yield checkRecordCount(2);
-
- registerHandlers(badSigGoodOldResponses);
- yield OneCRLBlocklistClient.maybeSync(5000, startTime);
-
- const allBadSigResponses = {
- // In this test, we deliberately serve only a bad signature.
- "GET:/v1/buckets/blocklists/collections/certificates?":
- [RESPONSE_META_BAD_SIG],
- // The first collection state is the three item collection (since
- // there's a sync with no updates) - but, since the signature is wrong,
- // another request will be made...
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
- [RESPONSE_EMPTY_NO_UPDATE],
- // The next request is for the full collection sorted by id. This will be
- // checked against the valid signature - so the sync should succeed.
- "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
- [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
- };
-
- registerHandlers(allBadSigResponses);
- try {
- yield OneCRLBlocklistClient.maybeSync(6000, startTime);
- do_throw("Sync should fail (the signature is intentionally bad)");
- } catch (e) {
- yield checkRecordCount(2);
- }
-});
-
-function run_test() {
- // ensure signatures are enforced
- Services.prefs.setBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING, true);
-
- // get a signature verifier to ensure nsNSSComponent is initialized
- Cc["@mozilla.org/security/contentsignatureverifier;1"]
- .createInstance(Ci.nsIContentSignatureVerifier);
-
- // set the content signing root to our test root
- setRoot();
-
- // Set up an HTTP Server
- server = new HttpServer();
- server.start(-1);
-
- run_next_test();
-
- do_register_cleanup(function() {
- server.stop(function() { });
- });
-}
-
-
diff --git a/services/common/tests/unit/test_blocklist_updater.js b/services/common/tests/unit/test_blocklist_updater.js
deleted file mode 100644
index 1b71c194a0..0000000000
--- a/services/common/tests/unit/test_blocklist_updater.js
+++ /dev/null
@@ -1,173 +0,0 @@
-Cu.import("resource://testing-common/httpd.js");
-
-var server;
-
-const PREF_SETTINGS_SERVER = "services.settings.server";
-const PREF_LAST_UPDATE = "services.blocklist.last_update_seconds";
-const PREF_LAST_ETAG = "services.blocklist.last_etag";
-const PREF_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
-
-// Check to ensure maybeSync is called with correct values when a changes
-// document contains information on when a collection was last modified
-add_task(function* test_check_maybeSync(){
- const changesPath = "/v1/buckets/monitor/collections/changes/records";
-
- // register a handler
- function handleResponse (serverTimeMillis, request, response) {
- try {
- const sampled = getSampleResponse(request, server.identity.primaryPort);
- if (!sampled) {
- do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
- }
-
- response.setStatusLine(null, sampled.status.status,
- sampled.status.statusText);
- // send the headers
- for (let headerLine of sampled.sampleHeaders) {
- let headerElements = headerLine.split(':');
- response.setHeader(headerElements[0], headerElements[1].trimLeft());
- }
-
- // set the server date
- response.setHeader("Date", (new Date(serverTimeMillis)).toUTCString());
-
- response.write(sampled.responseBody);
- } catch (e) {
- dump(`${e}\n`);
- }
- }
-
- server.registerPathHandler(changesPath, handleResponse.bind(null, 2000));
-
- // set up prefs so the kinto updater talks to the test server
- Services.prefs.setCharPref(PREF_SETTINGS_SERVER,
- `http://localhost:${server.identity.primaryPort}/v1`);
-
- // set some initial values so we can check these are updated appropriately
- Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
- Services.prefs.setIntPref(PREF_CLOCK_SKEW_SECONDS, 0);
- Services.prefs.clearUserPref(PREF_LAST_ETAG);
-
-
- let startTime = Date.now();
-
- let updater = Cu.import("resource://services-common/blocklist-updater.js");
-
- let syncPromise = new Promise(function(resolve, reject) {
- // add a test kinto client that will respond to lastModified information
- // for a collection called 'test-collection'
- updater.addTestBlocklistClient("test-collection", {
- maybeSync(lastModified, serverTime) {
- do_check_eq(lastModified, 1000);
- do_check_eq(serverTime, 2000);
- resolve();
- }
- });
- updater.checkVersions();
- });
-
- // ensure we get the maybeSync call
- yield syncPromise;
-
- // check the last_update is updated
- do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
-
- // How does the clock difference look?
- let endTime = Date.now();
- let clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
- // we previously set the serverTime to 2 (seconds past epoch)
- do_check_true(clockDifference <= endTime / 1000
- && clockDifference >= Math.floor(startTime / 1000) - 2);
- // Last timestamp was saved. An ETag header value is a quoted string.
- let lastEtag = Services.prefs.getCharPref(PREF_LAST_ETAG);
- do_check_eq(lastEtag, "\"1100\"");
-
- // Simulate a poll with up-to-date collection.
- Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
- // If server has no change, a 304 is received, maybeSync() is not called.
- updater.addTestBlocklistClient("test-collection", {
- maybeSync: () => {throw new Error("Should not be called");}
- });
- yield updater.checkVersions();
- // Last update is overwritten
- do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
-
-
- // Simulate a server error.
- function simulateErrorResponse (request, response) {
- response.setHeader("Date", (new Date(3000)).toUTCString());
- response.setHeader("Content-Type", "application/json; charset=UTF-8");
- response.write(JSON.stringify({
- code: 503,
- errno: 999,
- error: "Service Unavailable",
- }));
- response.setStatusLine(null, 503, "Service Unavailable");
- }
- server.registerPathHandler(changesPath, simulateErrorResponse);
- // checkVersions() fails with adequate error.
- let error;
- try {
- yield updater.checkVersions();
- } catch (e) {
- error = e;
- }
- do_check_eq(error.message, "Polling for changes failed.");
- // When an error occurs, last update was not overwritten (see Date header above).
- do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
-
- // check negative clock skew times
-
- // set to a time in the future
- server.registerPathHandler(changesPath, handleResponse.bind(null, Date.now() + 10000));
-
- yield updater.checkVersions();
-
- clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
- // we previously set the serverTime to Date.now() + 10000 ms past epoch
- do_check_true(clockDifference <= 0 && clockDifference >= -10);
-});
-
-function run_test() {
- // Set up an HTTP Server
- server = new HttpServer();
- server.start(-1);
-
- run_next_test();
-
- do_register_cleanup(function() {
- server.stop(function() { });
- });
-}
-
-// get a response for a given request from sample data
-function getSampleResponse(req, port) {
- const responses = {
- "GET:/v1/buckets/monitor/collections/changes/records?": {
- "sampleHeaders": [
- "Content-Type: application/json; charset=UTF-8",
- "ETag: \"1100\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data": [{
- "host": "localhost",
- "last_modified": 1100,
- "bucket": "blocklists:aurora",
- "id": "330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a",
- "collection": "test-collection"
- }, {
- "host": "localhost",
- "last_modified": 1000,
- "bucket": "blocklists",
- "id": "254cbb9e-6888-4d9f-8e60-58b74faa8778",
- "collection": "test-collection"
- }]})
- }
- };
-
- if (req.hasHeader("if-none-match") && req.getHeader("if-none-match", "") == "\"1100\"")
- return {sampleHeaders: [], status: {status: 304, statusText: "Not Modified"}, responseBody: ""};
-
- return responses[`${req.method}:${req.path}?${req.queryString}`] ||
- responses[req.method];
-}
diff --git a/services/common/tests/unit/test_kinto.js b/services/common/tests/unit/test_kinto.js
deleted file mode 100644
index 9c5ce58d9c..0000000000
--- a/services/common/tests/unit/test_kinto.js
+++ /dev/null
@@ -1,412 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://services-common/kinto-offline-client.js");
-Cu.import("resource://testing-common/httpd.js");
-
-const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
- "nsIBinaryInputStream", "setInputStream");
-
-var server;
-
-// set up what we need to make storage adapters
-const Kinto = loadKinto();
-const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
-const kintoFilename = "kinto.sqlite";
-
-let kintoClient;
-
-function do_get_kinto_collection() {
- if (!kintoClient) {
- let config = {
- remote:`http://localhost:${server.identity.primaryPort}/v1/`,
- headers: {Authorization: "Basic " + btoa("user:pass")},
- adapter: FirefoxAdapter
- };
- kintoClient = new Kinto(config);
- }
- return kintoClient.collection("test_collection");
-}
-
-function* clear_collection() {
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
- yield collection.clear();
- } finally {
- yield collection.db.close();
- }
-}
-
-// test some operations on a local collection
-add_task(function* test_kinto_add_get() {
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
-
- let newRecord = { foo: "bar" };
- // check a record is created
- let createResult = yield collection.create(newRecord);
- do_check_eq(createResult.data.foo, newRecord.foo);
- // check getting the record gets the same info
- let getResult = yield collection.get(createResult.data.id);
- deepEqual(createResult.data, getResult.data);
- // check what happens if we create the same item again (it should throw
- // since you can't create with id)
- try {
- yield collection.create(createResult.data);
- do_throw("Creation of a record with an id should fail");
- } catch (err) { }
- // try a few creates without waiting for the first few to resolve
- let promises = [];
- promises.push(collection.create(newRecord));
- promises.push(collection.create(newRecord));
- promises.push(collection.create(newRecord));
- yield collection.create(newRecord);
- yield Promise.all(promises);
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(clear_collection);
-
-// test some operations on multiple connections
-add_task(function* test_kinto_add_get() {
- const collection1 = do_get_kinto_collection();
- const collection2 = kintoClient.collection("test_collection_2");
-
- try {
- yield collection1.db.open();
- yield collection2.db.open();
-
- let newRecord = { foo: "bar" };
-
- // perform several write operations alternately without waiting for promises
- // to resolve
- let promises = [];
- for (let i = 0; i < 10; i++) {
- promises.push(collection1.create(newRecord));
- promises.push(collection2.create(newRecord));
- }
-
- // ensure subsequent operations still work
- yield Promise.all([collection1.create(newRecord),
- collection2.create(newRecord)]);
- yield Promise.all(promises);
- } finally {
- yield collection1.db.close();
- yield collection2.db.close();
- }
-});
-
-add_task(clear_collection);
-
-add_task(function* test_kinto_update() {
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
- const newRecord = { foo: "bar" };
- // check a record is created
- let createResult = yield collection.create(newRecord);
- do_check_eq(createResult.data.foo, newRecord.foo);
- do_check_eq(createResult.data._status, "created");
- // check we can update this OK
- let copiedRecord = Object.assign(createResult.data, {});
- deepEqual(createResult.data, copiedRecord);
- copiedRecord.foo = "wibble";
- let updateResult = yield collection.update(copiedRecord);
- // check the field was updated
- do_check_eq(updateResult.data.foo, copiedRecord.foo);
- // check the status is still "created", since we haven't synced
- // the record
- do_check_eq(updateResult.data._status, "created");
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(clear_collection);
-
-add_task(function* test_kinto_clear() {
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
-
- // create an expected number of records
- const expected = 10;
- const newRecord = { foo: "bar" };
- for (let i = 0; i < expected; i++) {
- yield collection.create(newRecord);
- }
- // check the collection contains the correct number
- let list = yield collection.list();
- do_check_eq(list.data.length, expected);
- // clear the collection and check again - should be 0
- yield collection.clear();
- list = yield collection.list();
- do_check_eq(list.data.length, 0);
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(clear_collection);
-
-add_task(function* test_kinto_delete(){
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
- const newRecord = { foo: "bar" };
- // check a record is created
- let createResult = yield collection.create(newRecord);
- do_check_eq(createResult.data.foo, newRecord.foo);
- // check getting the record gets the same info
- let getResult = yield collection.get(createResult.data.id);
- deepEqual(createResult.data, getResult.data);
- // delete that record
- let deleteResult = yield collection.delete(createResult.data.id);
- // check the ID is set on the result
- do_check_eq(getResult.data.id, deleteResult.data.id);
- // and check that get no longer returns the record
- try {
- getResult = yield collection.get(createResult.data.id);
- do_throw("there should not be a result");
- } catch (e) { }
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(function* test_kinto_list(){
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
- const expected = 10;
- const created = [];
- for (let i = 0; i < expected; i++) {
- let newRecord = { foo: "test " + i };
- let createResult = yield collection.create(newRecord);
- created.push(createResult.data);
- }
- // check the collection contains the correct number
- let list = yield collection.list();
- do_check_eq(list.data.length, expected);
-
- // check that all created records exist in the retrieved list
- for (let createdRecord of created) {
- let found = false;
- for (let retrievedRecord of list.data) {
- if (createdRecord.id == retrievedRecord.id) {
- deepEqual(createdRecord, retrievedRecord);
- found = true;
- }
- }
- do_check_true(found);
- }
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(clear_collection);
-
-add_task(function* test_loadDump_ignores_already_imported_records(){
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
- const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
- yield collection.loadDump([record]);
- let impactedRecords = yield collection.loadDump([record]);
- do_check_eq(impactedRecords.length, 0);
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(clear_collection);
-
-add_task(function* test_loadDump_should_overwrite_old_records(){
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
- const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
- yield collection.loadDump([record]);
- const updated = Object.assign({}, record, {last_modified: 1457896543});
- let impactedRecords = yield collection.loadDump([updated]);
- do_check_eq(impactedRecords.length, 1);
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(clear_collection);
-
-add_task(function* test_loadDump_should_not_overwrite_unsynced_records(){
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
- const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
- yield collection.create({id: recordId, title: "foo"}, {useRecordId: true});
- const record = {id: recordId, title: "bar", last_modified: 1457896541};
- let impactedRecords = yield collection.loadDump([record]);
- do_check_eq(impactedRecords.length, 0);
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(clear_collection);
-
-add_task(function* test_loadDump_should_not_overwrite_records_without_last_modified(){
- const collection = do_get_kinto_collection();
- try {
- yield collection.db.open();
- const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
- yield collection.create({id: recordId, title: "foo"}, {synced: true});
- const record = {id: recordId, title: "bar", last_modified: 1457896541};
- let impactedRecords = yield collection.loadDump([record]);
- do_check_eq(impactedRecords.length, 0);
- } finally {
- yield collection.db.close();
- }
-});
-
-add_task(clear_collection);
-
-// Now do some sanity checks against a server - we're not looking to test
-// core kinto.js functionality here (there is excellent test coverage in
-// kinto.js), more making sure things are basically working as expected.
-add_task(function* test_kinto_sync(){
- const configPath = "/v1/";
- const recordsPath = "/v1/buckets/default/collections/test_collection/records";
- // register a handler
- function handleResponse (request, response) {
- try {
- const sampled = getSampleResponse(request, server.identity.primaryPort);
- if (!sampled) {
- do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
- }
-
- response.setStatusLine(null, sampled.status.status,
- sampled.status.statusText);
- // send the headers
- for (let headerLine of sampled.sampleHeaders) {
- let headerElements = headerLine.split(':');
- response.setHeader(headerElements[0], headerElements[1].trimLeft());
- }
- response.setHeader("Date", (new Date()).toUTCString());
-
- response.write(sampled.responseBody);
- } catch (e) {
- dump(`${e}\n`);
- }
- }
- server.registerPathHandler(configPath, handleResponse);
- server.registerPathHandler(recordsPath, handleResponse);
-
- // create an empty collection, sync to populate
- const collection = do_get_kinto_collection();
- try {
- let result;
-
- yield collection.db.open();
- result = yield collection.sync();
- do_check_true(result.ok);
-
- // our test data has a single record; it should be in the local collection
- let list = yield collection.list();
- do_check_eq(list.data.length, 1);
-
- // now sync again; we should now have 2 records
- result = yield collection.sync();
- do_check_true(result.ok);
- list = yield collection.list();
- do_check_eq(list.data.length, 2);
-
- // sync again; the second records should have been modified
- const before = list.data[0].title;
- result = yield collection.sync();
- do_check_true(result.ok);
- list = yield collection.list();
- const after = list.data[0].title;
- do_check_neq(before, after);
- } finally {
- yield collection.db.close();
- }
-});
-
-function run_test() {
- // Set up an HTTP Server
- server = new HttpServer();
- server.start(-1);
-
- run_next_test();
-
- do_register_cleanup(function() {
- server.stop(function() { });
- });
-}
-
-// get a response for a given request from sample data
-function getSampleResponse(req, port) {
- const responses = {
- "OPTIONS": {
- "sampleHeaders": [
- "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
- "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
- "Access-Control-Allow-Origin: *",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress"
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": "null"
- },
- "GET:/v1/?": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress"
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
- },
- "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"1445606341071\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{"last_modified":1445606341071, "done":false, "id":"68db8313-686e-4fff-835e-07d78ad6f2af", "title":"New test"}]})
- },
- "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"1445607941223\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{"last_modified":1445607941223, "done":false, "id":"901967b0-f729-4b30-8d8d-499cba7f4b1d", "title":"Another new test"}]})
- },
- "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445607941223": {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- "Etag: \"1445607541265\""
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": JSON.stringify({"data":[{"last_modified":1445607541265, "done":false, "id":"901967b0-f729-4b30-8d8d-499cba7f4b1d", "title":"Modified title"}]})
- }
- };
- return responses[`${req.method}:${req.path}?${req.queryString}`] ||
- responses[req.method];
-
-}
diff --git a/services/common/tests/unit/xpcshell.ini b/services/common/tests/unit/xpcshell.ini
index dbec09519a..f1185c2c04 100644
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -9,14 +9,6 @@ support-files =
# Test load modules first so syntax failures are caught early.
[test_load_modules.js]
-[test_blocklist_certificates.js]
-[test_blocklist_clients.js]
-[test_blocklist_updater.js]
-
-[test_kinto.js]
-[test_blocklist_signatures.js]
-[test_storage_adapter.js]
-
[test_utils_atob.js]
[test_utils_convert_string.js]
[test_utils_dateprefs.js]
diff --git a/services/sync/modules/engines/extension-storage.js b/services/sync/modules/engines/extension-storage.js
deleted file mode 100644
index f8f15b1283..0000000000
--- a/services/sync/modules/engines/extension-storage.js
+++ /dev/null
@@ -1,277 +0,0 @@
-/* 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";
-
-this.EXPORTED_SYMBOLS = ['ExtensionStorageEngine', 'EncryptionRemoteTransformer',
- 'KeyRingEncryptionRemoteTransformer'];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://services-crypto/utils.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-common/async.js");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync",
- "resource://gre/modules/ExtensionStorageSync.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
- "resource://gre/modules/FxAccounts.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
- "resource://gre/modules/Task.jsm");
-
-/**
- * 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 Firefox Sync
- * framework, so this is something of a stub.
- */
-this.ExtensionStorageEngine = function ExtensionStorageEngine(service) {
- SyncEngine.call(this, "Extension-Storage", service);
-};
-ExtensionStorageEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _trackerObj: ExtensionStorageTracker,
- // we don't need these since we implement our own sync logic
- _storeObj: undefined,
- _recordObj: undefined,
-
- syncPriority: 10,
- allowSkippedRecord: false,
-
- _sync: function () {
- return Async.promiseSpinningly(ExtensionStorageSync.syncAll());
- },
-
- get enabled() {
- // 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.
- const forced = Svc.Prefs.get("engine." + this.prefName + ".force", undefined);
- if (forced !== undefined) {
- return forced;
- }
- return Svc.Prefs.get("engine.addons", false);
- },
-};
-
-function ExtensionStorageTracker(name, engine) {
- Tracker.call(this, name, engine);
-}
-ExtensionStorageTracker.prototype = {
- __proto__: Tracker.prototype,
-
- startTracking: function () {
- Svc.Obs.add("ext.storage.sync-changed", this);
- },
-
- stopTracking: function () {
- Svc.Obs.remove("ext.storage.sync-changed", this);
- },
-
- observe: function (subject, topic, data) {
- Tracker.prototype.observe.call(this, subject, topic, data);
-
- 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 += SCORE_INCREMENT_MEDIUM;
- },
-
- // Override a bunch of methods which don't do anything for us.
- // This is a performance hack.
- saveChangedIDs: function() {
- },
- loadChangedIDs: function() {
- },
- ignoreID: function() {
- },
- unignoreID: function() {
- },
- addChangedID: function() {
- },
- removeChangedID: function() {
- },
- clearChangedIDs: function() {
- },
-};
-
-/**
- * Utility function to enforce an order of fields when computing an HMAC.
- */
-function ciphertextHMAC(keyBundle, id, IV, ciphertext) {
- const hasher = keyBundle.sha256HMACHasher;
- return Utils.bytesAsHex(Utils.digestUTF8(id + IV + ciphertext, hasher));
-}
-
-/**
- * A "remote transformer" that the Kinto library will use to
- * encrypt/decrypt records when syncing.
- *
- * This is an "abstract base class". Subclass this and override
- * getKeys() to use it.
- */
-class EncryptionRemoteTransformer {
- encode(record) {
- const self = this;
- return Task.spawn(function* () {
- const keyBundle = yield self.getKeys();
- if (record.ciphertext) {
- throw new Error("Attempt to reencrypt??");
- }
- let id = record.id;
- if (!record.id) {
- throw new Error("Record ID is missing or invalid");
- }
-
- let IV = Svc.Crypto.generateRandomIV();
- let ciphertext = Svc.Crypto.encrypt(JSON.stringify(record),
- keyBundle.encryptionKeyB64, IV);
- let hmac = ciphertextHMAC(keyBundle, id, IV, ciphertext);
- const encryptedResult = {ciphertext, IV, hmac, id};
- if (record.hasOwnProperty("last_modified")) {
- encryptedResult.last_modified = record.last_modified;
- }
- return encryptedResult;
- });
- }
-
- decode(record) {
- const self = this;
- return Task.spawn(function* () {
- if (!record.ciphertext) {
- // This can happen for tombstones if a record is deleted.
- if (record.deleted) {
- return record;
- }
- throw new Error("No ciphertext: nothing to decrypt?");
- }
- const keyBundle = yield self.getKeys();
- // Authenticate the encrypted blob with the expected HMAC
- let computedHMAC = ciphertextHMAC(keyBundle, record.id, record.IV, record.ciphertext);
-
- if (computedHMAC != record.hmac) {
- Utils.throwHMACMismatch(record.hmac, computedHMAC);
- }
-
- // Handle invalid data here. Elsewhere we assume that cleartext is an object.
- let cleartext = Svc.Crypto.decrypt(record.ciphertext,
- keyBundle.encryptionKeyB64, record.IV);
- let jsonResult = JSON.parse(cleartext);
- if (!jsonResult || typeof jsonResult !== "object") {
- throw new Error("Decryption failed: result is <" + jsonResult + ">, not an object.");
- }
-
- // Verify that the encrypted id matches the requested record's id.
- // This should always be true, because we compute the HMAC over
- // the original record's ID, and that was verified already (above).
- if (jsonResult.id != record.id) {
- throw new Error("Record id mismatch: " + jsonResult.id + " != " + record.id);
- }
-
- if (record.hasOwnProperty("last_modified")) {
- jsonResult.last_modified = record.last_modified;
- }
-
- return jsonResult;
- });
- }
-
- /**
- * Retrieve keys to use during encryption.
- *
- * Returns a Promise<KeyBundle>.
- */
- getKeys() {
- throw new Error("override getKeys in a subclass");
- }
-}
-// You can inject this
-EncryptionRemoteTransformer.prototype._fxaService = fxAccounts;
-
-/**
- * An EncryptionRemoteTransformer that provides a keybundle derived
- * from the user's kB, suitable for encrypting a keyring.
- */
-class KeyRingEncryptionRemoteTransformer extends EncryptionRemoteTransformer {
- getKeys() {
- const self = this;
- return Task.spawn(function* () {
- const user = yield self._fxaService.getSignedInUser();
- // FIXME: we should permit this if the user is self-hosting
- // their storage
- if (!user) {
- throw new Error("user isn't signed in to FxA; can't sync");
- }
-
- if (!user.kB) {
- throw new Error("user doesn't have kB");
- }
-
- let kB = Utils.hexToBytes(user.kB);
-
- let keyMaterial = CryptoUtils.hkdf(kB, undefined,
- "identity.mozilla.com/picl/v1/chrome.storage.sync", 2*32);
- let bundle = new BulkKeyBundle();
- // [encryptionKey, hmacKey]
- bundle.keyPair = [keyMaterial.slice(0, 32), keyMaterial.slice(32, 64)];
- return bundle;
- });
- }
- // Pass through the kbHash field from the unencrypted record. If
- // encryption fails, we can use this to try to detect whether we are
- // being compromised or if the record here was encoded with a
- // different kB.
- encode(record) {
- const encodePromise = super.encode(record);
- return Task.spawn(function* () {
- const encoded = yield encodePromise;
- encoded.kbHash = record.kbHash;
- return encoded;
- });
- }
-
- decode(record) {
- const decodePromise = super.decode(record);
- return Task.spawn(function* () {
- try {
- return yield decodePromise;
- } catch (e) {
- if (Utils.isHMACMismatch(e)) {
- const currentKBHash = yield ExtensionStorageSync.getKBHash();
- if (record.kbHash != currentKBHash) {
- // Some other client encoded this with a kB that we don't
- // have access to.
- KeyRingEncryptionRemoteTransformer.throwOutdatedKB(currentKBHash, record.kbHash);
- }
- }
- throw e;
- }
- });
- }
-
- // Generator and discriminator for KB-is-outdated exceptions.
- static throwOutdatedKB(shouldBe, is) {
- throw new Error(`kB hash on record is outdated: should be ${shouldBe}, is ${is}`);
- }
-
- static isOutdatedKB(exc) {
- const kbMessage = "kB hash on record is outdated: ";
- return exc && exc.message && exc.message.indexOf &&
- (exc.message.indexOf(kbMessage) == 0);
- }
-}
diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js
index 5fc0fa7a7c..b0eb0f41d6 100644
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -44,7 +44,6 @@ const ENGINE_MODULES = {
Password: "passwords.js",
Prefs: "prefs.js",
Tab: "tabs.js",
- ExtensionStorage: "extension-storage.js",
};
const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
diff --git a/services/sync/moz.build b/services/sync/moz.build
index 156f437972..c4d3607b56 100644
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -52,7 +52,6 @@ EXTRA_JS_MODULES['services-sync'].engines += [
'modules/engines/addons.js',
'modules/engines/bookmarks.js',
'modules/engines/clients.js',
- 'modules/engines/extension-storage.js',
'modules/engines/forms.js',
'modules/engines/history.js',
'modules/engines/passwords.js',
diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js
index e945703e0c..91218b5f39 100644
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -119,8 +119,6 @@ user_pref("extensions.getAddons.get.url", "http://%(server)s/extensions-dummy/re
user_pref("extensions.getAddons.getWithPerformance.url", "http://%(server)s/extensions-dummy/repositoryGetWithPerformanceURL");
user_pref("extensions.getAddons.search.browseURL", "http://%(server)s/extensions-dummy/repositoryBrowseURL");
user_pref("extensions.getAddons.search.url", "http://%(server)s/extensions-dummy/repositorySearchURL");
-// Ensure blocklist updates don't hit the network
-user_pref("services.settings.server", "http://%(server)s/dummy-kinto/v1");
// Make sure SNTP requests don't hit the network
user_pref("network.sntp.pools", "%(server)s");
// We know the SNTP request will fail, since localhost isn't listening on
diff --git a/toolkit/components/extensions/ExtensionStorageSync.jsm b/toolkit/components/extensions/ExtensionStorageSync.jsm
deleted file mode 100644
index 2455b8e0a1..0000000000
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ /dev/null
@@ -1,848 +0,0 @@
-/* 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/. */
-
-// TODO:
-// * find out how the Chrome implementation deals with conflicts
-
-"use strict";
-
-/* exported extensionIdToCollectionId */
-
-this.EXPORTED_SYMBOLS = ["ExtensionStorageSync"];
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
-const global = this;
-
-Cu.import("resource://gre/modules/AppConstants.jsm");
-const KINTO_PROD_SERVER_URL = "https://webextensions.settings.services.mozilla.com/v1";
-const KINTO_DEV_SERVER_URL = "https://webextensions.dev.mozaws.net/v1";
-const KINTO_DEFAULT_SERVER_URL = AppConstants.RELEASE_OR_BETA ? KINTO_PROD_SERVER_URL : KINTO_DEV_SERVER_URL;
-
-const STORAGE_SYNC_ENABLED_PREF = "webextensions.storage.sync.enabled";
-const STORAGE_SYNC_SERVER_URL_PREF = "webextensions.storage.sync.serverURL";
-const STORAGE_SYNC_SCOPE = "sync:addon_storage";
-const STORAGE_SYNC_CRYPTO_COLLECTION_NAME = "storage-sync-crypto";
-const STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID = "keys";
-const FXA_OAUTH_OPTIONS = {
- scope: STORAGE_SYNC_SCOPE,
-};
-// Default is 5sec, which seems a bit aggressive on the open internet
-const KINTO_REQUEST_TIMEOUT = 30000;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-const {
- runSafeSyncWithoutClone,
-} = Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "AppsUtils",
- "resource://gre/modules/AppsUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
- "resource://gre/modules/AsyncShutdown.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "CollectionKeyManager",
- "resource://services-sync/record.js");
-XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
- "resource://services-common/utils.js");
-XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
- "resource://services-crypto/utils.js");
-XPCOMUtils.defineLazyModuleGetter(this, "EncryptionRemoteTransformer",
- "resource://services-sync/engines/extension-storage.js");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
- "resource://gre/modules/ExtensionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
- "resource://gre/modules/FxAccounts.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "KintoHttpClient",
- "resource://services-common/kinto-http-client.js");
-XPCOMUtils.defineLazyModuleGetter(this, "loadKinto",
- "resource://services-common/kinto-offline-client.js");
-XPCOMUtils.defineLazyModuleGetter(this, "Log",
- "resource://gre/modules/Log.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Observers",
- "resource://services-common/observers.js");
-XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
- "resource://gre/modules/Sqlite.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
- "resource://gre/modules/Task.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "KeyRingEncryptionRemoteTransformer",
- "resource://services-sync/engines/extension-storage.js");
-XPCOMUtils.defineLazyPreferenceGetter(this, "prefPermitsStorageSync",
- STORAGE_SYNC_ENABLED_PREF, false);
-XPCOMUtils.defineLazyPreferenceGetter(this, "prefStorageSyncServerURL",
- STORAGE_SYNC_SERVER_URL_PREF,
- KINTO_DEFAULT_SERVER_URL);
-
-/* globals prefPermitsStorageSync, prefStorageSyncServerURL */
-
-// Map of Extensions to Set<Contexts> to track contexts that are still
-// "live" and use storage.sync.
-const extensionContexts = new Map();
-// Borrow logger from Sync.
-const log = Log.repository.getLogger("Sync.Engine.Extension-Storage");
-
-/**
- * A Promise that centralizes initialization of ExtensionStorageSync.
- *
- * This centralizes the use of the Sqlite database, to which there is
- * only one connection which is shared by all threads.
- *
- * Fields in the object returned by this Promise:
- *
- * - connection: a Sqlite connection. Meant for internal use only.
- * - kinto: a KintoBase object, suitable for using in Firefox. All
- * collections in this database will use the same Sqlite connection.
- */
-const storageSyncInit = Task.spawn(function* () {
- const Kinto = loadKinto();
- const path = "storage-sync.sqlite";
- const opts = {path, sharedMemoryCache: false};
- const connection = yield Sqlite.openConnection(opts);
- yield Kinto.adapters.FirefoxAdapter._init(connection);
- return {
- connection,
- kinto: new Kinto({
- adapter: Kinto.adapters.FirefoxAdapter,
- adapterOptions: {sqliteHandle: connection},
- timeout: KINTO_REQUEST_TIMEOUT,
- }),
- };
-});
-
-AsyncShutdown.profileBeforeChange.addBlocker(
- "ExtensionStorageSync: close Sqlite handle",
- Task.async(function* () {
- const ret = yield storageSyncInit;
- const {connection} = ret;
- yield connection.close();
- })
-);
-// Kinto record IDs have two condtions:
-//
-// - They must contain only ASCII alphanumerics plus - and _. To fix
-// this, we encode all non-letters using _C_, where C is the
-// percent-encoded character, so space becomes _20_
-// and underscore becomes _5F_.
-//
-// - They must start with an ASCII letter. To ensure this, we prefix
-// all keys with "key-".
-function keyToId(key) {
- function escapeChar(match) {
- return "_" + match.codePointAt(0).toString(16).toUpperCase() + "_";
- }
- return "key-" + key.replace(/[^a-zA-Z0-9]/g, escapeChar);
-}
-
-// Convert a Kinto ID back into a chrome.storage key.
-// Returns null if a key couldn't be parsed.
-function idToKey(id) {
- function unescapeNumber(match, group1) {
- return String.fromCodePoint(parseInt(group1, 16));
- }
- // An escaped ID should match this regex.
- // An escaped ID should consist of only letters and numbers, plus
- // code points escaped as _[0-9a-f]+_.
- const ESCAPED_ID_FORMAT = /^(?:[a-zA-Z0-9]|_[0-9A-F]+_)*$/;
-
- if (!id.startsWith("key-")) {
- return null;
- }
- const unprefixed = id.slice(4);
- // Verify that the ID is the correct format.
- if (!ESCAPED_ID_FORMAT.test(unprefixed)) {
- return null;
- }
- return unprefixed.replace(/_([0-9A-F]+)_/g, unescapeNumber);
-}
-
-// An "id schema" used to validate Kinto IDs and generate new ones.
-const storageSyncIdSchema = {
- // We should never generate IDs; chrome.storage only acts as a
- // key-value store, so we should always have a key.
- generate() {
- throw new Error("cannot generate IDs");
- },
-
- // See keyToId and idToKey for more details.
- validate(id) {
- return idToKey(id) !== null;
- },
-};
-
-// An "id schema" used for the system collection, which doesn't
-// require validation or generation of IDs.
-const cryptoCollectionIdSchema = {
- generate() {
- throw new Error("cannot generate IDs for system collection");
- },
-
- validate(id) {
- return true;
- },
-};
-
-let cryptoCollection, CollectionKeyEncryptionRemoteTransformer;
-if (AppConstants.platform != "android") {
- /**
- * Wrapper around the crypto collection providing some handy utilities.
- */
- cryptoCollection = this.cryptoCollection = {
- getCollection: Task.async(function* () {
- const {kinto} = yield storageSyncInit;
- return kinto.collection(STORAGE_SYNC_CRYPTO_COLLECTION_NAME, {
- idSchema: cryptoCollectionIdSchema,
- remoteTransformers: [new KeyRingEncryptionRemoteTransformer()],
- });
- }),
-
- /**
- * Retrieve the keyring record from the crypto collection.
- *
- * You can use this if you want to check metadata on the keyring
- * record rather than use the keyring itself.
- *
- * @returns {Promise<Object>}
- */
- getKeyRingRecord: Task.async(function* () {
- const collection = yield this.getCollection();
- const cryptoKeyRecord = yield collection.getAny(STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID);
-
- let data = cryptoKeyRecord.data;
- if (!data) {
- // This is a new keyring. Invent an ID for this record. If this
- // changes, it means a client replaced the keyring, so we need to
- // reupload everything.
- const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
- const uuid = uuidgen.generateUUID().toString();
- data = {uuid};
- }
- return data;
- }),
-
- /**
- * Retrieve the actual keyring from the crypto collection.
- *
- * @returns {Promise<CollectionKeyManager>}
- */
- getKeyRing: Task.async(function* () {
- const cryptoKeyRecord = yield this.getKeyRingRecord();
- const collectionKeys = new CollectionKeyManager();
- if (cryptoKeyRecord.keys) {
- collectionKeys.setContents(cryptoKeyRecord.keys, cryptoKeyRecord.last_modified);
- } else {
- // We never actually use the default key, so it's OK if we
- // generate one multiple times.
- collectionKeys.generateDefaultKey();
- }
- // Pass through uuid field so that we can save it if we need to.
- collectionKeys.uuid = cryptoKeyRecord.uuid;
- return collectionKeys;
- }),
-
- updateKBHash: Task.async(function* (kbHash) {
- const coll = yield this.getCollection();
- yield coll.update({id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
- kbHash: kbHash},
- {patch: true});
- }),
-
- upsert: Task.async(function* (record) {
- const collection = yield this.getCollection();
- yield collection.upsert(record);
- }),
-
- sync: Task.async(function* () {
- const collection = yield this.getCollection();
- return yield ExtensionStorageSync._syncCollection(collection, {
- strategy: "server_wins",
- });
- }),
-
- /**
- * Reset sync status for ALL collections by directly
- * accessing the FirefoxAdapter.
- */
- resetSyncStatus: Task.async(function* () {
- const coll = yield this.getCollection();
- yield coll.db.resetSyncStatus();
- }),
-
- // Used only for testing.
- _clear: Task.async(function* () {
- const collection = yield this.getCollection();
- yield collection.clear();
- }),
- };
-
- /**
- * An EncryptionRemoteTransformer that uses the special "keys" record
- * to find a key for a given extension.
- *
- * @param {string} extensionId The extension ID for which to find a key.
- */
- CollectionKeyEncryptionRemoteTransformer = class extends EncryptionRemoteTransformer {
- constructor(extensionId) {
- super();
- this.extensionId = extensionId;
- }
-
- getKeys() {
- const self = this;
- return Task.spawn(function* () {
- // FIXME: cache the crypto record for the duration of a sync cycle?
- const collectionKeys = yield cryptoCollection.getKeyRing();
- if (!collectionKeys.hasKeysFor([self.extensionId])) {
- // This should never happen. Keys should be created (and
- // synced) at the beginning of the sync cycle.
- throw new Error(`tried to encrypt records for ${this.extensionId}, but key is not present`);
- }
- return collectionKeys.keyForCollection(self.extensionId);
- });
- }
- };
- global.CollectionKeyEncryptionRemoteTransformer = CollectionKeyEncryptionRemoteTransformer;
-}
-/**
- * Clean up now that one context is no longer using this extension's collection.
- *
- * @param {Extension} extension
- * The extension whose context just ended.
- * @param {Context} context
- * The context that just ended.
- */
-function cleanUpForContext(extension, context) {
- const contexts = extensionContexts.get(extension);
- if (!contexts) {
- Cu.reportError(new Error(`Internal error: cannot find any contexts for extension ${extension.id}`));
- }
- contexts.delete(context);
- if (contexts.size === 0) {
- // Nobody else is using this collection. Clean up.
- extensionContexts.delete(extension);
- }
-}
-
-/**
- * Generate a promise that produces the Collection for an extension.
- *
- * @param {Extension} extension
- * The extension whose collection needs to
- * be opened.
- * @param {Context} context
- * The context for this extension. The Collection
- * will shut down automatically when all contexts
- * close.
- * @returns {Promise<Collection>}
- */
-const openCollection = Task.async(function* (extension, context) {
- let collectionId = extension.id;
- const {kinto} = yield storageSyncInit;
- const remoteTransformers = [];
- if (CollectionKeyEncryptionRemoteTransformer) {
- remoteTransformers.push(new CollectionKeyEncryptionRemoteTransformer(extension.id));
- }
- const coll = kinto.collection(collectionId, {
- idSchema: storageSyncIdSchema,
- remoteTransformers,
- });
- return coll;
-});
-
-/**
- * Hash an extension ID for a given user so that an attacker can't
- * identify the extensions a user has installed.
- *
- * @param {User} user
- * The user for whom to choose a collection to sync
- * an extension to.
- * @param {string} extensionId The extension ID to obfuscate.
- * @returns {string} A collection ID suitable for use to sync to.
- */
-function extensionIdToCollectionId(user, extensionId) {
- const userFingerprint = CryptoUtils.hkdf(user.uid, undefined,
- "identity.mozilla.com/picl/v1/chrome.storage.sync.collectionIds", 2 * 32);
- let data = new TextEncoder().encode(userFingerprint + extensionId);
- let hasher = Cc["@mozilla.org/security/hash;1"]
- .createInstance(Ci.nsICryptoHash);
- hasher.init(hasher.SHA256);
- hasher.update(data, data.length);
-
- return CommonUtils.bytesAsHex(hasher.finish(false));
-}
-
-/**
- * Verify that we were built on not-Android. Call this as a sanity
- * check before using cryptoCollection.
- */
-function ensureCryptoCollection() {
- if (!cryptoCollection) {
- throw new Error("Call to ensureKeysFor, but no sync code; are you on Android?");
- }
-}
-
-// FIXME: This is kind of ugly. Probably we should have
-// ExtensionStorageSync not be a singleton, but a constructed object,
-// and this should be a constructor argument.
-let _fxaService = null;
-if (AppConstants.platform != "android") {
- _fxaService = fxAccounts;
-}
-
-this.ExtensionStorageSync = {
- _fxaService,
- listeners: new WeakMap(),
-
- syncAll: Task.async(function* () {
- const extensions = extensionContexts.keys();
- const extIds = Array.from(extensions, extension => extension.id);
- log.debug(`Syncing extension settings for ${JSON.stringify(extIds)}\n`);
- if (extIds.length == 0) {
- // No extensions to sync. Get out.
- return;
- }
- yield this.ensureKeysFor(extIds);
- yield this.checkSyncKeyRing();
- const promises = Array.from(extensionContexts.keys(), extension => {
- return openCollection(extension).then(coll => {
- return this.sync(extension, coll);
- });
- });
- yield Promise.all(promises);
- }),
-
- sync: Task.async(function* (extension, collection) {
- const signedInUser = yield this._fxaService.getSignedInUser();
- if (!signedInUser) {
- // FIXME: this should support syncing to self-hosted
- log.info("User was not signed into FxA; cannot sync");
- throw new Error("Not signed in to FxA");
- }
- const collectionId = extensionIdToCollectionId(signedInUser, extension.id);
- let syncResults;
- try {
- syncResults = yield this._syncCollection(collection, {
- strategy: "client_wins",
- collection: collectionId,
- });
- } catch (err) {
- log.warn("Syncing failed", err);
- throw err;
- }
-
- let changes = {};
- for (const record of syncResults.created) {
- changes[record.key] = {
- newValue: record.data,
- };
- }
- for (const record of syncResults.updated) {
- // N.B. It's safe to just pick old.key because it's not
- // possible to "rename" a record in the storage.sync API.
- const key = record.old.key;
- changes[key] = {
- oldValue: record.old.data,
- newValue: record.new.data,
- };
- }
- for (const record of syncResults.deleted) {
- changes[record.key] = {
- oldValue: record.data,
- };
- }
- for (const conflict of syncResults.resolved) {
- // FIXME: Should we even send a notification? If so, what
- // best values for "old" and "new"? This might violate
- // client code's assumptions, since from their perspective,
- // we were in state L, but this diff is from R -> L.
- changes[conflict.remote.key] = {
- oldValue: conflict.local.data,
- newValue: conflict.remote.data,
- };
- }
- if (Object.keys(changes).length > 0) {
- this.notifyListeners(extension, changes);
- }
- }),
-
- /**
- * Utility function that handles the common stuff about syncing all
- * Kinto collections (including "meta" collections like the crypto
- * one).
- *
- * @param {Collection} collection
- * @param {Object} options
- * Additional options to be passed to sync().
- * @returns {Promise<SyncResultObject>}
- */
- _syncCollection: Task.async(function* (collection, options) {
- // FIXME: this should support syncing to self-hosted
- return yield this._requestWithToken(`Syncing ${collection.name}`, function* (token) {
- const allOptions = Object.assign({}, {
- remote: prefStorageSyncServerURL,
- headers: {
- Authorization: "Bearer " + token,
- },
- }, options);
-
- return yield collection.sync(allOptions);
- });
- }),
-
- // Make a Kinto request with a current FxA token.
- // If the response indicates that the token might have expired,
- // retry the request.
- _requestWithToken: Task.async(function* (description, f) {
- const fxaToken = yield this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
- try {
- return yield f(fxaToken);
- } catch (e) {
- log.error(`${description}: request failed`, e);
- if (e && e.data && e.data.code == 401) {
- // Our token might have expired. Refresh and retry.
- log.info("Token might have expired");
- yield this._fxaService.removeCachedOAuthToken({token: fxaToken});
- const newToken = yield this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
-
- // If this fails too, let it go.
- return yield f(newToken);
- }
- // Otherwise, we don't know how to handle this error, so just reraise.
- throw e;
- }
- }),
-
- /**
- * Helper similar to _syncCollection, but for deleting the user's bucket.
- */
- _deleteBucket: Task.async(function* () {
- return yield this._requestWithToken("Clearing server", function* (token) {
- const headers = {Authorization: "Bearer " + token};
- const kintoHttp = new KintoHttpClient(prefStorageSyncServerURL, {
- headers: headers,
- timeout: KINTO_REQUEST_TIMEOUT,
- });
- return yield kintoHttp.deleteBucket("default");
- });
- }),
-
- /**
- * Recursive promise that terminates when our local collectionKeys,
- * as well as that on the server, have keys for all the extensions
- * in extIds.
- *
- * @param {Array<string>} extIds
- * The IDs of the extensions which need keys.
- * @returns {Promise<CollectionKeyManager>}
- */
- ensureKeysFor: Task.async(function* (extIds) {
- ensureCryptoCollection();
-
- const collectionKeys = yield cryptoCollection.getKeyRing();
- if (collectionKeys.hasKeysFor(extIds)) {
- return collectionKeys;
- }
-
- const kbHash = yield this.getKBHash();
- const newKeys = yield collectionKeys.ensureKeysFor(extIds);
- const newRecord = {
- id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
- keys: newKeys.asWBO().cleartext,
- uuid: collectionKeys.uuid,
- // Add a field for the current kB hash.
- kbHash: kbHash,
- };
- yield cryptoCollection.upsert(newRecord);
- const result = yield this._syncKeyRing(newRecord);
- if (result.resolved.length != 0) {
- // We had a conflict which was automatically resolved. We now
- // have a new keyring which might have keys for the
- // collections. Recurse.
- return yield this.ensureKeysFor(extIds);
- }
-
- // No conflicts. We're good.
- return newKeys;
- }),
-
- /**
- * Get the current user's hashed kB.
- *
- * @returns sha256 of the user's kB as a hex string
- */
- getKBHash: Task.async(function* () {
- const signedInUser = yield this._fxaService.getSignedInUser();
- if (!signedInUser) {
- throw new Error("User isn't signed in!");
- }
-
- if (!signedInUser.kB) {
- throw new Error("User doesn't have kB??");
- }
-
- let kBbytes = CommonUtils.hexToBytes(signedInUser.kB);
- let hasher = Cc["@mozilla.org/security/hash;1"]
- .createInstance(Ci.nsICryptoHash);
- hasher.init(hasher.SHA256);
- return CommonUtils.bytesAsHex(CryptoUtils.digestBytes(signedInUser.uid + kBbytes, hasher));
- }),
-
- /**
- * Update the kB in the crypto record.
- */
- updateKeyRingKB: Task.async(function* () {
- ensureCryptoCollection();
-
- const signedInUser = yield this._fxaService.getSignedInUser();
- if (!signedInUser) {
- // Although this function is meant to be called on login,
- // it's not unreasonable to check any time, even if we aren't
- // logged in.
- //
- // If we aren't logged in, we don't have any information about
- // the user's kB, so we can't be sure that the user changed
- // their kB, so just return.
- return;
- }
-
- const thisKBHash = yield this.getKBHash();
- yield cryptoCollection.updateKBHash(thisKBHash);
- }),
-
- /**
- * Make sure the keyring is up to date and synced.
- *
- * This is called on syncs to make sure that we don't sync anything
- * to any collection unless the key for that collection is on the
- * server.
- */
- checkSyncKeyRing: Task.async(function* () {
- ensureCryptoCollection();
-
- yield this.updateKeyRingKB();
-
- const cryptoKeyRecord = yield cryptoCollection.getKeyRingRecord();
- if (cryptoKeyRecord && cryptoKeyRecord._status !== "synced") {
- // We haven't successfully synced the keyring since the last
- // change. This could be because kB changed and we touched the
- // keyring, or it could be because we failed to sync after
- // adding a key. Either way, take this opportunity to sync the
- // keyring.
- yield this._syncKeyRing(cryptoKeyRecord);
- }
- }),
-
- _syncKeyRing: Task.async(function* (cryptoKeyRecord) {
- ensureCryptoCollection();
-
- try {
- // Try to sync using server_wins.
- //
- // We use server_wins here because whatever is on the server is
- // at least consistent with itself -- the crypto in the keyring
- // matches the crypto on the collection records. This is because
- // we generate and upload keys just before syncing data.
- //
- // It's possible that we can't decode the version on the server.
- // This can happen if a user is locked out of their account, and
- // does a "reset password" to get in on a new device. In this
- // case, we are in a bind -- we can't decrypt the record on the
- // server, so we can't merge keys. If this happens, we try to
- // figure out if we're the one with the correct (new) kB or if
- // we just got locked out because we have the old kB. If we're
- // the one with the correct kB, we wipe the server and reupload
- // everything, including a new keyring.
- //
- // If another device has wiped the server, we need to reupload
- // everything we have on our end too, so we detect this by
- // adding a UUID to the keyring. UUIDs are preserved throughout
- // the lifetime of a keyring, so the only time a keyring UUID
- // changes is when a new keyring is uploaded, which only happens
- // after a server wipe. So when we get a "conflict" (resolved by
- // server_wins), we check whether the server version has a new
- // UUID. If so, reset our sync status, so that we'll reupload
- // everything.
- const result = yield cryptoCollection.sync();
- if (result.resolved.length > 0) {
- if (result.resolved[0].uuid != cryptoKeyRecord.uuid) {
- log.info(`Detected a new UUID (${result.resolved[0].uuid}, was ${cryptoKeyRecord.uuid}). Reseting sync status for everything.`);
- yield cryptoCollection.resetSyncStatus();
-
- // Server version is now correct. Return that result.
- return result;
- }
- }
- // No conflicts, or conflict was just someone else adding keys.
- return result;
- } catch (e) {
- if (KeyRingEncryptionRemoteTransformer.isOutdatedKB(e)) {
- // Check if our token is still valid, or if we got locked out
- // between starting the sync and talking to Kinto.
- const isSessionValid = yield this._fxaService.sessionStatus();
- if (isSessionValid) {
- yield this._deleteBucket();
- yield cryptoCollection.resetSyncStatus();
-
- // Reupload our keyring, which is the only new keyring.
- // We don't want client_wins here because another device
- // could have uploaded another keyring in the meantime.
- return yield cryptoCollection.sync();
- }
- }
- throw e;
- }
- }),
-
- /**
- * Get the collection for an extension, and register the extension
- * as being "in use".
- *
- * @param {Extension} extension
- * The extension for which we are seeking
- * a collection.
- * @param {Context} context
- * The context of the extension, so that we can
- * stop syncing the collection when the extension ends.
- * @returns {Promise<Collection>}
- */
- getCollection(extension, context) {
- if (prefPermitsStorageSync !== true) {
- return Promise.reject({message: `Please set ${STORAGE_SYNC_ENABLED_PREF} to true in about:config`});
- }
- // Register that the extension and context are in use.
- if (!extensionContexts.has(extension)) {
- extensionContexts.set(extension, new Set());
- }
- const contexts = extensionContexts.get(extension);
- if (!contexts.has(context)) {
- // New context. Register it and make sure it cleans itself up
- // when it closes.
- contexts.add(context);
- context.callOnClose({
- close: () => cleanUpForContext(extension, context),
- });
- }
-
- return openCollection(extension, context);
- },
-
- set: Task.async(function* (extension, items, context) {
- const coll = yield this.getCollection(extension, context);
- const keys = Object.keys(items);
- const ids = keys.map(keyToId);
- const changes = yield coll.execute(txn => {
- let changes = {};
- for (let [i, key] of keys.entries()) {
- const id = ids[i];
- let item = items[key];
- let {oldRecord} = txn.upsert({
- id,
- key,
- data: item,
- });
- changes[key] = {
- newValue: item,
- };
- if (oldRecord && oldRecord.data) {
- // Extract the "data" field from the old record, which
- // represents the value part of the key-value store
- changes[key].oldValue = oldRecord.data;
- }
- }
- return changes;
- }, {preloadIds: ids});
- this.notifyListeners(extension, changes);
- }),
-
- remove: Task.async(function* (extension, keys, context) {
- const coll = yield this.getCollection(extension, context);
- keys = [].concat(keys);
- const ids = keys.map(keyToId);
- let changes = {};
- yield coll.execute(txn => {
- for (let [i, key] of keys.entries()) {
- const id = ids[i];
- const res = txn.deleteAny(id);
- if (res.deleted) {
- changes[key] = {
- oldValue: res.data.data,
- };
- }
- }
- return changes;
- }, {preloadIds: ids});
- if (Object.keys(changes).length > 0) {
- this.notifyListeners(extension, changes);
- }
- }),
-
- clear: Task.async(function* (extension, context) {
- // We can't call Collection#clear here, because that just clears
- // the local database. We have to explicitly delete everything so
- // that the deletions can be synced as well.
- const coll = yield this.getCollection(extension, context);
- const res = yield coll.list();
- const records = res.data;
- const keys = records.map(record => record.key);
- yield this.remove(extension, keys, context);
- }),
-
- get: Task.async(function* (extension, spec, context) {
- const coll = yield this.getCollection(extension, context);
- let keys, records;
- if (spec === null) {
- records = {};
- const res = yield coll.list();
- for (let record of res.data) {
- records[record.key] = record.data;
- }
- return records;
- }
- if (typeof spec === "string") {
- keys = [spec];
- records = {};
- } else if (Array.isArray(spec)) {
- keys = spec;
- records = {};
- } else {
- keys = Object.keys(spec);
- records = Cu.cloneInto(spec, global);
- }
-
- for (let key of keys) {
- const res = yield coll.getAny(keyToId(key));
- if (res.data && res.data._status != "deleted") {
- records[res.data.key] = res.data.data;
- }
- }
-
- return records;
- }),
-
- addOnChangedListener(extension, listener, context) {
- let listeners = this.listeners.get(extension) || new Set();
- listeners.add(listener);
- this.listeners.set(extension, listeners);
-
- // Force opening the collection so that we will sync for this extension.
- return this.getCollection(extension, context);
- },
-
- removeOnChangedListener(extension, listener) {
- let listeners = this.listeners.get(extension);
- listeners.delete(listener);
- if (listeners.size == 0) {
- this.listeners.delete(extension);
- }
- },
-
- notifyListeners(extension, changes) {
- Observers.notify("ext.storage.sync-changed");
- let listeners = this.listeners.get(extension) || new Set();
- if (listeners) {
- for (let listener of listeners) {
- runSafeSyncWithoutClone(listener, changes);
- }
- }
- },
-};
diff --git a/toolkit/components/extensions/ext-storage.js b/toolkit/components/extensions/ext-storage.js
index 46d4fe13c9..b1e22c46c0 100644
--- a/toolkit/components/extensions/ext-storage.js
+++ b/toolkit/components/extensions/ext-storage.js
@@ -4,8 +4,6 @@ var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync",
- "resource://gre/modules/ExtensionStorageSync.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
@@ -31,34 +29,14 @@ function storageApiFactory(context) {
},
},
- sync: {
- get: function(spec) {
- return ExtensionStorageSync.get(extension, spec, context);
- },
- set: function(items) {
- return ExtensionStorageSync.set(extension, items, context);
- },
- remove: function(keys) {
- return ExtensionStorageSync.remove(extension, keys, context);
- },
- clear: function() {
- return ExtensionStorageSync.clear(extension, context);
- },
- },
-
onChanged: new EventManager(context, "storage.onChanged", fire => {
let listenerLocal = changes => {
fire(changes, "local");
};
- let listenerSync = changes => {
- fire(changes, "sync");
- };
ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
- ExtensionStorageSync.addOnChangedListener(extension, listenerSync, context);
return () => {
ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
- ExtensionStorageSync.removeOnChangedListener(extension, listenerSync);
};
}).api(),
},
diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build
index f22a4b5d0c..f32f526f96 100644
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -13,7 +13,6 @@ EXTRA_JS_MODULES += [
'ExtensionManagement.jsm',
'ExtensionParent.jsm',
'ExtensionStorage.jsm',
- 'ExtensionStorageSync.jsm',
'ExtensionUtils.jsm',
'LegacyExtensionsUtils.jsm',
'MessageChannel.jsm',
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
deleted file mode 100644
index 4258289e39..0000000000
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
+++ /dev/null
@@ -1,1073 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-do_get_profile(); // so we can use FxAccounts
-
-Cu.import("resource://testing-common/httpd.js");
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://gre/modules/ExtensionStorageSync.jsm");
-const {
- CollectionKeyEncryptionRemoteTransformer,
- cryptoCollection,
- idToKey,
- extensionIdToCollectionId,
- keyToId,
-} = Cu.import("resource://gre/modules/ExtensionStorageSync.jsm");
-Cu.import("resource://services-sync/engines/extension-storage.js");
-Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/util.js");
-
-/* globals BulkKeyBundle, CommonUtils, EncryptionRemoteTransformer */
-/* globals KeyRingEncryptionRemoteTransformer */
-/* globals Utils */
-
-function handleCannedResponse(cannedResponse, request, response) {
- response.setStatusLine(null, cannedResponse.status.status,
- cannedResponse.status.statusText);
- // send the headers
- for (let headerLine of cannedResponse.sampleHeaders) {
- let headerElements = headerLine.split(":");
- response.setHeader(headerElements[0], headerElements[1].trimLeft());
- }
- response.setHeader("Date", (new Date()).toUTCString());
-
- response.write(cannedResponse.responseBody);
-}
-
-function collectionRecordsPath(collectionId) {
- return `/buckets/default/collections/${collectionId}/records`;
-}
-
-class KintoServer {
- constructor() {
- // Set up an HTTP Server
- this.httpServer = new HttpServer();
- this.httpServer.start(-1);
-
- // Map<CollectionId, Set<Object>> corresponding to the data in the
- // Kinto server
- this.collections = new Map();
-
- // ETag to serve with responses
- this.etag = 1;
-
- this.port = this.httpServer.identity.primaryPort;
- // POST requests we receive from the client go here
- this.posts = [];
- // DELETEd buckets will go here.
- this.deletedBuckets = [];
- // Anything in here will force the next POST to generate a conflict
- this.conflicts = [];
-
- this.installConfigPath();
- this.installBatchPath();
- this.installCatchAll();
- }
-
- clearPosts() {
- this.posts = [];
- }
-
- getPosts() {
- return this.posts;
- }
-
- getDeletedBuckets() {
- return this.deletedBuckets;
- }
-
- installConfigPath() {
- const configPath = "/v1/";
- const responseBody = JSON.stringify({
- "settings": {"batch_max_requests": 25},
- "url": `http://localhost:${this.port}/v1/`,
- "documentation": "https://kinto.readthedocs.org/",
- "version": "1.5.1",
- "commit": "cbc6f58",
- "hello": "kinto",
- });
- const configResponse = {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": responseBody,
- };
-
- function handleGetConfig(request, response) {
- if (request.method != "GET") {
- dump(`ARGH, got ${request.method}\n`);
- }
- return handleCannedResponse(configResponse, request, response);
- }
-
- this.httpServer.registerPathHandler(configPath, handleGetConfig);
- }
-
- installBatchPath() {
- const batchPath = "/v1/batch";
-
- function handlePost(request, response) {
- let bodyStr = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
- let body = JSON.parse(bodyStr);
- let defaults = body.defaults;
- for (let req of body.requests) {
- let headers = Object.assign({}, defaults && defaults.headers || {}, req.headers);
- // FIXME: assert auth is "Bearer ...token..."
- this.posts.push(Object.assign({}, req, {headers}));
- }
-
- response.setStatusLine(null, 200, "OK");
- response.setHeader("Content-Type", "application/json; charset=UTF-8");
- response.setHeader("Date", (new Date()).toUTCString());
-
- let postResponse = {
- responses: body.requests.map(req => {
- let oneBody;
- if (req.method == "DELETE") {
- let id = req.path.match(/^\/buckets\/default\/collections\/.+\/records\/(.+)$/)[1];
- oneBody = {
- "data": {
- "deleted": true,
- "id": id,
- "last_modified": this.etag,
- },
- };
- } else {
- oneBody = {"data": Object.assign({}, req.body.data, {last_modified: this.etag}),
- "permissions": []};
- }
-
- return {
- path: req.path,
- status: 201, // FIXME -- only for new posts??
- headers: {"ETag": 3000}, // FIXME???
- body: oneBody,
- };
- }),
- };
-
- if (this.conflicts.length > 0) {
- const {collectionId, encrypted} = this.conflicts.shift();
- this.collections.get(collectionId).add(encrypted);
- dump(`responding with etag ${this.etag}\n`);
- postResponse = {
- responses: body.requests.map(req => {
- return {
- path: req.path,
- status: 412,
- headers: {"ETag": this.etag}, // is this correct??
- body: {
- details: {
- existing: encrypted,
- },
- },
- };
- }),
- };
- }
-
- response.write(JSON.stringify(postResponse));
-
- // "sampleHeaders": [
- // "Access-Control-Allow-Origin: *",
- // "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- // "Server: waitress",
- // "Etag: \"4000\""
- // ],
- }
-
- this.httpServer.registerPathHandler(batchPath, handlePost.bind(this));
- }
-
- installCatchAll() {
- this.httpServer.registerPathHandler("/", (request, response) => {
- dump(`got request: ${request.method}:${request.path}?${request.queryString}\n`);
- dump(`${CommonUtils.readBytesFromInputStream(request.bodyInputStream)}\n`);
- });
- }
-
- installCollection(collectionId) {
- this.collections.set(collectionId, new Set());
-
- const remoteRecordsPath = "/v1" + collectionRecordsPath(encodeURIComponent(collectionId));
-
- function handleGetRecords(request, response) {
- if (request.method != "GET") {
- do_throw(`only GET is supported on ${remoteRecordsPath}`);
- }
-
- response.setStatusLine(null, 200, "OK");
- response.setHeader("Content-Type", "application/json; charset=UTF-8");
- response.setHeader("Date", (new Date()).toUTCString());
- response.setHeader("ETag", this.etag.toString());
-
- const records = this.collections.get(collectionId);
- // Can't JSON a Set directly, so convert to Array
- let data = Array.from(records);
- if (request.queryString.includes("_since=")) {
- data = data.filter(r => !(r._inPast || false));
- }
-
- // Remove records that we only needed to serve once.
- // FIXME: come up with a more coherent idea of time here.
- // See bug 1321570.
- for (const record of records) {
- if (record._onlyOnce) {
- records.delete(record);
- }
- }
-
- const body = JSON.stringify({
- "data": data,
- });
- response.write(body);
- }
-
- this.httpServer.registerPathHandler(remoteRecordsPath, handleGetRecords.bind(this));
- }
-
- installDeleteBucket() {
- this.httpServer.registerPrefixHandler("/v1/buckets/", (request, response) => {
- if (request.method != "DELETE") {
- dump(`got a non-delete action on bucket: ${request.method} ${request.path}\n`);
- return;
- }
-
- const noPrefix = request.path.slice("/v1/buckets/".length);
- const [bucket, afterBucket] = noPrefix.split("/", 1);
- if (afterBucket && afterBucket != "") {
- dump(`got a delete for a non-bucket: ${request.method} ${request.path}\n`);
- }
-
- this.deletedBuckets.push(bucket);
- // Fake like this actually deletes the records.
- for (const [, set] of this.collections) {
- set.clear();
- }
-
- response.write(JSON.stringify({
- data: {
- deleted: true,
- last_modified: 1475161309026,
- id: "b09f1618-d789-302d-696e-74ec53ee18a8", // FIXME
- },
- }));
- });
- }
-
- // Utility function to install a keyring at the start of a test.
- installKeyRing(keysData, etag, {conflict = false} = {}) {
- this.installCollection("storage-sync-crypto");
- const keysRecord = {
- "id": "keys",
- "keys": keysData,
- "last_modified": etag,
- };
- this.etag = etag;
- const methodName = conflict ? "encryptAndAddRecordWithConflict" : "encryptAndAddRecord";
- this[methodName](new KeyRingEncryptionRemoteTransformer(),
- "storage-sync-crypto", keysRecord);
- }
-
- // Add an already-encrypted record.
- addRecord(collectionId, record) {
- this.collections.get(collectionId).add(record);
- }
-
- // Add a record that is only served if no `_since` is present.
- //
- // Since in real life, Kinto only serves a record as part of a
- // changes feed if `_since` is before the record's modification
- // time, this can be helpful to test certain kinds of syncing logic.
- //
- // FIXME: tracking of "time" in this mock server really needs to be
- // implemented correctly rather than these hacks. See bug 1321570.
- addRecordInPast(collectionId, record) {
- record._inPast = true;
- this.addRecord(collectionId, record);
- }
-
- encryptAndAddRecord(transformer, collectionId, record) {
- return transformer.encode(record).then(encrypted => {
- this.addRecord(collectionId, encrypted);
- });
- }
-
- // Like encryptAndAddRecord, but add a flag that will only serve
- // this record once.
- //
- // Since in real life, Kinto only serves a record as part of a changes feed
- // once, this can be useful for testing complicated syncing logic.
- //
- // FIXME: This kind of logic really needs to be subsumed into some
- // more-realistic tracking of "time" (simulated by etags). See bug 1321570.
- encryptAndAddRecordOnlyOnce(transformer, collectionId, record) {
- return transformer.encode(record).then(encrypted => {
- encrypted._onlyOnce = true;
- this.addRecord(collectionId, encrypted);
- });
- }
-
- // Conflicts block the next push and then appear in the collection specified.
- encryptAndAddRecordWithConflict(transformer, collectionId, record) {
- return transformer.encode(record).then(encrypted => {
- this.conflicts.push({collectionId, encrypted});
- });
- }
-
- clearCollection(collectionId) {
- this.collections.get(collectionId).clear();
- }
-
- stop() {
- this.httpServer.stop(() => { });
- }
-}
-
-// Run a block of code with access to a KintoServer.
-function* withServer(f) {
- let server = new KintoServer();
- // Point the sync.storage client to use the test server we've just started.
- Services.prefs.setCharPref("webextensions.storage.sync.serverURL",
- `http://localhost:${server.port}/v1`);
- try {
- yield* f(server);
- } finally {
- server.stop();
- }
-}
-
-// Run a block of code with access to both a sync context and a
-// KintoServer. This is meant as a workaround for eslint's refusal to
-// let me have 5 nested callbacks.
-function* withContextAndServer(f) {
- yield* withSyncContext(function* (context) {
- yield* withServer(function* (server) {
- yield* f(context, server);
- });
- });
-}
-
-// Run a block of code with fxa mocked out to return a specific user.
-function* withSignedInUser(user, f) {
- const oldESSFxAccounts = ExtensionStorageSync._fxaService;
- const oldERTFxAccounts = EncryptionRemoteTransformer.prototype._fxaService;
- ExtensionStorageSync._fxaService = EncryptionRemoteTransformer.prototype._fxaService = {
- getSignedInUser() {
- return Promise.resolve(user);
- },
- getOAuthToken() {
- return Promise.resolve("some-access-token");
- },
- sessionStatus() {
- return Promise.resolve(true);
- },
- };
-
- try {
- yield* f();
- } finally {
- ExtensionStorageSync._fxaService = oldESSFxAccounts;
- EncryptionRemoteTransformer.prototype._fxaService = oldERTFxAccounts;
- }
-}
-
-// Some assertions that make it easier to write tests about what was
-// posted and when.
-
-// Assert that the request was made with the correct access token.
-// This should be true of all requests, so this is usually called from
-// another assertion.
-function assertAuthenticatedRequest(post) {
- equal(post.headers.Authorization, "Bearer some-access-token");
-}
-
-// Assert that this post was made with the correct request headers to
-// create a new resource while protecting against someone else
-// creating it at the same time (in other words, "If-None-Match: *").
-// Also calls assertAuthenticatedRequest(post).
-function assertPostedNewRecord(post) {
- assertAuthenticatedRequest(post);
- equal(post.headers["If-None-Match"], "*");
-}
-
-// Assert that this post was made with the correct request headers to
-// update an existing resource while protecting against concurrent
-// modification (in other words, `If-Match: "${etag}"`).
-// Also calls assertAuthenticatedRequest(post).
-function assertPostedUpdatedRecord(post, since) {
- assertAuthenticatedRequest(post);
- equal(post.headers["If-Match"], `"${since}"`);
-}
-
-// Assert that this post was an encrypted keyring, and produce the
-// decrypted body. Sanity check the body while we're here.
-const assertPostedEncryptedKeys = Task.async(function* (post) {
- equal(post.path, collectionRecordsPath("storage-sync-crypto") + "/keys");
-
- let body = yield new KeyRingEncryptionRemoteTransformer().decode(post.body.data);
- ok(body.keys, `keys object should be present in decoded body`);
- ok(body.keys.default, `keys object should have a default key`);
- return body;
-});
-
-// assertEqual, but for keyring[extensionId] == key.
-function assertKeyRingKey(keyRing, extensionId, expectedKey, message) {
- if (!message) {
- message = `expected keyring's key for ${extensionId} to match ${expectedKey.keyPairB64}`;
- }
- ok(keyRing.hasKeysFor([extensionId]),
- `expected keyring to have a key for ${extensionId}\n`);
- deepEqual(keyRing.keyForCollection(extensionId).keyPairB64, expectedKey.keyPairB64,
- message);
-}
-
-// Tests using this ID will share keys in local storage, so be careful.
-const defaultExtensionId = "{13bdde76-4dc7-11e6-9bdc-54ee758d6342}";
-const defaultExtension = {id: defaultExtensionId};
-
-const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
-const ANOTHER_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde0";
-const loggedInUser = {
- uid: "0123456789abcdef0123456789abcdef",
- kB: BORING_KB,
- oauthTokens: {
- "sync:addon-storage": {
- token: "some-access-token",
- },
- },
-};
-const defaultCollectionId = extensionIdToCollectionId(loggedInUser, defaultExtensionId);
-
-function uuid() {
- const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
- return uuidgen.generateUUID().toString();
-}
-
-add_task(function* test_key_to_id() {
- equal(keyToId("foo"), "key-foo");
- equal(keyToId("my-new-key"), "key-my_2D_new_2D_key");
- equal(keyToId(""), "key-");
- equal(keyToId("™"), "key-_2122_");
- equal(keyToId("\b"), "key-_8_");
- equal(keyToId("abc\ndef"), "key-abc_A_def");
- equal(keyToId("Kinto's fancy_string"), "key-Kinto_27_s_20_fancy_5F_string");
-
- const KEYS = ["foo", "my-new-key", "", "Kinto's fancy_string", "™", "\b"];
- for (let key of KEYS) {
- equal(idToKey(keyToId(key)), key);
- }
-
- equal(idToKey("hi"), null);
- equal(idToKey("-key-hi"), null);
- equal(idToKey("key--abcd"), null);
- equal(idToKey("key-%"), null);
- equal(idToKey("key-_HI"), null);
- equal(idToKey("key-_HI_"), null);
- equal(idToKey("key-"), "");
- equal(idToKey("key-1"), "1");
- equal(idToKey("key-_2D_"), "-");
-});
-
-add_task(function* test_extension_id_to_collection_id() {
- const newKBUser = Object.assign(loggedInUser, {kB: ANOTHER_KB});
- const extensionId = "{9419cce6-5435-11e6-84bf-54ee758d6342}";
- const extensionId2 = "{9419cce6-5435-11e6-84bf-54ee758d6343}";
-
- // "random" 32-char hex userid
- equal(extensionIdToCollectionId(loggedInUser, extensionId),
- "abf4e257dad0c89027f8f25bd196d4d69c100df375655a0c49f4cea7b791ea7d");
- equal(extensionIdToCollectionId(loggedInUser, extensionId),
- extensionIdToCollectionId(newKBUser, extensionId));
- equal(extensionIdToCollectionId(loggedInUser, extensionId2),
- "6584b0153336fb274912b31a3225c15a92b703cdc3adfe1917c1aa43122a52b8");
-});
-
-add_task(function* ensureKeysFor_posts_new_keys() {
- const extensionId = uuid();
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- server.installCollection("storage-sync-crypto");
- server.etag = 1000;
-
- let newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- ok(newKeys.hasKeysFor([extensionId]), `key isn't present for ${extensionId}`);
-
- let posts = server.getPosts();
- equal(posts.length, 1);
- const post = posts[0];
- assertPostedNewRecord(post);
- const body = yield assertPostedEncryptedKeys(post);
- ok(body.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
-
- // Try adding another key to make sure that the first post was
- // OK, even on a new profile.
- yield cryptoCollection._clear();
- server.clearPosts();
- // Restore the first posted keyring
- server.addRecordInPast("storage-sync-crypto", post.body.data);
- const extensionId2 = uuid();
- newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId2]);
- ok(newKeys.hasKeysFor([extensionId]), `didn't forget key for ${extensionId}`);
- ok(newKeys.hasKeysFor([extensionId2]), `new key generated for ${extensionId2}`);
-
- posts = server.getPosts();
- // FIXME: some kind of bug where we try to repush the
- // server_wins version multiple times in a single sync. We
- // actually push 5 times as of this writing.
- // See bug 1321571.
- // equal(posts.length, 1);
- const newPost = posts[posts.length - 1];
- const newBody = yield assertPostedEncryptedKeys(newPost);
- ok(newBody.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
- ok(newBody.keys.collections[extensionId2], `keys object should have a key for ${extensionId2}`);
-
- });
- });
-});
-
-add_task(function* ensureKeysFor_pulls_key() {
- // ensureKeysFor is implemented by adding a key to our local record
- // and doing a sync. This means that if the same key exists
- // remotely, we get a "conflict". Ensure that we handle this
- // correctly -- we keep the server key (since presumably it's
- // already been used to encrypt records) and we don't wipe out other
- // collections' keys.
- const extensionId = uuid();
- const extensionId2 = uuid();
- const DEFAULT_KEY = new BulkKeyBundle("[default]");
- DEFAULT_KEY.generateRandom();
- const RANDOM_KEY = new BulkKeyBundle(extensionId);
- RANDOM_KEY.generateRandom();
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- const keysData = {
- "default": DEFAULT_KEY.keyPairB64,
- "collections": {
- [extensionId]: RANDOM_KEY.keyPairB64,
- },
- };
- server.installKeyRing(keysData, 999);
-
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY);
-
- let posts = server.getPosts();
- equal(posts.length, 0,
- "ensureKeysFor shouldn't push when the server keyring has the right key");
-
- // Another client generates a key for extensionId2
- const newKey = new BulkKeyBundle(extensionId2);
- newKey.generateRandom();
- keysData.collections[extensionId2] = newKey.keyPairB64;
- server.clearCollection("storage-sync-crypto");
- server.installKeyRing(keysData, 1000);
-
- let newCollectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId, extensionId2]);
- assertKeyRingKey(newCollectionKeys, extensionId2, newKey);
- assertKeyRingKey(newCollectionKeys, extensionId, RANDOM_KEY,
- `ensureKeysFor shouldn't lose the old key for ${extensionId}`);
-
- posts = server.getPosts();
- equal(posts.length, 0, "ensureKeysFor shouldn't push when updating keys");
- });
- });
-});
-
-add_task(function* ensureKeysFor_handles_conflicts() {
- // Syncing is done through a pull followed by a push of any merged
- // changes. Accordingly, the only way to have a "true" conflict --
- // i.e. with the server rejecting a change -- is if
- // someone pushes changes between our pull and our push. Ensure that
- // if this happens, we still behave sensibly (keep the remote key).
- const extensionId = uuid();
- const DEFAULT_KEY = new BulkKeyBundle("[default]");
- DEFAULT_KEY.generateRandom();
- const RANDOM_KEY = new BulkKeyBundle(extensionId);
- RANDOM_KEY.generateRandom();
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- const keysData = {
- "default": DEFAULT_KEY.keyPairB64,
- "collections": {
- [extensionId]: RANDOM_KEY.keyPairB64,
- },
- };
- server.installKeyRing(keysData, 765, {conflict: true});
-
- yield cryptoCollection._clear();
-
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY,
- `syncing keyring should keep the server key for ${extensionId}`);
-
- let posts = server.getPosts();
- equal(posts.length, 1,
- "syncing keyring should have tried to post a keyring");
- const failedPost = posts[0];
- assertPostedNewRecord(failedPost);
- let body = yield assertPostedEncryptedKeys(failedPost);
- // This key will be the one the client generated locally, so
- // we don't know what its value will be
- ok(body.keys.collections[extensionId],
- `decrypted failed post should have a key for ${extensionId}`);
- notEqual(body.keys.collections[extensionId], RANDOM_KEY.keyPairB64,
- `decrypted failed post should have a randomly-generated key for ${extensionId}`);
- });
- });
-});
-
-add_task(function* checkSyncKeyRing_reuploads_keys() {
- // Verify that when keys are present, they are reuploaded with the
- // new kB when we call touchKeys().
- const extensionId = uuid();
- let extensionKey;
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- server.installCollection("storage-sync-crypto");
- server.etag = 765;
-
- yield cryptoCollection._clear();
-
- // Do an `ensureKeysFor` to generate some keys.
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- ok(collectionKeys.hasKeysFor([extensionId]),
- `ensureKeysFor should return a keyring that has a key for ${extensionId}`);
- extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
- equal(server.getPosts().length, 1,
- "generating a key that doesn't exist on the server should post it");
- });
-
- // The user changes their password. This is their new kB, with
- // the last f changed to an e.
- const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
- const newUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
- let postedKeys;
- yield* withSignedInUser(newUser, function* () {
- yield ExtensionStorageSync.checkSyncKeyRing();
-
- let posts = server.getPosts();
- equal(posts.length, 2,
- "when kB changes, checkSyncKeyRing should post the keyring reencrypted with the new kB");
- postedKeys = posts[1];
- assertPostedUpdatedRecord(postedKeys, 765);
-
- let body = yield assertPostedEncryptedKeys(postedKeys);
- deepEqual(body.keys.collections[extensionId], extensionKey,
- `the posted keyring should have the same key for ${extensionId} as the old one`);
- });
-
- // Verify that with the old kB, we can't decrypt the record.
- yield* withSignedInUser(loggedInUser, function* () {
- let error;
- try {
- yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
- } catch (e) {
- error = e;
- }
- ok(error, "decrypting the keyring with the old kB should fail");
- ok(Utils.isHMACMismatch(error) || KeyRingEncryptionRemoteTransformer.isOutdatedKB(error),
- "decrypting the keyring with the old kB should throw an HMAC mismatch");
- });
- });
-});
-
-add_task(function* checkSyncKeyRing_overwrites_on_conflict() {
- // If there is already a record on the server that was encrypted
- // with a different kB, we wipe the server, clear sync state, and
- // overwrite it with our keys.
- const extensionId = uuid();
- const transformer = new KeyRingEncryptionRemoteTransformer();
- let extensionKey;
- yield* withSyncContext(function* (context) {
- yield* withServer(function* (server) {
- // The old device has this kB, which is very similar to the
- // current kB but with the last f changed to an e.
- const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
- const oldUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
- server.installCollection("storage-sync-crypto");
- server.installDeleteBucket();
- server.etag = 765;
- yield* withSignedInUser(oldUser, function* () {
- const FAKE_KEYRING = {
- id: "keys",
- keys: {},
- uuid: "abcd",
- kbHash: "abcd",
- };
- yield server.encryptAndAddRecord(transformer, "storage-sync-crypto", FAKE_KEYRING);
- });
-
- // Now we have this new user with a different kB.
- yield* withSignedInUser(loggedInUser, function* () {
- yield cryptoCollection._clear();
-
- // Do an `ensureKeysFor` to generate some keys.
- // This will try to sync, notice that the record is
- // undecryptable, and clear the server.
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- ok(collectionKeys.hasKeysFor([extensionId]),
- `ensureKeysFor should always return a keyring with a key for ${extensionId}`);
- extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
-
- deepEqual(server.getDeletedBuckets(), ["default"],
- "Kinto server should have been wiped when keyring was thrown away");
-
- let posts = server.getPosts();
- equal(posts.length, 1,
- "new keyring should have been uploaded");
- const postedKeys = posts[0];
- // The POST was to an empty server, so etag shouldn't be respected
- equal(postedKeys.headers.Authorization, "Bearer some-access-token",
- "keyring upload should be authorized");
- equal(postedKeys.headers["If-None-Match"], "*",
- "keyring upload should be to empty Kinto server");
- equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
- "keyring upload should be to keyring path");
-
- let body = yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
- ok(body.uuid, "new keyring should have a UUID");
- equal(typeof body.uuid, "string", "keyring UUIDs should be strings");
- notEqual(body.uuid, "abcd",
- "new keyring should not have the same UUID as previous keyring");
- ok(body.keys,
- "new keyring should have a keys attribute");
- ok(body.keys.default, "new keyring should have a default key");
- // We should keep the extension key that was in our uploaded version.
- deepEqual(extensionKey, body.keys.collections[extensionId],
- "ensureKeysFor should have returned keyring with the same key that was uploaded");
-
- // This should be a no-op; the keys were uploaded as part of ensurekeysfor
- yield ExtensionStorageSync.checkSyncKeyRing();
- equal(server.getPosts().length, 1,
- "checkSyncKeyRing should not need to post keys after they were reuploaded");
- });
- });
- });
-});
-
-add_task(function* checkSyncKeyRing_flushes_on_uuid_change() {
- // If we can decrypt the record, but the UUID has changed, that
- // means another client has wiped the server and reuploaded a
- // keyring, so reset sync state and reupload everything.
- const extensionId = uuid();
- const extension = {id: extensionId};
- const collectionId = extensionIdToCollectionId(loggedInUser, extensionId);
- const transformer = new KeyRingEncryptionRemoteTransformer();
- yield* withSyncContext(function* (context) {
- yield* withServer(function* (server) {
- server.installCollection("storage-sync-crypto");
- server.installCollection(collectionId);
- server.installDeleteBucket();
- yield* withSignedInUser(loggedInUser, function* () {
- yield cryptoCollection._clear();
-
- // Do an `ensureKeysFor` to get access to keys.
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- ok(collectionKeys.hasKeysFor([extensionId]),
- `ensureKeysFor should always return a keyring that has a key for ${extensionId}`);
- const extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
-
- // Set something to make sure that it gets re-uploaded when
- // uuid changes.
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
- yield ExtensionStorageSync.syncAll();
-
- let posts = server.getPosts();
- equal(posts.length, 2,
- "should have posted a new keyring and an extension datum");
- const postedKeys = posts[0];
- equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
- "should have posted keyring to /keys");
-
- let body = yield transformer.decode(postedKeys.body.data);
- ok(body.uuid,
- "keyring should have a UUID");
- ok(body.keys,
- "keyring should have a keys attribute");
- ok(body.keys.default,
- "keyring should have a default key");
- deepEqual(extensionKey, body.keys.collections[extensionId],
- "new keyring should have the same key that we uploaded");
-
- // Another client comes along and replaces the UUID.
- // In real life, this would mean changing the keys too, but
- // this test verifies that just changing the UUID is enough.
- const newKeyRingData = Object.assign({}, body, {
- uuid: "abcd",
- // Technically, last_modified should be served outside the
- // object, but the transformer will pass it through in
- // either direction, so this is OK.
- last_modified: 765,
- });
- server.clearCollection("storage-sync-crypto");
- server.etag = 765;
- yield server.encryptAndAddRecordOnlyOnce(transformer, "storage-sync-crypto", newKeyRingData);
-
- // Fake adding another extension just so that the keyring will
- // really get synced.
- const newExtension = uuid();
- const newKeyRing = yield ExtensionStorageSync.ensureKeysFor([newExtension]);
-
- // This should have detected the UUID change and flushed everything.
- // The keyring should, however, be the same, since we just
- // changed the UUID of the previously POSTed one.
- deepEqual(newKeyRing.keyForCollection(extensionId).keyPairB64, extensionKey,
- "ensureKeysFor should have pulled down a new keyring with the same keys");
-
- // Syncing should reupload the data for the extension.
- yield ExtensionStorageSync.syncAll();
- posts = server.getPosts();
- equal(posts.length, 4,
- "should have posted keyring for new extension and reuploaded extension data");
-
- const finalKeyRingPost = posts[2];
- const reuploadedPost = posts[3];
-
- equal(finalKeyRingPost.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
- "keyring for new extension should have been posted to /keys");
- let finalKeyRing = yield transformer.decode(finalKeyRingPost.body.data);
- equal(finalKeyRing.uuid, "abcd",
- "newly uploaded keyring should preserve UUID from replacement keyring");
-
- // Confirm that the data got reuploaded
- equal(reuploadedPost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
- "extension data should be posted to path corresponding to its key");
- let reuploadedData = yield new CollectionKeyEncryptionRemoteTransformer(extensionId).decode(reuploadedPost.body.data);
- equal(reuploadedData.key, "my-key",
- "extension data should have a key attribute corresponding to the extension data key");
- equal(reuploadedData.data, 5,
- "extension data should have a data attribute corresponding to the extension data value");
- });
- });
- });
-});
-
-add_task(function* test_storage_sync_pulls_changes() {
- const extensionId = defaultExtensionId;
- const collectionId = defaultCollectionId;
- const extension = defaultExtension;
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
- server.installCollection(collectionId);
- server.installCollection("storage-sync-crypto");
-
- let calls = [];
- yield ExtensionStorageSync.addOnChangedListener(extension, function() {
- calls.push(arguments);
- }, context);
-
- yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- yield server.encryptAndAddRecord(transformer, collectionId, {
- "id": "key-remote_2D_key",
- "key": "remote-key",
- "data": 6,
- });
-
- yield ExtensionStorageSync.syncAll();
- const remoteValue = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];
- equal(remoteValue, 6,
- "ExtensionStorageSync.get() returns value retrieved from sync");
-
- equal(calls.length, 1,
- "syncing calls on-changed listener");
- deepEqual(calls[0][0], {"remote-key": {newValue: 6}});
- calls = [];
-
- // Syncing again doesn't do anything
- yield ExtensionStorageSync.syncAll();
-
- equal(calls.length, 0,
- "syncing again shouldn't call on-changed listener");
-
- // Updating the server causes us to pull down the new value
- server.etag = 1000;
- server.clearCollection(collectionId);
- yield server.encryptAndAddRecord(transformer, collectionId, {
- "id": "key-remote_2D_key",
- "key": "remote-key",
- "data": 7,
- });
-
- yield ExtensionStorageSync.syncAll();
- const remoteValue2 = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];
- equal(remoteValue2, 7,
- "ExtensionStorageSync.get() returns value updated from sync");
-
- equal(calls.length, 1,
- "syncing calls on-changed listener on update");
- deepEqual(calls[0][0], {"remote-key": {oldValue: 6, newValue: 7}});
- });
- });
-});
-
-add_task(function* test_storage_sync_pushes_changes() {
- const extensionId = defaultExtensionId;
- const collectionId = defaultCollectionId;
- const extension = defaultExtension;
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
- server.installCollection(collectionId);
- server.installCollection("storage-sync-crypto");
- server.etag = 1000;
-
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
-
- // install this AFTER we set the key to 5...
- let calls = [];
- ExtensionStorageSync.addOnChangedListener(extension, function() {
- calls.push(arguments);
- }, context);
-
- yield ExtensionStorageSync.syncAll();
- const localValue = (yield ExtensionStorageSync.get(extension, "my-key", context))["my-key"];
- equal(localValue, 5,
- "pushing an ExtensionStorageSync value shouldn't change local value");
-
- let posts = server.getPosts();
- equal(posts.length, 1,
- "pushing a value should cause a post to the server");
- const post = posts[0];
- assertPostedNewRecord(post);
- equal(post.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
- "pushing a value should have a path corresponding to its id");
-
- const encrypted = post.body.data;
- ok(encrypted.ciphertext,
- "pushing a value should post an encrypted record");
- ok(!encrypted.data,
- "pushing a value should not have any plaintext data");
- equal(encrypted.id, "key-my_2D_key",
- "pushing a value should use a kinto-friendly record ID");
-
- const record = yield transformer.decode(encrypted);
- equal(record.key, "my-key",
- "when decrypted, a pushed value should have a key field corresponding to its storage.sync key");
- equal(record.data, 5,
- "when decrypted, a pushed value should have a data field corresponding to its storage.sync value");
- equal(record.id, "key-my_2D_key",
- "when decrypted, a pushed value should have an id field corresponding to its record ID");
-
- equal(calls.length, 0,
- "pushing a value shouldn't call the on-changed listener");
-
- yield ExtensionStorageSync.set(extension, {"my-key": 6}, context);
- yield ExtensionStorageSync.syncAll();
-
- // Doesn't push keys because keys were pushed by a previous test.
- posts = server.getPosts();
- equal(posts.length, 2,
- "updating a value should trigger another push");
- const updatePost = posts[1];
- assertPostedUpdatedRecord(updatePost, 1000);
- equal(updatePost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
- "pushing an updated value should go to the same path");
-
- const updateEncrypted = updatePost.body.data;
- ok(updateEncrypted.ciphertext,
- "pushing an updated value should still be encrypted");
- ok(!updateEncrypted.data,
- "pushing an updated value should not have any plaintext visible");
- equal(updateEncrypted.id, "key-my_2D_key",
- "pushing an updated value should maintain the same ID");
- });
- });
-});
-
-add_task(function* test_storage_sync_pulls_deletes() {
- const collectionId = defaultCollectionId;
- const extension = defaultExtension;
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- server.installCollection(collectionId);
- server.installCollection("storage-sync-crypto");
-
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
- yield ExtensionStorageSync.syncAll();
- server.clearPosts();
-
- let calls = [];
- yield ExtensionStorageSync.addOnChangedListener(extension, function() {
- calls.push(arguments);
- }, context);
-
- yield server.addRecord(collectionId, {
- "id": "key-my_2D_key",
- "deleted": true,
- });
-
- yield ExtensionStorageSync.syncAll();
- const remoteValues = (yield ExtensionStorageSync.get(extension, "my-key", context));
- ok(!remoteValues["my-key"],
- "ExtensionStorageSync.get() shows value was deleted by sync");
-
- equal(server.getPosts().length, 0,
- "pulling the delete shouldn't cause posts");
-
- equal(calls.length, 1,
- "syncing calls on-changed listener");
- deepEqual(calls[0][0], {"my-key": {oldValue: 5}});
- calls = [];
-
- // Syncing again doesn't do anything
- yield ExtensionStorageSync.syncAll();
-
- equal(calls.length, 0,
- "syncing again shouldn't call on-changed listener");
- });
- });
-});
-
-add_task(function* test_storage_sync_pushes_deletes() {
- const extensionId = uuid();
- const collectionId = extensionIdToCollectionId(loggedInUser, extensionId);
- const extension = {id: extensionId};
- yield cryptoCollection._clear();
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- server.installCollection(collectionId);
- server.installCollection("storage-sync-crypto");
- server.etag = 1000;
-
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
-
- let calls = [];
- ExtensionStorageSync.addOnChangedListener(extension, function() {
- calls.push(arguments);
- }, context);
-
- yield ExtensionStorageSync.syncAll();
- let posts = server.getPosts();
- equal(posts.length, 2,
- "pushing a non-deleted value should post keys and post the value to the server");
-
- yield ExtensionStorageSync.remove(extension, ["my-key"], context);
- equal(calls.length, 1,
- "deleting a value should call the on-changed listener");
-
- yield ExtensionStorageSync.syncAll();
- equal(calls.length, 1,
- "pushing a deleted value shouldn't call the on-changed listener");
-
- // Doesn't push keys because keys were pushed by a previous test.
- posts = server.getPosts();
- equal(posts.length, 3,
- "deleting a value should trigger another push");
- const post = posts[2];
- assertPostedUpdatedRecord(post, 1000);
- equal(post.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
- "pushing a deleted value should go to the same path");
- ok(post.method, "DELETE");
- ok(!post.body,
- "deleting a value shouldn't have a body");
- });
- });
-});
diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell.ini b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
index 3d0198ee93..d2c6fd5d07 100644
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -58,9 +58,6 @@ skip-if = release_or_beta
[test_ext_schemas_allowed_contexts.js]
[test_ext_simple.js]
[test_ext_storage.js]
-[test_ext_storage_sync.js]
-head = head.js head_sync.js
-skip-if = os == "android"
[test_ext_topSites.js]
skip-if = os == "android"
[test_getAPILevelForWindow.js]
diff --git a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
index bbfb56ad5e..6422929b1c 100644
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -253,7 +253,6 @@ var AddonTestUtils = {
Services.prefs.setCharPref("extensions.update.url", "http://127.0.0.1/updateURL");
Services.prefs.setCharPref("extensions.update.background.url", "http://127.0.0.1/updateBackgroundURL");
Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL");
- Services.prefs.setCharPref("services.settings.server", "http://localhost/dummy-kinto/v1");
// By default ignore bundled add-ons
Services.prefs.setBoolPref("extensions.installDistroAddons", false);
diff --git a/toolkit/mozapps/extensions/nsBlocklistService.js b/toolkit/mozapps/extensions/nsBlocklistService.js
index a7b49a99ca..0af90430cb 100644
--- a/toolkit/mozapps/extensions/nsBlocklistService.js
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -627,17 +627,6 @@ Blocklist.prototype = {
// make sure we have loaded it.
if (!this._isBlocklistLoaded())
this._loadBlocklist();
-
- // If kinto update is enabled, do the kinto update
- if (gPref.getBoolPref(PREF_BLOCKLIST_UPDATE_ENABLED)) {
- const updater =
- Components.utils.import("resource://services-common/blocklist-updater.js",
- {});
- updater.checkVersions().catch(() => {
- // Before we enable this in release, we want to collect telemetry on
- // failed kinto updates - see bug 1254099
- });
- }
},
onXMLLoad: Task.async(function*(aEvent) {
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js
index 6e664adaed..c89ccdef88 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js
@@ -64,12 +64,6 @@ function load_blocklist(aFile, aCallback) {
gPort + "/data/" + aFile);
var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
getService(Ci.nsITimerCallback);
- // if we're not using the blocklist.xml for certificate blocklist state,
- // ensure that kinto update is enabled
- if (!Services.prefs.getBoolPref("security.onecrl.via.amo")) {
- ok(Services.prefs.getBoolPref("services.blocklist.update_enabled", false),
- "Kinto update should be enabled");
- }
blocklist.notify(null);
}