From 9627f18cebab38cdfe45592d83371ee7bbc62cfa Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 09:21:33 -0500 Subject: Remove kinto client, Firefox kinto storage adapter, blocklist update client and integration with sync, OneCRL and the custom time check for derives system time. --- browser/base/content/content.js | 43 +- .../locales/en-US/chrome/overrides/netError.dtd | 2 +- modules/libpref/init/all.js | 25 - security/manager/ssl/CertBlocklist.cpp | 23 +- security/manager/ssl/CertBlocklist.h | 2 - services/common/blocklist-clients.js | 310 -- services/common/blocklist-updater.js | 117 - services/common/kinto-http-client.js | 1891 --------- services/common/kinto-offline-client.js | 4286 -------------------- services/common/moz.build | 4 - .../tests/unit/test_blocklist_certificates.js | 224 - .../common/tests/unit/test_blocklist_clients.js | 412 -- .../common/tests/unit/test_blocklist_signatures.js | 510 --- .../common/tests/unit/test_blocklist_updater.js | 173 - services/common/tests/unit/test_kinto.js | 412 -- services/common/tests/unit/xpcshell.ini | 8 - services/sync/modules/engines/extension-storage.js | 277 -- services/sync/modules/service.js | 1 - services/sync/moz.build | 1 - testing/profiles/prefs_general.js | 2 - .../components/extensions/ExtensionStorageSync.jsm | 848 ---- toolkit/components/extensions/ext-storage.js | 22 - toolkit/components/extensions/moz.build | 1 - .../test/xpcshell/test_ext_storage_sync.js | 1073 ----- .../extensions/test/xpcshell/xpcshell.ini | 3 - .../mozapps/extensions/internal/AddonTestUtils.jsm | 1 - toolkit/mozapps/extensions/nsBlocklistService.js | 11 - .../test/xpcshell/test_blocklist_regexp.js | 6 - 28 files changed, 24 insertions(+), 10664 deletions(-) delete mode 100644 services/common/blocklist-clients.js delete mode 100644 services/common/blocklist-updater.js delete mode 100644 services/common/kinto-http-client.js delete mode 100644 services/common/kinto-offline-client.js delete mode 100644 services/common/tests/unit/test_blocklist_certificates.js delete mode 100644 services/common/tests/unit/test_blocklist_clients.js delete mode 100644 services/common/tests/unit/test_blocklist_signatures.js delete mode 100644 services/common/tests/unit/test_blocklist_updater.js delete mode 100644 services/common/tests/unit/test_kinto.js delete mode 100644 services/sync/modules/engines/extension-storage.js delete mode 100644 toolkit/components/extensions/ExtensionStorageSync.jsm delete mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js 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. --> -A secure connection to isn’t possible because your clock appears to show the wrong time.

Your computer thinks it is , when it should be . To fix this problem, change your date and time settings to match the correct time.

"> +&brandShortName; did not connect to because your computer’s clock appears to show the wrong time and this is preventing a secure connection.

Your computer is set to . To fix this problem, change your date and time settings to match the correct time.

"> 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 { - 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} - */ - 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} - */ - - fetchServerSettings(options = {}) { - return this.fetchServerInfo(options).then(({ settings }) => settings); - } - - /** - * Retrieve server capabilities information. - * - * @param {Object} [options={}] The request options. - * @return {Promise} - */ - - fetchServerCapabilities(options = {}) { - return this.fetchServerInfo(options).then(({ capabilities }) => capabilities); - } - - /** - * Retrieve authenticated user information. - * - * @param {Object} [options={}] The request options. - * @return {Promise} - */ - - fetchUser(options = {}) { - return this.fetchServerInfo(options).then(({ user }) => user); - } - - /** - * Retrieve authenticated user information. - * - * @param {Object} [options={}] The request options. - * @return {Promise} - */ - - 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} - */ - _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} - */ - - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - - 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} - */ - 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} - */ - 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, 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} - */ - 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} - */ - 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, 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} - */ - 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} [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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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} 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 "?"); - 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 - * @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} 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. - */ - 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 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} - */ - 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} - */ - 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} - */ -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} - */ - _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} extIds - * The IDs of the extensions which need keys. - * @returns {Promise} - */ - 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} - */ - 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> 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); } -- cgit v1.2.3