diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-02 09:21:33 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-02 09:21:33 -0500 |
commit | 9627f18cebab38cdfe45592d83371ee7bbc62cfa (patch) | |
tree | 9ac98ca9a764666bd0edd4cfd59ae970705b98a3 | |
parent | c28c5b704fb3f3af6e7846abd73f63da1e35921f (diff) | |
download | UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.tar UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.tar.gz UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.tar.lz UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.tar.xz UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.zip |
Remove kinto client, Firefox kinto storage adapter, blocklist update client and integration with sync, OneCRL and the custom time check for derives system time.
28 files changed, 24 insertions, 10664 deletions
diff --git a/browser/base/content/content.js b/browser/base/content/content.js index 658d2014d..8d6f0745e 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 30dd2346a..92db8ee3a 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 1cb9e1921..72eb8524e 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 56473eca3..c5e66b0d9 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 60f675cd8..2cad45eef 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 fc51aaca4..000000000 --- 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 3b39b9552..000000000 --- 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 57f6946d1..000000000 --- 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 4d0dbd0f3..000000000 --- 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 c09e6bed0..26a1bd9b4 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 e85970321..000000000 --- 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 121fac926..000000000 --- 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 b2ee1019a..000000000 --- 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 1b71c194a..000000000 --- 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 9c5ce58d9..000000000 --- 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 dbec09519..f1185c2c0 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 f8f15b128..000000000 --- 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 5fc0fa7a7..b0eb0f41d 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 156f43797..c4d3607b5 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 e945703e0..91218b5f3 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 2455b8e0a..000000000 --- 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 46d4fe13c..b1e22c46c 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 f22a4b5d0..f32f526f9 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 4258289e3..000000000 --- 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 3d0198ee9..d2c6fd5d0 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 bbfb56ad5..6422929b1 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 a7b49a99c..0af90430c 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 6e664adae..c89ccdef8 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); } |