diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-02 09:21:33 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-02 09:21:33 -0500 |
commit | 9627f18cebab38cdfe45592d83371ee7bbc62cfa (patch) | |
tree | 9ac98ca9a764666bd0edd4cfd59ae970705b98a3 /services | |
parent | c28c5b704fb3f3af6e7846abd73f63da1e35921f (diff) | |
download | UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.tar UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.tar.gz UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.tar.lz UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.tar.xz UXP-9627f18cebab38cdfe45592d83371ee7bbc62cfa.zip |
Remove kinto client, Firefox kinto storage adapter, blocklist update client and integration with sync, OneCRL and the custom time check for derives system time.
Diffstat (limited to 'services')
-rw-r--r-- | services/common/blocklist-clients.js | 310 | ||||
-rw-r--r-- | services/common/blocklist-updater.js | 117 | ||||
-rw-r--r-- | services/common/kinto-http-client.js | 1891 | ||||
-rw-r--r-- | services/common/kinto-offline-client.js | 4286 | ||||
-rw-r--r-- | services/common/moz.build | 4 | ||||
-rw-r--r-- | services/common/tests/unit/test_blocklist_certificates.js | 224 | ||||
-rw-r--r-- | services/common/tests/unit/test_blocklist_clients.js | 412 | ||||
-rw-r--r-- | services/common/tests/unit/test_blocklist_signatures.js | 510 | ||||
-rw-r--r-- | services/common/tests/unit/test_blocklist_updater.js | 173 | ||||
-rw-r--r-- | services/common/tests/unit/test_kinto.js | 412 | ||||
-rw-r--r-- | services/common/tests/unit/xpcshell.ini | 8 | ||||
-rw-r--r-- | services/sync/modules/engines/extension-storage.js | 277 | ||||
-rw-r--r-- | services/sync/modules/service.js | 1 | ||||
-rw-r--r-- | services/sync/moz.build | 1 |
14 files changed, 0 insertions, 8626 deletions
diff --git a/services/common/blocklist-clients.js b/services/common/blocklist-clients.js deleted file mode 100644 index fc51aaca4..000000000 --- a/services/common/blocklist-clients.js +++ /dev/null @@ -1,310 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["AddonBlocklistClient", - "GfxBlocklistClient", - "OneCRLBlocklistClient", - "PluginBlocklistClient", - "FILENAME_ADDONS_JSON", - "FILENAME_GFX_JSON", - "FILENAME_PLUGINS_JSON"]; - -const { classes: Cc, interfaces: Ci, utils: Cu } = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -const { Task } = Cu.import("resource://gre/modules/Task.jsm"); -const { OS } = Cu.import("resource://gre/modules/osfile.jsm"); -Cu.importGlobalProperties(["fetch"]); - -const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js"); -const { KintoHttpClient } = Cu.import("resource://services-common/kinto-http-client.js"); -const { CanonicalJSON } = Components.utils.import("resource://gre/modules/CanonicalJSON.jsm"); - -const PREF_SETTINGS_SERVER = "services.settings.server"; -const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket"; -const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection"; -const PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS = "services.blocklist.onecrl.checked"; -const PREF_BLOCKLIST_ADDONS_COLLECTION = "services.blocklist.addons.collection"; -const PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS = "services.blocklist.addons.checked"; -const PREF_BLOCKLIST_PLUGINS_COLLECTION = "services.blocklist.plugins.collection"; -const PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS = "services.blocklist.plugins.checked"; -const PREF_BLOCKLIST_GFX_COLLECTION = "services.blocklist.gfx.collection"; -const PREF_BLOCKLIST_GFX_CHECKED_SECONDS = "services.blocklist.gfx.checked"; -const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced"; - -const INVALID_SIGNATURE = "Invalid content/signature"; - -this.FILENAME_ADDONS_JSON = "blocklist-addons.json"; -this.FILENAME_GFX_JSON = "blocklist-gfx.json"; -this.FILENAME_PLUGINS_JSON = "blocklist-plugins.json"; - -function mergeChanges(localRecords, changes) { - // Kinto.js adds attributes to local records that aren't present on server. - // (e.g. _status) - const stripPrivateProps = (obj) => { - return Object.keys(obj).reduce((current, key) => { - if (!key.startsWith("_")) { - current[key] = obj[key]; - } - return current; - }, {}); - }; - - const records = {}; - // Local records by id. - localRecords.forEach((record) => records[record.id] = stripPrivateProps(record)); - // All existing records are replaced by the version from the server. - changes.forEach((record) => records[record.id] = record); - - return Object.values(records) - // Filter out deleted records. - .filter((record) => record.deleted != true) - // Sort list by record id. - .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0); -} - - -function fetchCollectionMetadata(collection) { - const client = new KintoHttpClient(collection.api.remote); - return client.bucket(collection.bucket).collection(collection.name).getData() - .then(result => { - return result.signature; - }); -} - -function fetchRemoteCollection(collection) { - const client = new KintoHttpClient(collection.api.remote); - return client.bucket(collection.bucket) - .collection(collection.name) - .listRecords({sort: "id"}); -} - -/** - * Helper to instantiate a Kinto client based on preferences for remote server - * URL and bucket name. It uses the `FirefoxAdapter` which relies on SQLite to - * persist the local DB. - */ -function kintoClient() { - let base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER); - let bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET); - - let Kinto = loadKinto(); - - let FirefoxAdapter = Kinto.adapters.FirefoxAdapter; - - let config = { - remote: base, - bucket: bucket, - adapter: FirefoxAdapter, - }; - - return new Kinto(config); -} - - -class BlocklistClient { - - constructor(collectionName, lastCheckTimePref, processCallback, signerName) { - this.collectionName = collectionName; - this.lastCheckTimePref = lastCheckTimePref; - this.processCallback = processCallback; - this.signerName = signerName; - } - - validateCollectionSignature(payload, collection, ignoreLocal) { - return Task.spawn((function* () { - // this is a content-signature field from an autograph response. - const {x5u, signature} = yield fetchCollectionMetadata(collection); - const certChain = yield fetch(x5u).then((res) => res.text()); - - const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"] - .createInstance(Ci.nsIContentSignatureVerifier); - - let toSerialize; - if (ignoreLocal) { - toSerialize = { - last_modified: `${payload.last_modified}`, - data: payload.data - }; - } else { - const localRecords = (yield collection.list()).data; - const records = mergeChanges(localRecords, payload.changes); - toSerialize = { - last_modified: `${payload.lastModified}`, - data: records - }; - } - - const serialized = CanonicalJSON.stringify(toSerialize); - - if (verifier.verifyContentSignature(serialized, "p384ecdsa=" + signature, - certChain, - this.signerName)) { - // In case the hash is valid, apply the changes locally. - return payload; - } - throw new Error(INVALID_SIGNATURE); - }).bind(this)); - } - - /** - * Synchronize from Kinto server, if necessary. - * - * @param {int} lastModified the lastModified date (on the server) for - the remote collection. - * @param {Date} serverTime the current date return by the server. - * @return {Promise} which rejects on sync or process failure. - */ - maybeSync(lastModified, serverTime) { - let db = kintoClient(); - let opts = {}; - let enforceCollectionSigning = - Services.prefs.getBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING); - - // if there is a signerName and collection signing is enforced, add a - // hook for incoming changes that validates the signature - if (this.signerName && enforceCollectionSigning) { - opts.hooks = { - "incoming-changes": [this.validateCollectionSignature.bind(this)] - } - } - - let collection = db.collection(this.collectionName, opts); - - return Task.spawn((function* syncCollection() { - try { - yield collection.db.open(); - - let collectionLastModified = yield collection.db.getLastModified(); - // If the data is up to date, there's no need to sync. We still need - // to record the fact that a check happened. - if (lastModified <= collectionLastModified) { - this.updateLastCheck(serverTime); - return; - } - // Fetch changes from server. - try { - let syncResult = yield collection.sync(); - if (!syncResult.ok) { - throw new Error("Sync failed"); - } - } catch (e) { - if (e.message == INVALID_SIGNATURE) { - // if sync fails with a signature error, it's likely that our - // local data has been modified in some way. - // We will attempt to fix this by retrieving the whole - // remote collection. - let payload = yield fetchRemoteCollection(collection); - yield this.validateCollectionSignature(payload, collection, true); - // if the signature is good (we haven't thrown), and the remote - // last_modified is newer than the local last_modified, replace the - // local data - const localLastModified = yield collection.db.getLastModified(); - if (payload.last_modified >= localLastModified) { - yield collection.clear(); - yield collection.loadDump(payload.data); - } - } else { - throw e; - } - } - // Read local collection of records. - let list = yield collection.list(); - - yield this.processCallback(list.data); - - // Track last update. - this.updateLastCheck(serverTime); - } finally { - collection.db.close(); - } - }).bind(this)); - } - - /** - * Save last time server was checked in users prefs. - * - * @param {Date} serverTime the current date return by server. - */ - updateLastCheck(serverTime) { - let checkedServerTimeInSeconds = Math.round(serverTime / 1000); - Services.prefs.setIntPref(this.lastCheckTimePref, checkedServerTimeInSeconds); - } -} - -/** - * Revoke the appropriate certificates based on the records from the blocklist. - * - * @param {Object} records current records in the local db. - */ -function* updateCertBlocklist(records) { - let certList = Cc["@mozilla.org/security/certblocklist;1"] - .getService(Ci.nsICertBlocklist); - for (let item of records) { - try { - if (item.issuerName && item.serialNumber) { - certList.revokeCertByIssuerAndSerial(item.issuerName, - item.serialNumber); - } else if (item.subject && item.pubKeyHash) { - certList.revokeCertBySubjectAndPubKey(item.subject, - item.pubKeyHash); - } - } catch (e) { - // prevent errors relating to individual blocklist entries from - // causing sync to fail. At some point in the future, we may want to - // accumulate telemetry on these failures. - Cu.reportError(e); - } - } - certList.saveEntries(); -} - -/** - * Write list of records into JSON file, and notify nsBlocklistService. - * - * @param {String} filename path relative to profile dir. - * @param {Object} records current records in the local db. - */ -function* updateJSONBlocklist(filename, records) { - // Write JSON dump for synchronous load at startup. - const path = OS.Path.join(OS.Constants.Path.profileDir, filename); - const serialized = JSON.stringify({data: records}, null, 2); - try { - yield OS.File.writeAtomic(path, serialized, {tmpPath: path + ".tmp"}); - - // Notify change to `nsBlocklistService` - const eventData = {filename: filename}; - Services.cpmm.sendAsyncMessage("Blocklist:reload-from-disk", eventData); - } catch(e) { - Cu.reportError(e); - } -} - - -this.OneCRLBlocklistClient = new BlocklistClient( - Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION), - PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS, - updateCertBlocklist, - "onecrl.content-signature.mozilla.org" -); - -this.AddonBlocklistClient = new BlocklistClient( - Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_COLLECTION), - PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS, - updateJSONBlocklist.bind(undefined, FILENAME_ADDONS_JSON) -); - -this.GfxBlocklistClient = new BlocklistClient( - Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_COLLECTION), - PREF_BLOCKLIST_GFX_CHECKED_SECONDS, - updateJSONBlocklist.bind(undefined, FILENAME_GFX_JSON) -); - -this.PluginBlocklistClient = new BlocklistClient( - Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_COLLECTION), - PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS, - updateJSONBlocklist.bind(undefined, FILENAME_PLUGINS_JSON) -); diff --git a/services/common/blocklist-updater.js b/services/common/blocklist-updater.js deleted file mode 100644 index 3b39b9552..000000000 --- a/services/common/blocklist-updater.js +++ /dev/null @@ -1,117 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -this.EXPORTED_SYMBOLS = ["checkVersions", "addTestBlocklistClient"]; - -const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.importGlobalProperties(['fetch']); -const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js", {}); - -const PREF_SETTINGS_SERVER = "services.settings.server"; -const PREF_BLOCKLIST_CHANGES_PATH = "services.blocklist.changes.path"; -const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket"; -const PREF_BLOCKLIST_LAST_UPDATE = "services.blocklist.last_update_seconds"; -const PREF_BLOCKLIST_LAST_ETAG = "services.blocklist.last_etag"; -const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds"; - - -const gBlocklistClients = { - [BlocklistClients.OneCRLBlocklistClient.collectionName]: BlocklistClients.OneCRLBlocklistClient, - [BlocklistClients.AddonBlocklistClient.collectionName]: BlocklistClients.AddonBlocklistClient, - [BlocklistClients.GfxBlocklistClient.collectionName]: BlocklistClients.GfxBlocklistClient, - [BlocklistClients.PluginBlocklistClient.collectionName]: BlocklistClients.PluginBlocklistClient -}; - -// Add a blocklist client for testing purposes. Do not use for any other purpose -this.addTestBlocklistClient = (name, client) => { gBlocklistClients[name] = client; } - -// This is called by the ping mechanism. -// returns a promise that rejects if something goes wrong -this.checkVersions = function() { - return Task.spawn(function* syncClients() { - // Fetch a versionInfo object that looks like: - // {"data":[ - // { - // "host":"kinto-ota.dev.mozaws.net", - // "last_modified":1450717104423, - // "bucket":"blocklists", - // "collection":"certificates" - // }]} - // Right now, we only use the collection name and the last modified info - let kintoBase = Services.prefs.getCharPref(PREF_SETTINGS_SERVER); - let changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_BLOCKLIST_CHANGES_PATH); - let blocklistsBucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET); - - // Use ETag to obtain a `304 Not modified` when no change occurred. - const headers = {}; - if (Services.prefs.prefHasUserValue(PREF_BLOCKLIST_LAST_ETAG)) { - const lastEtag = Services.prefs.getCharPref(PREF_BLOCKLIST_LAST_ETAG); - if (lastEtag) { - headers["If-None-Match"] = lastEtag; - } - } - - let response = yield fetch(changesEndpoint, {headers}); - - let versionInfo; - // No changes since last time. Go on with empty list of changes. - if (response.status == 304) { - versionInfo = {data: []}; - } else { - versionInfo = yield response.json(); - } - - // If the server is failing, the JSON response might not contain the - // expected data (e.g. error response - Bug 1259145) - if (!versionInfo.hasOwnProperty("data")) { - throw new Error("Polling for changes failed."); - } - - // Record new update time and the difference between local and server time - let serverTimeMillis = Date.parse(response.headers.get("Date")); - - // negative clockDifference means local time is behind server time - // by the absolute of that value in seconds (positive means it's ahead) - let clockDifference = Math.floor((Date.now() - serverTimeMillis) / 1000); - Services.prefs.setIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, clockDifference); - Services.prefs.setIntPref(PREF_BLOCKLIST_LAST_UPDATE, serverTimeMillis / 1000); - - let firstError; - for (let collectionInfo of versionInfo.data) { - // Skip changes that don't concern configured blocklist bucket. - if (collectionInfo.bucket != blocklistsBucket) { - continue; - } - - let collection = collectionInfo.collection; - let client = gBlocklistClients[collection]; - if (client && client.maybeSync) { - let lastModified = 0; - if (collectionInfo.last_modified) { - lastModified = collectionInfo.last_modified; - } - try { - yield client.maybeSync(lastModified, serverTimeMillis); - } catch (e) { - if (!firstError) { - firstError = e; - } - } - } - } - if (firstError) { - // cause the promise to reject by throwing the first observed error - throw firstError; - } - - // Save current Etag for next poll. - if (response.headers.has("ETag")) { - const currentEtag = response.headers.get("ETag"); - Services.prefs.setCharPref(PREF_BLOCKLIST_LAST_ETAG, currentEtag); - } - }); -}; diff --git a/services/common/kinto-http-client.js b/services/common/kinto-http-client.js deleted file mode 100644 index 57f6946d1..000000000 --- a/services/common/kinto-http-client.js +++ /dev/null @@ -1,1891 +0,0 @@ -/* - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * This file is generated from kinto-http.js - do not modify directly. - */ - -this.EXPORTED_SYMBOLS = ["KintoHttpClient"]; - -/* - * Version 2.0.0 - 61435f3 - */ - -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.KintoHttpClient = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ -/* - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = undefined; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _base = require("../src/base"); - -var _base2 = _interopRequireDefault(_base); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/Timer.jsm"); -Cu.importGlobalProperties(['fetch']); -const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {}); - -let KintoHttpClient = class KintoHttpClient extends _base2.default { - constructor(remote, options = {}) { - const events = {}; - EventEmitter.decorate(events); - super(remote, _extends({ events }, options)); - } -}; - -// This fixes compatibility with CommonJS required by browserify. -// See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495 - -exports.default = KintoHttpClient; -if (typeof module === "object") { - module.exports = KintoHttpClient; -} - -},{"../src/base":2}],2:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = exports.SUPPORTED_PROTOCOL_VERSION = undefined; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _desc, _value, _class; - -var _utils = require("./utils"); - -var _http = require("./http"); - -var _http2 = _interopRequireDefault(_http); - -var _endpoint = require("./endpoint"); - -var _endpoint2 = _interopRequireDefault(_endpoint); - -var _requests = require("./requests"); - -var requests = _interopRequireWildcard(_requests); - -var _batch = require("./batch"); - -var _bucket = require("./bucket"); - -var _bucket2 = _interopRequireDefault(_bucket); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { - var desc = {}; - Object['ke' + 'ys'](descriptor).forEach(function (key) { - desc[key] = descriptor[key]; - }); - desc.enumerable = !!desc.enumerable; - desc.configurable = !!desc.configurable; - - if ('value' in desc || desc.initializer) { - desc.writable = true; - } - - desc = decorators.slice().reverse().reduce(function (desc, decorator) { - return decorator(target, property, desc) || desc; - }, desc); - - if (context && desc.initializer !== void 0) { - desc.value = desc.initializer ? desc.initializer.call(context) : void 0; - desc.initializer = undefined; - } - - if (desc.initializer === void 0) { - Object['define' + 'Property'](target, property, desc); - desc = null; - } - - return desc; -} - -/** - * Currently supported protocol version. - * @type {String} - */ -const SUPPORTED_PROTOCOL_VERSION = exports.SUPPORTED_PROTOCOL_VERSION = "v1"; - -/** - * High level HTTP client for the Kinto API. - * - * @example - * const client = new KintoClient("https://kinto.dev.mozaws.net/v1"); - * client.bucket("default") -* .collection("my-blog") -* .createRecord({title: "First article"}) - * .then(console.log.bind(console)) - * .catch(console.error.bind(console)); - */ -let KintoClientBase = (_dec = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec2 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec3 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec4 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec5 = (0, _utils.nobatch)("Can't use batch within a batch!"), _dec6 = (0, _utils.support)("1.4", "2.0"), (_class = class KintoClientBase { - /** - * Constructor. - * - * @param {String} remote The remote URL. - * @param {Object} [options={}] The options object. - * @param {Boolean} [options.safe=true] Adds concurrency headers to every requests. - * @param {EventEmitter} [options.events=EventEmitter] The events handler instance. - * @param {Object} [options.headers={}] The key-value headers to pass to each request. - * @param {String} [options.bucket="default"] The default bucket to use. - * @param {String} [options.requestMode="cors"] The HTTP request mode (from ES6 fetch spec). - * @param {Number} [options.timeout=5000] The requests timeout in ms. - */ - constructor(remote, options = {}) { - if (typeof remote !== "string" || !remote.length) { - throw new Error("Invalid remote URL: " + remote); - } - if (remote[remote.length - 1] === "/") { - remote = remote.slice(0, -1); - } - this._backoffReleaseTime = null; - - /** - * Default request options container. - * @private - * @type {Object} - */ - this.defaultReqOptions = { - bucket: options.bucket || "default", - headers: options.headers || {}, - safe: !!options.safe - }; - - this._options = options; - this._requests = []; - this._isBatch = !!options.batch; - - // public properties - /** - * The remote server base URL. - * @type {String} - */ - this.remote = remote; - /** - * Current server information. - * @ignore - * @type {Object|null} - */ - this.serverInfo = null; - /** - * The event emitter instance. Should comply with the `EventEmitter` - * interface. - * @ignore - * @type {Class} - */ - this.events = options.events; - - const { requestMode, timeout } = options; - /** - * The HTTP instance. - * @ignore - * @type {HTTP} - */ - this.http = new _http2.default(this.events, { requestMode, timeout }); - this._registerHTTPEvents(); - } - - /** - * The remote endpoint base URL. Setting the value will also extract and - * validate the version. - * @type {String} - */ - get remote() { - return this._remote; - } - - /** - * @ignore - */ - set remote(url) { - let version; - try { - version = url.match(/\/(v\d+)\/?$/)[1]; - } catch (err) { - throw new Error("The remote URL must contain the version: " + url); - } - if (version !== SUPPORTED_PROTOCOL_VERSION) { - throw new Error(`Unsupported protocol version: ${ version }`); - } - this._remote = url; - this._version = version; - } - - /** - * The current server protocol version, eg. `v1`. - * @type {String} - */ - get version() { - return this._version; - } - - /** - * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is - * ongoing. - * - * @type {Number} - */ - get backoff() { - const currentTime = new Date().getTime(); - if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) { - return this._backoffReleaseTime - currentTime; - } - return 0; - } - - /** - * Registers HTTP events. - * @private - */ - _registerHTTPEvents() { - // Prevent registering event from a batch client instance - if (!this._isBatch) { - this.events.on("backoff", backoffMs => { - this._backoffReleaseTime = backoffMs; - }); - } - } - - /** - * Retrieve a bucket object to perform operations on it. - * - * @param {String} name The bucket name. - * @param {Object} [options={}] The request options. - * @param {Boolean} [options.safe] The resulting safe option. - * @param {String} [options.bucket] The resulting bucket name option. - * @param {Object} [options.headers] The extended headers object option. - * @return {Bucket} - */ - bucket(name, options = {}) { - const bucketOptions = (0, _utils.omit)(this._getRequestOptions(options), "bucket"); - return new _bucket2.default(this, name, bucketOptions); - } - - /** - * Generates a request options object, deeply merging the client configured - * defaults with the ones provided as argument. - * - * Note: Headers won't be overriden but merged with instance default ones. - * - * @private - * @param {Object} [options={}] The request options. - * @property {Boolean} [options.safe] The resulting safe option. - * @property {String} [options.bucket] The resulting bucket name option. - * @property {Object} [options.headers] The extended headers object option. - * @return {Object} - */ - _getRequestOptions(options = {}) { - return _extends({}, this.defaultReqOptions, options, { - batch: this._isBatch, - // Note: headers should never be overriden but extended - headers: _extends({}, this.defaultReqOptions.headers, options.headers) - }); - } - - /** - * Retrieves server information and persist them locally. This operation is - * usually performed a single time during the instance lifecycle. - * - * @param {Object} [options={}] The request options. - * @return {Promise<Object, Error>} - */ - fetchServerInfo(options = {}) { - if (this.serverInfo) { - return Promise.resolve(this.serverInfo); - } - return this.http.request(this.remote + (0, _endpoint2.default)("root"), { - headers: _extends({}, this.defaultReqOptions.headers, options.headers) - }).then(({ json }) => { - this.serverInfo = json; - return this.serverInfo; - }); - } - - /** - * Retrieves Kinto server settings. - * - * @param {Object} [options={}] The request options. - * @return {Promise<Object, Error>} - */ - - fetchServerSettings(options = {}) { - return this.fetchServerInfo(options).then(({ settings }) => settings); - } - - /** - * Retrieve server capabilities information. - * - * @param {Object} [options={}] The request options. - * @return {Promise<Object, Error>} - */ - - fetchServerCapabilities(options = {}) { - return this.fetchServerInfo(options).then(({ capabilities }) => capabilities); - } - - /** - * Retrieve authenticated user information. - * - * @param {Object} [options={}] The request options. - * @return {Promise<Object, Error>} - */ - - fetchUser(options = {}) { - return this.fetchServerInfo(options).then(({ user }) => user); - } - - /** - * Retrieve authenticated user information. - * - * @param {Object} [options={}] The request options. - * @return {Promise<Object, Error>} - */ - - fetchHTTPApiVersion(options = {}) { - return this.fetchServerInfo(options).then(({ http_api_version }) => { - return http_api_version; - }); - } - - /** - * Process batch requests, chunking them according to the batch_max_requests - * server setting when needed. - * - * @param {Array} requests The list of batch subrequests to perform. - * @param {Object} [options={}] The options object. - * @return {Promise<Object, Error>} - */ - _batchRequests(requests, options = {}) { - const headers = _extends({}, this.defaultReqOptions.headers, options.headers); - if (!requests.length) { - return Promise.resolve([]); - } - return this.fetchServerSettings().then(serverSettings => { - const maxRequests = serverSettings["batch_max_requests"]; - if (maxRequests && requests.length > maxRequests) { - const chunks = (0, _utils.partition)(requests, maxRequests); - return (0, _utils.pMap)(chunks, chunk => this._batchRequests(chunk, options)); - } - return this.execute({ - path: (0, _endpoint2.default)("batch"), - method: "POST", - headers: headers, - body: { - defaults: { headers }, - requests: requests - } - }) - // we only care about the responses - .then(({ responses }) => responses); - }); - } - - /** - * Sends batch requests to the remote server. - * - * Note: Reserved for internal use only. - * - * @ignore - * @param {Function} fn The function to use for describing batch ops. - * @param {Object} [options={}] The options object. - * @param {Boolean} [options.safe] The safe option. - * @param {String} [options.bucket] The bucket name option. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.aggregate=false] Produces an aggregated result object. - * @return {Promise<Object, Error>} - */ - - batch(fn, options = {}) { - const rootBatch = new KintoClientBase(this.remote, _extends({}, this._options, this._getRequestOptions(options), { - batch: true - })); - let bucketBatch, collBatch; - if (options.bucket) { - bucketBatch = rootBatch.bucket(options.bucket); - if (options.collection) { - collBatch = bucketBatch.collection(options.collection); - } - } - const batchClient = collBatch || bucketBatch || rootBatch; - try { - fn(batchClient); - } catch (err) { - return Promise.reject(err); - } - return this._batchRequests(rootBatch._requests, options).then(responses => { - if (options.aggregate) { - return (0, _batch.aggregate)(responses, rootBatch._requests); - } - return responses; - }); - } - - /** - * Executes an atomic HTTP request. - * - * @private - * @param {Object} request The request object. - * @param {Object} [options={}] The options object. - * @param {Boolean} [options.raw=false] If true, resolve with full response object, including json body and headers instead of just json. - * @return {Promise<Object, Error>} - */ - execute(request, options = { raw: false }) { - // If we're within a batch, add the request to the stack to send at once. - if (this._isBatch) { - this._requests.push(request); - // Resolve with a message in case people attempt at consuming the result - // from within a batch operation. - const msg = "This result is generated from within a batch " + "operation and should not be consumed."; - return Promise.resolve(options.raw ? { json: msg } : msg); - } - const promise = this.fetchServerSettings().then(_ => { - return this.http.request(this.remote + request.path, _extends({}, request, { - body: JSON.stringify(request.body) - })); - }); - return options.raw ? promise : promise.then(({ json }) => json); - } - - /** - * Retrieves the list of buckets. - * - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object[], Error>} - */ - listBuckets(options = {}) { - return this.execute({ - path: (0, _endpoint2.default)("bucket"), - headers: _extends({}, this.defaultReqOptions.headers, options.headers) - }); - } - - /** - * Creates a new bucket on the server. - * - * @param {String} id The bucket name. - * @param {Object} [options={}] The options object. - * @param {Boolean} [options.data] The bucket data option. - * @param {Boolean} [options.safe] The safe option. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object, Error>} - */ - createBucket(id, options = {}) { - if (!id) { - throw new Error("A bucket id is required."); - } - // Note that we simply ignore any "bucket" option passed here, as the one - // we're interested in is the one provided as a required argument. - const reqOptions = this._getRequestOptions(options); - const { data = {}, permissions } = reqOptions; - data.id = id; - const path = (0, _endpoint2.default)("bucket", id); - return this.execute(requests.createRequest(path, { data, permissions }, reqOptions)); - } - - /** - * Deletes a bucket from the server. - * - * @ignore - * @param {Object|String} bucket The bucket to delete. - * @param {Object} [options={}] The options object. - * @param {Boolean} [options.safe] The safe option. - * @param {Object} [options.headers] The headers object option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - deleteBucket(bucket, options = {}) { - const bucketObj = (0, _utils.toDataBody)(bucket); - if (!bucketObj.id) { - throw new Error("A bucket id is required."); - } - const path = (0, _endpoint2.default)("bucket", bucketObj.id); - const { last_modified } = { bucketObj }; - const reqOptions = this._getRequestOptions(_extends({ last_modified }, options)); - return this.execute(requests.deleteRequest(path, reqOptions)); - } - - /** - * Deletes all buckets on the server. - * - * @ignore - * @param {Object} [options={}] The options object. - * @param {Boolean} [options.safe] The safe option. - * @param {Object} [options.headers] The headers object option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - - deleteBuckets(options = {}) { - const reqOptions = this._getRequestOptions(options); - const path = (0, _endpoint2.default)("bucket"); - return this.execute(requests.deleteRequest(path, reqOptions)); - } -}, (_applyDecoratedDescriptor(_class.prototype, "fetchServerSettings", [_dec], Object.getOwnPropertyDescriptor(_class.prototype, "fetchServerSettings"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchServerCapabilities", [_dec2], Object.getOwnPropertyDescriptor(_class.prototype, "fetchServerCapabilities"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchUser", [_dec3], Object.getOwnPropertyDescriptor(_class.prototype, "fetchUser"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchHTTPApiVersion", [_dec4], Object.getOwnPropertyDescriptor(_class.prototype, "fetchHTTPApiVersion"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "batch", [_dec5], Object.getOwnPropertyDescriptor(_class.prototype, "batch"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "deleteBuckets", [_dec6], Object.getOwnPropertyDescriptor(_class.prototype, "deleteBuckets"), _class.prototype)), _class)); -exports.default = KintoClientBase; - -},{"./batch":3,"./bucket":4,"./endpoint":6,"./http":8,"./requests":9,"./utils":10}],3:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.aggregate = aggregate; -/** - * Exports batch responses as a result object. - * - * @private - * @param {Array} responses The batch subrequest responses. - * @param {Array} requests The initial issued requests. - * @return {Object} - */ -function aggregate(responses = [], requests = []) { - if (responses.length !== requests.length) { - throw new Error("Responses length should match requests one."); - } - const results = { - errors: [], - published: [], - conflicts: [], - skipped: [] - }; - return responses.reduce((acc, response, index) => { - const { status } = response; - if (status >= 200 && status < 400) { - acc.published.push(response.body); - } else if (status === 404) { - acc.skipped.push(response.body); - } else if (status === 412) { - acc.conflicts.push({ - // XXX: specifying the type is probably superfluous - type: "outgoing", - local: requests[index].body, - remote: response.body.details && response.body.details.existing || null - }); - } else { - acc.errors.push({ - path: response.path, - sent: requests[index], - error: response.body - }); - } - return acc; - }, results); -} - -},{}],4:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = undefined; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _utils = require("./utils"); - -var _collection = require("./collection"); - -var _collection2 = _interopRequireDefault(_collection); - -var _requests = require("./requests"); - -var requests = _interopRequireWildcard(_requests); - -var _endpoint = require("./endpoint"); - -var _endpoint2 = _interopRequireDefault(_endpoint); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/** - * Abstract representation of a selected bucket. - * - */ -let Bucket = class Bucket { - /** - * Constructor. - * - * @param {KintoClient} client The client instance. - * @param {String} name The bucket name. - * @param {Object} [options={}] The headers object option. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - */ - constructor(client, name, options = {}) { - /** - * @ignore - */ - this.client = client; - /** - * The bucket name. - * @type {String} - */ - this.name = name; - /** - * The default options object. - * @ignore - * @type {Object} - */ - this.options = options; - /** - * @ignore - */ - this._isBatch = !!options.batch; - } - - /** - * Merges passed request options with default bucket ones, if any. - * - * @private - * @param {Object} [options={}] The options to merge. - * @return {Object} The merged options. - */ - _bucketOptions(options = {}) { - const headers = _extends({}, this.options && this.options.headers, options.headers); - return _extends({}, this.options, options, { - headers, - bucket: this.name, - batch: this._isBatch - }); - } - - /** - * Selects a collection. - * - * @param {String} name The collection name. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @return {Collection} - */ - collection(name, options = {}) { - return new _collection2.default(this.client, this, name, this._bucketOptions(options)); - } - - /** - * Retrieves bucket data. - * - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object, Error>} - */ - getData(options = {}) { - return this.client.execute({ - path: (0, _endpoint2.default)("bucket", this.name), - headers: _extends({}, this.options.headers, options.headers) - }).then(res => res.data); - } - - /** - * Set bucket data. - * @param {Object} data The bucket data object. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Boolean} [options.patch] The patch option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - setData(data, options = {}) { - if (!(0, _utils.isObject)(data)) { - throw new Error("A bucket object is required."); - } - - const bucket = _extends({}, data, { id: this.name }); - - // For default bucket, we need to drop the id from the data object. - // Bug in Kinto < 3.1.1 - const bucketId = bucket.id; - if (bucket.id === "default") { - delete bucket.id; - } - - const path = (0, _endpoint2.default)("bucket", bucketId); - const { permissions } = options; - const reqOptions = _extends({}, this._bucketOptions(options)); - const request = requests.updateRequest(path, { data: bucket, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Retrieves the list of collections in the current bucket. - * - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Array<Object>, Error>} - */ - listCollections(options = {}) { - return this.client.execute({ - path: (0, _endpoint2.default)("collection", this.name), - headers: _extends({}, this.options.headers, options.headers) - }); - } - - /** - * Creates a new collection in current bucket. - * - * @param {String|undefined} id The collection id. - * @param {Object} [options={}] The options object. - * @param {Boolean} [options.safe] The safe option. - * @param {Object} [options.headers] The headers object option. - * @param {Object} [options.permissions] The permissions object. - * @param {Object} [options.data] The data object. - * @return {Promise<Object, Error>} - */ - createCollection(id, options = {}) { - const reqOptions = this._bucketOptions(options); - const { permissions, data = {} } = reqOptions; - data.id = id; - const path = (0, _endpoint2.default)("collection", this.name, id); - const request = requests.createRequest(path, { data, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Deletes a collection from the current bucket. - * - * @param {Object|String} collection The collection to delete. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - deleteCollection(collection, options = {}) { - const collectionObj = (0, _utils.toDataBody)(collection); - if (!collectionObj.id) { - throw new Error("A collection id is required."); - } - const { id, last_modified } = collectionObj; - const reqOptions = this._bucketOptions(_extends({ last_modified }, options)); - const path = (0, _endpoint2.default)("collection", this.name, id); - const request = requests.deleteRequest(path, reqOptions); - return this.client.execute(request); - } - - /** - * Retrieves the list of groups in the current bucket. - * - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Array<Object>, Error>} - */ - listGroups(options = {}) { - return this.client.execute({ - path: (0, _endpoint2.default)("group", this.name), - headers: _extends({}, this.options.headers, options.headers) - }); - } - - /** - * Creates a new group in current bucket. - * - * @param {String} id The group id. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object, Error>} - */ - getGroup(id, options = {}) { - return this.client.execute({ - path: (0, _endpoint2.default)("group", this.name, id), - headers: _extends({}, this.options.headers, options.headers) - }); - } - - /** - * Creates a new group in current bucket. - * - * @param {String|undefined} id The group id. - * @param {Array<String>} [members=[]] The list of principals. - * @param {Object} [options={}] The options object. - * @param {Object} [options.data] The data object. - * @param {Object} [options.permissions] The permissions object. - * @param {Boolean} [options.safe] The safe option. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object, Error>} - */ - createGroup(id, members = [], options = {}) { - const reqOptions = this._bucketOptions(options); - const data = _extends({}, options.data, { - id, - members - }); - const path = (0, _endpoint2.default)("group", this.name, id); - const { permissions } = options; - const request = requests.createRequest(path, { data, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Updates an existing group in current bucket. - * - * @param {Object} group The group object. - * @param {Object} [options={}] The options object. - * @param {Object} [options.data] The data object. - * @param {Object} [options.permissions] The permissions object. - * @param {Boolean} [options.safe] The safe option. - * @param {Object} [options.headers] The headers object option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - updateGroup(group, options = {}) { - if (!(0, _utils.isObject)(group)) { - throw new Error("A group object is required."); - } - if (!group.id) { - throw new Error("A group id is required."); - } - const reqOptions = this._bucketOptions(options); - const data = _extends({}, options.data, group); - const path = (0, _endpoint2.default)("group", this.name, group.id); - const { permissions } = options; - const request = requests.updateRequest(path, { data, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Deletes a group from the current bucket. - * - * @param {Object|String} group The group to delete. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - deleteGroup(group, options = {}) { - const groupObj = (0, _utils.toDataBody)(group); - const { id, last_modified } = groupObj; - const reqOptions = this._bucketOptions(_extends({ last_modified }, options)); - const path = (0, _endpoint2.default)("group", this.name, id); - const request = requests.deleteRequest(path, reqOptions); - return this.client.execute(request); - } - - /** - * Retrieves the list of permissions for this bucket. - * - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object, Error>} - */ - getPermissions(options = {}) { - return this.client.execute({ - path: (0, _endpoint2.default)("bucket", this.name), - headers: _extends({}, this.options.headers, options.headers) - }).then(res => res.permissions); - } - - /** - * Replaces all existing bucket permissions with the ones provided. - * - * @param {Object} permissions The permissions object. - * @param {Object} [options={}] The options object - * @param {Boolean} [options.safe] The safe option. - * @param {Object} [options.headers] The headers object option. - * @param {Object} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - setPermissions(permissions, options = {}) { - if (!(0, _utils.isObject)(permissions)) { - throw new Error("A permissions object is required."); - } - const path = (0, _endpoint2.default)("bucket", this.name); - const reqOptions = _extends({}, this._bucketOptions(options)); - const { last_modified } = options; - const data = { last_modified }; - const request = requests.updateRequest(path, { data, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Performs batch operations at the current bucket level. - * - * @param {Function} fn The batch operation function. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Boolean} [options.aggregate] Produces a grouped result object. - * @return {Promise<Object, Error>} - */ - batch(fn, options = {}) { - return this.client.batch(fn, this._bucketOptions(options)); - } -}; -exports.default = Bucket; - -},{"./collection":5,"./endpoint":6,"./requests":9,"./utils":10}],5:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = undefined; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _utils = require("./utils"); - -var _requests = require("./requests"); - -var requests = _interopRequireWildcard(_requests); - -var _endpoint = require("./endpoint"); - -var _endpoint2 = _interopRequireDefault(_endpoint); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -/** - * Abstract representation of a selected collection. - * - */ -let Collection = class Collection { - /** - * Constructor. - * - * @param {KintoClient} client The client instance. - * @param {Bucket} bucket The bucket instance. - * @param {String} name The collection name. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - */ - constructor(client, bucket, name, options = {}) { - /** - * @ignore - */ - this.client = client; - /** - * @ignore - */ - this.bucket = bucket; - /** - * The collection name. - * @type {String} - */ - this.name = name; - - /** - * The default collection options object, embedding the default bucket ones. - * @ignore - * @type {Object} - */ - this.options = _extends({}, this.bucket.options, options, { - headers: _extends({}, this.bucket.options && this.bucket.options.headers, options.headers) - }); - /** - * @ignore - */ - this._isBatch = !!options.batch; - } - - /** - * Merges passed request options with default bucket and collection ones, if - * any. - * - * @private - * @param {Object} [options={}] The options to merge. - * @return {Object} The merged options. - */ - _collOptions(options = {}) { - const headers = _extends({}, this.options && this.options.headers, options.headers); - return _extends({}, this.options, options, { - headers - }); - } - - /** - * Retrieves collection data. - * - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object, Error>} - */ - getData(options = {}) { - const { headers } = this._collOptions(options); - return this.client.execute({ - path: (0, _endpoint2.default)("collection", this.bucket.name, this.name), - headers - }).then(res => res.data); - } - - /** - * Set collection data. - * @param {Object} data The collection data object. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Boolean} [options.patch] The patch option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - setData(data, options = {}) { - if (!(0, _utils.isObject)(data)) { - throw new Error("A collection object is required."); - } - const reqOptions = this._collOptions(options); - const { permissions } = reqOptions; - - const path = (0, _endpoint2.default)("collection", this.bucket.name, this.name); - const request = requests.updateRequest(path, { data, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Retrieves the list of permissions for this collection. - * - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object, Error>} - */ - getPermissions(options = {}) { - const { headers } = this._collOptions(options); - return this.client.execute({ - path: (0, _endpoint2.default)("collection", this.bucket.name, this.name), - headers - }).then(res => res.permissions); - } - - /** - * Replaces all existing collection permissions with the ones provided. - * - * @param {Object} permissions The permissions object. - * @param {Object} [options={}] The options object - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - setPermissions(permissions, options = {}) { - if (!(0, _utils.isObject)(permissions)) { - throw new Error("A permissions object is required."); - } - const reqOptions = this._collOptions(options); - const path = (0, _endpoint2.default)("collection", this.bucket.name, this.name); - const data = { last_modified: options.last_modified }; - const request = requests.updateRequest(path, { data, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Creates a record in current collection. - * - * @param {Object} record The record to create. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @return {Promise<Object, Error>} - */ - createRecord(record, options = {}) { - const reqOptions = this._collOptions(options); - const { permissions } = reqOptions; - const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, record.id); - const request = requests.createRequest(path, { data: record, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Updates a record in current collection. - * - * @param {Object} record The record to update. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - updateRecord(record, options = {}) { - if (!(0, _utils.isObject)(record)) { - throw new Error("A record object is required."); - } - if (!record.id) { - throw new Error("A record id is required."); - } - const reqOptions = this._collOptions(options); - const { permissions } = reqOptions; - const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, record.id); - const request = requests.updateRequest(path, { data: record, permissions }, reqOptions); - return this.client.execute(request); - } - - /** - * Deletes a record from the current collection. - * - * @param {Object|String} record The record to delete. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Number} [options.last_modified] The last_modified option. - * @return {Promise<Object, Error>} - */ - deleteRecord(record, options = {}) { - const recordObj = (0, _utils.toDataBody)(record); - if (!recordObj.id) { - throw new Error("A record id is required."); - } - const { id, last_modified } = recordObj; - const reqOptions = this._collOptions(_extends({ last_modified }, options)); - const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, id); - const request = requests.deleteRequest(path, reqOptions); - return this.client.execute(request); - } - - /** - * Retrieves a record from the current collection. - * - * @param {String} id The record id to retrieve. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @return {Promise<Object, Error>} - */ - getRecord(id, options = {}) { - return this.client.execute(_extends({ - path: (0, _endpoint2.default)("record", this.bucket.name, this.name, id) - }, this._collOptions(options))); - } - - /** - * Lists records from the current collection. - * - * Sorting is done by passing a `sort` string option: - * - * - The field to order the results by, prefixed with `-` for descending. - * Default: `-last_modified`. - * - * @see http://kinto.readthedocs.io/en/stable/core/api/resource.html#sorting - * - * Filtering is done by passing a `filters` option object: - * - * - `{fieldname: "value"}` - * - `{min_fieldname: 4000}` - * - `{in_fieldname: "1,2,3"}` - * - `{not_fieldname: 0}` - * - `{exclude_fieldname: "0,1"}` - * - * @see http://kinto.readthedocs.io/en/stable/core/api/resource.html#filtering - * - * Paginating is done by passing a `limit` option, then calling the `next()` - * method from the resolved result object to fetch the next page, if any. - * - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Object} [options.filters=[]] The filters object. - * @param {String} [options.sort="-last_modified"] The sort field. - * @param {String} [options.limit=null] The limit field. - * @param {String} [options.pages=1] The number of result pages to aggregate. - * @param {Number} [options.since=null] Only retrieve records modified since the provided timestamp. - * @return {Promise<Object, Error>} - */ - listRecords(options = {}) { - const { http } = this.client; - const { sort, filters, limit, pages, since } = _extends({ - sort: "-last_modified" - }, options); - // Safety/Consistency check on ETag value. - if (since && typeof since !== "string") { - throw new Error(`Invalid value for since (${ since }), should be ETag value.`); - } - const collHeaders = this.options.headers; - const path = (0, _endpoint2.default)("record", this.bucket.name, this.name); - const querystring = (0, _utils.qsify)(_extends({}, filters, { - _sort: sort, - _limit: limit, - _since: since - })); - let results = [], - current = 0; - - const next = function (nextPage) { - if (!nextPage) { - throw new Error("Pagination exhausted."); - } - return processNextPage(nextPage); - }; - - const processNextPage = nextPage => { - return http.request(nextPage, { headers: collHeaders }).then(handleResponse); - }; - - const pageResults = (results, nextPage, etag) => { - // ETag string is supposed to be opaque and stored «as-is». - // ETag header values are quoted (because of * and W/"foo"). - return { - last_modified: etag ? etag.replace(/"/g, "") : etag, - data: results, - next: next.bind(null, nextPage) - }; - }; - - const handleResponse = ({ headers, json }) => { - const nextPage = headers.get("Next-Page"); - const etag = headers.get("ETag"); - if (!pages) { - return pageResults(json.data, nextPage, etag); - } - // Aggregate new results with previous ones - results = results.concat(json.data); - current += 1; - if (current >= pages || !nextPage) { - // Pagination exhausted - return pageResults(results, nextPage, etag); - } - // Follow next page - return processNextPage(nextPage); - }; - - return this.client.execute(_extends({ - path: path + "?" + querystring - }, this._collOptions(options)), { raw: true }).then(handleResponse); - } - - /** - * Performs batch operations at the current collection level. - * - * @param {Function} fn The batch operation function. - * @param {Object} [options={}] The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Boolean} [options.safe] The safe option. - * @param {Boolean} [options.aggregate] Produces a grouped result object. - * @return {Promise<Object, Error>} - */ - batch(fn, options = {}) { - const reqOptions = this._collOptions(options); - return this.client.batch(fn, _extends({}, reqOptions, { - bucket: this.bucket.name, - collection: this.name - })); - } -}; -exports.default = Collection; - -},{"./endpoint":6,"./requests":9,"./utils":10}],6:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = endpoint; -/** - * Endpoints templates. - * @type {Object} - */ -const ENDPOINTS = { - root: () => "/", - batch: () => "/batch", - bucket: bucket => "/buckets" + (bucket ? `/${ bucket }` : ""), - collection: (bucket, coll) => `${ ENDPOINTS.bucket(bucket) }/collections` + (coll ? `/${ coll }` : ""), - group: (bucket, group) => `${ ENDPOINTS.bucket(bucket) }/groups` + (group ? `/${ group }` : ""), - record: (bucket, coll, id) => `${ ENDPOINTS.collection(bucket, coll) }/records` + (id ? `/${ id }` : "") -}; - -/** - * Retrieves a server enpoint by its name. - * - * @private - * @param {String} name The endpoint name. - * @param {...string} args The endpoint parameters. - * @return {String} - */ -function endpoint(name, ...args) { - return ENDPOINTS[name](...args); -} - -},{}],7:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -/** - * Kinto server error code descriptors. - * @type {Object} - */ -exports.default = { - 104: "Missing Authorization Token", - 105: "Invalid Authorization Token", - 106: "Request body was not valid JSON", - 107: "Invalid request parameter", - 108: "Missing request parameter", - 109: "Invalid posted data", - 110: "Invalid Token / id", - 111: "Missing Token / id", - 112: "Content-Length header was not provided", - 113: "Request body too large", - 114: "Resource was modified meanwhile", - 115: "Method not allowed on this end point (hint: server may be readonly)", - 116: "Requested version not available on this server", - 117: "Client has sent too many requests", - 121: "Resource access is forbidden for this user", - 122: "Another resource violates constraint", - 201: "Service Temporary unavailable due to high load", - 202: "Service deprecated", - 999: "Internal Server Error" -}; - -},{}],8:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = undefined; - -var _errors = require("./errors"); - -var _errors2 = _interopRequireDefault(_errors); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/** - * Enhanced HTTP client for the Kinto protocol. - * @private - */ -let HTTP = class HTTP { - /** - * Default HTTP request headers applied to each outgoing request. - * - * @type {Object} - */ - static get DEFAULT_REQUEST_HEADERS() { - return { - "Accept": "application/json", - "Content-Type": "application/json" - }; - } - - /** - * Default options. - * - * @type {Object} - */ - static get defaultOptions() { - return { timeout: 5000, requestMode: "cors" }; - } - - /** - * Constructor. - * - * @param {EventEmitter} events The event handler. - * @param {Object} [options={}} The options object. - * @param {Number} [options.timeout=5000] The request timeout in ms (default: `5000`). - * @param {String} [options.requestMode="cors"] The HTTP request mode (default: `"cors"`). - */ - constructor(events, options = {}) { - // public properties - /** - * The event emitter instance. - * @type {EventEmitter} - */ - if (!events) { - throw new Error("No events handler provided"); - } - this.events = events; - - /** - * The request mode. - * @see https://fetch.spec.whatwg.org/#requestmode - * @type {String} - */ - this.requestMode = options.requestMode || HTTP.defaultOptions.requestMode; - - /** - * The request timeout. - * @type {Number} - */ - this.timeout = options.timeout || HTTP.defaultOptions.timeout; - } - - /** - * Performs an HTTP request to the Kinto server. - * - * Resolves with an objet containing the following HTTP response properties: - * - `{Number} status` The HTTP status code. - * - `{Object} json` The JSON response body. - * - `{Headers} headers` The response headers object; see the ES6 fetch() spec. - * - * @param {String} url The URL. - * @param {Object} [options={}] The fetch() options object. - * @param {Object} [options.headers] The request headers object (default: {}) - * @return {Promise} - */ - request(url, options = { headers: {} }) { - let response, status, statusText, headers, hasTimedout; - // Ensure default request headers are always set - options.headers = Object.assign({}, HTTP.DEFAULT_REQUEST_HEADERS, options.headers); - options.mode = this.requestMode; - return new Promise((resolve, reject) => { - const _timeoutId = setTimeout(() => { - hasTimedout = true; - reject(new Error("Request timeout.")); - }, this.timeout); - fetch(url, options).then(res => { - if (!hasTimedout) { - clearTimeout(_timeoutId); - resolve(res); - } - }).catch(err => { - if (!hasTimedout) { - clearTimeout(_timeoutId); - reject(err); - } - }); - }).then(res => { - response = res; - headers = res.headers; - status = res.status; - statusText = res.statusText; - this._checkForDeprecationHeader(headers); - this._checkForBackoffHeader(status, headers); - this._checkForRetryAfterHeader(status, headers); - return res.text(); - }) - // Check if we have a body; if so parse it as JSON. - .then(text => { - if (text.length === 0) { - return null; - } - // Note: we can't consume the response body twice. - return JSON.parse(text); - }).catch(err => { - const error = new Error(`HTTP ${ status || 0 }; ${ err }`); - error.response = response; - error.stack = err.stack; - throw error; - }).then(json => { - if (json && status >= 400) { - let message = `HTTP ${ status } ${ json.error || "" }: `; - if (json.errno && json.errno in _errors2.default) { - const errnoMsg = _errors2.default[json.errno]; - message += errnoMsg; - if (json.message && json.message !== errnoMsg) { - message += ` (${ json.message })`; - } - } else { - message += statusText || ""; - } - const error = new Error(message.trim()); - error.response = response; - error.data = json; - throw error; - } - return { status, json, headers }; - }); - } - - _checkForDeprecationHeader(headers) { - const alertHeader = headers.get("Alert"); - if (!alertHeader) { - return; - } - let alert; - try { - alert = JSON.parse(alertHeader); - } catch (err) { - console.warn("Unable to parse Alert header message", alertHeader); - return; - } - console.warn(alert.message, alert.url); - this.events.emit("deprecated", alert); - } - - _checkForBackoffHeader(status, headers) { - let backoffMs; - const backoffSeconds = parseInt(headers.get("Backoff"), 10); - if (backoffSeconds > 0) { - backoffMs = new Date().getTime() + backoffSeconds * 1000; - } else { - backoffMs = 0; - } - this.events.emit("backoff", backoffMs); - } - - _checkForRetryAfterHeader(status, headers) { - let retryAfter = headers.get("Retry-After"); - if (!retryAfter) { - return; - } - retryAfter = new Date().getTime() + parseInt(retryAfter, 10) * 1000; - this.events.emit("retry-after", retryAfter); - } -}; -exports.default = HTTP; - -},{"./errors":7}],9:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -exports.createRequest = createRequest; -exports.updateRequest = updateRequest; -exports.deleteRequest = deleteRequest; - -var _utils = require("./utils"); - -const requestDefaults = { - safe: false, - // check if we should set default content type here - headers: {}, - permissions: undefined, - data: undefined, - patch: false -}; - -/** - * @private - */ -function safeHeader(safe, last_modified) { - if (!safe) { - return {}; - } - if (last_modified) { - return { "If-Match": `"${ last_modified }"` }; - } - return { "If-None-Match": "*" }; -} - -/** - * @private - */ -function createRequest(path, { data, permissions }, options = {}) { - const { headers, safe } = _extends({}, requestDefaults, options); - return { - method: data && data.id ? "PUT" : "POST", - path, - headers: _extends({}, headers, safeHeader(safe)), - body: { - data, - permissions - } - }; -} - -/** - * @private - */ -function updateRequest(path, { data, permissions }, options = {}) { - const { - headers, - safe, - patch - } = _extends({}, requestDefaults, options); - const { last_modified } = _extends({}, data, options); - - if (Object.keys((0, _utils.omit)(data, "id", "last_modified")).length === 0) { - data = undefined; - } - - return { - method: patch ? "PATCH" : "PUT", - path, - headers: _extends({}, headers, safeHeader(safe, last_modified)), - body: { - data, - permissions - } - }; -} - -/** - * @private - */ -function deleteRequest(path, options = {}) { - const { headers, safe, last_modified } = _extends({}, requestDefaults, options); - if (safe && !last_modified) { - throw new Error("Safe concurrency check requires a last_modified value."); - } - return { - method: "DELETE", - path, - headers: _extends({}, headers, safeHeader(safe, last_modified)) - }; -} - -},{"./utils":10}],10:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.partition = partition; -exports.pMap = pMap; -exports.omit = omit; -exports.toDataBody = toDataBody; -exports.qsify = qsify; -exports.checkVersion = checkVersion; -exports.support = support; -exports.capable = capable; -exports.nobatch = nobatch; -exports.isObject = isObject; -/** - * Chunks an array into n pieces. - * - * @private - * @param {Array} array - * @param {Number} n - * @return {Array} - */ -function partition(array, n) { - if (n <= 0) { - return array; - } - return array.reduce((acc, x, i) => { - if (i === 0 || i % n === 0) { - acc.push([x]); - } else { - acc[acc.length - 1].push(x); - } - return acc; - }, []); -} - -/** - * Maps a list to promises using the provided mapping function, executes them - * sequentially then returns a Promise resolving with ordered results obtained. - * Think of this as a sequential Promise.all. - * - * @private - * @param {Array} list The list to map. - * @param {Function} fn The mapping function. - * @return {Promise} - */ -function pMap(list, fn) { - let results = []; - return list.reduce((promise, entry) => { - return promise.then(() => { - return Promise.resolve(fn(entry)).then(result => results = results.concat(result)); - }); - }, Promise.resolve()).then(() => results); -} - -/** - * Takes an object and returns a copy of it with the provided keys omitted. - * - * @private - * @param {Object} obj The source object. - * @param {...String} keys The keys to omit. - * @return {Object} - */ -function omit(obj, ...keys) { - return Object.keys(obj).reduce((acc, key) => { - if (keys.indexOf(key) === -1) { - acc[key] = obj[key]; - } - return acc; - }, {}); -} - -/** - * Always returns a resource data object from the provided argument. - * - * @private - * @param {Object|String} resource - * @return {Object} - */ -function toDataBody(resource) { - if (isObject(resource)) { - return resource; - } - if (typeof resource === "string") { - return { id: resource }; - } - throw new Error("Invalid argument."); -} - -/** - * Transforms an object into an URL query string, stripping out any undefined - * values. - * - * @param {Object} obj - * @return {String} - */ -function qsify(obj) { - const sep = "&"; - const encode = v => encodeURIComponent(typeof v === "boolean" ? String(v) : v); - const stripUndefined = o => JSON.parse(JSON.stringify(o)); - const stripped = stripUndefined(obj); - return Object.keys(stripped).map(k => { - const ks = encode(k) + "="; - if (Array.isArray(stripped[k])) { - return stripped[k].map(v => ks + encode(v)).join(sep); - } else { - return ks + encode(stripped[k]); - } - }).join(sep); -} - -/** - * Checks if a version is within the provided range. - * - * @param {String} version The version to check. - * @param {String} minVersion The minimum supported version (inclusive). - * @param {String} maxVersion The minimum supported version (exclusive). - * @throws {Error} If the version is outside of the provided range. - */ -function checkVersion(version, minVersion, maxVersion) { - const extract = str => str.split(".").map(x => parseInt(x, 10)); - const [verMajor, verMinor] = extract(version); - const [minMajor, minMinor] = extract(minVersion); - const [maxMajor, maxMinor] = extract(maxVersion); - const checks = [verMajor < minMajor, verMajor === minMajor && verMinor < minMinor, verMajor > maxMajor, verMajor === maxMajor && verMinor >= maxMinor]; - if (checks.some(x => x)) { - throw new Error(`Version ${ version } doesn't satisfy ` + `${ minVersion } <= x < ${ maxVersion }`); - } -} - -/** - * Generates a decorator function ensuring a version check is performed against - * the provided requirements before executing it. - * - * @param {String} min The required min version (inclusive). - * @param {String} max The required max version (inclusive). - * @return {Function} - */ -function support(min, max) { - return function (target, key, descriptor) { - const fn = descriptor.value; - return { - configurable: true, - get() { - const wrappedMethod = (...args) => { - // "this" is the current instance which its method is decorated. - const client = "client" in this ? this.client : this; - return client.fetchHTTPApiVersion().then(version => checkVersion(version, min, max)).then(Promise.resolve(fn.apply(this, args))); - }; - Object.defineProperty(this, key, { - value: wrappedMethod, - configurable: true, - writable: true - }); - return wrappedMethod; - } - }; - }; -} - -/** - * Generates a decorator function ensuring that the specified capabilities are - * available on the server before executing it. - * - * @param {Array<String>} capabilities The required capabilities. - * @return {Function} - */ -function capable(capabilities) { - return function (target, key, descriptor) { - const fn = descriptor.value; - return { - configurable: true, - get() { - const wrappedMethod = (...args) => { - // "this" is the current instance which its method is decorated. - const client = "client" in this ? this.client : this; - return client.fetchServerCapabilities().then(available => { - const missing = capabilities.filter(c => available.indexOf(c) < 0); - if (missing.length > 0) { - throw new Error(`Required capabilities ${ missing.join(", ") } ` + "not present on server"); - } - }).then(Promise.resolve(fn.apply(this, args))); - }; - Object.defineProperty(this, key, { - value: wrappedMethod, - configurable: true, - writable: true - }); - return wrappedMethod; - } - }; - }; -} - -/** - * Generates a decorator function ensuring an operation is not performed from - * within a batch request. - * - * @param {String} message The error message to throw. - * @return {Function} - */ -function nobatch(message) { - return function (target, key, descriptor) { - const fn = descriptor.value; - return { - configurable: true, - get() { - const wrappedMethod = (...args) => { - // "this" is the current instance which its method is decorated. - if (this._isBatch) { - throw new Error(message); - } - return fn.apply(this, args); - }; - Object.defineProperty(this, key, { - value: wrappedMethod, - configurable: true, - writable: true - }); - return wrappedMethod; - } - }; - }; -} - -/** - * Returns true if the specified value is an object (i.e. not an array nor null). - * @param {Object} thing The value to inspect. - * @return {bool} - */ -function isObject(thing) { - return typeof thing === "object" && thing !== null && !Array.isArray(thing); -} - -},{}]},{},[1])(1) -});
\ No newline at end of file diff --git a/services/common/kinto-offline-client.js b/services/common/kinto-offline-client.js deleted file mode 100644 index 4d0dbd0f3..000000000 --- a/services/common/kinto-offline-client.js +++ /dev/null @@ -1,4286 +0,0 @@ -/* - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * This file is generated from kinto.js - do not modify directly. - */ - -this.EXPORTED_SYMBOLS = ["loadKinto"]; - -/* - * Version 5.1.0 - 8beb61d - */ - -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.loadKinto = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _extends2 = require("babel-runtime/helpers/extends"); - -var _extends3 = _interopRequireDefault(_extends2); - -var _stringify = require("babel-runtime/core-js/json/stringify"); - -var _stringify2 = _interopRequireDefault(_stringify); - -var _promise = require("babel-runtime/core-js/promise"); - -var _promise2 = _interopRequireDefault(_promise); - -exports.reduceRecords = reduceRecords; - -var _base = require("../src/adapters/base"); - -var _base2 = _interopRequireDefault(_base); - -var _utils = require("../src/utils"); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -Components.utils.import("resource://gre/modules/Sqlite.jsm"); -Components.utils.import("resource://gre/modules/Task.jsm"); - -const SQLITE_PATH = "kinto.sqlite"; - -const statements = { - "createCollectionData": ` - CREATE TABLE collection_data ( - collection_name TEXT, - record_id TEXT, - record TEXT - );`, - - "createCollectionMetadata": ` - CREATE TABLE collection_metadata ( - collection_name TEXT PRIMARY KEY, - last_modified INTEGER - ) WITHOUT ROWID;`, - - "createCollectionDataRecordIdIndex": ` - CREATE UNIQUE INDEX unique_collection_record - ON collection_data(collection_name, record_id);`, - - "clearData": ` - DELETE FROM collection_data - WHERE collection_name = :collection_name;`, - - "createData": ` - INSERT INTO collection_data (collection_name, record_id, record) - VALUES (:collection_name, :record_id, :record);`, - - "updateData": ` - INSERT OR REPLACE INTO collection_data (collection_name, record_id, record) - VALUES (:collection_name, :record_id, :record);`, - - "deleteData": ` - DELETE FROM collection_data - WHERE collection_name = :collection_name - AND record_id = :record_id;`, - - "saveLastModified": ` - REPLACE INTO collection_metadata (collection_name, last_modified) - VALUES (:collection_name, :last_modified);`, - - "getLastModified": ` - SELECT last_modified - FROM collection_metadata - WHERE collection_name = :collection_name;`, - - "getRecord": ` - SELECT record - FROM collection_data - WHERE collection_name = :collection_name - AND record_id = :record_id;`, - - "listRecords": ` - SELECT record - FROM collection_data - WHERE collection_name = :collection_name;`, - - // N.B. we have to have a dynamic number of placeholders, which you - // can't do without building your own statement. See `execute` for details - "listRecordsById": ` - SELECT record_id, record - FROM collection_data - WHERE collection_name = ? - AND record_id IN `, - - "importData": ` - REPLACE INTO collection_data (collection_name, record_id, record) - VALUES (:collection_name, :record_id, :record);`, - - "scanAllRecords": `SELECT * FROM collection_data;`, - - "clearCollectionMetadata": `DELETE FROM collection_metadata;` -}; - -const createStatements = ["createCollectionData", "createCollectionMetadata", "createCollectionDataRecordIdIndex"]; - -const currentSchemaVersion = 1; - -/** - * Firefox adapter. - * - * Uses Sqlite as a backing store. - * - * Options: - * - path: the filename/path for the Sqlite database. If absent, use SQLITE_PATH. - */ -class FirefoxAdapter extends _base2.default { - constructor(collection, options = {}) { - super(); - const { sqliteHandle = null } = options; - this.collection = collection; - this._connection = sqliteHandle; - this._options = options; - } - - // We need to be capable of calling this from "outside" the adapter - // so that someone can initialize a connection and pass it to us in - // adapterOptions. - static _init(connection) { - return Task.spawn(function* () { - yield connection.executeTransaction(function* doSetup() { - const schema = yield connection.getSchemaVersion(); - - if (schema == 0) { - - for (let statementName of createStatements) { - yield connection.execute(statements[statementName]); - } - - yield connection.setSchemaVersion(currentSchemaVersion); - } else if (schema != 1) { - throw new Error("Unknown database schema: " + schema); - } - }); - return connection; - }); - } - - _executeStatement(statement, params) { - if (!this._connection) { - throw new Error("The storage adapter is not open"); - } - return this._connection.executeCached(statement, params); - } - - open() { - const self = this; - return Task.spawn(function* () { - if (!self._connection) { - const path = self._options.path || SQLITE_PATH; - const opts = { path, sharedMemoryCache: false }; - self._connection = yield Sqlite.openConnection(opts).then(FirefoxAdapter._init); - } - }); - } - - close() { - if (this._connection) { - const promise = this._connection.close(); - this._connection = null; - return promise; - } - return _promise2.default.resolve(); - } - - clear() { - const params = { collection_name: this.collection }; - return this._executeStatement(statements.clearData, params); - } - - execute(callback, options = { preload: [] }) { - if (!this._connection) { - throw new Error("The storage adapter is not open"); - } - - let result; - const conn = this._connection; - const collection = this.collection; - - return conn.executeTransaction(function* doExecuteTransaction() { - // Preload specified records from DB, within transaction. - const parameters = [collection, ...options.preload]; - const placeholders = options.preload.map(_ => "?"); - const stmt = statements.listRecordsById + "(" + placeholders.join(",") + ");"; - const rows = yield conn.execute(stmt, parameters); - - const preloaded = rows.reduce((acc, row) => { - const record = JSON.parse(row.getResultByName("record")); - acc[row.getResultByName("record_id")] = record; - return acc; - }, {}); - - const proxy = transactionProxy(collection, preloaded); - result = callback(proxy); - - for (let { statement, params } of proxy.operations) { - yield conn.executeCached(statement, params); - } - }, conn.TRANSACTION_EXCLUSIVE).then(_ => result); - } - - get(id) { - const params = { - collection_name: this.collection, - record_id: id - }; - return this._executeStatement(statements.getRecord, params).then(result => { - if (result.length == 0) { - return; - } - return JSON.parse(result[0].getResultByName("record")); - }); - } - - list(params = { filters: {}, order: "" }) { - const parameters = { - collection_name: this.collection - }; - return this._executeStatement(statements.listRecords, parameters).then(result => { - const records = []; - for (let k = 0; k < result.length; k++) { - const row = result[k]; - records.push(JSON.parse(row.getResultByName("record"))); - } - return records; - }).then(results => { - // The resulting list of records is filtered and sorted. - // XXX: with some efforts, this could be implemented using SQL. - return reduceRecords(params.filters, params.order, results); - }); - } - - /** - * Load a list of records into the local database. - * - * Note: The adapter is not in charge of filtering the already imported - * records. This is done in `Collection#loadDump()`, as a common behaviour - * between every adapters. - * - * @param {Array} records. - * @return {Array} imported records. - */ - loadDump(records) { - const connection = this._connection; - const collection_name = this.collection; - return Task.spawn(function* () { - yield connection.executeTransaction(function* doImport() { - for (let record of records) { - const params = { - collection_name: collection_name, - record_id: record.id, - record: (0, _stringify2.default)(record) - }; - yield connection.execute(statements.importData, params); - } - const lastModified = Math.max(...records.map(record => record.last_modified)); - const params = { - collection_name: collection_name - }; - const previousLastModified = yield connection.execute(statements.getLastModified, params).then(result => { - return result.length > 0 ? result[0].getResultByName("last_modified") : -1; - }); - if (lastModified > previousLastModified) { - const params = { - collection_name: collection_name, - last_modified: lastModified - }; - yield connection.execute(statements.saveLastModified, params); - } - }); - return records; - }); - } - - saveLastModified(lastModified) { - const parsedLastModified = parseInt(lastModified, 10) || null; - const params = { - collection_name: this.collection, - last_modified: parsedLastModified - }; - return this._executeStatement(statements.saveLastModified, params).then(() => parsedLastModified); - } - - getLastModified() { - const params = { - collection_name: this.collection - }; - return this._executeStatement(statements.getLastModified, params).then(result => { - if (result.length == 0) { - return 0; - } - return result[0].getResultByName("last_modified"); - }); - } - - /** - * Reset the sync status of every record and collection we have - * access to. - */ - resetSyncStatus() { - // We're going to use execute instead of executeCached, so build - // in our own sanity check - if (!this._connection) { - throw new Error("The storage adapter is not open"); - } - - return this._connection.executeTransaction(function* (conn) { - const promises = []; - yield conn.execute(statements.scanAllRecords, null, function (row) { - const record = JSON.parse(row.getResultByName("record")); - const record_id = row.getResultByName("record_id"); - const collection_name = row.getResultByName("collection_name"); - if (record._status === "deleted") { - // Garbage collect deleted records. - promises.push(conn.execute(statements.deleteData, { collection_name, record_id })); - } else { - const newRecord = (0, _extends3.default)({}, record, { - _status: "created", - last_modified: undefined - }); - promises.push(conn.execute(statements.updateData, { record: (0, _stringify2.default)(newRecord), record_id, collection_name })); - } - }); - yield _promise2.default.all(promises); - yield conn.execute(statements.clearCollectionMetadata); - }); - } -} - -exports.default = FirefoxAdapter; -function transactionProxy(collection, preloaded) { - const _operations = []; - - return { - get operations() { - return _operations; - }, - - create(record) { - _operations.push({ - statement: statements.createData, - params: { - collection_name: collection, - record_id: record.id, - record: (0, _stringify2.default)(record) - } - }); - }, - - update(record) { - _operations.push({ - statement: statements.updateData, - params: { - collection_name: collection, - record_id: record.id, - record: (0, _stringify2.default)(record) - } - }); - }, - - delete(id) { - _operations.push({ - statement: statements.deleteData, - params: { - collection_name: collection, - record_id: id - } - }); - }, - - get(id) { - // Gecko JS engine outputs undesired warnings if id is not in preloaded. - return id in preloaded ? preloaded[id] : undefined; - } - }; -} - -/** - * Filter and sort list against provided filters and order. - * - * @param {Object} filters The filters to apply. - * @param {String} order The order to apply. - * @param {Array} list The list to reduce. - * @return {Array} - */ -function reduceRecords(filters, order, list) { - const filtered = filters ? (0, _utils.filterObjects)(filters, list) : list; - return order ? (0, _utils.sortObjects)(order, filtered) : filtered; -} - -},{"../src/adapters/base":85,"../src/utils":87,"babel-runtime/core-js/json/stringify":3,"babel-runtime/core-js/promise":6,"babel-runtime/helpers/extends":8}],2:[function(require,module,exports){ -/* - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _extends2 = require("babel-runtime/helpers/extends"); - -var _extends3 = _interopRequireDefault(_extends2); - -exports.default = loadKinto; - -var _base = require("../src/adapters/base"); - -var _base2 = _interopRequireDefault(_base); - -var _KintoBase = require("../src/KintoBase"); - -var _KintoBase2 = _interopRequireDefault(_KintoBase); - -var _FirefoxStorage = require("./FirefoxStorage"); - -var _FirefoxStorage2 = _interopRequireDefault(_FirefoxStorage); - -var _utils = require("../src/utils"); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -const { classes: Cc, interfaces: Ci, utils: Cu } = Components; - -function loadKinto() { - const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {}); - const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); - - // Use standalone kinto-http module landed in FFx. - const { KintoHttpClient } = Cu.import("resource://services-common/kinto-http-client.js"); - - Cu.import("resource://gre/modules/Timer.jsm"); - Cu.importGlobalProperties(['fetch']); - - // Leverage Gecko service to generate UUIDs. - function makeIDSchema() { - return { - validate: _utils.RE_UUID.test.bind(_utils.RE_UUID), - generate: function () { - return generateUUID().toString().replace(/[{}]/g, ""); - } - }; - } - - class KintoFX extends _KintoBase2.default { - static get adapters() { - return { - BaseAdapter: _base2.default, - FirefoxAdapter: _FirefoxStorage2.default - }; - } - - constructor(options = {}) { - const emitter = {}; - EventEmitter.decorate(emitter); - - const defaults = { - events: emitter, - ApiClass: KintoHttpClient, - adapter: _FirefoxStorage2.default - }; - - const expandedOptions = (0, _extends3.default)({}, defaults, options); - super(expandedOptions); - } - - collection(collName, options = {}) { - const idSchema = makeIDSchema(); - const expandedOptions = (0, _extends3.default)({ idSchema }, options); - return super.collection(collName, expandedOptions); - } - } - - return KintoFX; -} - -// This fixes compatibility with CommonJS required by browserify. -// See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495 -if (typeof module === "object") { - module.exports = loadKinto; -} - -},{"../src/KintoBase":83,"../src/adapters/base":85,"../src/utils":87,"./FirefoxStorage":1,"babel-runtime/helpers/extends":8}],3:[function(require,module,exports){ -module.exports = { "default": require("core-js/library/fn/json/stringify"), __esModule: true }; -},{"core-js/library/fn/json/stringify":10}],4:[function(require,module,exports){ -module.exports = { "default": require("core-js/library/fn/object/assign"), __esModule: true }; -},{"core-js/library/fn/object/assign":11}],5:[function(require,module,exports){ -module.exports = { "default": require("core-js/library/fn/object/keys"), __esModule: true }; -},{"core-js/library/fn/object/keys":12}],6:[function(require,module,exports){ -module.exports = { "default": require("core-js/library/fn/promise"), __esModule: true }; -},{"core-js/library/fn/promise":13}],7:[function(require,module,exports){ -"use strict"; - -exports.__esModule = true; - -var _promise = require("../core-js/promise"); - -var _promise2 = _interopRequireDefault(_promise); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -exports.default = function (fn) { - return function () { - var gen = fn.apply(this, arguments); - return new _promise2.default(function (resolve, reject) { - function step(key, arg) { - try { - var info = gen[key](arg); - var value = info.value; - } catch (error) { - reject(error); - return; - } - - if (info.done) { - resolve(value); - } else { - return _promise2.default.resolve(value).then(function (value) { - return step("next", value); - }, function (err) { - return step("throw", err); - }); - } - } - - return step("next"); - }); - }; -}; -},{"../core-js/promise":6}],8:[function(require,module,exports){ -"use strict"; - -exports.__esModule = true; - -var _assign = require("../core-js/object/assign"); - -var _assign2 = _interopRequireDefault(_assign); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -exports.default = _assign2.default || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; -}; -},{"../core-js/object/assign":4}],9:[function(require,module,exports){ - -},{}],10:[function(require,module,exports){ -var core = require('../../modules/_core') - , $JSON = core.JSON || (core.JSON = {stringify: JSON.stringify}); -module.exports = function stringify(it){ // eslint-disable-line no-unused-vars - return $JSON.stringify.apply($JSON, arguments); -}; -},{"../../modules/_core":21}],11:[function(require,module,exports){ -require('../../modules/es6.object.assign'); -module.exports = require('../../modules/_core').Object.assign; -},{"../../modules/_core":21,"../../modules/es6.object.assign":77}],12:[function(require,module,exports){ -require('../../modules/es6.object.keys'); -module.exports = require('../../modules/_core').Object.keys; -},{"../../modules/_core":21,"../../modules/es6.object.keys":78}],13:[function(require,module,exports){ -require('../modules/es6.object.to-string'); -require('../modules/es6.string.iterator'); -require('../modules/web.dom.iterable'); -require('../modules/es6.promise'); -module.exports = require('../modules/_core').Promise; -},{"../modules/_core":21,"../modules/es6.object.to-string":79,"../modules/es6.promise":80,"../modules/es6.string.iterator":81,"../modules/web.dom.iterable":82}],14:[function(require,module,exports){ -module.exports = function(it){ - if(typeof it != 'function')throw TypeError(it + ' is not a function!'); - return it; -}; -},{}],15:[function(require,module,exports){ -module.exports = function(){ /* empty */ }; -},{}],16:[function(require,module,exports){ -module.exports = function(it, Constructor, name, forbiddenField){ - if(!(it instanceof Constructor) || (forbiddenField !== undefined && forbiddenField in it)){ - throw TypeError(name + ': incorrect invocation!'); - } return it; -}; -},{}],17:[function(require,module,exports){ -var isObject = require('./_is-object'); -module.exports = function(it){ - if(!isObject(it))throw TypeError(it + ' is not an object!'); - return it; -}; -},{"./_is-object":38}],18:[function(require,module,exports){ -// false -> Array#indexOf -// true -> Array#includes -var toIObject = require('./_to-iobject') - , toLength = require('./_to-length') - , toIndex = require('./_to-index'); -module.exports = function(IS_INCLUDES){ - return function($this, el, fromIndex){ - var O = toIObject($this) - , length = toLength(O.length) - , index = toIndex(fromIndex, length) - , value; - // Array#includes uses SameValueZero equality algorithm - if(IS_INCLUDES && el != el)while(length > index){ - value = O[index++]; - if(value != value)return true; - // Array#toIndex ignores holes, Array#includes - not - } else for(;length > index; index++)if(IS_INCLUDES || index in O){ - if(O[index] === el)return IS_INCLUDES || index || 0; - } return !IS_INCLUDES && -1; - }; -}; -},{"./_to-index":67,"./_to-iobject":69,"./_to-length":70}],19:[function(require,module,exports){ -// getting tag from 19.1.3.6 Object.prototype.toString() -var cof = require('./_cof') - , TAG = require('./_wks')('toStringTag') - // ES3 wrong here - , ARG = cof(function(){ return arguments; }()) == 'Arguments'; - -// fallback for IE11 Script Access Denied error -var tryGet = function(it, key){ - try { - return it[key]; - } catch(e){ /* empty */ } -}; - -module.exports = function(it){ - var O, T, B; - return it === undefined ? 'Undefined' : it === null ? 'Null' - // @@toStringTag case - : typeof (T = tryGet(O = Object(it), TAG)) == 'string' ? T - // builtinTag case - : ARG ? cof(O) - // ES3 arguments fallback - : (B = cof(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : B; -}; -},{"./_cof":20,"./_wks":74}],20:[function(require,module,exports){ -var toString = {}.toString; - -module.exports = function(it){ - return toString.call(it).slice(8, -1); -}; -},{}],21:[function(require,module,exports){ -var core = module.exports = {version: '2.4.0'}; -if(typeof __e == 'number')__e = core; // eslint-disable-line no-undef -},{}],22:[function(require,module,exports){ -// optional / simple context binding -var aFunction = require('./_a-function'); -module.exports = function(fn, that, length){ - aFunction(fn); - if(that === undefined)return fn; - switch(length){ - case 1: return function(a){ - return fn.call(that, a); - }; - case 2: return function(a, b){ - return fn.call(that, a, b); - }; - case 3: return function(a, b, c){ - return fn.call(that, a, b, c); - }; - } - return function(/* ...args */){ - return fn.apply(that, arguments); - }; -}; -},{"./_a-function":14}],23:[function(require,module,exports){ -// 7.2.1 RequireObjectCoercible(argument) -module.exports = function(it){ - if(it == undefined)throw TypeError("Can't call method on " + it); - return it; -}; -},{}],24:[function(require,module,exports){ -// Thank's IE8 for his funny defineProperty -module.exports = !require('./_fails')(function(){ - return Object.defineProperty({}, 'a', {get: function(){ return 7; }}).a != 7; -}); -},{"./_fails":28}],25:[function(require,module,exports){ -var isObject = require('./_is-object') - , document = require('./_global').document - // in old IE typeof document.createElement is 'object' - , is = isObject(document) && isObject(document.createElement); -module.exports = function(it){ - return is ? document.createElement(it) : {}; -}; -},{"./_global":30,"./_is-object":38}],26:[function(require,module,exports){ -// IE 8- don't enum bug keys -module.exports = ( - 'constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf' -).split(','); -},{}],27:[function(require,module,exports){ -var global = require('./_global') - , core = require('./_core') - , ctx = require('./_ctx') - , hide = require('./_hide') - , PROTOTYPE = 'prototype'; - -var $export = function(type, name, source){ - var IS_FORCED = type & $export.F - , IS_GLOBAL = type & $export.G - , IS_STATIC = type & $export.S - , IS_PROTO = type & $export.P - , IS_BIND = type & $export.B - , IS_WRAP = type & $export.W - , exports = IS_GLOBAL ? core : core[name] || (core[name] = {}) - , expProto = exports[PROTOTYPE] - , target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE] - , key, own, out; - if(IS_GLOBAL)source = name; - for(key in source){ - // contains in native - own = !IS_FORCED && target && target[key] !== undefined; - if(own && key in exports)continue; - // export native or passed - out = own ? target[key] : source[key]; - // prevent global pollution for namespaces - exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key] - // bind timers to global for call from export context - : IS_BIND && own ? ctx(out, global) - // wrap global constructors for prevent change them in library - : IS_WRAP && target[key] == out ? (function(C){ - var F = function(a, b, c){ - if(this instanceof C){ - switch(arguments.length){ - case 0: return new C; - case 1: return new C(a); - case 2: return new C(a, b); - } return new C(a, b, c); - } return C.apply(this, arguments); - }; - F[PROTOTYPE] = C[PROTOTYPE]; - return F; - // make static versions for prototype methods - })(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out; - // export proto methods to core.%CONSTRUCTOR%.methods.%NAME% - if(IS_PROTO){ - (exports.virtual || (exports.virtual = {}))[key] = out; - // export proto methods to core.%CONSTRUCTOR%.prototype.%NAME% - if(type & $export.R && expProto && !expProto[key])hide(expProto, key, out); - } - } -}; -// type bitmap -$export.F = 1; // forced -$export.G = 2; // global -$export.S = 4; // static -$export.P = 8; // proto -$export.B = 16; // bind -$export.W = 32; // wrap -$export.U = 64; // safe -$export.R = 128; // real proto method for `library` -module.exports = $export; -},{"./_core":21,"./_ctx":22,"./_global":30,"./_hide":32}],28:[function(require,module,exports){ -module.exports = function(exec){ - try { - return !!exec(); - } catch(e){ - return true; - } -}; -},{}],29:[function(require,module,exports){ -var ctx = require('./_ctx') - , call = require('./_iter-call') - , isArrayIter = require('./_is-array-iter') - , anObject = require('./_an-object') - , toLength = require('./_to-length') - , getIterFn = require('./core.get-iterator-method') - , BREAK = {} - , RETURN = {}; -var exports = module.exports = function(iterable, entries, fn, that, ITERATOR){ - var iterFn = ITERATOR ? function(){ return iterable; } : getIterFn(iterable) - , f = ctx(fn, that, entries ? 2 : 1) - , index = 0 - , length, step, iterator, result; - if(typeof iterFn != 'function')throw TypeError(iterable + ' is not iterable!'); - // fast case for arrays with default iterator - if(isArrayIter(iterFn))for(length = toLength(iterable.length); length > index; index++){ - result = entries ? f(anObject(step = iterable[index])[0], step[1]) : f(iterable[index]); - if(result === BREAK || result === RETURN)return result; - } else for(iterator = iterFn.call(iterable); !(step = iterator.next()).done; ){ - result = call(iterator, f, step.value, entries); - if(result === BREAK || result === RETURN)return result; - } -}; -exports.BREAK = BREAK; -exports.RETURN = RETURN; -},{"./_an-object":17,"./_ctx":22,"./_is-array-iter":37,"./_iter-call":39,"./_to-length":70,"./core.get-iterator-method":75}],30:[function(require,module,exports){ -// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 -var global = module.exports = typeof window != 'undefined' && window.Math == Math - ? window : typeof self != 'undefined' && self.Math == Math ? self : Function('return this')(); -if(typeof __g == 'number')__g = global; // eslint-disable-line no-undef -},{}],31:[function(require,module,exports){ -var hasOwnProperty = {}.hasOwnProperty; -module.exports = function(it, key){ - return hasOwnProperty.call(it, key); -}; -},{}],32:[function(require,module,exports){ -var dP = require('./_object-dp') - , createDesc = require('./_property-desc'); -module.exports = require('./_descriptors') ? function(object, key, value){ - return dP.f(object, key, createDesc(1, value)); -} : function(object, key, value){ - object[key] = value; - return object; -}; -},{"./_descriptors":24,"./_object-dp":49,"./_property-desc":57}],33:[function(require,module,exports){ -module.exports = require('./_global').document && document.documentElement; -},{"./_global":30}],34:[function(require,module,exports){ -module.exports = !require('./_descriptors') && !require('./_fails')(function(){ - return Object.defineProperty(require('./_dom-create')('div'), 'a', {get: function(){ return 7; }}).a != 7; -}); -},{"./_descriptors":24,"./_dom-create":25,"./_fails":28}],35:[function(require,module,exports){ -// fast apply, http://jsperf.lnkit.com/fast-apply/5 -module.exports = function(fn, args, that){ - var un = that === undefined; - switch(args.length){ - case 0: return un ? fn() - : fn.call(that); - case 1: return un ? fn(args[0]) - : fn.call(that, args[0]); - case 2: return un ? fn(args[0], args[1]) - : fn.call(that, args[0], args[1]); - case 3: return un ? fn(args[0], args[1], args[2]) - : fn.call(that, args[0], args[1], args[2]); - case 4: return un ? fn(args[0], args[1], args[2], args[3]) - : fn.call(that, args[0], args[1], args[2], args[3]); - } return fn.apply(that, args); -}; -},{}],36:[function(require,module,exports){ -// fallback for non-array-like ES3 and non-enumerable old V8 strings -var cof = require('./_cof'); -module.exports = Object('z').propertyIsEnumerable(0) ? Object : function(it){ - return cof(it) == 'String' ? it.split('') : Object(it); -}; -},{"./_cof":20}],37:[function(require,module,exports){ -// check on default Array iterator -var Iterators = require('./_iterators') - , ITERATOR = require('./_wks')('iterator') - , ArrayProto = Array.prototype; - -module.exports = function(it){ - return it !== undefined && (Iterators.Array === it || ArrayProto[ITERATOR] === it); -}; -},{"./_iterators":44,"./_wks":74}],38:[function(require,module,exports){ -module.exports = function(it){ - return typeof it === 'object' ? it !== null : typeof it === 'function'; -}; -},{}],39:[function(require,module,exports){ -// call something on iterator step with safe closing on error -var anObject = require('./_an-object'); -module.exports = function(iterator, fn, value, entries){ - try { - return entries ? fn(anObject(value)[0], value[1]) : fn(value); - // 7.4.6 IteratorClose(iterator, completion) - } catch(e){ - var ret = iterator['return']; - if(ret !== undefined)anObject(ret.call(iterator)); - throw e; - } -}; -},{"./_an-object":17}],40:[function(require,module,exports){ -'use strict'; -var create = require('./_object-create') - , descriptor = require('./_property-desc') - , setToStringTag = require('./_set-to-string-tag') - , IteratorPrototype = {}; - -// 25.1.2.1.1 %IteratorPrototype%[@@iterator]() -require('./_hide')(IteratorPrototype, require('./_wks')('iterator'), function(){ return this; }); - -module.exports = function(Constructor, NAME, next){ - Constructor.prototype = create(IteratorPrototype, {next: descriptor(1, next)}); - setToStringTag(Constructor, NAME + ' Iterator'); -}; -},{"./_hide":32,"./_object-create":48,"./_property-desc":57,"./_set-to-string-tag":61,"./_wks":74}],41:[function(require,module,exports){ -'use strict'; -var LIBRARY = require('./_library') - , $export = require('./_export') - , redefine = require('./_redefine') - , hide = require('./_hide') - , has = require('./_has') - , Iterators = require('./_iterators') - , $iterCreate = require('./_iter-create') - , setToStringTag = require('./_set-to-string-tag') - , getPrototypeOf = require('./_object-gpo') - , ITERATOR = require('./_wks')('iterator') - , BUGGY = !([].keys && 'next' in [].keys()) // Safari has buggy iterators w/o `next` - , FF_ITERATOR = '@@iterator' - , KEYS = 'keys' - , VALUES = 'values'; - -var returnThis = function(){ return this; }; - -module.exports = function(Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCED){ - $iterCreate(Constructor, NAME, next); - var getMethod = function(kind){ - if(!BUGGY && kind in proto)return proto[kind]; - switch(kind){ - case KEYS: return function keys(){ return new Constructor(this, kind); }; - case VALUES: return function values(){ return new Constructor(this, kind); }; - } return function entries(){ return new Constructor(this, kind); }; - }; - var TAG = NAME + ' Iterator' - , DEF_VALUES = DEFAULT == VALUES - , VALUES_BUG = false - , proto = Base.prototype - , $native = proto[ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT] - , $default = $native || getMethod(DEFAULT) - , $entries = DEFAULT ? !DEF_VALUES ? $default : getMethod('entries') : undefined - , $anyNative = NAME == 'Array' ? proto.entries || $native : $native - , methods, key, IteratorPrototype; - // Fix native - if($anyNative){ - IteratorPrototype = getPrototypeOf($anyNative.call(new Base)); - if(IteratorPrototype !== Object.prototype){ - // Set @@toStringTag to native iterators - setToStringTag(IteratorPrototype, TAG, true); - // fix for some old engines - if(!LIBRARY && !has(IteratorPrototype, ITERATOR))hide(IteratorPrototype, ITERATOR, returnThis); - } - } - // fix Array#{values, @@iterator}.name in V8 / FF - if(DEF_VALUES && $native && $native.name !== VALUES){ - VALUES_BUG = true; - $default = function values(){ return $native.call(this); }; - } - // Define iterator - if((!LIBRARY || FORCED) && (BUGGY || VALUES_BUG || !proto[ITERATOR])){ - hide(proto, ITERATOR, $default); - } - // Plug for library - Iterators[NAME] = $default; - Iterators[TAG] = returnThis; - if(DEFAULT){ - methods = { - values: DEF_VALUES ? $default : getMethod(VALUES), - keys: IS_SET ? $default : getMethod(KEYS), - entries: $entries - }; - if(FORCED)for(key in methods){ - if(!(key in proto))redefine(proto, key, methods[key]); - } else $export($export.P + $export.F * (BUGGY || VALUES_BUG), NAME, methods); - } - return methods; -}; -},{"./_export":27,"./_has":31,"./_hide":32,"./_iter-create":40,"./_iterators":44,"./_library":45,"./_object-gpo":52,"./_redefine":59,"./_set-to-string-tag":61,"./_wks":74}],42:[function(require,module,exports){ -var ITERATOR = require('./_wks')('iterator') - , SAFE_CLOSING = false; - -try { - var riter = [7][ITERATOR](); - riter['return'] = function(){ SAFE_CLOSING = true; }; - Array.from(riter, function(){ throw 2; }); -} catch(e){ /* empty */ } - -module.exports = function(exec, skipClosing){ - if(!skipClosing && !SAFE_CLOSING)return false; - var safe = false; - try { - var arr = [7] - , iter = arr[ITERATOR](); - iter.next = function(){ return {done: safe = true}; }; - arr[ITERATOR] = function(){ return iter; }; - exec(arr); - } catch(e){ /* empty */ } - return safe; -}; -},{"./_wks":74}],43:[function(require,module,exports){ -module.exports = function(done, value){ - return {value: value, done: !!done}; -}; -},{}],44:[function(require,module,exports){ -module.exports = {}; -},{}],45:[function(require,module,exports){ -module.exports = true; -},{}],46:[function(require,module,exports){ -var global = require('./_global') - , macrotask = require('./_task').set - , Observer = global.MutationObserver || global.WebKitMutationObserver - , process = global.process - , Promise = global.Promise - , isNode = require('./_cof')(process) == 'process'; - -module.exports = function(){ - var head, last, notify; - - var flush = function(){ - var parent, fn; - if(isNode && (parent = process.domain))parent.exit(); - while(head){ - fn = head.fn; - head = head.next; - try { - fn(); - } catch(e){ - if(head)notify(); - else last = undefined; - throw e; - } - } last = undefined; - if(parent)parent.enter(); - }; - - // Node.js - if(isNode){ - notify = function(){ - process.nextTick(flush); - }; - // browsers with MutationObserver - } else if(Observer){ - var toggle = true - , node = document.createTextNode(''); - new Observer(flush).observe(node, {characterData: true}); // eslint-disable-line no-new - notify = function(){ - node.data = toggle = !toggle; - }; - // environments with maybe non-completely correct, but existent Promise - } else if(Promise && Promise.resolve){ - var promise = Promise.resolve(); - notify = function(){ - promise.then(flush); - }; - // for other environments - macrotask based on: - // - setImmediate - // - MessageChannel - // - window.postMessag - // - onreadystatechange - // - setTimeout - } else { - notify = function(){ - // strange IE + webpack dev server bug - use .call(global) - macrotask.call(global, flush); - }; - } - - return function(fn){ - var task = {fn: fn, next: undefined}; - if(last)last.next = task; - if(!head){ - head = task; - notify(); - } last = task; - }; -}; -},{"./_cof":20,"./_global":30,"./_task":66}],47:[function(require,module,exports){ -'use strict'; -// 19.1.2.1 Object.assign(target, source, ...) -var getKeys = require('./_object-keys') - , gOPS = require('./_object-gops') - , pIE = require('./_object-pie') - , toObject = require('./_to-object') - , IObject = require('./_iobject') - , $assign = Object.assign; - -// should work with symbols and should have deterministic property order (V8 bug) -module.exports = !$assign || require('./_fails')(function(){ - var A = {} - , B = {} - , S = Symbol() - , K = 'abcdefghijklmnopqrst'; - A[S] = 7; - K.split('').forEach(function(k){ B[k] = k; }); - return $assign({}, A)[S] != 7 || Object.keys($assign({}, B)).join('') != K; -}) ? function assign(target, source){ // eslint-disable-line no-unused-vars - var T = toObject(target) - , aLen = arguments.length - , index = 1 - , getSymbols = gOPS.f - , isEnum = pIE.f; - while(aLen > index){ - var S = IObject(arguments[index++]) - , keys = getSymbols ? getKeys(S).concat(getSymbols(S)) : getKeys(S) - , length = keys.length - , j = 0 - , key; - while(length > j)if(isEnum.call(S, key = keys[j++]))T[key] = S[key]; - } return T; -} : $assign; -},{"./_fails":28,"./_iobject":36,"./_object-gops":51,"./_object-keys":54,"./_object-pie":55,"./_to-object":71}],48:[function(require,module,exports){ -// 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties]) -var anObject = require('./_an-object') - , dPs = require('./_object-dps') - , enumBugKeys = require('./_enum-bug-keys') - , IE_PROTO = require('./_shared-key')('IE_PROTO') - , Empty = function(){ /* empty */ } - , PROTOTYPE = 'prototype'; - -// Create object with fake `null` prototype: use iframe Object with cleared prototype -var createDict = function(){ - // Thrash, waste and sodomy: IE GC bug - var iframe = require('./_dom-create')('iframe') - , i = enumBugKeys.length - , lt = '<' - , gt = '>' - , iframeDocument; - iframe.style.display = 'none'; - require('./_html').appendChild(iframe); - iframe.src = 'javascript:'; // eslint-disable-line no-script-url - // createDict = iframe.contentWindow.Object; - // html.removeChild(iframe); - iframeDocument = iframe.contentWindow.document; - iframeDocument.open(); - iframeDocument.write(lt + 'script' + gt + 'document.F=Object' + lt + '/script' + gt); - iframeDocument.close(); - createDict = iframeDocument.F; - while(i--)delete createDict[PROTOTYPE][enumBugKeys[i]]; - return createDict(); -}; - -module.exports = Object.create || function create(O, Properties){ - var result; - if(O !== null){ - Empty[PROTOTYPE] = anObject(O); - result = new Empty; - Empty[PROTOTYPE] = null; - // add "__proto__" for Object.getPrototypeOf polyfill - result[IE_PROTO] = O; - } else result = createDict(); - return Properties === undefined ? result : dPs(result, Properties); -}; - -},{"./_an-object":17,"./_dom-create":25,"./_enum-bug-keys":26,"./_html":33,"./_object-dps":50,"./_shared-key":62}],49:[function(require,module,exports){ -var anObject = require('./_an-object') - , IE8_DOM_DEFINE = require('./_ie8-dom-define') - , toPrimitive = require('./_to-primitive') - , dP = Object.defineProperty; - -exports.f = require('./_descriptors') ? Object.defineProperty : function defineProperty(O, P, Attributes){ - anObject(O); - P = toPrimitive(P, true); - anObject(Attributes); - if(IE8_DOM_DEFINE)try { - return dP(O, P, Attributes); - } catch(e){ /* empty */ } - if('get' in Attributes || 'set' in Attributes)throw TypeError('Accessors not supported!'); - if('value' in Attributes)O[P] = Attributes.value; - return O; -}; -},{"./_an-object":17,"./_descriptors":24,"./_ie8-dom-define":34,"./_to-primitive":72}],50:[function(require,module,exports){ -var dP = require('./_object-dp') - , anObject = require('./_an-object') - , getKeys = require('./_object-keys'); - -module.exports = require('./_descriptors') ? Object.defineProperties : function defineProperties(O, Properties){ - anObject(O); - var keys = getKeys(Properties) - , length = keys.length - , i = 0 - , P; - while(length > i)dP.f(O, P = keys[i++], Properties[P]); - return O; -}; -},{"./_an-object":17,"./_descriptors":24,"./_object-dp":49,"./_object-keys":54}],51:[function(require,module,exports){ -exports.f = Object.getOwnPropertySymbols; -},{}],52:[function(require,module,exports){ -// 19.1.2.9 / 15.2.3.2 Object.getPrototypeOf(O) -var has = require('./_has') - , toObject = require('./_to-object') - , IE_PROTO = require('./_shared-key')('IE_PROTO') - , ObjectProto = Object.prototype; - -module.exports = Object.getPrototypeOf || function(O){ - O = toObject(O); - if(has(O, IE_PROTO))return O[IE_PROTO]; - if(typeof O.constructor == 'function' && O instanceof O.constructor){ - return O.constructor.prototype; - } return O instanceof Object ? ObjectProto : null; -}; -},{"./_has":31,"./_shared-key":62,"./_to-object":71}],53:[function(require,module,exports){ -var has = require('./_has') - , toIObject = require('./_to-iobject') - , arrayIndexOf = require('./_array-includes')(false) - , IE_PROTO = require('./_shared-key')('IE_PROTO'); - -module.exports = function(object, names){ - var O = toIObject(object) - , i = 0 - , result = [] - , key; - for(key in O)if(key != IE_PROTO)has(O, key) && result.push(key); - // Don't enum bug & hidden keys - while(names.length > i)if(has(O, key = names[i++])){ - ~arrayIndexOf(result, key) || result.push(key); - } - return result; -}; -},{"./_array-includes":18,"./_has":31,"./_shared-key":62,"./_to-iobject":69}],54:[function(require,module,exports){ -// 19.1.2.14 / 15.2.3.14 Object.keys(O) -var $keys = require('./_object-keys-internal') - , enumBugKeys = require('./_enum-bug-keys'); - -module.exports = Object.keys || function keys(O){ - return $keys(O, enumBugKeys); -}; -},{"./_enum-bug-keys":26,"./_object-keys-internal":53}],55:[function(require,module,exports){ -exports.f = {}.propertyIsEnumerable; -},{}],56:[function(require,module,exports){ -// most Object methods by ES6 should accept primitives -var $export = require('./_export') - , core = require('./_core') - , fails = require('./_fails'); -module.exports = function(KEY, exec){ - var fn = (core.Object || {})[KEY] || Object[KEY] - , exp = {}; - exp[KEY] = exec(fn); - $export($export.S + $export.F * fails(function(){ fn(1); }), 'Object', exp); -}; -},{"./_core":21,"./_export":27,"./_fails":28}],57:[function(require,module,exports){ -module.exports = function(bitmap, value){ - return { - enumerable : !(bitmap & 1), - configurable: !(bitmap & 2), - writable : !(bitmap & 4), - value : value - }; -}; -},{}],58:[function(require,module,exports){ -var hide = require('./_hide'); -module.exports = function(target, src, safe){ - for(var key in src){ - if(safe && target[key])target[key] = src[key]; - else hide(target, key, src[key]); - } return target; -}; -},{"./_hide":32}],59:[function(require,module,exports){ -module.exports = require('./_hide'); -},{"./_hide":32}],60:[function(require,module,exports){ -'use strict'; -var global = require('./_global') - , core = require('./_core') - , dP = require('./_object-dp') - , DESCRIPTORS = require('./_descriptors') - , SPECIES = require('./_wks')('species'); - -module.exports = function(KEY){ - var C = typeof core[KEY] == 'function' ? core[KEY] : global[KEY]; - if(DESCRIPTORS && C && !C[SPECIES])dP.f(C, SPECIES, { - configurable: true, - get: function(){ return this; } - }); -}; -},{"./_core":21,"./_descriptors":24,"./_global":30,"./_object-dp":49,"./_wks":74}],61:[function(require,module,exports){ -var def = require('./_object-dp').f - , has = require('./_has') - , TAG = require('./_wks')('toStringTag'); - -module.exports = function(it, tag, stat){ - if(it && !has(it = stat ? it : it.prototype, TAG))def(it, TAG, {configurable: true, value: tag}); -}; -},{"./_has":31,"./_object-dp":49,"./_wks":74}],62:[function(require,module,exports){ -var shared = require('./_shared')('keys') - , uid = require('./_uid'); -module.exports = function(key){ - return shared[key] || (shared[key] = uid(key)); -}; -},{"./_shared":63,"./_uid":73}],63:[function(require,module,exports){ -var global = require('./_global') - , SHARED = '__core-js_shared__' - , store = global[SHARED] || (global[SHARED] = {}); -module.exports = function(key){ - return store[key] || (store[key] = {}); -}; -},{"./_global":30}],64:[function(require,module,exports){ -// 7.3.20 SpeciesConstructor(O, defaultConstructor) -var anObject = require('./_an-object') - , aFunction = require('./_a-function') - , SPECIES = require('./_wks')('species'); -module.exports = function(O, D){ - var C = anObject(O).constructor, S; - return C === undefined || (S = anObject(C)[SPECIES]) == undefined ? D : aFunction(S); -}; -},{"./_a-function":14,"./_an-object":17,"./_wks":74}],65:[function(require,module,exports){ -var toInteger = require('./_to-integer') - , defined = require('./_defined'); -// true -> String#at -// false -> String#codePointAt -module.exports = function(TO_STRING){ - return function(that, pos){ - var s = String(defined(that)) - , i = toInteger(pos) - , l = s.length - , a, b; - if(i < 0 || i >= l)return TO_STRING ? '' : undefined; - a = s.charCodeAt(i); - return a < 0xd800 || a > 0xdbff || i + 1 === l || (b = s.charCodeAt(i + 1)) < 0xdc00 || b > 0xdfff - ? TO_STRING ? s.charAt(i) : a - : TO_STRING ? s.slice(i, i + 2) : (a - 0xd800 << 10) + (b - 0xdc00) + 0x10000; - }; -}; -},{"./_defined":23,"./_to-integer":68}],66:[function(require,module,exports){ -var ctx = require('./_ctx') - , invoke = require('./_invoke') - , html = require('./_html') - , cel = require('./_dom-create') - , global = require('./_global') - , process = global.process - , setTask = global.setImmediate - , clearTask = global.clearImmediate - , MessageChannel = global.MessageChannel - , counter = 0 - , queue = {} - , ONREADYSTATECHANGE = 'onreadystatechange' - , defer, channel, port; -var run = function(){ - var id = +this; - if(queue.hasOwnProperty(id)){ - var fn = queue[id]; - delete queue[id]; - fn(); - } -}; -var listener = function(event){ - run.call(event.data); -}; -// Node.js 0.9+ & IE10+ has setImmediate, otherwise: -if(!setTask || !clearTask){ - setTask = function setImmediate(fn){ - var args = [], i = 1; - while(arguments.length > i)args.push(arguments[i++]); - queue[++counter] = function(){ - invoke(typeof fn == 'function' ? fn : Function(fn), args); - }; - defer(counter); - return counter; - }; - clearTask = function clearImmediate(id){ - delete queue[id]; - }; - // Node.js 0.8- - if(require('./_cof')(process) == 'process'){ - defer = function(id){ - process.nextTick(ctx(run, id, 1)); - }; - // Browsers with MessageChannel, includes WebWorkers - } else if(MessageChannel){ - channel = new MessageChannel; - port = channel.port2; - channel.port1.onmessage = listener; - defer = ctx(port.postMessage, port, 1); - // Browsers with postMessage, skip WebWorkers - // IE8 has postMessage, but it's sync & typeof its postMessage is 'object' - } else if(global.addEventListener && typeof postMessage == 'function' && !global.importScripts){ - defer = function(id){ - global.postMessage(id + '', '*'); - }; - global.addEventListener('message', listener, false); - // IE8- - } else if(ONREADYSTATECHANGE in cel('script')){ - defer = function(id){ - html.appendChild(cel('script'))[ONREADYSTATECHANGE] = function(){ - html.removeChild(this); - run.call(id); - }; - }; - // Rest old browsers - } else { - defer = function(id){ - setTimeout(ctx(run, id, 1), 0); - }; - } -} -module.exports = { - set: setTask, - clear: clearTask -}; -},{"./_cof":20,"./_ctx":22,"./_dom-create":25,"./_global":30,"./_html":33,"./_invoke":35}],67:[function(require,module,exports){ -var toInteger = require('./_to-integer') - , max = Math.max - , min = Math.min; -module.exports = function(index, length){ - index = toInteger(index); - return index < 0 ? max(index + length, 0) : min(index, length); -}; -},{"./_to-integer":68}],68:[function(require,module,exports){ -// 7.1.4 ToInteger -var ceil = Math.ceil - , floor = Math.floor; -module.exports = function(it){ - return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it); -}; -},{}],69:[function(require,module,exports){ -// to indexed object, toObject with fallback for non-array-like ES3 strings -var IObject = require('./_iobject') - , defined = require('./_defined'); -module.exports = function(it){ - return IObject(defined(it)); -}; -},{"./_defined":23,"./_iobject":36}],70:[function(require,module,exports){ -// 7.1.15 ToLength -var toInteger = require('./_to-integer') - , min = Math.min; -module.exports = function(it){ - return it > 0 ? min(toInteger(it), 0x1fffffffffffff) : 0; // pow(2, 53) - 1 == 9007199254740991 -}; -},{"./_to-integer":68}],71:[function(require,module,exports){ -// 7.1.13 ToObject(argument) -var defined = require('./_defined'); -module.exports = function(it){ - return Object(defined(it)); -}; -},{"./_defined":23}],72:[function(require,module,exports){ -// 7.1.1 ToPrimitive(input [, PreferredType]) -var isObject = require('./_is-object'); -// instead of the ES6 spec version, we didn't implement @@toPrimitive case -// and the second argument - flag - preferred type is a string -module.exports = function(it, S){ - if(!isObject(it))return it; - var fn, val; - if(S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it)))return val; - if(typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it)))return val; - if(!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it)))return val; - throw TypeError("Can't convert object to primitive value"); -}; -},{"./_is-object":38}],73:[function(require,module,exports){ -var id = 0 - , px = Math.random(); -module.exports = function(key){ - return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36)); -}; -},{}],74:[function(require,module,exports){ -var store = require('./_shared')('wks') - , uid = require('./_uid') - , Symbol = require('./_global').Symbol - , USE_SYMBOL = typeof Symbol == 'function'; - -var $exports = module.exports = function(name){ - return store[name] || (store[name] = - USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)('Symbol.' + name)); -}; - -$exports.store = store; -},{"./_global":30,"./_shared":63,"./_uid":73}],75:[function(require,module,exports){ -var classof = require('./_classof') - , ITERATOR = require('./_wks')('iterator') - , Iterators = require('./_iterators'); -module.exports = require('./_core').getIteratorMethod = function(it){ - if(it != undefined)return it[ITERATOR] - || it['@@iterator'] - || Iterators[classof(it)]; -}; -},{"./_classof":19,"./_core":21,"./_iterators":44,"./_wks":74}],76:[function(require,module,exports){ -'use strict'; -var addToUnscopables = require('./_add-to-unscopables') - , step = require('./_iter-step') - , Iterators = require('./_iterators') - , toIObject = require('./_to-iobject'); - -// 22.1.3.4 Array.prototype.entries() -// 22.1.3.13 Array.prototype.keys() -// 22.1.3.29 Array.prototype.values() -// 22.1.3.30 Array.prototype[@@iterator]() -module.exports = require('./_iter-define')(Array, 'Array', function(iterated, kind){ - this._t = toIObject(iterated); // target - this._i = 0; // next index - this._k = kind; // kind -// 22.1.5.2.1 %ArrayIteratorPrototype%.next() -}, function(){ - var O = this._t - , kind = this._k - , index = this._i++; - if(!O || index >= O.length){ - this._t = undefined; - return step(1); - } - if(kind == 'keys' )return step(0, index); - if(kind == 'values')return step(0, O[index]); - return step(0, [index, O[index]]); -}, 'values'); - -// argumentsList[@@iterator] is %ArrayProto_values% (9.4.4.6, 9.4.4.7) -Iterators.Arguments = Iterators.Array; - -addToUnscopables('keys'); -addToUnscopables('values'); -addToUnscopables('entries'); -},{"./_add-to-unscopables":15,"./_iter-define":41,"./_iter-step":43,"./_iterators":44,"./_to-iobject":69}],77:[function(require,module,exports){ -// 19.1.3.1 Object.assign(target, source) -var $export = require('./_export'); - -$export($export.S + $export.F, 'Object', {assign: require('./_object-assign')}); -},{"./_export":27,"./_object-assign":47}],78:[function(require,module,exports){ -// 19.1.2.14 Object.keys(O) -var toObject = require('./_to-object') - , $keys = require('./_object-keys'); - -require('./_object-sap')('keys', function(){ - return function keys(it){ - return $keys(toObject(it)); - }; -}); -},{"./_object-keys":54,"./_object-sap":56,"./_to-object":71}],79:[function(require,module,exports){ -arguments[4][9][0].apply(exports,arguments) -},{"dup":9}],80:[function(require,module,exports){ -'use strict'; -var LIBRARY = require('./_library') - , global = require('./_global') - , ctx = require('./_ctx') - , classof = require('./_classof') - , $export = require('./_export') - , isObject = require('./_is-object') - , aFunction = require('./_a-function') - , anInstance = require('./_an-instance') - , forOf = require('./_for-of') - , speciesConstructor = require('./_species-constructor') - , task = require('./_task').set - , microtask = require('./_microtask')() - , PROMISE = 'Promise' - , TypeError = global.TypeError - , process = global.process - , $Promise = global[PROMISE] - , process = global.process - , isNode = classof(process) == 'process' - , empty = function(){ /* empty */ } - , Internal, GenericPromiseCapability, Wrapper; - -var USE_NATIVE = !!function(){ - try { - // correct subclassing with @@species support - var promise = $Promise.resolve(1) - , FakePromise = (promise.constructor = {})[require('./_wks')('species')] = function(exec){ exec(empty, empty); }; - // unhandled rejections tracking support, NodeJS Promise without it fails @@species test - return (isNode || typeof PromiseRejectionEvent == 'function') && promise.then(empty) instanceof FakePromise; - } catch(e){ /* empty */ } -}(); - -// helpers -var sameConstructor = function(a, b){ - // with library wrapper special case - return a === b || a === $Promise && b === Wrapper; -}; -var isThenable = function(it){ - var then; - return isObject(it) && typeof (then = it.then) == 'function' ? then : false; -}; -var newPromiseCapability = function(C){ - return sameConstructor($Promise, C) - ? new PromiseCapability(C) - : new GenericPromiseCapability(C); -}; -var PromiseCapability = GenericPromiseCapability = function(C){ - var resolve, reject; - this.promise = new C(function($$resolve, $$reject){ - if(resolve !== undefined || reject !== undefined)throw TypeError('Bad Promise constructor'); - resolve = $$resolve; - reject = $$reject; - }); - this.resolve = aFunction(resolve); - this.reject = aFunction(reject); -}; -var perform = function(exec){ - try { - exec(); - } catch(e){ - return {error: e}; - } -}; -var notify = function(promise, isReject){ - if(promise._n)return; - promise._n = true; - var chain = promise._c; - microtask(function(){ - var value = promise._v - , ok = promise._s == 1 - , i = 0; - var run = function(reaction){ - var handler = ok ? reaction.ok : reaction.fail - , resolve = reaction.resolve - , reject = reaction.reject - , domain = reaction.domain - , result, then; - try { - if(handler){ - if(!ok){ - if(promise._h == 2)onHandleUnhandled(promise); - promise._h = 1; - } - if(handler === true)result = value; - else { - if(domain)domain.enter(); - result = handler(value); - if(domain)domain.exit(); - } - if(result === reaction.promise){ - reject(TypeError('Promise-chain cycle')); - } else if(then = isThenable(result)){ - then.call(result, resolve, reject); - } else resolve(result); - } else reject(value); - } catch(e){ - reject(e); - } - }; - while(chain.length > i)run(chain[i++]); // variable length - can't use forEach - promise._c = []; - promise._n = false; - if(isReject && !promise._h)onUnhandled(promise); - }); -}; -var onUnhandled = function(promise){ - task.call(global, function(){ - var value = promise._v - , abrupt, handler, console; - if(isUnhandled(promise)){ - abrupt = perform(function(){ - if(isNode){ - process.emit('unhandledRejection', value, promise); - } else if(handler = global.onunhandledrejection){ - handler({promise: promise, reason: value}); - } else if((console = global.console) && console.error){ - console.error('Unhandled promise rejection', value); - } - }); - // Browsers should not trigger `rejectionHandled` event if it was handled here, NodeJS - should - promise._h = isNode || isUnhandled(promise) ? 2 : 1; - } promise._a = undefined; - if(abrupt)throw abrupt.error; - }); -}; -var isUnhandled = function(promise){ - if(promise._h == 1)return false; - var chain = promise._a || promise._c - , i = 0 - , reaction; - while(chain.length > i){ - reaction = chain[i++]; - if(reaction.fail || !isUnhandled(reaction.promise))return false; - } return true; -}; -var onHandleUnhandled = function(promise){ - task.call(global, function(){ - var handler; - if(isNode){ - process.emit('rejectionHandled', promise); - } else if(handler = global.onrejectionhandled){ - handler({promise: promise, reason: promise._v}); - } - }); -}; -var $reject = function(value){ - var promise = this; - if(promise._d)return; - promise._d = true; - promise = promise._w || promise; // unwrap - promise._v = value; - promise._s = 2; - if(!promise._a)promise._a = promise._c.slice(); - notify(promise, true); -}; -var $resolve = function(value){ - var promise = this - , then; - if(promise._d)return; - promise._d = true; - promise = promise._w || promise; // unwrap - try { - if(promise === value)throw TypeError("Promise can't be resolved itself"); - if(then = isThenable(value)){ - microtask(function(){ - var wrapper = {_w: promise, _d: false}; // wrap - try { - then.call(value, ctx($resolve, wrapper, 1), ctx($reject, wrapper, 1)); - } catch(e){ - $reject.call(wrapper, e); - } - }); - } else { - promise._v = value; - promise._s = 1; - notify(promise, false); - } - } catch(e){ - $reject.call({_w: promise, _d: false}, e); // wrap - } -}; - -// constructor polyfill -if(!USE_NATIVE){ - // 25.4.3.1 Promise(executor) - $Promise = function Promise(executor){ - anInstance(this, $Promise, PROMISE, '_h'); - aFunction(executor); - Internal.call(this); - try { - executor(ctx($resolve, this, 1), ctx($reject, this, 1)); - } catch(err){ - $reject.call(this, err); - } - }; - Internal = function Promise(executor){ - this._c = []; // <- awaiting reactions - this._a = undefined; // <- checked in isUnhandled reactions - this._s = 0; // <- state - this._d = false; // <- done - this._v = undefined; // <- value - this._h = 0; // <- rejection state, 0 - default, 1 - handled, 2 - unhandled - this._n = false; // <- notify - }; - Internal.prototype = require('./_redefine-all')($Promise.prototype, { - // 25.4.5.3 Promise.prototype.then(onFulfilled, onRejected) - then: function then(onFulfilled, onRejected){ - var reaction = newPromiseCapability(speciesConstructor(this, $Promise)); - reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true; - reaction.fail = typeof onRejected == 'function' && onRejected; - reaction.domain = isNode ? process.domain : undefined; - this._c.push(reaction); - if(this._a)this._a.push(reaction); - if(this._s)notify(this, false); - return reaction.promise; - }, - // 25.4.5.1 Promise.prototype.catch(onRejected) - 'catch': function(onRejected){ - return this.then(undefined, onRejected); - } - }); - PromiseCapability = function(){ - var promise = new Internal; - this.promise = promise; - this.resolve = ctx($resolve, promise, 1); - this.reject = ctx($reject, promise, 1); - }; -} - -$export($export.G + $export.W + $export.F * !USE_NATIVE, {Promise: $Promise}); -require('./_set-to-string-tag')($Promise, PROMISE); -require('./_set-species')(PROMISE); -Wrapper = require('./_core')[PROMISE]; - -// statics -$export($export.S + $export.F * !USE_NATIVE, PROMISE, { - // 25.4.4.5 Promise.reject(r) - reject: function reject(r){ - var capability = newPromiseCapability(this) - , $$reject = capability.reject; - $$reject(r); - return capability.promise; - } -}); -$export($export.S + $export.F * (LIBRARY || !USE_NATIVE), PROMISE, { - // 25.4.4.6 Promise.resolve(x) - resolve: function resolve(x){ - // instanceof instead of internal slot check because we should fix it without replacement native Promise core - if(x instanceof $Promise && sameConstructor(x.constructor, this))return x; - var capability = newPromiseCapability(this) - , $$resolve = capability.resolve; - $$resolve(x); - return capability.promise; - } -}); -$export($export.S + $export.F * !(USE_NATIVE && require('./_iter-detect')(function(iter){ - $Promise.all(iter)['catch'](empty); -})), PROMISE, { - // 25.4.4.1 Promise.all(iterable) - all: function all(iterable){ - var C = this - , capability = newPromiseCapability(C) - , resolve = capability.resolve - , reject = capability.reject; - var abrupt = perform(function(){ - var values = [] - , index = 0 - , remaining = 1; - forOf(iterable, false, function(promise){ - var $index = index++ - , alreadyCalled = false; - values.push(undefined); - remaining++; - C.resolve(promise).then(function(value){ - if(alreadyCalled)return; - alreadyCalled = true; - values[$index] = value; - --remaining || resolve(values); - }, reject); - }); - --remaining || resolve(values); - }); - if(abrupt)reject(abrupt.error); - return capability.promise; - }, - // 25.4.4.4 Promise.race(iterable) - race: function race(iterable){ - var C = this - , capability = newPromiseCapability(C) - , reject = capability.reject; - var abrupt = perform(function(){ - forOf(iterable, false, function(promise){ - C.resolve(promise).then(capability.resolve, reject); - }); - }); - if(abrupt)reject(abrupt.error); - return capability.promise; - } -}); -},{"./_a-function":14,"./_an-instance":16,"./_classof":19,"./_core":21,"./_ctx":22,"./_export":27,"./_for-of":29,"./_global":30,"./_is-object":38,"./_iter-detect":42,"./_library":45,"./_microtask":46,"./_redefine-all":58,"./_set-species":60,"./_set-to-string-tag":61,"./_species-constructor":64,"./_task":66,"./_wks":74}],81:[function(require,module,exports){ -'use strict'; -var $at = require('./_string-at')(true); - -// 21.1.3.27 String.prototype[@@iterator]() -require('./_iter-define')(String, 'String', function(iterated){ - this._t = String(iterated); // target - this._i = 0; // next index -// 21.1.5.2.1 %StringIteratorPrototype%.next() -}, function(){ - var O = this._t - , index = this._i - , point; - if(index >= O.length)return {value: undefined, done: true}; - point = $at(O, index); - this._i += point.length; - return {value: point, done: false}; -}); -},{"./_iter-define":41,"./_string-at":65}],82:[function(require,module,exports){ -require('./es6.array.iterator'); -var global = require('./_global') - , hide = require('./_hide') - , Iterators = require('./_iterators') - , TO_STRING_TAG = require('./_wks')('toStringTag'); - -for(var collections = ['NodeList', 'DOMTokenList', 'MediaList', 'StyleSheetList', 'CSSRuleList'], i = 0; i < 5; i++){ - var NAME = collections[i] - , Collection = global[NAME] - , proto = Collection && Collection.prototype; - if(proto && !proto[TO_STRING_TAG])hide(proto, TO_STRING_TAG, NAME); - Iterators[NAME] = Iterators.Array; -} -},{"./_global":30,"./_hide":32,"./_iterators":44,"./_wks":74,"./es6.array.iterator":76}],83:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _extends2 = require("babel-runtime/helpers/extends"); - -var _extends3 = _interopRequireDefault(_extends2); - -var _collection = require("./collection"); - -var _collection2 = _interopRequireDefault(_collection); - -var _base = require("./adapters/base"); - -var _base2 = _interopRequireDefault(_base); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -const DEFAULT_BUCKET_NAME = "default"; -const DEFAULT_REMOTE = "http://localhost:8888/v1"; - -/** - * KintoBase class. - */ -class KintoBase { - /** - * Provides a public access to the base adapter class. Users can create a - * custom DB adapter by extending {@link BaseAdapter}. - * - * @type {Object} - */ - static get adapters() { - return { - BaseAdapter: _base2.default - }; - } - - /** - * Synchronization strategies. Available strategies are: - * - * - `MANUAL`: Conflicts will be reported in a dedicated array. - * - `SERVER_WINS`: Conflicts are resolved using remote data. - * - `CLIENT_WINS`: Conflicts are resolved using local data. - * - * @type {Object} - */ - static get syncStrategy() { - return _collection2.default.strategy; - } - - /** - * Constructor. - * - * Options: - * - `{String}` `remote` The server URL to use. - * - `{String}` `bucket` The collection bucket name. - * - `{EventEmitter}` `events` Events handler. - * - `{BaseAdapter}` `adapter` The base DB adapter class. - * - `{Object}` `adapterOptions` Options given to the adapter. - * - `{String}` `dbPrefix` The DB name prefix. - * - `{Object}` `headers` The HTTP headers to use. - * - `{String}` `requestMode` The HTTP CORS mode to use. - * - `{Number}` `timeout` The requests timeout in ms (default: `5000`). - * - * @param {Object} options The options object. - */ - constructor(options = {}) { - const defaults = { - bucket: DEFAULT_BUCKET_NAME, - remote: DEFAULT_REMOTE - }; - this._options = (0, _extends3.default)({}, defaults, options); - if (!this._options.adapter) { - throw new Error("No adapter provided"); - } - - const { remote, events, headers, requestMode, timeout, ApiClass } = this._options; - - // public properties - - /** - * The kinto HTTP client instance. - * @type {KintoClient} - */ - this.api = new ApiClass(remote, { events, headers, requestMode, timeout }); - /** - * The event emitter instance. - * @type {EventEmitter} - */ - this.events = this._options.events; - } - - /** - * Creates a {@link Collection} instance. The second (optional) parameter - * will set collection-level options like e.g. `remoteTransformers`. - * - * @param {String} collName The collection name. - * @param {Object} options May contain the following fields: - * remoteTransformers: Array<RemoteTransformer> - * @return {Collection} - */ - collection(collName, options = {}) { - if (!collName) { - throw new Error("missing collection name"); - } - - const bucket = this._options.bucket; - return new _collection2.default(bucket, collName, this.api, { - events: this._options.events, - adapter: this._options.adapter, - adapterOptions: this._options.adapterOptions, - dbPrefix: this._options.dbPrefix, - idSchema: options.idSchema, - remoteTransformers: options.remoteTransformers, - hooks: options.hooks - }); - } -} -exports.default = KintoBase; - -},{"./adapters/base":85,"./collection":86,"babel-runtime/helpers/extends":8}],84:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _asyncToGenerator2 = require("babel-runtime/helpers/asyncToGenerator"); - -var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); - -var _promise = require("babel-runtime/core-js/promise"); - -var _promise2 = _interopRequireDefault(_promise); - -var _keys = require("babel-runtime/core-js/object/keys"); - -var _keys2 = _interopRequireDefault(_keys); - -var _base = require("./base.js"); - -var _base2 = _interopRequireDefault(_base); - -var _utils = require("../utils"); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -const INDEXED_FIELDS = ["id", "_status", "last_modified"]; - -/** - * IDB cursor handlers. - * @type {Object} - */ -const cursorHandlers = { - all(filters, done) { - const results = []; - return function (event) { - const cursor = event.target.result; - if (cursor) { - if ((0, _utils.filterObject)(filters, cursor.value)) { - results.push(cursor.value); - } - cursor.continue(); - } else { - done(results); - } - }; - }, - - in(values, done) { - if (values.length === 0) { - return done([]); - } - const sortedValues = [].slice.call(values).sort(); - const results = []; - return function (event) { - const cursor = event.target.result; - if (!cursor) { - done(results); - return; - } - const { key, value } = cursor; - let i = 0; - while (key > sortedValues[i]) { - // The cursor has passed beyond this key. Check next. - ++i; - if (i === sortedValues.length) { - done(results); // There is no next. Stop searching. - return; - } - } - if (key === sortedValues[i]) { - results.push(value); - cursor.continue(); - } else { - cursor.continue(sortedValues[i]); - } - }; - } -}; - -/** - * Extract from filters definition the first indexed field. Since indexes were - * created on single-columns, extracting a single one makes sense. - * - * @param {Object} filters The filters object. - * @return {String|undefined} - */ -function findIndexedField(filters) { - const filteredFields = (0, _keys2.default)(filters); - const indexedFields = filteredFields.filter(field => { - return INDEXED_FIELDS.indexOf(field) !== -1; - }); - return indexedFields[0]; -} - -/** - * Creates an IDB request and attach it the appropriate cursor event handler to - * perform a list query. - * - * Multiple matching values are handled by passing an array. - * - * @param {IDBStore} store The IDB store. - * @param {String|undefined} indexField The indexed field to query, if any. - * @param {Any} value The value to filter, if any. - * @param {Object} filters More filters. - * @param {Function} done The operation completion handler. - * @return {IDBRequest} - */ -function createListRequest(store, indexField, value, filters, done) { - if (!indexField) { - // Get all records. - const request = store.openCursor(); - request.onsuccess = cursorHandlers.all(filters, done); - return request; - } - - // WHERE IN equivalent clause - if (Array.isArray(value)) { - const request = store.index(indexField).openCursor(); - request.onsuccess = cursorHandlers.in(value, done); - return request; - } - - // WHERE field = value clause - const request = store.index(indexField).openCursor(IDBKeyRange.only(value)); - request.onsuccess = cursorHandlers.all(filters, done); - return request; -} - -/** - * IndexedDB adapter. - * - * This adapter doesn't support any options. - */ -class IDB extends _base2.default { - /** - * Constructor. - * - * @param {String} dbname The database nale. - */ - constructor(dbname) { - super(); - this._db = null; - // public properties - /** - * The database name. - * @type {String} - */ - this.dbname = dbname; - } - - _handleError(method, err) { - const error = new Error(method + "() " + err.message); - error.stack = err.stack; - throw error; - } - - /** - * Ensures a connection to the IndexedDB database has been opened. - * - * @override - * @return {Promise} - */ - open() { - if (this._db) { - return _promise2.default.resolve(this); - } - return new _promise2.default((resolve, reject) => { - const request = indexedDB.open(this.dbname, 1); - request.onupgradeneeded = event => { - // DB object - const db = event.target.result; - // Main collection store - const collStore = db.createObjectStore(this.dbname, { - keyPath: "id" - }); - // Primary key (generated by IdSchema, UUID by default) - collStore.createIndex("id", "id", { unique: true }); - // Local record status ("synced", "created", "updated", "deleted") - collStore.createIndex("_status", "_status"); - // Last modified field - collStore.createIndex("last_modified", "last_modified"); - - // Metadata store - const metaStore = db.createObjectStore("__meta__", { - keyPath: "name" - }); - metaStore.createIndex("name", "name", { unique: true }); - }; - request.onerror = event => reject(event.target.error); - request.onsuccess = event => { - this._db = event.target.result; - resolve(this); - }; - }); - } - - /** - * Closes current connection to the database. - * - * @override - * @return {Promise} - */ - close() { - if (this._db) { - this._db.close(); // indexedDB.close is synchronous - this._db = null; - } - return super.close(); - } - - /** - * Returns a transaction and a store objects for this collection. - * - * To determine if a transaction has completed successfully, we should rather - * listen to the transaction’s complete event rather than the IDBObjectStore - * request’s success event, because the transaction may still fail after the - * success event fires. - * - * @param {String} mode Transaction mode ("readwrite" or undefined) - * @param {String|null} name Store name (defaults to coll name) - * @return {Object} - */ - prepare(mode = undefined, name = null) { - const storeName = name || this.dbname; - // On Safari, calling IDBDatabase.transaction with mode == undefined raises - // a TypeError. - const transaction = mode ? this._db.transaction([storeName], mode) : this._db.transaction([storeName]); - const store = transaction.objectStore(storeName); - return { transaction, store }; - } - - /** - * Deletes every records in the current collection. - * - * @override - * @return {Promise} - */ - clear() { - var _this = this; - - return (0, _asyncToGenerator3.default)(function* () { - try { - yield _this.open(); - return new _promise2.default(function (resolve, reject) { - const { transaction, store } = _this.prepare("readwrite"); - store.clear(); - transaction.onerror = function (event) { - return reject(new Error(event.target.error)); - }; - transaction.oncomplete = function () { - return resolve(); - }; - }); - } catch (e) { - _this._handleError("clear", e); - } - })(); - } - - /** - * Executes the set of synchronous CRUD operations described in the provided - * callback within an IndexedDB transaction, for current db store. - * - * The callback will be provided an object exposing the following synchronous - * CRUD operation methods: get, create, update, delete. - * - * Important note: because limitations in IndexedDB implementations, no - * asynchronous code should be performed within the provided callback; the - * promise will therefore be rejected if the callback returns a Promise. - * - * Options: - * - {Array} preload: The list of record IDs to fetch and make available to - * the transaction object get() method (default: []) - * - * @example - * const db = new IDB("example"); - * db.execute(transaction => { - * transaction.create({id: 1, title: "foo"}); - * transaction.update({id: 2, title: "bar"}); - * transaction.delete(3); - * return "foo"; - * }) - * .catch(console.error.bind(console)); - * .then(console.log.bind(console)); // => "foo" - * - * @param {Function} callback The operation description callback. - * @param {Object} options The options object. - * @return {Promise} - */ - execute(callback, options = { preload: [] }) { - var _this2 = this; - - return (0, _asyncToGenerator3.default)(function* () { - // Transactions in IndexedDB are autocommited when a callback does not - // perform any additional operation. - // The way Promises are implemented in Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=1193394) - // prevents using within an opened transaction. - // To avoid managing asynchronocity in the specified `callback`, we preload - // a list of record in order to execute the `callback` synchronously. - // See also: - // - http://stackoverflow.com/a/28388805/330911 - // - http://stackoverflow.com/a/10405196 - // - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ - yield _this2.open(); - return new _promise2.default(function (resolve, reject) { - // Start transaction. - const { transaction, store } = _this2.prepare("readwrite"); - // Preload specified records using index. - const ids = options.preload; - store.index("id").openCursor().onsuccess = cursorHandlers.in(ids, function (records) { - // Store obtained records by id. - const preloaded = records.reduce(function (acc, record) { - acc[record.id] = record; - return acc; - }, {}); - // Expose a consistent API for every adapter instead of raw store methods. - const proxy = transactionProxy(store, preloaded); - // The callback is executed synchronously within the same transaction. - let result; - try { - result = callback(proxy); - } catch (e) { - transaction.abort(); - reject(e); - } - if (result instanceof _promise2.default) { - // XXX: investigate how to provide documentation details in error. - reject(new Error("execute() callback should not return a Promise.")); - } - // XXX unsure if we should manually abort the transaction on error - transaction.onerror = function (event) { - return reject(new Error(event.target.error)); - }; - transaction.oncomplete = function (event) { - return resolve(result); - }; - }); - }); - })(); - } - - /** - * Retrieve a record by its primary key from the IndexedDB database. - * - * @override - * @param {String} id The record id. - * @return {Promise} - */ - get(id) { - var _this3 = this; - - return (0, _asyncToGenerator3.default)(function* () { - try { - yield _this3.open(); - return new _promise2.default(function (resolve, reject) { - const { transaction, store } = _this3.prepare(); - const request = store.get(id); - transaction.onerror = function (event) { - return reject(new Error(event.target.error)); - }; - transaction.oncomplete = function () { - return resolve(request.result); - }; - }); - } catch (e) { - _this3._handleError("get", e); - } - })(); - } - - /** - * Lists all records from the IndexedDB database. - * - * @override - * @return {Promise} - */ - list(params = { filters: {} }) { - var _this4 = this; - - return (0, _asyncToGenerator3.default)(function* () { - const { filters } = params; - const indexField = findIndexedField(filters); - const value = filters[indexField]; - try { - yield _this4.open(); - const results = yield new _promise2.default(function (resolve, reject) { - let results = []; - // If `indexField` was used already, don't filter again. - const remainingFilters = (0, _utils.omitKeys)(filters, indexField); - - const { transaction, store } = _this4.prepare(); - createListRequest(store, indexField, value, remainingFilters, function (_results) { - // we have received all requested records, parking them within - // current scope - results = _results; - }); - transaction.onerror = function (event) { - return reject(new Error(event.target.error)); - }; - transaction.oncomplete = function (event) { - return resolve(results); - }; - }); - - // The resulting list of records is sorted. - // XXX: with some efforts, this could be fully implemented using IDB API. - return params.order ? (0, _utils.sortObjects)(params.order, results) : results; - } catch (e) { - _this4._handleError("list", e); - } - })(); - } - - /** - * Store the lastModified value into metadata store. - * - * @override - * @param {Number} lastModified - * @return {Promise} - */ - saveLastModified(lastModified) { - var _this5 = this; - - return (0, _asyncToGenerator3.default)(function* () { - const value = parseInt(lastModified, 10) || null; - yield _this5.open(); - return new _promise2.default(function (resolve, reject) { - const { transaction, store } = _this5.prepare("readwrite", "__meta__"); - store.put({ name: "lastModified", value: value }); - transaction.onerror = function (event) { - return reject(event.target.error); - }; - transaction.oncomplete = function (event) { - return resolve(value); - }; - }); - })(); - } - - /** - * Retrieve saved lastModified value. - * - * @override - * @return {Promise} - */ - getLastModified() { - var _this6 = this; - - return (0, _asyncToGenerator3.default)(function* () { - yield _this6.open(); - return new _promise2.default(function (resolve, reject) { - const { transaction, store } = _this6.prepare(undefined, "__meta__"); - const request = store.get("lastModified"); - transaction.onerror = function (event) { - return reject(event.target.error); - }; - transaction.oncomplete = function (event) { - resolve(request.result && request.result.value || null); - }; - }); - })(); - } - - /** - * Load a dump of records exported from a server. - * - * @abstract - * @return {Promise} - */ - loadDump(records) { - var _this7 = this; - - return (0, _asyncToGenerator3.default)(function* () { - try { - yield _this7.execute(function (transaction) { - records.forEach(function (record) { - return transaction.update(record); - }); - }); - const previousLastModified = yield _this7.getLastModified(); - const lastModified = Math.max(...records.map(function (record) { - return record.last_modified; - })); - if (lastModified > previousLastModified) { - yield _this7.saveLastModified(lastModified); - } - return records; - } catch (e) { - _this7._handleError("loadDump", e); - } - })(); - } -} - -exports.default = IDB; /** - * IDB transaction proxy. - * - * @param {IDBStore} store The IndexedDB database store. - * @param {Array} preloaded The list of records to make available to - * get() (default: []). - * @return {Object} - */ - -function transactionProxy(store, preloaded = []) { - return { - create(record) { - store.add(record); - }, - - update(record) { - store.put(record); - }, - - delete(id) { - store.delete(id); - }, - - get(id) { - return preloaded[id]; - } - }; -} - -},{"../utils":87,"./base.js":85,"babel-runtime/core-js/object/keys":5,"babel-runtime/core-js/promise":6,"babel-runtime/helpers/asyncToGenerator":7}],85:[function(require,module,exports){ -"use strict"; - -/** - * Base db adapter. - * - * @abstract - */ - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _promise = require("babel-runtime/core-js/promise"); - -var _promise2 = _interopRequireDefault(_promise); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -class BaseAdapter { - /** - * Opens a connection to the database. - * - * @abstract - * @return {Promise} - */ - open() { - return _promise2.default.resolve(); - } - - /** - * Closes current connection to the database. - * - * @abstract - * @return {Promise} - */ - close() { - return _promise2.default.resolve(); - } - - /** - * Deletes every records present in the database. - * - * @abstract - * @return {Promise} - */ - clear() { - throw new Error("Not Implemented."); - } - - /** - * Executes a batch of operations within a single transaction. - * - * @abstract - * @param {Function} callback The operation callback. - * @param {Object} options The options object. - * @return {Promise} - */ - execute(callback, options = { preload: [] }) { - throw new Error("Not Implemented."); - } - - /** - * Retrieve a record by its primary key from the database. - * - * @abstract - * @param {String} id The record id. - * @return {Promise} - */ - get(id) { - throw new Error("Not Implemented."); - } - - /** - * Lists all records from the database. - * - * @abstract - * @param {Object} params The filters and order to apply to the results. - * @return {Promise} - */ - list(params = { filters: {}, order: "" }) { - throw new Error("Not Implemented."); - } - - /** - * Store the lastModified value. - * - * @abstract - * @param {Number} lastModified - * @return {Promise} - */ - saveLastModified(lastModified) { - throw new Error("Not Implemented."); - } - - /** - * Retrieve saved lastModified value. - * - * @abstract - * @return {Promise} - */ - getLastModified() { - throw new Error("Not Implemented."); - } - - /** - * Load a dump of records exported from a server. - * - * @abstract - * @return {Promise} - */ - loadDump(records) { - throw new Error("Not Implemented."); - } -} -exports.default = BaseAdapter; - -},{"babel-runtime/core-js/promise":6}],86:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.CollectionTransaction = exports.SyncResultObject = undefined; - -var _stringify = require("babel-runtime/core-js/json/stringify"); - -var _stringify2 = _interopRequireDefault(_stringify); - -var _promise = require("babel-runtime/core-js/promise"); - -var _promise2 = _interopRequireDefault(_promise); - -var _asyncToGenerator2 = require("babel-runtime/helpers/asyncToGenerator"); - -var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); - -var _extends2 = require("babel-runtime/helpers/extends"); - -var _extends3 = _interopRequireDefault(_extends2); - -var _assign = require("babel-runtime/core-js/object/assign"); - -var _assign2 = _interopRequireDefault(_assign); - -exports.recordsEqual = recordsEqual; - -var _base = require("./adapters/base"); - -var _base2 = _interopRequireDefault(_base); - -var _IDB = require("./adapters/IDB"); - -var _IDB2 = _interopRequireDefault(_IDB); - -var _utils = require("./utils"); - -var _uuid = require("uuid"); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -const RECORD_FIELDS_TO_CLEAN = ["_status"]; -const AVAILABLE_HOOKS = ["incoming-changes"]; - -/** - * Compare two records omitting local fields and synchronization - * attributes (like _status and last_modified) - * @param {Object} a A record to compare. - * @param {Object} b A record to compare. - * @return {boolean} - */ -function recordsEqual(a, b, localFields = []) { - const fieldsToClean = RECORD_FIELDS_TO_CLEAN.concat(["last_modified"]).concat(localFields); - const cleanLocal = r => (0, _utils.omitKeys)(r, fieldsToClean); - return (0, _utils.deepEqual)(cleanLocal(a), cleanLocal(b)); -} - -/** - * Synchronization result object. - */ -class SyncResultObject { - /** - * Object default values. - * @type {Object} - */ - static get defaults() { - return { - ok: true, - lastModified: null, - errors: [], - created: [], - updated: [], - deleted: [], - published: [], - conflicts: [], - skipped: [], - resolved: [] - }; - } - - /** - * Public constructor. - */ - constructor() { - /** - * Current synchronization result status; becomes `false` when conflicts or - * errors are registered. - * @type {Boolean} - */ - this.ok = true; - (0, _assign2.default)(this, SyncResultObject.defaults); - } - - /** - * Adds entries for a given result type. - * - * @param {String} type The result type. - * @param {Array} entries The result entries. - * @return {SyncResultObject} - */ - add(type, entries) { - if (!Array.isArray(this[type])) { - return; - } - // Deduplicate entries by id. If the values don't have `id` attribute, just - // keep all. - const deduplicated = this[type].concat(entries).reduce((acc, cur) => { - const existing = acc.filter(r => cur.id && r.id ? cur.id != r.id : true); - return existing.concat(cur); - }, []); - this[type] = deduplicated; - this.ok = this.errors.length + this.conflicts.length === 0; - return this; - } - - /** - * Reinitializes result entries for a given result type. - * - * @param {String} type The result type. - * @return {SyncResultObject} - */ - reset(type) { - this[type] = SyncResultObject.defaults[type]; - this.ok = this.errors.length + this.conflicts.length === 0; - return this; - } -} - -exports.SyncResultObject = SyncResultObject; -function createUUIDSchema() { - return { - generate() { - return (0, _uuid.v4)(); - }, - - validate(id) { - return (0, _utils.isUUID)(id); - } - }; -} - -function markStatus(record, status) { - return (0, _extends3.default)({}, record, { _status: status }); -} - -function markDeleted(record) { - return markStatus(record, "deleted"); -} - -function markSynced(record) { - return markStatus(record, "synced"); -} - -/** - * Import a remote change into the local database. - * - * @param {IDBTransactionProxy} transaction The transaction handler. - * @param {Object} remote The remote change object to import. - * @param {Array<String>} localFields The list of fields that remain local. - * @return {Object} - */ -function importChange(transaction, remote, localFields) { - const local = transaction.get(remote.id); - if (!local) { - // Not found locally but remote change is marked as deleted; skip to - // avoid recreation. - if (remote.deleted) { - return { type: "skipped", data: remote }; - } - const synced = markSynced(remote); - transaction.create(synced); - return { type: "created", data: synced }; - } - // Compare local and remote, ignoring local fields. - const isIdentical = recordsEqual(local, remote, localFields); - // Apply remote changes on local record. - const synced = (0, _extends3.default)({}, local, markSynced(remote)); - // Detect or ignore conflicts if record has also been modified locally. - if (local._status !== "synced") { - // Locally deleted, unsynced: scheduled for remote deletion. - if (local._status === "deleted") { - return { type: "skipped", data: local }; - } - if (isIdentical) { - // If records are identical, import anyway, so we bump the - // local last_modified value from the server and set record - // status to "synced". - transaction.update(synced); - return { type: "updated", data: { old: local, new: synced } }; - } - if (local.last_modified !== undefined && local.last_modified === remote.last_modified) { - // If our local version has the same last_modified as the remote - // one, this represents an object that corresponds to a resolved - // conflict. Our local version represents the final output, so - // we keep that one. (No transaction operation to do.) - // But if our last_modified is undefined, - // that means we've created the same object locally as one on - // the server, which *must* be a conflict. - return { type: "void" }; - } - return { - type: "conflicts", - data: { type: "incoming", local: local, remote: remote } - }; - } - // Local record was synced. - if (remote.deleted) { - transaction.delete(remote.id); - return { type: "deleted", data: local }; - } - // Import locally. - transaction.update(synced); - // if identical, simply exclude it from all SyncResultObject lists - const type = isIdentical ? "void" : "updated"; - return { type, data: { old: local, new: synced } }; -} - -/** - * Abstracts a collection of records stored in the local database, providing - * CRUD operations and synchronization helpers. - */ -class Collection { - /** - * Constructor. - * - * Options: - * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`) - * - `{String} dbPrefix` The DB name prefix (default: `""`) - * - * @param {String} bucket The bucket identifier. - * @param {String} name The collection name. - * @param {Api} api The Api instance. - * @param {Object} options The options object. - */ - constructor(bucket, name, api, options = {}) { - this._bucket = bucket; - this._name = name; - this._lastModified = null; - - const DBAdapter = options.adapter || _IDB2.default; - if (!DBAdapter) { - throw new Error("No adapter provided"); - } - const dbPrefix = options.dbPrefix || ""; - const db = new DBAdapter(`${ dbPrefix }${ bucket }/${ name }`, options.adapterOptions); - if (!(db instanceof _base2.default)) { - throw new Error("Unsupported adapter."); - } - // public properties - /** - * The db adapter instance - * @type {BaseAdapter} - */ - this.db = db; - /** - * The Api instance. - * @type {KintoClient} - */ - this.api = api; - /** - * The event emitter instance. - * @type {EventEmitter} - */ - this.events = options.events; - /** - * The IdSchema instance. - * @type {Object} - */ - this.idSchema = this._validateIdSchema(options.idSchema); - /** - * The list of remote transformers. - * @type {Array} - */ - this.remoteTransformers = this._validateRemoteTransformers(options.remoteTransformers); - /** - * The list of hooks. - * @type {Object} - */ - this.hooks = this._validateHooks(options.hooks); - /** - * The list of fields names that will remain local. - * @type {Array} - */ - this.localFields = options.localFields || []; - } - - /** - * The collection name. - * @type {String} - */ - get name() { - return this._name; - } - - /** - * The bucket name. - * @type {String} - */ - get bucket() { - return this._bucket; - } - - /** - * The last modified timestamp. - * @type {Number} - */ - get lastModified() { - return this._lastModified; - } - - /** - * Synchronization strategies. Available strategies are: - * - * - `MANUAL`: Conflicts will be reported in a dedicated array. - * - `SERVER_WINS`: Conflicts are resolved using remote data. - * - `CLIENT_WINS`: Conflicts are resolved using local data. - * - * @type {Object} - */ - static get strategy() { - return { - CLIENT_WINS: "client_wins", - SERVER_WINS: "server_wins", - MANUAL: "manual" - }; - } - - /** - * Validates an idSchema. - * - * @param {Object|undefined} idSchema - * @return {Object} - */ - _validateIdSchema(idSchema) { - if (typeof idSchema === "undefined") { - return createUUIDSchema(); - } - if (typeof idSchema !== "object") { - throw new Error("idSchema must be an object."); - } else if (typeof idSchema.generate !== "function") { - throw new Error("idSchema must provide a generate function."); - } else if (typeof idSchema.validate !== "function") { - throw new Error("idSchema must provide a validate function."); - } - return idSchema; - } - - /** - * Validates a list of remote transformers. - * - * @param {Array|undefined} remoteTransformers - * @return {Array} - */ - _validateRemoteTransformers(remoteTransformers) { - if (typeof remoteTransformers === "undefined") { - return []; - } - if (!Array.isArray(remoteTransformers)) { - throw new Error("remoteTransformers should be an array."); - } - return remoteTransformers.map(transformer => { - if (typeof transformer !== "object") { - throw new Error("A transformer must be an object."); - } else if (typeof transformer.encode !== "function") { - throw new Error("A transformer must provide an encode function."); - } else if (typeof transformer.decode !== "function") { - throw new Error("A transformer must provide a decode function."); - } - return transformer; - }); - } - - /** - * Validate the passed hook is correct. - * - * @param {Array|undefined} hook. - * @return {Array} - **/ - _validateHook(hook) { - if (!Array.isArray(hook)) { - throw new Error("A hook definition should be an array of functions."); - } - return hook.map(fn => { - if (typeof fn !== "function") { - throw new Error("A hook definition should be an array of functions."); - } - return fn; - }); - } - - /** - * Validates a list of hooks. - * - * @param {Object|undefined} hooks - * @return {Object} - */ - _validateHooks(hooks) { - if (typeof hooks === "undefined") { - return {}; - } - if (Array.isArray(hooks)) { - throw new Error("hooks should be an object, not an array."); - } - if (typeof hooks !== "object") { - throw new Error("hooks should be an object."); - } - - const validatedHooks = {}; - - for (let hook in hooks) { - if (AVAILABLE_HOOKS.indexOf(hook) === -1) { - throw new Error("The hook should be one of " + AVAILABLE_HOOKS.join(", ")); - } - validatedHooks[hook] = this._validateHook(hooks[hook]); - } - return validatedHooks; - } - - /** - * Deletes every records in the current collection and marks the collection as - * never synced. - * - * @return {Promise} - */ - clear() { - var _this = this; - - return (0, _asyncToGenerator3.default)(function* () { - yield _this.db.clear(); - yield _this.db.saveLastModified(null); - return { data: [], permissions: {} }; - })(); - } - - /** - * Encodes a record. - * - * @param {String} type Either "remote" or "local". - * @param {Object} record The record object to encode. - * @return {Promise} - */ - _encodeRecord(type, record) { - if (!this[`${ type }Transformers`].length) { - return _promise2.default.resolve(record); - } - return (0, _utils.waterfall)(this[`${ type }Transformers`].map(transformer => { - return record => transformer.encode(record); - }), record); - } - - /** - * Decodes a record. - * - * @param {String} type Either "remote" or "local". - * @param {Object} record The record object to decode. - * @return {Promise} - */ - _decodeRecord(type, record) { - if (!this[`${ type }Transformers`].length) { - return _promise2.default.resolve(record); - } - return (0, _utils.waterfall)(this[`${ type }Transformers`].reverse().map(transformer => { - return record => transformer.decode(record); - }), record); - } - - /** - * Adds a record to the local database, asserting that none - * already exist with this ID. - * - * Note: If either the `useRecordId` or `synced` options are true, then the - * record object must contain the id field to be validated. If none of these - * options are true, an id is generated using the current IdSchema; in this - * case, the record passed must not have an id. - * - * Options: - * - {Boolean} synced Sets record status to "synced" (default: `false`). - * - {Boolean} useRecordId Forces the `id` field from the record to be used, - * instead of one that is generated automatically - * (default: `false`). - * - * @param {Object} record - * @param {Object} options - * @return {Promise} - */ - create(record, options = { useRecordId: false, synced: false }) { - // Validate the record and its ID (if any), even though this - // validation is also done in the CollectionTransaction method, - // because we need to pass the ID to preloadIds. - const reject = msg => _promise2.default.reject(new Error(msg)); - if (typeof record !== "object") { - return reject("Record is not an object."); - } - if ((options.synced || options.useRecordId) && !record.hasOwnProperty("id")) { - return reject("Missing required Id; synced and useRecordId options require one"); - } - if (!options.synced && !options.useRecordId && record.hasOwnProperty("id")) { - return reject("Extraneous Id; can't create a record having one set."); - } - const newRecord = (0, _extends3.default)({}, record, { - id: options.synced || options.useRecordId ? record.id : this.idSchema.generate(), - _status: options.synced ? "synced" : "created" - }); - if (!this.idSchema.validate(newRecord.id)) { - return reject(`Invalid Id: ${ newRecord.id }`); - } - return this.execute(txn => txn.create(newRecord), { preloadIds: [newRecord.id] }).catch(err => { - if (options.useRecordId) { - throw new Error("Couldn't create record. It may have been virtually deleted."); - } - throw err; - }); - } - - /** - * Like {@link CollectionTransaction#update}, but wrapped in its own transaction. - * - * Options: - * - {Boolean} synced: Sets record status to "synced" (default: false) - * - {Boolean} patch: Extends the existing record instead of overwriting it - * (default: false) - * - * @param {Object} record - * @param {Object} options - * @return {Promise} - */ - update(record, options = { synced: false, patch: false }) { - // Validate the record and its ID, even though this validation is - // also done in the CollectionTransaction method, because we need - // to pass the ID to preloadIds. - if (typeof record !== "object") { - return _promise2.default.reject(new Error("Record is not an object.")); - } - if (!record.hasOwnProperty("id")) { - return _promise2.default.reject(new Error("Cannot update a record missing id.")); - } - if (!this.idSchema.validate(record.id)) { - return _promise2.default.reject(new Error(`Invalid Id: ${ record.id }`)); - } - - return this.execute(txn => txn.update(record, options), { preloadIds: [record.id] }); - } - - /** - * Like {@link CollectionTransaction#upsert}, but wrapped in its own transaction. - * - * @param {Object} record - * @return {Promise} - */ - upsert(record) { - // Validate the record and its ID, even though this validation is - // also done in the CollectionTransaction method, because we need - // to pass the ID to preloadIds. - if (typeof record !== "object") { - return _promise2.default.reject(new Error("Record is not an object.")); - } - if (!record.hasOwnProperty("id")) { - return _promise2.default.reject(new Error("Cannot update a record missing id.")); - } - if (!this.idSchema.validate(record.id)) { - return _promise2.default.reject(new Error(`Invalid Id: ${ record.id }`)); - } - - return this.execute(txn => txn.upsert(record), { preloadIds: [record.id] }); - } - - /** - * Like {@link CollectionTransaction#get}, but wrapped in its own transaction. - * - * Options: - * - {Boolean} includeDeleted: Include virtually deleted records. - * - * @param {String} id - * @param {Object} options - * @return {Promise} - */ - get(id, options = { includeDeleted: false }) { - return this.execute(txn => txn.get(id, options), { preloadIds: [id] }); - } - - /** - * Like {@link CollectionTransaction#getAny}, but wrapped in its own transaction. - * - * @param {String} id - * @return {Promise} - */ - getAny(id) { - return this.execute(txn => txn.getAny(id), { preloadIds: [id] }); - } - - /** - * Same as {@link Collection#delete}, but wrapped in its own transaction. - * - * Options: - * - {Boolean} virtual: When set to `true`, doesn't actually delete the record, - * update its `_status` attribute to `deleted` instead (default: true) - * - * @param {String} id The record's Id. - * @param {Object} options The options object. - * @return {Promise} - */ - delete(id, options = { virtual: true }) { - return this.execute(transaction => { - return transaction.delete(id, options); - }, { preloadIds: [id] }); - } - - /** - * The same as {@link CollectionTransaction#deleteAny}, but wrapped - * in its own transaction. - * - * @param {String} id The record's Id. - * @return {Promise} - */ - deleteAny(id) { - return this.execute(txn => txn.deleteAny(id), { preloadIds: [id] }); - } - - /** - * Lists records from the local database. - * - * Params: - * - {Object} filters Filter the results (default: `{}`). - * - {String} order The order to apply (default: `-last_modified`). - * - * Options: - * - {Boolean} includeDeleted: Include virtually deleted records. - * - * @param {Object} params The filters and order to apply to the results. - * @param {Object} options The options object. - * @return {Promise} - */ - list(params = {}, options = { includeDeleted: false }) { - var _this2 = this; - - return (0, _asyncToGenerator3.default)(function* () { - params = (0, _extends3.default)({ order: "-last_modified", filters: {} }, params); - const results = yield _this2.db.list(params); - let data = results; - if (!options.includeDeleted) { - data = results.filter(function (record) { - return record._status !== "deleted"; - }); - } - return { data, permissions: {} }; - })(); - } - - /** - * Imports remote changes into the local database. - * This method is in charge of detecting the conflicts, and resolve them - * according to the specified strategy. - * @param {SyncResultObject} syncResultObject The sync result object. - * @param {Array} decodedChanges The list of changes to import in the local database. - * @param {String} strategy The {@link Collection.strategy} (default: MANUAL) - * @return {Promise} - */ - importChanges(syncResultObject, decodedChanges, strategy = Collection.strategy.MANUAL) { - var _this3 = this; - - return (0, _asyncToGenerator3.default)(function* () { - // Retrieve records matching change ids. - try { - const { imports, resolved } = yield _this3.db.execute(function (transaction) { - const imports = decodedChanges.map(function (remote) { - // Store remote change into local database. - return importChange(transaction, remote, _this3.localFields); - }); - const conflicts = imports.filter(function (i) { - return i.type === "conflicts"; - }).map(function (i) { - return i.data; - }); - const resolved = _this3._handleConflicts(transaction, conflicts, strategy); - return { imports, resolved }; - }, { preload: decodedChanges.map(function (record) { - return record.id; - }) }); - - // Lists of created/updated/deleted records - imports.forEach(function ({ type, data }) { - return syncResultObject.add(type, data); - }); - - // Automatically resolved conflicts (if not manual) - if (resolved.length > 0) { - syncResultObject.reset("conflicts").add("resolved", resolved); - } - } catch (err) { - const data = { - type: "incoming", - message: err.message, - stack: err.stack - }; - // XXX one error of the whole transaction instead of per atomic op - syncResultObject.add("errors", data); - } - - return syncResultObject; - })(); - } - - /** - * Imports the responses of pushed changes into the local database. - * Basically it stores the timestamp assigned by the server into the local - * database. - * @param {SyncResultObject} syncResultObject The sync result object. - * @param {Array} toApplyLocally The list of changes to import in the local database. - * @param {Array} conflicts The list of conflicts that have to be resolved. - * @param {String} strategy The {@link Collection.strategy}. - * @return {Promise} - */ - _applyPushedResults(syncResultObject, toApplyLocally, conflicts, strategy = Collection.strategy.MANUAL) { - var _this4 = this; - - return (0, _asyncToGenerator3.default)(function* () { - const toDeleteLocally = toApplyLocally.filter(function (r) { - return r.deleted; - }); - const toUpdateLocally = toApplyLocally.filter(function (r) { - return !r.deleted; - }); - - const { published, resolved } = yield _this4.db.execute(function (transaction) { - const updated = toUpdateLocally.map(function (record) { - const synced = markSynced(record); - transaction.update(synced); - return synced; - }); - const deleted = toDeleteLocally.map(function (record) { - transaction.delete(record.id); - // Amend result data with the deleted attribute set - return { id: record.id, deleted: true }; - }); - const published = updated.concat(deleted); - // Handle conflicts, if any - const resolved = _this4._handleConflicts(transaction, conflicts, strategy); - return { published, resolved }; - }); - - syncResultObject.add("published", published); - - if (resolved.length > 0) { - syncResultObject.reset("conflicts").reset("resolved").add("resolved", resolved); - } - return syncResultObject; - })(); - } - - /** - * Handles synchronization conflicts according to specified strategy. - * - * @param {SyncResultObject} result The sync result object. - * @param {String} strategy The {@link Collection.strategy}. - * @return {Promise} - */ - _handleConflicts(transaction, conflicts, strategy) { - if (strategy === Collection.strategy.MANUAL) { - return []; - } - return conflicts.map(conflict => { - const resolution = strategy === Collection.strategy.CLIENT_WINS ? conflict.local : conflict.remote; - const updated = this._resolveRaw(conflict, resolution); - transaction.update(updated); - return updated; - }); - } - - /** - * Execute a bunch of operations in a transaction. - * - * This transaction should be atomic -- either all of its operations - * will succeed, or none will. - * - * The argument to this function is itself a function which will be - * called with a {@link CollectionTransaction}. Collection methods - * are available on this transaction, but instead of returning - * promises, they are synchronous. execute() returns a Promise whose - * value will be the return value of the provided function. - * - * Most operations will require access to the record itself, which - * must be preloaded by passing its ID in the preloadIds option. - * - * Options: - * - {Array} preloadIds: list of IDs to fetch at the beginning of - * the transaction - * - * @return {Promise} Resolves with the result of the given function - * when the transaction commits. - */ - execute(doOperations, { preloadIds = [] } = {}) { - for (let id of preloadIds) { - if (!this.idSchema.validate(id)) { - return _promise2.default.reject(Error(`Invalid Id: ${ id }`)); - } - } - - return this.db.execute(transaction => { - const txn = new CollectionTransaction(this, transaction); - const result = doOperations(txn); - txn.emitEvents(); - return result; - }, { preload: preloadIds }); - } - - /** - * Resets the local records as if they were never synced; existing records are - * marked as newly created, deleted records are dropped. - * - * A next call to {@link Collection.sync} will thus republish the whole - * content of the local collection to the server. - * - * @return {Promise} Resolves with the number of processed records. - */ - resetSyncStatus() { - var _this5 = this; - - return (0, _asyncToGenerator3.default)(function* () { - const unsynced = yield _this5.list({ filters: { _status: ["deleted", "synced"] }, order: "" }, { includeDeleted: true }); - yield _this5.db.execute(function (transaction) { - unsynced.data.forEach(function (record) { - if (record._status === "deleted") { - // Garbage collect deleted records. - transaction.delete(record.id); - } else { - // Records that were synced become «created». - transaction.update((0, _extends3.default)({}, record, { - last_modified: undefined, - _status: "created" - })); - } - }); - }); - _this5._lastModified = null; - yield _this5.db.saveLastModified(null); - return unsynced.data.length; - })(); - } - - /** - * Returns an object containing two lists: - * - * - `toDelete`: unsynced deleted records we can safely delete; - * - `toSync`: local updates to send to the server. - * - * @return {Promise} - */ - gatherLocalChanges() { - var _this6 = this; - - return (0, _asyncToGenerator3.default)(function* () { - const unsynced = yield _this6.list({ filters: { _status: ["created", "updated"] }, order: "" }); - const deleted = yield _this6.list({ filters: { _status: "deleted" }, order: "" }, { includeDeleted: true }); - - const toSync = yield _promise2.default.all(unsynced.data.map(_this6._encodeRecord.bind(_this6, "remote"))); - const toDelete = yield _promise2.default.all(deleted.data.map(_this6._encodeRecord.bind(_this6, "remote"))); - - return { toSync, toDelete }; - })(); - } - - /** - * Fetch remote changes, import them to the local database, and handle - * conflicts according to `options.strategy`. Then, updates the passed - * {@link SyncResultObject} with import results. - * - * Options: - * - {String} strategy: The selected sync strategy. - * - * @param {KintoClient.Collection} client Kinto client Collection instance. - * @param {SyncResultObject} syncResultObject The sync result object. - * @param {Object} options - * @return {Promise} - */ - pullChanges(client, syncResultObject, options = {}) { - var _this7 = this; - - return (0, _asyncToGenerator3.default)(function* () { - if (!syncResultObject.ok) { - return syncResultObject; - } - - const since = _this7.lastModified ? _this7.lastModified : yield _this7.db.getLastModified(); - - options = (0, _extends3.default)({ - strategy: Collection.strategy.MANUAL, - lastModified: since, - headers: {} - }, options); - - // Optionally ignore some records when pulling for changes. - // (avoid redownloading our own changes on last step of #sync()) - let filters; - if (options.exclude) { - // Limit the list of excluded records to the first 50 records in order - // to remain under de-facto URL size limit (~2000 chars). - // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184 - const exclude_id = options.exclude.slice(0, 50).map(function (r) { - return r.id; - }).join(","); - filters = { exclude_id }; - } - // First fetch remote changes from the server - const { data, last_modified } = yield client.listRecords({ - // Since should be ETag (see https://github.com/Kinto/kinto.js/issues/356) - since: options.lastModified ? `${ options.lastModified }` : undefined, - headers: options.headers, - filters - }); - // last_modified is the ETag header value (string). - // For retro-compatibility with first kinto.js versions - // parse it to integer. - const unquoted = last_modified ? parseInt(last_modified, 10) : undefined; - - // Check if server was flushed. - // This is relevant for the Kinto demo server - // (and thus for many new comers). - const localSynced = options.lastModified; - const serverChanged = unquoted > options.lastModified; - const emptyCollection = data.length === 0; - if (!options.exclude && localSynced && serverChanged && emptyCollection) { - throw Error("Server has been flushed."); - } - - syncResultObject.lastModified = unquoted; - - // Decode incoming changes. - const decodedChanges = yield _promise2.default.all(data.map(function (change) { - return _this7._decodeRecord("remote", change); - })); - // Hook receives decoded records. - const payload = { lastModified: unquoted, changes: decodedChanges }; - const afterHooks = yield _this7.applyHook("incoming-changes", payload); - - // No change, nothing to import. - if (afterHooks.changes.length > 0) { - // Reflect these changes locally - yield _this7.importChanges(syncResultObject, afterHooks.changes, options.strategy); - } - return syncResultObject; - })(); - } - - applyHook(hookName, payload) { - if (typeof this.hooks[hookName] == "undefined") { - return _promise2.default.resolve(payload); - } - return (0, _utils.waterfall)(this.hooks[hookName].map(hook => { - return record => { - const result = hook(payload, this); - const resultThenable = result && typeof result.then === "function"; - const resultChanges = result && result.hasOwnProperty("changes"); - if (!(resultThenable || resultChanges)) { - throw new Error(`Invalid return value for hook: ${ (0, _stringify2.default)(result) } has no 'then()' or 'changes' properties`); - } - return result; - }; - }), payload); - } - - /** - * Publish local changes to the remote server and updates the passed - * {@link SyncResultObject} with publication results. - * - * @param {KintoClient.Collection} client Kinto client Collection instance. - * @param {SyncResultObject} syncResultObject The sync result object. - * @param {Object} changes The change object. - * @param {Array} changes.toDelete The list of records to delete. - * @param {Array} changes.toSync The list of records to create/update. - * @param {Object} options The options object. - * @return {Promise} - */ - pushChanges(client, { toDelete = [], toSync }, syncResultObject, options = {}) { - var _this8 = this; - - return (0, _asyncToGenerator3.default)(function* () { - if (!syncResultObject.ok) { - return syncResultObject; - } - const safe = !options.strategy || options.strategy !== Collection.CLIENT_WINS; - - // Perform a batch request with every changes. - const synced = yield client.batch(function (batch) { - toDelete.forEach(function (r) { - // never published locally deleted records should not be pusblished - if (r.last_modified) { - batch.deleteRecord(r); - } - }); - toSync.forEach(function (r) { - // Clean local fields (like _status) before sending to server. - const published = _this8.cleanLocalFields(r); - if (r._status === "created") { - batch.createRecord(published); - } else { - batch.updateRecord(published); - } - }); - }, { headers: options.headers, safe, aggregate: true }); - - // Store outgoing errors into sync result object - syncResultObject.add("errors", synced.errors.map(function (e) { - return (0, _extends3.default)({}, e, { type: "outgoing" }); - })); - - // Store outgoing conflicts into sync result object - const conflicts = []; - for (let { type, local, remote } of synced.conflicts) { - // Note: we ensure that local data are actually available, as they may - // be missing in the case of a published deletion. - const safeLocal = local && local.data || { id: remote.id }; - const realLocal = yield _this8._decodeRecord("remote", safeLocal); - const realRemote = yield _this8._decodeRecord("remote", remote); - const conflict = { type, local: realLocal, remote: realRemote }; - conflicts.push(conflict); - } - syncResultObject.add("conflicts", conflicts); - - // Records that must be deleted are either deletions that were pushed - // to server (published) or deleted records that were never pushed (skipped). - const missingRemotely = synced.skipped.map(function (r) { - return (0, _extends3.default)({}, r, { deleted: true }); - }); - - // For created and updated records, the last_modified coming from server - // will be stored locally. - // Reflect publication results locally using the response from - // the batch request. - const published = synced.published.map(function (c) { - return c.data; - }); - const toApplyLocally = published.concat(missingRemotely); - - // Apply the decode transformers, if any - const decoded = yield _promise2.default.all(toApplyLocally.map(function (record) { - return _this8._decodeRecord("remote", record); - })); - - // We have to update the local records with the responses of the server - // (eg. last_modified values etc.). - if (decoded.length > 0 || conflicts.length > 0) { - yield _this8._applyPushedResults(syncResultObject, decoded, conflicts, options.strategy); - } - - return syncResultObject; - })(); - } - - /** - * Return a copy of the specified record without the local fields. - * - * @param {Object} record A record with potential local fields. - * @return {Object} - */ - cleanLocalFields(record) { - const localKeys = RECORD_FIELDS_TO_CLEAN.concat(this.localFields); - return (0, _utils.omitKeys)(record, localKeys); - } - - /** - * Resolves a conflict, updating local record according to proposed - * resolution — keeping remote record `last_modified` value as a reference for - * further batch sending. - * - * @param {Object} conflict The conflict object. - * @param {Object} resolution The proposed record. - * @return {Promise} - */ - resolve(conflict, resolution) { - return this.db.execute(transaction => { - const updated = this._resolveRaw(conflict, resolution); - transaction.update(updated); - return { data: updated, permissions: {} }; - }); - } - - /** - * @private - */ - _resolveRaw(conflict, resolution) { - const resolved = (0, _extends3.default)({}, resolution, { - // Ensure local record has the latest authoritative timestamp - last_modified: conflict.remote.last_modified - }); - // If the resolution object is strictly equal to the - // remote record, then we can mark it as synced locally. - // Otherwise, mark it as updated (so that the resolution is pushed). - const synced = (0, _utils.deepEqual)(resolved, conflict.remote); - return markStatus(resolved, synced ? "synced" : "updated"); - } - - /** - * Synchronize remote and local data. The promise will resolve with a - * {@link SyncResultObject}, though will reject: - * - * - if the server is currently backed off; - * - if the server has been detected flushed. - * - * Options: - * - {Object} headers: HTTP headers to attach to outgoing requests. - * - {Collection.strategy} strategy: See {@link Collection.strategy}. - * - {Boolean} ignoreBackoff: Force synchronization even if server is currently - * backed off. - * - {String} bucket: The remove bucket id to use (default: null) - * - {String} collection: The remove collection id to use (default: null) - * - {String} remote The remote Kinto server endpoint to use (default: null). - * - * @param {Object} options Options. - * @return {Promise} - * @throws {Error} If an invalid remote option is passed. - */ - sync(options = { - strategy: Collection.strategy.MANUAL, - headers: {}, - ignoreBackoff: false, - bucket: null, - collection: null, - remote: null - }) { - var _this9 = this; - - return (0, _asyncToGenerator3.default)(function* () { - const previousRemote = _this9.api.remote; - if (options.remote) { - // Note: setting the remote ensures it's valid, throws when invalid. - _this9.api.remote = options.remote; - } - if (!options.ignoreBackoff && _this9.api.backoff > 0) { - const seconds = Math.ceil(_this9.api.backoff / 1000); - return _promise2.default.reject(new Error(`Server is asking clients to back off; retry in ${ seconds }s or use the ignoreBackoff option.`)); - } - - const client = _this9.api.bucket(options.bucket || _this9.bucket).collection(options.collection || _this9.name); - - const result = new SyncResultObject(); - try { - // Fetch last changes from the server. - yield _this9.pullChanges(client, result, options); - const { lastModified } = result; - - // Fetch local changes - const { toDelete, toSync } = yield _this9.gatherLocalChanges(); - - // Publish local changes and pull local resolutions - yield _this9.pushChanges(client, { toDelete, toSync }, result, options); - - // Publish local resolution of push conflicts to server (on CLIENT_WINS) - const resolvedUnsynced = result.resolved.filter(function (r) { - return r._status !== "synced"; - }); - if (resolvedUnsynced.length > 0) { - const resolvedEncoded = yield _promise2.default.all(resolvedUnsynced.map(_this9._encodeRecord.bind(_this9, "remote"))); - yield _this9.pushChanges(client, { toSync: resolvedEncoded }, result, options); - } - // Perform a last pull to catch changes that occured after the last pull, - // while local changes were pushed. Do not do it nothing was pushed. - if (result.published.length > 0) { - // Avoid redownloading our own changes during the last pull. - const pullOpts = (0, _extends3.default)({}, options, { lastModified, exclude: result.published }); - yield _this9.pullChanges(client, result, pullOpts); - } - - // Don't persist lastModified value if any conflict or error occured - if (result.ok) { - // No conflict occured, persist collection's lastModified value - _this9._lastModified = yield _this9.db.saveLastModified(result.lastModified); - } - } finally { - // Ensure API default remote is reverted if a custom one's been used - _this9.api.remote = previousRemote; - } - return result; - })(); - } - - /** - * Load a list of records already synced with the remote server. - * - * The local records which are unsynced or whose timestamp is either missing - * or superior to those being loaded will be ignored. - * - * @param {Array} records The previously exported list of records to load. - * @return {Promise} with the effectively imported records. - */ - loadDump(records) { - var _this10 = this; - - return (0, _asyncToGenerator3.default)(function* () { - if (!Array.isArray(records)) { - throw new Error("Records is not an array."); - } - - for (let record of records) { - if (!record.hasOwnProperty("id") || !_this10.idSchema.validate(record.id)) { - throw new Error("Record has invalid ID: " + (0, _stringify2.default)(record)); - } - - if (!record.last_modified) { - throw new Error("Record has no last_modified value: " + (0, _stringify2.default)(record)); - } - } - - // Fetch all existing records from local database, - // and skip those who are newer or not marked as synced. - - // XXX filter by status / ids in records - - const { data } = yield _this10.list({}, { includeDeleted: true }); - const existingById = data.reduce(function (acc, record) { - acc[record.id] = record; - return acc; - }, {}); - - const newRecords = records.filter(function (record) { - const localRecord = existingById[record.id]; - const shouldKeep = - // No local record with this id. - localRecord === undefined || - // Or local record is synced - localRecord._status === "synced" && - // And was synced from server - localRecord.last_modified !== undefined && - // And is older than imported one. - record.last_modified > localRecord.last_modified; - return shouldKeep; - }); - - return yield _this10.db.loadDump(newRecords.map(markSynced)); - })(); - } -} - -exports.default = Collection; /** - * A Collection-oriented wrapper for an adapter's transaction. - * - * This defines the high-level functions available on a collection. - * The collection itself offers functions of the same name. These will - * perform just one operation in its own transaction. - */ - -class CollectionTransaction { - constructor(collection, adapterTransaction) { - this.collection = collection; - this.adapterTransaction = adapterTransaction; - - this._events = []; - } - - _queueEvent(action, payload) { - this._events.push({ action, payload }); - } - - /** - * Emit queued events, to be called once every transaction operations have - * been executed successfully. - */ - emitEvents() { - for (let { action, payload } of this._events) { - this.collection.events.emit(action, payload); - } - if (this._events.length > 0) { - const targets = this._events.map(({ action, payload }) => (0, _extends3.default)({ action }, payload)); - this.collection.events.emit("change", { targets }); - } - this._events = []; - } - - /** - * Retrieve a record by its id from the local database, or - * undefined if none exists. - * - * This will also return virtually deleted records. - * - * @param {String} id - * @return {Object} - */ - getAny(id) { - const record = this.adapterTransaction.get(id); - return { data: record, permissions: {} }; - } - - /** - * Retrieve a record by its id from the local database. - * - * Options: - * - {Boolean} includeDeleted: Include virtually deleted records. - * - * @param {String} id - * @param {Object} options - * @return {Object} - */ - get(id, options = { includeDeleted: false }) { - const res = this.getAny(id); - if (!res.data || !options.includeDeleted && res.data._status === "deleted") { - throw new Error(`Record with id=${ id } not found.`); - } - - return res; - } - - /** - * Deletes a record from the local database. - * - * Options: - * - {Boolean} virtual: When set to `true`, doesn't actually delete the record, - * update its `_status` attribute to `deleted` instead (default: true) - * - * @param {String} id The record's Id. - * @param {Object} options The options object. - * @return {Object} - */ - delete(id, options = { virtual: true }) { - // Ensure the record actually exists. - const existing = this.adapterTransaction.get(id); - const alreadyDeleted = existing && existing._status == "deleted"; - if (!existing || alreadyDeleted && options.virtual) { - throw new Error(`Record with id=${ id } not found.`); - } - // Virtual updates status. - if (options.virtual) { - this.adapterTransaction.update(markDeleted(existing)); - } else { - // Delete for real. - this.adapterTransaction.delete(id); - } - this._queueEvent("delete", { data: existing }); - return { data: existing, permissions: {} }; - } - - /** - * Deletes a record from the local database, if any exists. - * Otherwise, do nothing. - * - * @param {String} id The record's Id. - * @return {Object} - */ - deleteAny(id) { - const existing = this.adapterTransaction.get(id); - if (existing) { - this.adapterTransaction.update(markDeleted(existing)); - this._queueEvent("delete", { data: existing }); - } - return { data: (0, _extends3.default)({ id }, existing), deleted: !!existing, permissions: {} }; - } - - /** - * Adds a record to the local database, asserting that none - * already exist with this ID. - * - * @param {Object} record, which must contain an ID - * @return {Object} - */ - create(record) { - if (typeof record !== "object") { - throw new Error("Record is not an object."); - } - if (!record.hasOwnProperty("id")) { - throw new Error("Cannot create a record missing id"); - } - if (!this.collection.idSchema.validate(record.id)) { - throw new Error(`Invalid Id: ${ record.id }`); - } - - this.adapterTransaction.create(record); - this._queueEvent("create", { data: record }); - return { data: record, permissions: {} }; - } - - /** - * Updates a record from the local database. - * - * Options: - * - {Boolean} synced: Sets record status to "synced" (default: false) - * - {Boolean} patch: Extends the existing record instead of overwriting it - * (default: false) - * - * @param {Object} record - * @param {Object} options - * @return {Object} - */ - update(record, options = { synced: false, patch: false }) { - if (typeof record !== "object") { - throw new Error("Record is not an object."); - } - if (!record.hasOwnProperty("id")) { - throw new Error("Cannot update a record missing id."); - } - if (!this.collection.idSchema.validate(record.id)) { - throw new Error(`Invalid Id: ${ record.id }`); - } - - const oldRecord = this.adapterTransaction.get(record.id); - if (!oldRecord) { - throw new Error(`Record with id=${ record.id } not found.`); - } - const newRecord = options.patch ? (0, _extends3.default)({}, oldRecord, record) : record; - const updated = this._updateRaw(oldRecord, newRecord, options); - this.adapterTransaction.update(updated); - this._queueEvent("update", { data: updated, oldRecord }); - return { data: updated, oldRecord, permissions: {} }; - } - - /** - * Lower-level primitive for updating a record while respecting - * _status and last_modified. - * - * @param {Object} oldRecord: the record retrieved from the DB - * @param {Object} newRecord: the record to replace it with - * @return {Object} - */ - _updateRaw(oldRecord, newRecord, { synced = false } = {}) { - const updated = (0, _extends3.default)({}, newRecord); - // Make sure to never loose the existing timestamp. - if (oldRecord && oldRecord.last_modified && !updated.last_modified) { - updated.last_modified = oldRecord.last_modified; - } - // If only local fields have changed, then keep record as synced. - // If status is created, keep record as created. - // If status is deleted, mark as updated. - const isIdentical = oldRecord && recordsEqual(oldRecord, updated, this.localFields); - const keepSynced = isIdentical && oldRecord._status == "synced"; - const neverSynced = !oldRecord || oldRecord && oldRecord._status == "created"; - const newStatus = keepSynced || synced ? "synced" : neverSynced ? "created" : "updated"; - return markStatus(updated, newStatus); - } - - /** - * Upsert a record into the local database. - * - * This record must have an ID. - * - * If a record with this ID already exists, it will be replaced. - * Otherwise, this record will be inserted. - * - * @param {Object} record - * @return {Object} - */ - upsert(record) { - if (typeof record !== "object") { - throw new Error("Record is not an object."); - } - if (!record.hasOwnProperty("id")) { - throw new Error("Cannot update a record missing id."); - } - if (!this.collection.idSchema.validate(record.id)) { - throw new Error(`Invalid Id: ${ record.id }`); - } - let oldRecord = this.adapterTransaction.get(record.id); - const updated = this._updateRaw(oldRecord, record); - this.adapterTransaction.update(updated); - // Don't return deleted records -- pretend they are gone - if (oldRecord && oldRecord._status == "deleted") { - oldRecord = undefined; - } - if (oldRecord) { - this._queueEvent("update", { data: updated, oldRecord }); - } else { - this._queueEvent("create", { data: updated }); - } - return { data: updated, oldRecord, permissions: {} }; - } -} -exports.CollectionTransaction = CollectionTransaction; - -},{"./adapters/IDB":84,"./adapters/base":85,"./utils":87,"babel-runtime/core-js/json/stringify":3,"babel-runtime/core-js/object/assign":4,"babel-runtime/core-js/promise":6,"babel-runtime/helpers/asyncToGenerator":7,"babel-runtime/helpers/extends":8,"uuid":9}],87:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.RE_UUID = undefined; - -var _promise = require("babel-runtime/core-js/promise"); - -var _promise2 = _interopRequireDefault(_promise); - -var _keys = require("babel-runtime/core-js/object/keys"); - -var _keys2 = _interopRequireDefault(_keys); - -exports.sortObjects = sortObjects; -exports.filterObject = filterObject; -exports.filterObjects = filterObjects; -exports.isUUID = isUUID; -exports.waterfall = waterfall; -exports.deepEqual = deepEqual; -exports.omitKeys = omitKeys; - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -const RE_UUID = exports.RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - -/** - * Checks if a value is undefined. - * @param {Any} value - * @return {Boolean} - */ -function _isUndefined(value) { - return typeof value === "undefined"; -} - -/** - * Sorts records in a list according to a given ordering. - * - * @param {String} order The ordering, eg. `-last_modified`. - * @param {Array} list The collection to order. - * @return {Array} - */ -function sortObjects(order, list) { - const hasDash = order[0] === "-"; - const field = hasDash ? order.slice(1) : order; - const direction = hasDash ? -1 : 1; - return list.slice().sort((a, b) => { - if (a[field] && _isUndefined(b[field])) { - return direction; - } - if (b[field] && _isUndefined(a[field])) { - return -direction; - } - if (_isUndefined(a[field]) && _isUndefined(b[field])) { - return 0; - } - return a[field] > b[field] ? direction : -direction; - }); -} - -/** - * Test if a single object matches all given filters. - * - * @param {Object} filters The filters object. - * @param {Object} entry The object to filter. - * @return {Function} - */ -function filterObject(filters, entry) { - return (0, _keys2.default)(filters).every(filter => { - const value = filters[filter]; - if (Array.isArray(value)) { - return value.some(candidate => candidate === entry[filter]); - } - return entry[filter] === value; - }); -} - -/** - * Filters records in a list matching all given filters. - * - * @param {Object} filters The filters object. - * @param {Array} list The collection to filter. - * @return {Array} - */ -function filterObjects(filters, list) { - return list.filter(entry => { - return filterObject(filters, entry); - }); -} - -/** - * Checks if a string is an UUID. - * - * @param {String} uuid The uuid to validate. - * @return {Boolean} - */ -function isUUID(uuid) { - return RE_UUID.test(uuid); -} - -/** - * Resolves a list of functions sequentially, which can be sync or async; in - * case of async, functions must return a promise. - * - * @param {Array} fns The list of functions. - * @param {Any} init The initial value. - * @return {Promise} - */ -function waterfall(fns, init) { - if (!fns.length) { - return _promise2.default.resolve(init); - } - return fns.reduce((promise, nextFn) => { - return promise.then(nextFn); - }, _promise2.default.resolve(init)); -} - -/** - * Simple deep object comparison function. This only supports comparison of - * serializable JavaScript objects. - * - * @param {Object} a The source object. - * @param {Object} b The compared object. - * @return {Boolean} - */ -function deepEqual(a, b) { - if (a === b) { - return true; - } - if (typeof a !== typeof b) { - return false; - } - if (!(a && typeof a == "object") || !(b && typeof b == "object")) { - return false; - } - if ((0, _keys2.default)(a).length !== (0, _keys2.default)(b).length) { - return false; - } - for (let k in a) { - if (!deepEqual(a[k], b[k])) { - return false; - } - } - return true; -} - -/** - * Return an object without the specified keys. - * - * @param {Object} obj The original object. - * @param {Array} keys The list of keys to exclude. - * @return {Object} A copy without the specified keys. - */ -function omitKeys(obj, keys = []) { - return (0, _keys2.default)(obj).reduce((acc, key) => { - if (keys.indexOf(key) === -1) { - acc[key] = obj[key]; - } - return acc; - }, {}); -} - -},{"babel-runtime/core-js/object/keys":5,"babel-runtime/core-js/promise":6}]},{},[2])(2) -});
\ No newline at end of file diff --git a/services/common/moz.build b/services/common/moz.build index c09e6bed0..26a1bd9b4 100644 --- a/services/common/moz.build +++ b/services/common/moz.build @@ -15,10 +15,6 @@ EXTRA_COMPONENTS += [ EXTRA_JS_MODULES['services-common'] += [ 'async.js', - 'blocklist-clients.js', - 'blocklist-updater.js', - 'kinto-http-client.js', - 'kinto-offline-client.js', 'logmanager.js', 'observers.js', 'rest.js', diff --git a/services/common/tests/unit/test_blocklist_certificates.js b/services/common/tests/unit/test_blocklist_certificates.js deleted file mode 100644 index e85970321..000000000 --- a/services/common/tests/unit/test_blocklist_certificates.js +++ /dev/null @@ -1,224 +0,0 @@ -const { Constructor: CC } = Components; - -Cu.import("resource://testing-common/httpd.js"); - -const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js"); -const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js"); - -const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", - "nsIBinaryInputStream", "setInputStream"); - -let server; - -// set up what we need to make storage adapters -const Kinto = loadKinto(); -const FirefoxAdapter = Kinto.adapters.FirefoxAdapter; -const kintoFilename = "kinto.sqlite"; - -let kintoClient; - -function do_get_kinto_collection(collectionName) { - if (!kintoClient) { - let config = { - // Set the remote to be some server that will cause test failure when - // hit since we should never hit the server directly, only via maybeSync() - remote: "https://firefox.settings.services.mozilla.com/v1/", - // Set up the adapter and bucket as normal - adapter: FirefoxAdapter, - bucket: "blocklists" - }; - kintoClient = new Kinto(config); - } - return kintoClient.collection(collectionName); -} - -// Some simple tests to demonstrate that the logic inside maybeSync works -// correctly and that simple kinto operations are working as expected. There -// are more tests for core Kinto.js (and its storage adapter) in the -// xpcshell tests under /services/common -add_task(function* test_something(){ - const configPath = "/v1/"; - const recordsPath = "/v1/buckets/blocklists/collections/certificates/records"; - - Services.prefs.setCharPref("services.settings.server", - `http://localhost:${server.identity.primaryPort}/v1`); - - // register a handler - function handleResponse (request, response) { - try { - const sample = getSampleResponse(request, server.identity.primaryPort); - if (!sample) { - do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`); - } - - response.setStatusLine(null, sample.status.status, - sample.status.statusText); - // send the headers - for (let headerLine of sample.sampleHeaders) { - let headerElements = headerLine.split(':'); - response.setHeader(headerElements[0], headerElements[1].trimLeft()); - } - response.setHeader("Date", (new Date()).toUTCString()); - - response.write(sample.responseBody); - } catch (e) { - do_print(e); - } - } - server.registerPathHandler(configPath, handleResponse); - server.registerPathHandler(recordsPath, handleResponse); - - // Test an empty db populates - let result = yield OneCRLBlocklistClient.maybeSync(2000, Date.now()); - - // Open the collection, verify it's been populated: - // Our test data has a single record; it should be in the local collection - let collection = do_get_kinto_collection("certificates"); - yield collection.db.open(); - let list = yield collection.list(); - do_check_eq(list.data.length, 1); - yield collection.db.close(); - - // Test the db is updated when we call again with a later lastModified value - result = yield OneCRLBlocklistClient.maybeSync(4000, Date.now()); - - // Open the collection, verify it's been updated: - // Our test data now has two records; both should be in the local collection - collection = do_get_kinto_collection("certificates"); - yield collection.db.open(); - list = yield collection.list(); - do_check_eq(list.data.length, 3); - yield collection.db.close(); - - // Try to maybeSync with the current lastModified value - no connection - // should be attempted. - // Clear the kinto base pref so any connections will cause a test failure - Services.prefs.clearUserPref("services.settings.server"); - yield OneCRLBlocklistClient.maybeSync(4000, Date.now()); - - // Try again with a lastModified value at some point in the past - yield OneCRLBlocklistClient.maybeSync(3000, Date.now()); - - // Check the OneCRL check time pref is modified, even if the collection - // hasn't changed - Services.prefs.setIntPref("services.blocklist.onecrl.checked", 0); - yield OneCRLBlocklistClient.maybeSync(3000, Date.now()); - let newValue = Services.prefs.getIntPref("services.blocklist.onecrl.checked"); - do_check_neq(newValue, 0); - - // Check that a sync completes even when there's bad data in the - // collection. This will throw on fail, so just calling maybeSync is an - // acceptible test. - Services.prefs.setCharPref("services.settings.server", - `http://localhost:${server.identity.primaryPort}/v1`); - yield OneCRLBlocklistClient.maybeSync(5000, Date.now()); -}); - -function run_test() { - // Ensure that signature verification is disabled to prevent interference - // with basic certificate sync tests - Services.prefs.setBoolPref("services.blocklist.signing.enforced", false); - - // Set up an HTTP Server - server = new HttpServer(); - server.start(-1); - - run_next_test(); - - do_register_cleanup(function() { - server.stop(() => { }); - }); -} - -// get a response for a given request from sample data -function getSampleResponse(req, port) { - const responses = { - "OPTIONS": { - "sampleHeaders": [ - "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page", - "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS", - "Access-Control-Allow-Origin: *", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": "null" - }, - "GET:/v1/?": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"}) - }, - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"3000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "issuerName": "MEQxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwx0aGF3dGUsIEluYy4xHjAcBgNVBAMTFXRoYXd0ZSBFViBTU0wgQ0EgLSBHMw==", - "serialNumber":"CrTHPEE6AZSfI3jysin2bA==", - "id":"78cf8900-fdea-4ce5-f8fb-b78710617718", - "last_modified":3000 - }]}) - }, - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"4000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "issuerName":"MFkxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKjAoBgNVBAMTIVN0YWF0IGRlciBOZWRlcmxhbmRlbiBPdmVyaGVpZCBDQQ", - "serialNumber":"ATFpsA==", - "id":"dabafde9-df4a-ddba-2548-748da04cc02c", - "last_modified":4000 - },{ - "subject":"MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5", - "pubKeyHash":"VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=", - "id":"dabafde9-df4a-ddba-2548-748da04cc02d", - "last_modified":4000 - }]}) - }, - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"5000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "issuerName":"not a base64 encoded issuer", - "serialNumber":"not a base64 encoded serial", - "id":"dabafde9-df4a-ddba-2548-748da04cc02e", - "last_modified":5000 - },{ - "subject":"not a base64 encoded subject", - "pubKeyHash":"not a base64 encoded pubKeyHash", - "id":"dabafde9-df4a-ddba-2548-748da04cc02f", - "last_modified":5000 - },{ - "subject":"MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5", - "pubKeyHash":"VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=", - "id":"dabafde9-df4a-ddba-2548-748da04cc02g", - "last_modified":5000 - }]}) - } - }; - return responses[`${req.method}:${req.path}?${req.queryString}`] || - responses[req.method]; - -} diff --git a/services/common/tests/unit/test_blocklist_clients.js b/services/common/tests/unit/test_blocklist_clients.js deleted file mode 100644 index 121fac926..000000000 --- a/services/common/tests/unit/test_blocklist_clients.js +++ /dev/null @@ -1,412 +0,0 @@ -const { Constructor: CC } = Components; - -const KEY_PROFILEDIR = "ProfD"; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://testing-common/httpd.js"); -Cu.import("resource://gre/modules/Timer.jsm"); -const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm"); -const { OS } = Cu.import("resource://gre/modules/osfile.jsm"); - -const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js"); -const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js"); - -const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", - "nsIBinaryInputStream", "setInputStream"); - -const gBlocklistClients = [ - {client: BlocklistClients.AddonBlocklistClient, filename: BlocklistClients.FILENAME_ADDONS_JSON, testData: ["i808","i720", "i539"]}, - {client: BlocklistClients.PluginBlocklistClient, filename: BlocklistClients.FILENAME_PLUGINS_JSON, testData: ["p1044","p32","p28"]}, - {client: BlocklistClients.GfxBlocklistClient, filename: BlocklistClients.FILENAME_GFX_JSON, testData: ["g204","g200","g36"]}, -]; - - -let server; -let kintoClient; - -function kintoCollection(collectionName) { - if (!kintoClient) { - const Kinto = loadKinto(); - const FirefoxAdapter = Kinto.adapters.FirefoxAdapter; - const config = { - // Set the remote to be some server that will cause test failure when - // hit since we should never hit the server directly, only via maybeSync() - remote: "https://firefox.settings.services.mozilla.com/v1/", - adapter: FirefoxAdapter, - bucket: "blocklists" - }; - kintoClient = new Kinto(config); - } - return kintoClient.collection(collectionName); -} - -function* readJSON(filepath) { - const binaryData = yield OS.File.read(filepath); - const textData = (new TextDecoder()).decode(binaryData); - return Promise.resolve(JSON.parse(textData)); -} - -function* clear_state() { - for (let {client} of gBlocklistClients) { - // Remove last server times. - Services.prefs.clearUserPref(client.lastCheckTimePref); - - // Clear local DB. - const collection = kintoCollection(client.collectionName); - try { - yield collection.db.open(); - yield collection.clear(); - } finally { - yield collection.db.close(); - } - } - - // Remove profile data. - for (let {filename} of gBlocklistClients) { - const blocklist = FileUtils.getFile(KEY_PROFILEDIR, [filename]); - if (blocklist.exists()) { - blocklist.remove(true); - } - } -} - - -function run_test() { - // Set up an HTTP Server - server = new HttpServer(); - server.start(-1); - - // Point the blocklist clients to use this local HTTP server. - Services.prefs.setCharPref("services.settings.server", - `http://localhost:${server.identity.primaryPort}/v1`); - - // Setup server fake responses. - function handleResponse(request, response) { - try { - const sample = getSampleResponse(request, server.identity.primaryPort); - if (!sample) { - do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`); - } - - response.setStatusLine(null, sample.status.status, - sample.status.statusText); - // send the headers - for (let headerLine of sample.sampleHeaders) { - let headerElements = headerLine.split(':'); - response.setHeader(headerElements[0], headerElements[1].trimLeft()); - } - response.setHeader("Date", (new Date()).toUTCString()); - - response.write(sample.responseBody); - response.finish(); - } catch (e) { - do_print(e); - } - } - const configPath = "/v1/"; - const addonsRecordsPath = "/v1/buckets/blocklists/collections/addons/records"; - const gfxRecordsPath = "/v1/buckets/blocklists/collections/gfx/records"; - const pluginsRecordsPath = "/v1/buckets/blocklists/collections/plugins/records"; - server.registerPathHandler(configPath, handleResponse); - server.registerPathHandler(addonsRecordsPath, handleResponse); - server.registerPathHandler(gfxRecordsPath, handleResponse); - server.registerPathHandler(pluginsRecordsPath, handleResponse); - - - run_next_test(); - - do_register_cleanup(function() { - server.stop(() => { }); - }); -} - -add_task(function* test_records_obtained_from_server_are_stored_in_db(){ - for (let {client} of gBlocklistClients) { - // Test an empty db populates - let result = yield client.maybeSync(2000, Date.now()); - - // Open the collection, verify it's been populated: - // Our test data has a single record; it should be in the local collection - let collection = kintoCollection(client.collectionName); - yield collection.db.open(); - let list = yield collection.list(); - equal(list.data.length, 1); - yield collection.db.close(); - } -}); -add_task(clear_state); - -add_task(function* test_list_is_written_to_file_in_profile(){ - for (let {client, filename, testData} of gBlocklistClients) { - const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]); - strictEqual(profFile.exists(), false); - - let result = yield client.maybeSync(2000, Date.now()); - - strictEqual(profFile.exists(), true); - const content = yield readJSON(profFile.path); - equal(content.data[0].blockID, testData[testData.length - 1]); - } -}); -add_task(clear_state); - -add_task(function* test_current_server_time_is_saved_in_pref(){ - for (let {client} of gBlocklistClients) { - const before = Services.prefs.getIntPref(client.lastCheckTimePref); - const serverTime = Date.now(); - yield client.maybeSync(2000, serverTime); - const after = Services.prefs.getIntPref(client.lastCheckTimePref); - equal(after, Math.round(serverTime / 1000)); - } -}); -add_task(clear_state); - -add_task(function* test_update_json_file_when_addons_has_changes(){ - for (let {client, filename, testData} of gBlocklistClients) { - yield client.maybeSync(2000, Date.now() - 1000); - const before = Services.prefs.getIntPref(client.lastCheckTimePref); - const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]); - const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000; - const serverTime = Date.now(); - - yield client.maybeSync(3001, serverTime); - - // File was updated. - notEqual(fileLastModified, profFile.lastModifiedTime); - const content = yield readJSON(profFile.path); - deepEqual(content.data.map((r) => r.blockID), testData); - // Server time was updated. - const after = Services.prefs.getIntPref(client.lastCheckTimePref); - equal(after, Math.round(serverTime / 1000)); - } -}); -add_task(clear_state); - -add_task(function* test_sends_reload_message_when_blocklist_has_changes(){ - for (let {client, filename} of gBlocklistClients) { - let received = yield new Promise((resolve, reject) => { - Services.ppmm.addMessageListener("Blocklist:reload-from-disk", { - receiveMessage(aMsg) { resolve(aMsg) } - }); - - client.maybeSync(2000, Date.now() - 1000); - }); - - equal(received.data.filename, filename); - } -}); -add_task(clear_state); - -add_task(function* test_do_nothing_when_blocklist_is_up_to_date(){ - for (let {client, filename} of gBlocklistClients) { - yield client.maybeSync(2000, Date.now() - 1000); - const before = Services.prefs.getIntPref(client.lastCheckTimePref); - const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]); - const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000; - const serverTime = Date.now(); - - yield client.maybeSync(3000, serverTime); - - // File was not updated. - equal(fileLastModified, profFile.lastModifiedTime); - // Server time was updated. - const after = Services.prefs.getIntPref(client.lastCheckTimePref); - equal(after, Math.round(serverTime / 1000)); - } -}); -add_task(clear_state); - - - -// get a response for a given request from sample data -function getSampleResponse(req, port) { - const responses = { - "OPTIONS": { - "sampleHeaders": [ - "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page", - "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS", - "Access-Control-Allow-Origin: *", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": "null" - }, - "GET:/v1/?": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"}) - }, - "GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"3000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "prefs": [], - "blockID": "i539", - "last_modified": 3000, - "versionRange": [{ - "targetApplication": [], - "maxVersion": "*", - "minVersion": "0", - "severity": "1" - }], - "guid": "ScorpionSaver@jetpack", - "id": "9d500963-d80e-3a91-6e74-66f3811b99cc" - }]}) - }, - "GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"3000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "matchFilename": "NPFFAddOn.dll", - "blockID": "p28", - "id": "7b1e0b3c-e390-a817-11b6-a6887f65f56e", - "last_modified": 3000, - "versionRange": [] - }]}) - }, - "GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"3000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "driverVersionComparator": "LESS_THAN_OR_EQUAL", - "driverVersion": "8.17.12.5896", - "vendor": "0x10de", - "blockID": "g36", - "feature": "DIRECT3D_9_LAYERS", - "devices": ["0x0a6c"], - "featureStatus": "BLOCKED_DRIVER_VERSION", - "last_modified": 3000, - "os": "WINNT 6.1", - "id": "3f947f16-37c2-4e96-d356-78b26363729b" - }]}) - }, - "GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified&_since=3000": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"4000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "prefs": [], - "blockID": "i808", - "last_modified": 4000, - "versionRange": [{ - "targetApplication": [], - "maxVersion": "*", - "minVersion": "0", - "severity": "3" - }], - "guid": "{c96d1ae6-c4cf-4984-b110-f5f561b33b5a}", - "id": "9ccfac91-e463-c30c-f0bd-14143794a8dd" - }, { - "prefs": ["browser.startup.homepage"], - "blockID": "i720", - "last_modified": 3500, - "versionRange": [{ - "targetApplication": [], - "maxVersion": "*", - "minVersion": "0", - "severity": "1" - }], - "guid": "FXqG@xeeR.net", - "id": "cf9b3129-a97e-dbd7-9525-a8575ac03c25" - }]}) - }, - "GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified&_since=3000": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"4000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "infoURL": "https://get.adobe.com/flashplayer/", - "blockID": "p1044", - "matchFilename": "libflashplayer\\.so", - "last_modified": 4000, - "versionRange": [{ - "targetApplication": [], - "minVersion": "11.2.202.509", - "maxVersion": "11.2.202.539", - "severity": "0", - "vulnerabilityStatus": "1" - }], - "os": "Linux", - "id": "aabad965-e556-ffe7-4191-074f5dee3df3" - }, { - "matchFilename": "npViewpoint.dll", - "blockID": "p32", - "id": "1f48af42-c508-b8ef-b8d5-609d48e4f6c9", - "last_modified": 3500, - "versionRange": [{ - "targetApplication": [{ - "minVersion": "3.0", - "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", - "maxVersion": "*" - }] - }] - }]}) - }, - "GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified&_since=3000": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"4000\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{ - "vendor": "0x8086", - "blockID": "g204", - "feature": "WEBGL_MSAA", - "devices": [], - "id": "c96bca82-e6bd-044d-14c4-9c1d67e9283a", - "last_modified": 4000, - "os": "Darwin 10", - "featureStatus": "BLOCKED_DEVICE" - }, { - "vendor": "0x10de", - "blockID": "g200", - "feature": "WEBGL_MSAA", - "devices": [], - "id": "c3a15ba9-e0e2-421f-e399-c995e5b8d14e", - "last_modified": 3500, - "os": "Darwin 11", - "featureStatus": "BLOCKED_DEVICE" - }]}) - } - }; - return responses[`${req.method}:${req.path}?${req.queryString}`] || - responses[req.method]; - -} diff --git a/services/common/tests/unit/test_blocklist_signatures.js b/services/common/tests/unit/test_blocklist_signatures.js deleted file mode 100644 index b2ee1019a..000000000 --- a/services/common/tests/unit/test_blocklist_signatures.js +++ /dev/null @@ -1,510 +0,0 @@ -"use strict"; - -Cu.import("resource://services-common/blocklist-updater.js"); -Cu.import("resource://testing-common/httpd.js"); - -const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js"); -const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); -const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js"); - -let server; - -const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket"; -const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced"; -const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection"; -const PREF_SETTINGS_SERVER = "services.settings.server"; -const PREF_SIGNATURE_ROOT = "security.content.signature.root_hash"; - - -const CERT_DIR = "test_blocklist_signatures/"; -const CHAIN_FILES = - ["collection_signing_ee.pem", - "collection_signing_int.pem", - "collection_signing_root.pem"]; - -function getFileData(file) { - const stream = Cc["@mozilla.org/network/file-input-stream;1"] - .createInstance(Ci.nsIFileInputStream); - stream.init(file, -1, 0, 0); - const data = NetUtil.readInputStreamToString(stream, stream.available()); - stream.close(); - return data; -} - -function setRoot() { - const filename = CERT_DIR + CHAIN_FILES[0]; - - const certFile = do_get_file(filename, false); - const b64cert = getFileData(certFile) - .replace(/-----BEGIN CERTIFICATE-----/, "") - .replace(/-----END CERTIFICATE-----/, "") - .replace(/[\r\n]/g, ""); - const certdb = Cc["@mozilla.org/security/x509certdb;1"] - .getService(Ci.nsIX509CertDB); - const cert = certdb.constructX509FromBase64(b64cert); - Services.prefs.setCharPref(PREF_SIGNATURE_ROOT, cert.sha256Fingerprint); -} - -function getCertChain() { - const chain = []; - for (let file of CHAIN_FILES) { - chain.push(getFileData(do_get_file(CERT_DIR + file))); - } - return chain.join("\n"); -} - -function* checkRecordCount(count) { - // open the collection manually - const base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER); - const bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET); - const collectionName = - Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION); - - const Kinto = loadKinto(); - - const FirefoxAdapter = Kinto.adapters.FirefoxAdapter; - - const config = { - remote: base, - bucket: bucket, - adapter: FirefoxAdapter, - }; - - const db = new Kinto(config); - const collection = db.collection(collectionName); - - yield collection.db.open(); - - // Check we have the expected number of records - let records = yield collection.list(); - do_check_eq(count, records.data.length); - - // Close the collection so the test can exit cleanly - yield collection.db.close(); -} - -// Check to ensure maybeSync is called with correct values when a changes -// document contains information on when a collection was last modified -add_task(function* test_check_signatures(){ - const port = server.identity.primaryPort; - - // a response to give the client when the cert chain is expected - function makeMetaResponseBody(lastModified, signature) { - return { - data: { - id: "certificates", - last_modified: lastModified, - signature: { - x5u: `http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem`, - public_key: "fake", - "content-signature": `x5u=http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem;p384ecdsa=${signature}`, - signature_encoding: "rs_base64url", - signature: signature, - hash_algorithm: "sha384", - ref: "1yryrnmzou5rf31ou80znpnq8n" - } - } - }; - } - - function makeMetaResponse(eTag, body, comment) { - return { - comment: comment, - sampleHeaders: [ - "Content-Type: application/json; charset=UTF-8", - `ETag: \"${eTag}\"` - ], - status: {status: 200, statusText: "OK"}, - responseBody: JSON.stringify(body) - }; - } - - function registerHandlers(responses){ - function handleResponse (serverTimeMillis, request, response) { - const key = `${request.method}:${request.path}?${request.queryString}`; - const available = responses[key]; - const sampled = available.length > 1 ? available.shift() : available[0]; - - if (!sampled) { - do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`); - } - - response.setStatusLine(null, sampled.status.status, - sampled.status.statusText); - // send the headers - for (let headerLine of sampled.sampleHeaders) { - let headerElements = headerLine.split(':'); - response.setHeader(headerElements[0], headerElements[1].trimLeft()); - } - - // set the server date - response.setHeader("Date", (new Date(serverTimeMillis)).toUTCString()); - - response.write(sampled.responseBody); - } - - for (let key of Object.keys(responses)) { - const keyParts = key.split(":"); - const method = keyParts[0]; - const valueParts = keyParts[1].split("?"); - const path = valueParts[0]; - - server.registerPathHandler(path, handleResponse.bind(null, 2000)); - } - } - - // First, perform a signature verification with known data and signature - // to ensure things are working correctly - let verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"] - .createInstance(Ci.nsIContentSignatureVerifier); - - const emptyData = '[]'; - const emptySignature = "p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9"; - const name = "onecrl.content-signature.mozilla.org"; - ok(verifier.verifyContentSignature(emptyData, emptySignature, - getCertChain(), name)); - - verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"] - .createInstance(Ci.nsIContentSignatureVerifier); - - const collectionData = '[{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:43:37Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"97fbf7c4-3ef2-f54f-0029-1ba6540c63ea","issuerName":"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==","last_modified":2000,"serialNumber":"BAAAAAABA/A35EU="},{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:48:11Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc","issuerName":"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB","last_modified":3000,"serialNumber":"BAAAAAABI54PryQ="}]'; - const collectionSignature = "p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p"; - - ok(verifier.verifyContentSignature(collectionData, collectionSignature, getCertChain(), name)); - - // set up prefs so the kinto updater talks to the test server - Services.prefs.setCharPref(PREF_SETTINGS_SERVER, - `http://localhost:${server.identity.primaryPort}/v1`); - - // Set up some data we need for our test - let startTime = Date.now(); - - // These are records we'll use in the test collections - const RECORD1 = { - details: { - bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145", - created: "2016-01-18T14:43:37Z", - name: "GlobalSign certs", - who: ".", - why: "." - }, - enabled: true, - id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea", - issuerName: "MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==", - last_modified: 2000, - serialNumber: "BAAAAAABA/A35EU=" - }; - - const RECORD2 = { - details: { - bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145", - created: "2016-01-18T14:48:11Z", - name: "GlobalSign certs", - who: ".", - why: "." - }, - enabled: true, - id: "e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc", - issuerName: "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB", - last_modified: 3000, - serialNumber: "BAAAAAABI54PryQ=" - }; - - const RECORD3 = { - details: { - bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145", - created: "2016-01-18T14:48:11Z", - name: "GlobalSign certs", - who: ".", - why: "." - }, - enabled: true, - id: "c7c49b69-a4ab-418e-92a9-e1961459aa7f", - issuerName: "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB", - last_modified: 4000, - serialNumber: "BAAAAAABI54PryQ=" - }; - - const RECORD1_DELETION = { - deleted: true, - enabled: true, - id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea", - last_modified: 3500, - }; - - // Check that a signature on an empty collection is OK - // We need to set up paths on the HTTP server to return specific data from - // specific paths for each test. Here we prepare data for each response. - - // A cert chain response (this the cert chain that contains the signing - // cert, the root and any intermediates in between). This is used in each - // sync. - const RESPONSE_CERT_CHAIN = { - comment: "RESPONSE_CERT_CHAIN", - sampleHeaders: [ - "Content-Type: text/plain; charset=UTF-8" - ], - status: {status: 200, statusText: "OK"}, - responseBody: getCertChain() - }; - - // A server settings response. This is used in each sync. - const RESPONSE_SERVER_SETTINGS = { - comment: "RESPONSE_SERVER_SETTINGS", - sampleHeaders: [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress" - ], - status: {status: 200, statusText: "OK"}, - responseBody: JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"}) - }; - - // This is the initial, empty state of the collection. This is only used - // for the first sync. - const RESPONSE_EMPTY_INITIAL = { - comment: "RESPONSE_EMPTY_INITIAL", - sampleHeaders: [ - "Content-Type: application/json; charset=UTF-8", - "ETag: \"1000\"" - ], - status: {status: 200, statusText: "OK"}, - responseBody: JSON.stringify({"data": []}) - }; - - const RESPONSE_BODY_META_EMPTY_SIG = makeMetaResponseBody(1000, - "vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u"); - - // The collection metadata containing the signature for the empty - // collection. - const RESPONSE_META_EMPTY_SIG = - makeMetaResponse(1000, RESPONSE_BODY_META_EMPTY_SIG, - "RESPONSE_META_EMPTY_SIG"); - - // Here, we map request method and path to the available responses - const emptyCollectionResponses = { - "GET:/test_blocklist_signatures/test_cert_chain.pem?":[RESPONSE_CERT_CHAIN], - "GET:/v1/?": [RESPONSE_SERVER_SETTINGS], - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified": - [RESPONSE_EMPTY_INITIAL], - "GET:/v1/buckets/blocklists/collections/certificates?": - [RESPONSE_META_EMPTY_SIG] - }; - - // .. and use this map to register handlers for each path - registerHandlers(emptyCollectionResponses); - - // With all of this set up, we attempt a sync. This will resolve if all is - // well and throw if something goes wrong. - yield OneCRLBlocklistClient.maybeSync(1000, startTime); - - // Check that some additions (2 records) to the collection have a valid - // signature. - - // This response adds two entries (RECORD1 and RECORD2) to the collection - const RESPONSE_TWO_ADDED = { - comment: "RESPONSE_TWO_ADDED", - sampleHeaders: [ - "Content-Type: application/json; charset=UTF-8", - "ETag: \"3000\"" - ], - status: {status: 200, statusText: "OK"}, - responseBody: JSON.stringify({"data": [RECORD2, RECORD1]}) - }; - - const RESPONSE_BODY_META_TWO_ITEMS_SIG = makeMetaResponseBody(3000, - "dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy"); - - // A signature response for the collection containg RECORD1 and RECORD2 - const RESPONSE_META_TWO_ITEMS_SIG = - makeMetaResponse(3000, RESPONSE_BODY_META_TWO_ITEMS_SIG, - "RESPONSE_META_TWO_ITEMS_SIG"); - - const twoItemsResponses = { - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=1000": - [RESPONSE_TWO_ADDED], - "GET:/v1/buckets/blocklists/collections/certificates?": - [RESPONSE_META_TWO_ITEMS_SIG] - }; - registerHandlers(twoItemsResponses); - yield OneCRLBlocklistClient.maybeSync(3000, startTime); - - // Check the collection with one addition and one removal has a valid - // signature - - // Remove RECORD1, add RECORD3 - const RESPONSE_ONE_ADDED_ONE_REMOVED = { - comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ", - sampleHeaders: [ - "Content-Type: application/json; charset=UTF-8", - "ETag: \"4000\"" - ], - status: {status: 200, statusText: "OK"}, - responseBody: JSON.stringify({"data": [RECORD3, RECORD1_DELETION]}) - }; - - const RESPONSE_BODY_META_THREE_ITEMS_SIG = makeMetaResponseBody(4000, - "MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw"); - - // signature response for the collection containing RECORD2 and RECORD3 - const RESPONSE_META_THREE_ITEMS_SIG = - makeMetaResponse(4000, RESPONSE_BODY_META_THREE_ITEMS_SIG, - "RESPONSE_META_THREE_ITEMS_SIG"); - - const oneAddedOneRemovedResponses = { - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000": - [RESPONSE_ONE_ADDED_ONE_REMOVED], - "GET:/v1/buckets/blocklists/collections/certificates?": - [RESPONSE_META_THREE_ITEMS_SIG] - }; - registerHandlers(oneAddedOneRemovedResponses); - yield OneCRLBlocklistClient.maybeSync(4000, startTime); - - // Check the signature is still valid with no operation (no changes) - - // Leave the collection unchanged - const RESPONSE_EMPTY_NO_UPDATE = { - comment: "RESPONSE_EMPTY_NO_UPDATE ", - sampleHeaders: [ - "Content-Type: application/json; charset=UTF-8", - "ETag: \"4000\"" - ], - status: {status: 200, statusText: "OK"}, - responseBody: JSON.stringify({"data": []}) - }; - - const noOpResponses = { - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000": - [RESPONSE_EMPTY_NO_UPDATE], - "GET:/v1/buckets/blocklists/collections/certificates?": - [RESPONSE_META_THREE_ITEMS_SIG] - }; - registerHandlers(noOpResponses); - yield OneCRLBlocklistClient.maybeSync(4100, startTime); - - // Check the collection is reset when the signature is invalid - - // Prepare a (deliberately) bad signature to check the collection state is - // reset if something is inconsistent - const RESPONSE_COMPLETE_INITIAL = { - comment: "RESPONSE_COMPLETE_INITIAL ", - sampleHeaders: [ - "Content-Type: application/json; charset=UTF-8", - "ETag: \"4000\"" - ], - status: {status: 200, statusText: "OK"}, - responseBody: JSON.stringify({"data": [RECORD2, RECORD3]}) - }; - - const RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID = { - comment: "RESPONSE_COMPLETE_INITIAL ", - sampleHeaders: [ - "Content-Type: application/json; charset=UTF-8", - "ETag: \"4000\"" - ], - status: {status: 200, statusText: "OK"}, - responseBody: JSON.stringify({"data": [RECORD3, RECORD2]}) - }; - - const RESPONSE_BODY_META_BAD_SIG = makeMetaResponseBody(4000, - "aW52YWxpZCBzaWduYXR1cmUK"); - - const RESPONSE_META_BAD_SIG = - makeMetaResponse(4000, RESPONSE_BODY_META_BAD_SIG, "RESPONSE_META_BAD_SIG"); - - const badSigGoodSigResponses = { - // In this test, we deliberately serve a bad signature initially. The - // subsequent signature returned is a valid one for the three item - // collection. - "GET:/v1/buckets/blocklists/collections/certificates?": - [RESPONSE_META_BAD_SIG, RESPONSE_META_THREE_ITEMS_SIG], - // The first collection state is the three item collection (since - // there's a sync with no updates) - but, since the signature is wrong, - // another request will be made... - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000": - [RESPONSE_EMPTY_NO_UPDATE], - // The next request is for the full collection. This will be checked - // against the valid signature - so the sync should succeed. - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified": - [RESPONSE_COMPLETE_INITIAL], - // The next request is for the full collection sorted by id. This will be - // checked against the valid signature - so the sync should succeed. - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id": - [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID] - }; - - registerHandlers(badSigGoodSigResponses); - yield OneCRLBlocklistClient.maybeSync(5000, startTime); - - const badSigGoodOldResponses = { - // In this test, we deliberately serve a bad signature initially. The - // subsequent sitnature returned is a valid one for the three item - // collection. - "GET:/v1/buckets/blocklists/collections/certificates?": - [RESPONSE_META_BAD_SIG, RESPONSE_META_EMPTY_SIG], - // The first collection state is the current state (since there's no update - // - but, since the signature is wrong, another request will be made) - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000": - [RESPONSE_EMPTY_NO_UPDATE], - // The next request is for the full collection sorted by id. This will be - // checked against the valid signature and last_modified times will be - // compared. Sync should fail, even though the signature is good, - // because the local collection is newer. - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id": - [RESPONSE_EMPTY_INITIAL], - }; - - // ensure our collection hasn't been replaced with an older, empty one - yield checkRecordCount(2); - - registerHandlers(badSigGoodOldResponses); - yield OneCRLBlocklistClient.maybeSync(5000, startTime); - - const allBadSigResponses = { - // In this test, we deliberately serve only a bad signature. - "GET:/v1/buckets/blocklists/collections/certificates?": - [RESPONSE_META_BAD_SIG], - // The first collection state is the three item collection (since - // there's a sync with no updates) - but, since the signature is wrong, - // another request will be made... - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000": - [RESPONSE_EMPTY_NO_UPDATE], - // The next request is for the full collection sorted by id. This will be - // checked against the valid signature - so the sync should succeed. - "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id": - [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID] - }; - - registerHandlers(allBadSigResponses); - try { - yield OneCRLBlocklistClient.maybeSync(6000, startTime); - do_throw("Sync should fail (the signature is intentionally bad)"); - } catch (e) { - yield checkRecordCount(2); - } -}); - -function run_test() { - // ensure signatures are enforced - Services.prefs.setBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING, true); - - // get a signature verifier to ensure nsNSSComponent is initialized - Cc["@mozilla.org/security/contentsignatureverifier;1"] - .createInstance(Ci.nsIContentSignatureVerifier); - - // set the content signing root to our test root - setRoot(); - - // Set up an HTTP Server - server = new HttpServer(); - server.start(-1); - - run_next_test(); - - do_register_cleanup(function() { - server.stop(function() { }); - }); -} - - diff --git a/services/common/tests/unit/test_blocklist_updater.js b/services/common/tests/unit/test_blocklist_updater.js deleted file mode 100644 index 1b71c194a..000000000 --- a/services/common/tests/unit/test_blocklist_updater.js +++ /dev/null @@ -1,173 +0,0 @@ -Cu.import("resource://testing-common/httpd.js"); - -var server; - -const PREF_SETTINGS_SERVER = "services.settings.server"; -const PREF_LAST_UPDATE = "services.blocklist.last_update_seconds"; -const PREF_LAST_ETAG = "services.blocklist.last_etag"; -const PREF_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds"; - -// Check to ensure maybeSync is called with correct values when a changes -// document contains information on when a collection was last modified -add_task(function* test_check_maybeSync(){ - const changesPath = "/v1/buckets/monitor/collections/changes/records"; - - // register a handler - function handleResponse (serverTimeMillis, request, response) { - try { - const sampled = getSampleResponse(request, server.identity.primaryPort); - if (!sampled) { - do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`); - } - - response.setStatusLine(null, sampled.status.status, - sampled.status.statusText); - // send the headers - for (let headerLine of sampled.sampleHeaders) { - let headerElements = headerLine.split(':'); - response.setHeader(headerElements[0], headerElements[1].trimLeft()); - } - - // set the server date - response.setHeader("Date", (new Date(serverTimeMillis)).toUTCString()); - - response.write(sampled.responseBody); - } catch (e) { - dump(`${e}\n`); - } - } - - server.registerPathHandler(changesPath, handleResponse.bind(null, 2000)); - - // set up prefs so the kinto updater talks to the test server - Services.prefs.setCharPref(PREF_SETTINGS_SERVER, - `http://localhost:${server.identity.primaryPort}/v1`); - - // set some initial values so we can check these are updated appropriately - Services.prefs.setIntPref(PREF_LAST_UPDATE, 0); - Services.prefs.setIntPref(PREF_CLOCK_SKEW_SECONDS, 0); - Services.prefs.clearUserPref(PREF_LAST_ETAG); - - - let startTime = Date.now(); - - let updater = Cu.import("resource://services-common/blocklist-updater.js"); - - let syncPromise = new Promise(function(resolve, reject) { - // add a test kinto client that will respond to lastModified information - // for a collection called 'test-collection' - updater.addTestBlocklistClient("test-collection", { - maybeSync(lastModified, serverTime) { - do_check_eq(lastModified, 1000); - do_check_eq(serverTime, 2000); - resolve(); - } - }); - updater.checkVersions(); - }); - - // ensure we get the maybeSync call - yield syncPromise; - - // check the last_update is updated - do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2); - - // How does the clock difference look? - let endTime = Date.now(); - let clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS); - // we previously set the serverTime to 2 (seconds past epoch) - do_check_true(clockDifference <= endTime / 1000 - && clockDifference >= Math.floor(startTime / 1000) - 2); - // Last timestamp was saved. An ETag header value is a quoted string. - let lastEtag = Services.prefs.getCharPref(PREF_LAST_ETAG); - do_check_eq(lastEtag, "\"1100\""); - - // Simulate a poll with up-to-date collection. - Services.prefs.setIntPref(PREF_LAST_UPDATE, 0); - // If server has no change, a 304 is received, maybeSync() is not called. - updater.addTestBlocklistClient("test-collection", { - maybeSync: () => {throw new Error("Should not be called");} - }); - yield updater.checkVersions(); - // Last update is overwritten - do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2); - - - // Simulate a server error. - function simulateErrorResponse (request, response) { - response.setHeader("Date", (new Date(3000)).toUTCString()); - response.setHeader("Content-Type", "application/json; charset=UTF-8"); - response.write(JSON.stringify({ - code: 503, - errno: 999, - error: "Service Unavailable", - })); - response.setStatusLine(null, 503, "Service Unavailable"); - } - server.registerPathHandler(changesPath, simulateErrorResponse); - // checkVersions() fails with adequate error. - let error; - try { - yield updater.checkVersions(); - } catch (e) { - error = e; - } - do_check_eq(error.message, "Polling for changes failed."); - // When an error occurs, last update was not overwritten (see Date header above). - do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2); - - // check negative clock skew times - - // set to a time in the future - server.registerPathHandler(changesPath, handleResponse.bind(null, Date.now() + 10000)); - - yield updater.checkVersions(); - - clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS); - // we previously set the serverTime to Date.now() + 10000 ms past epoch - do_check_true(clockDifference <= 0 && clockDifference >= -10); -}); - -function run_test() { - // Set up an HTTP Server - server = new HttpServer(); - server.start(-1); - - run_next_test(); - - do_register_cleanup(function() { - server.stop(function() { }); - }); -} - -// get a response for a given request from sample data -function getSampleResponse(req, port) { - const responses = { - "GET:/v1/buckets/monitor/collections/changes/records?": { - "sampleHeaders": [ - "Content-Type: application/json; charset=UTF-8", - "ETag: \"1100\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data": [{ - "host": "localhost", - "last_modified": 1100, - "bucket": "blocklists:aurora", - "id": "330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a", - "collection": "test-collection" - }, { - "host": "localhost", - "last_modified": 1000, - "bucket": "blocklists", - "id": "254cbb9e-6888-4d9f-8e60-58b74faa8778", - "collection": "test-collection" - }]}) - } - }; - - if (req.hasHeader("if-none-match") && req.getHeader("if-none-match", "") == "\"1100\"") - return {sampleHeaders: [], status: {status: 304, statusText: "Not Modified"}, responseBody: ""}; - - return responses[`${req.method}:${req.path}?${req.queryString}`] || - responses[req.method]; -} diff --git a/services/common/tests/unit/test_kinto.js b/services/common/tests/unit/test_kinto.js deleted file mode 100644 index 9c5ce58d9..000000000 --- a/services/common/tests/unit/test_kinto.js +++ /dev/null @@ -1,412 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -Cu.import("resource://services-common/kinto-offline-client.js"); -Cu.import("resource://testing-common/httpd.js"); - -const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1", - "nsIBinaryInputStream", "setInputStream"); - -var server; - -// set up what we need to make storage adapters -const Kinto = loadKinto(); -const FirefoxAdapter = Kinto.adapters.FirefoxAdapter; -const kintoFilename = "kinto.sqlite"; - -let kintoClient; - -function do_get_kinto_collection() { - if (!kintoClient) { - let config = { - remote:`http://localhost:${server.identity.primaryPort}/v1/`, - headers: {Authorization: "Basic " + btoa("user:pass")}, - adapter: FirefoxAdapter - }; - kintoClient = new Kinto(config); - } - return kintoClient.collection("test_collection"); -} - -function* clear_collection() { - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - yield collection.clear(); - } finally { - yield collection.db.close(); - } -} - -// test some operations on a local collection -add_task(function* test_kinto_add_get() { - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - - let newRecord = { foo: "bar" }; - // check a record is created - let createResult = yield collection.create(newRecord); - do_check_eq(createResult.data.foo, newRecord.foo); - // check getting the record gets the same info - let getResult = yield collection.get(createResult.data.id); - deepEqual(createResult.data, getResult.data); - // check what happens if we create the same item again (it should throw - // since you can't create with id) - try { - yield collection.create(createResult.data); - do_throw("Creation of a record with an id should fail"); - } catch (err) { } - // try a few creates without waiting for the first few to resolve - let promises = []; - promises.push(collection.create(newRecord)); - promises.push(collection.create(newRecord)); - promises.push(collection.create(newRecord)); - yield collection.create(newRecord); - yield Promise.all(promises); - } finally { - yield collection.db.close(); - } -}); - -add_task(clear_collection); - -// test some operations on multiple connections -add_task(function* test_kinto_add_get() { - const collection1 = do_get_kinto_collection(); - const collection2 = kintoClient.collection("test_collection_2"); - - try { - yield collection1.db.open(); - yield collection2.db.open(); - - let newRecord = { foo: "bar" }; - - // perform several write operations alternately without waiting for promises - // to resolve - let promises = []; - for (let i = 0; i < 10; i++) { - promises.push(collection1.create(newRecord)); - promises.push(collection2.create(newRecord)); - } - - // ensure subsequent operations still work - yield Promise.all([collection1.create(newRecord), - collection2.create(newRecord)]); - yield Promise.all(promises); - } finally { - yield collection1.db.close(); - yield collection2.db.close(); - } -}); - -add_task(clear_collection); - -add_task(function* test_kinto_update() { - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - const newRecord = { foo: "bar" }; - // check a record is created - let createResult = yield collection.create(newRecord); - do_check_eq(createResult.data.foo, newRecord.foo); - do_check_eq(createResult.data._status, "created"); - // check we can update this OK - let copiedRecord = Object.assign(createResult.data, {}); - deepEqual(createResult.data, copiedRecord); - copiedRecord.foo = "wibble"; - let updateResult = yield collection.update(copiedRecord); - // check the field was updated - do_check_eq(updateResult.data.foo, copiedRecord.foo); - // check the status is still "created", since we haven't synced - // the record - do_check_eq(updateResult.data._status, "created"); - } finally { - yield collection.db.close(); - } -}); - -add_task(clear_collection); - -add_task(function* test_kinto_clear() { - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - - // create an expected number of records - const expected = 10; - const newRecord = { foo: "bar" }; - for (let i = 0; i < expected; i++) { - yield collection.create(newRecord); - } - // check the collection contains the correct number - let list = yield collection.list(); - do_check_eq(list.data.length, expected); - // clear the collection and check again - should be 0 - yield collection.clear(); - list = yield collection.list(); - do_check_eq(list.data.length, 0); - } finally { - yield collection.db.close(); - } -}); - -add_task(clear_collection); - -add_task(function* test_kinto_delete(){ - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - const newRecord = { foo: "bar" }; - // check a record is created - let createResult = yield collection.create(newRecord); - do_check_eq(createResult.data.foo, newRecord.foo); - // check getting the record gets the same info - let getResult = yield collection.get(createResult.data.id); - deepEqual(createResult.data, getResult.data); - // delete that record - let deleteResult = yield collection.delete(createResult.data.id); - // check the ID is set on the result - do_check_eq(getResult.data.id, deleteResult.data.id); - // and check that get no longer returns the record - try { - getResult = yield collection.get(createResult.data.id); - do_throw("there should not be a result"); - } catch (e) { } - } finally { - yield collection.db.close(); - } -}); - -add_task(function* test_kinto_list(){ - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - const expected = 10; - const created = []; - for (let i = 0; i < expected; i++) { - let newRecord = { foo: "test " + i }; - let createResult = yield collection.create(newRecord); - created.push(createResult.data); - } - // check the collection contains the correct number - let list = yield collection.list(); - do_check_eq(list.data.length, expected); - - // check that all created records exist in the retrieved list - for (let createdRecord of created) { - let found = false; - for (let retrievedRecord of list.data) { - if (createdRecord.id == retrievedRecord.id) { - deepEqual(createdRecord, retrievedRecord); - found = true; - } - } - do_check_true(found); - } - } finally { - yield collection.db.close(); - } -}); - -add_task(clear_collection); - -add_task(function* test_loadDump_ignores_already_imported_records(){ - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541}; - yield collection.loadDump([record]); - let impactedRecords = yield collection.loadDump([record]); - do_check_eq(impactedRecords.length, 0); - } finally { - yield collection.db.close(); - } -}); - -add_task(clear_collection); - -add_task(function* test_loadDump_should_overwrite_old_records(){ - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541}; - yield collection.loadDump([record]); - const updated = Object.assign({}, record, {last_modified: 1457896543}); - let impactedRecords = yield collection.loadDump([updated]); - do_check_eq(impactedRecords.length, 1); - } finally { - yield collection.db.close(); - } -}); - -add_task(clear_collection); - -add_task(function* test_loadDump_should_not_overwrite_unsynced_records(){ - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f"; - yield collection.create({id: recordId, title: "foo"}, {useRecordId: true}); - const record = {id: recordId, title: "bar", last_modified: 1457896541}; - let impactedRecords = yield collection.loadDump([record]); - do_check_eq(impactedRecords.length, 0); - } finally { - yield collection.db.close(); - } -}); - -add_task(clear_collection); - -add_task(function* test_loadDump_should_not_overwrite_records_without_last_modified(){ - const collection = do_get_kinto_collection(); - try { - yield collection.db.open(); - const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f"; - yield collection.create({id: recordId, title: "foo"}, {synced: true}); - const record = {id: recordId, title: "bar", last_modified: 1457896541}; - let impactedRecords = yield collection.loadDump([record]); - do_check_eq(impactedRecords.length, 0); - } finally { - yield collection.db.close(); - } -}); - -add_task(clear_collection); - -// Now do some sanity checks against a server - we're not looking to test -// core kinto.js functionality here (there is excellent test coverage in -// kinto.js), more making sure things are basically working as expected. -add_task(function* test_kinto_sync(){ - const configPath = "/v1/"; - const recordsPath = "/v1/buckets/default/collections/test_collection/records"; - // register a handler - function handleResponse (request, response) { - try { - const sampled = getSampleResponse(request, server.identity.primaryPort); - if (!sampled) { - do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`); - } - - response.setStatusLine(null, sampled.status.status, - sampled.status.statusText); - // send the headers - for (let headerLine of sampled.sampleHeaders) { - let headerElements = headerLine.split(':'); - response.setHeader(headerElements[0], headerElements[1].trimLeft()); - } - response.setHeader("Date", (new Date()).toUTCString()); - - response.write(sampled.responseBody); - } catch (e) { - dump(`${e}\n`); - } - } - server.registerPathHandler(configPath, handleResponse); - server.registerPathHandler(recordsPath, handleResponse); - - // create an empty collection, sync to populate - const collection = do_get_kinto_collection(); - try { - let result; - - yield collection.db.open(); - result = yield collection.sync(); - do_check_true(result.ok); - - // our test data has a single record; it should be in the local collection - let list = yield collection.list(); - do_check_eq(list.data.length, 1); - - // now sync again; we should now have 2 records - result = yield collection.sync(); - do_check_true(result.ok); - list = yield collection.list(); - do_check_eq(list.data.length, 2); - - // sync again; the second records should have been modified - const before = list.data[0].title; - result = yield collection.sync(); - do_check_true(result.ok); - list = yield collection.list(); - const after = list.data[0].title; - do_check_neq(before, after); - } finally { - yield collection.db.close(); - } -}); - -function run_test() { - // Set up an HTTP Server - server = new HttpServer(); - server.start(-1); - - run_next_test(); - - do_register_cleanup(function() { - server.stop(function() { }); - }); -} - -// get a response for a given request from sample data -function getSampleResponse(req, port) { - const responses = { - "OPTIONS": { - "sampleHeaders": [ - "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page", - "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS", - "Access-Control-Allow-Origin: *", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": "null" - }, - "GET:/v1/?": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"}) - }, - "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"1445606341071\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{"last_modified":1445606341071, "done":false, "id":"68db8313-686e-4fff-835e-07d78ad6f2af", "title":"New test"}]}) - }, - "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"1445607941223\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{"last_modified":1445607941223, "done":false, "id":"901967b0-f729-4b30-8d8d-499cba7f4b1d", "title":"Another new test"}]}) - }, - "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445607941223": { - "sampleHeaders": [ - "Access-Control-Allow-Origin: *", - "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", - "Content-Type: application/json; charset=UTF-8", - "Server: waitress", - "Etag: \"1445607541265\"" - ], - "status": {status: 200, statusText: "OK"}, - "responseBody": JSON.stringify({"data":[{"last_modified":1445607541265, "done":false, "id":"901967b0-f729-4b30-8d8d-499cba7f4b1d", "title":"Modified title"}]}) - } - }; - return responses[`${req.method}:${req.path}?${req.queryString}`] || - responses[req.method]; - -} diff --git a/services/common/tests/unit/xpcshell.ini b/services/common/tests/unit/xpcshell.ini index dbec09519..f1185c2c0 100644 --- a/services/common/tests/unit/xpcshell.ini +++ b/services/common/tests/unit/xpcshell.ini @@ -9,14 +9,6 @@ support-files = # Test load modules first so syntax failures are caught early. [test_load_modules.js] -[test_blocklist_certificates.js] -[test_blocklist_clients.js] -[test_blocklist_updater.js] - -[test_kinto.js] -[test_blocklist_signatures.js] -[test_storage_adapter.js] - [test_utils_atob.js] [test_utils_convert_string.js] [test_utils_dateprefs.js] diff --git a/services/sync/modules/engines/extension-storage.js b/services/sync/modules/engines/extension-storage.js deleted file mode 100644 index f8f15b128..000000000 --- a/services/sync/modules/engines/extension-storage.js +++ /dev/null @@ -1,277 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ['ExtensionStorageEngine', 'EncryptionRemoteTransformer', - 'KeyRingEncryptionRemoteTransformer']; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/keys.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-common/async.js"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync", - "resource://gre/modules/ExtensionStorageSync.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); - -/** - * The Engine that manages syncing for the web extension "storage" - * API, and in particular ext.storage.sync. - * - * ext.storage.sync is implemented using Kinto, so it has mechanisms - * for syncing that we do not need to integrate in the Firefox Sync - * framework, so this is something of a stub. - */ -this.ExtensionStorageEngine = function ExtensionStorageEngine(service) { - SyncEngine.call(this, "Extension-Storage", service); -}; -ExtensionStorageEngine.prototype = { - __proto__: SyncEngine.prototype, - _trackerObj: ExtensionStorageTracker, - // we don't need these since we implement our own sync logic - _storeObj: undefined, - _recordObj: undefined, - - syncPriority: 10, - allowSkippedRecord: false, - - _sync: function () { - return Async.promiseSpinningly(ExtensionStorageSync.syncAll()); - }, - - get enabled() { - // By default, we sync extension storage if we sync addons. This - // lets us simplify the UX since users probably don't consider - // "extension preferences" a separate category of syncing. - // However, we also respect engine.extension-storage.force, which - // can be set to true or false, if a power user wants to customize - // the behavior despite the lack of UI. - const forced = Svc.Prefs.get("engine." + this.prefName + ".force", undefined); - if (forced !== undefined) { - return forced; - } - return Svc.Prefs.get("engine.addons", false); - }, -}; - -function ExtensionStorageTracker(name, engine) { - Tracker.call(this, name, engine); -} -ExtensionStorageTracker.prototype = { - __proto__: Tracker.prototype, - - startTracking: function () { - Svc.Obs.add("ext.storage.sync-changed", this); - }, - - stopTracking: function () { - Svc.Obs.remove("ext.storage.sync-changed", this); - }, - - observe: function (subject, topic, data) { - Tracker.prototype.observe.call(this, subject, topic, data); - - if (this.ignoreAll) { - return; - } - - if (topic !== "ext.storage.sync-changed") { - return; - } - - // Single adds, removes and changes are not so important on their - // own, so let's just increment score a bit. - this.score += SCORE_INCREMENT_MEDIUM; - }, - - // Override a bunch of methods which don't do anything for us. - // This is a performance hack. - saveChangedIDs: function() { - }, - loadChangedIDs: function() { - }, - ignoreID: function() { - }, - unignoreID: function() { - }, - addChangedID: function() { - }, - removeChangedID: function() { - }, - clearChangedIDs: function() { - }, -}; - -/** - * Utility function to enforce an order of fields when computing an HMAC. - */ -function ciphertextHMAC(keyBundle, id, IV, ciphertext) { - const hasher = keyBundle.sha256HMACHasher; - return Utils.bytesAsHex(Utils.digestUTF8(id + IV + ciphertext, hasher)); -} - -/** - * A "remote transformer" that the Kinto library will use to - * encrypt/decrypt records when syncing. - * - * This is an "abstract base class". Subclass this and override - * getKeys() to use it. - */ -class EncryptionRemoteTransformer { - encode(record) { - const self = this; - return Task.spawn(function* () { - const keyBundle = yield self.getKeys(); - if (record.ciphertext) { - throw new Error("Attempt to reencrypt??"); - } - let id = record.id; - if (!record.id) { - throw new Error("Record ID is missing or invalid"); - } - - let IV = Svc.Crypto.generateRandomIV(); - let ciphertext = Svc.Crypto.encrypt(JSON.stringify(record), - keyBundle.encryptionKeyB64, IV); - let hmac = ciphertextHMAC(keyBundle, id, IV, ciphertext); - const encryptedResult = {ciphertext, IV, hmac, id}; - if (record.hasOwnProperty("last_modified")) { - encryptedResult.last_modified = record.last_modified; - } - return encryptedResult; - }); - } - - decode(record) { - const self = this; - return Task.spawn(function* () { - if (!record.ciphertext) { - // This can happen for tombstones if a record is deleted. - if (record.deleted) { - return record; - } - throw new Error("No ciphertext: nothing to decrypt?"); - } - const keyBundle = yield self.getKeys(); - // Authenticate the encrypted blob with the expected HMAC - let computedHMAC = ciphertextHMAC(keyBundle, record.id, record.IV, record.ciphertext); - - if (computedHMAC != record.hmac) { - Utils.throwHMACMismatch(record.hmac, computedHMAC); - } - - // Handle invalid data here. Elsewhere we assume that cleartext is an object. - let cleartext = Svc.Crypto.decrypt(record.ciphertext, - keyBundle.encryptionKeyB64, record.IV); - let jsonResult = JSON.parse(cleartext); - if (!jsonResult || typeof jsonResult !== "object") { - throw new Error("Decryption failed: result is <" + jsonResult + ">, not an object."); - } - - // Verify that the encrypted id matches the requested record's id. - // This should always be true, because we compute the HMAC over - // the original record's ID, and that was verified already (above). - if (jsonResult.id != record.id) { - throw new Error("Record id mismatch: " + jsonResult.id + " != " + record.id); - } - - if (record.hasOwnProperty("last_modified")) { - jsonResult.last_modified = record.last_modified; - } - - return jsonResult; - }); - } - - /** - * Retrieve keys to use during encryption. - * - * Returns a Promise<KeyBundle>. - */ - getKeys() { - throw new Error("override getKeys in a subclass"); - } -} -// You can inject this -EncryptionRemoteTransformer.prototype._fxaService = fxAccounts; - -/** - * An EncryptionRemoteTransformer that provides a keybundle derived - * from the user's kB, suitable for encrypting a keyring. - */ -class KeyRingEncryptionRemoteTransformer extends EncryptionRemoteTransformer { - getKeys() { - const self = this; - return Task.spawn(function* () { - const user = yield self._fxaService.getSignedInUser(); - // FIXME: we should permit this if the user is self-hosting - // their storage - if (!user) { - throw new Error("user isn't signed in to FxA; can't sync"); - } - - if (!user.kB) { - throw new Error("user doesn't have kB"); - } - - let kB = Utils.hexToBytes(user.kB); - - let keyMaterial = CryptoUtils.hkdf(kB, undefined, - "identity.mozilla.com/picl/v1/chrome.storage.sync", 2*32); - let bundle = new BulkKeyBundle(); - // [encryptionKey, hmacKey] - bundle.keyPair = [keyMaterial.slice(0, 32), keyMaterial.slice(32, 64)]; - return bundle; - }); - } - // Pass through the kbHash field from the unencrypted record. If - // encryption fails, we can use this to try to detect whether we are - // being compromised or if the record here was encoded with a - // different kB. - encode(record) { - const encodePromise = super.encode(record); - return Task.spawn(function* () { - const encoded = yield encodePromise; - encoded.kbHash = record.kbHash; - return encoded; - }); - } - - decode(record) { - const decodePromise = super.decode(record); - return Task.spawn(function* () { - try { - return yield decodePromise; - } catch (e) { - if (Utils.isHMACMismatch(e)) { - const currentKBHash = yield ExtensionStorageSync.getKBHash(); - if (record.kbHash != currentKBHash) { - // Some other client encoded this with a kB that we don't - // have access to. - KeyRingEncryptionRemoteTransformer.throwOutdatedKB(currentKBHash, record.kbHash); - } - } - throw e; - } - }); - } - - // Generator and discriminator for KB-is-outdated exceptions. - static throwOutdatedKB(shouldBe, is) { - throw new Error(`kB hash on record is outdated: should be ${shouldBe}, is ${is}`); - } - - static isOutdatedKB(exc) { - const kbMessage = "kB hash on record is outdated: "; - return exc && exc.message && exc.message.indexOf && - (exc.message.indexOf(kbMessage) == 0); - } -} diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5fc0fa7a7..b0eb0f41d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -44,7 +44,6 @@ const ENGINE_MODULES = { Password: "passwords.js", Prefs: "prefs.js", Tab: "tabs.js", - ExtensionStorage: "extension-storage.js", }; const STORAGE_INFO_TYPES = [INFO_COLLECTIONS, diff --git a/services/sync/moz.build b/services/sync/moz.build index 156f43797..c4d3607b5 100644 --- a/services/sync/moz.build +++ b/services/sync/moz.build @@ -52,7 +52,6 @@ EXTRA_JS_MODULES['services-sync'].engines += [ 'modules/engines/addons.js', 'modules/engines/bookmarks.js', 'modules/engines/clients.js', - 'modules/engines/extension-storage.js', 'modules/engines/forms.js', 'modules/engines/history.js', 'modules/engines/passwords.js', |