services/sync/modules/record.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 this.EXPORTED_SYMBOLS = [
michael@0 6 "WBORecord",
michael@0 7 "RecordManager",
michael@0 8 "CryptoWrapper",
michael@0 9 "CollectionKeyManager",
michael@0 10 "Collection",
michael@0 11 ];
michael@0 12
michael@0 13 const Cc = Components.classes;
michael@0 14 const Ci = Components.interfaces;
michael@0 15 const Cr = Components.results;
michael@0 16 const Cu = Components.utils;
michael@0 17
michael@0 18 const CRYPTO_COLLECTION = "crypto";
michael@0 19 const KEYS_WBO = "keys";
michael@0 20
michael@0 21 Cu.import("resource://gre/modules/Log.jsm");
michael@0 22 Cu.import("resource://services-sync/constants.js");
michael@0 23 Cu.import("resource://services-sync/keys.js");
michael@0 24 Cu.import("resource://services-sync/resource.js");
michael@0 25 Cu.import("resource://services-sync/util.js");
michael@0 26
michael@0 27 this.WBORecord = function WBORecord(collection, id) {
michael@0 28 this.data = {};
michael@0 29 this.payload = {};
michael@0 30 this.collection = collection; // Optional.
michael@0 31 this.id = id; // Optional.
michael@0 32 }
michael@0 33 WBORecord.prototype = {
michael@0 34 _logName: "Sync.Record.WBO",
michael@0 35
michael@0 36 get sortindex() {
michael@0 37 if (this.data.sortindex)
michael@0 38 return this.data.sortindex;
michael@0 39 return 0;
michael@0 40 },
michael@0 41
michael@0 42 // Get thyself from your URI, then deserialize.
michael@0 43 // Set thine 'response' field.
michael@0 44 fetch: function fetch(resource) {
michael@0 45 if (!resource instanceof Resource) {
michael@0 46 throw new Error("First argument must be a Resource instance.");
michael@0 47 }
michael@0 48
michael@0 49 let r = resource.get();
michael@0 50 if (r.success) {
michael@0 51 this.deserialize(r); // Warning! Muffles exceptions!
michael@0 52 }
michael@0 53 this.response = r;
michael@0 54 return this;
michael@0 55 },
michael@0 56
michael@0 57 upload: function upload(resource) {
michael@0 58 if (!resource instanceof Resource) {
michael@0 59 throw new Error("First argument must be a Resource instance.");
michael@0 60 }
michael@0 61
michael@0 62 return resource.put(this);
michael@0 63 },
michael@0 64
michael@0 65 // Take a base URI string, with trailing slash, and return the URI of this
michael@0 66 // WBO based on collection and ID.
michael@0 67 uri: function(base) {
michael@0 68 if (this.collection && this.id) {
michael@0 69 let url = Utils.makeURI(base + this.collection + "/" + this.id);
michael@0 70 url.QueryInterface(Ci.nsIURL);
michael@0 71 return url;
michael@0 72 }
michael@0 73 return null;
michael@0 74 },
michael@0 75
michael@0 76 deserialize: function deserialize(json) {
michael@0 77 this.data = json.constructor.toString() == String ? JSON.parse(json) : json;
michael@0 78
michael@0 79 try {
michael@0 80 // The payload is likely to be JSON, but if not, keep it as a string
michael@0 81 this.payload = JSON.parse(this.payload);
michael@0 82 } catch(ex) {}
michael@0 83 },
michael@0 84
michael@0 85 toJSON: function toJSON() {
michael@0 86 // Copy fields from data to be stringified, making sure payload is a string
michael@0 87 let obj = {};
michael@0 88 for (let [key, val] in Iterator(this.data))
michael@0 89 obj[key] = key == "payload" ? JSON.stringify(val) : val;
michael@0 90 if (this.ttl)
michael@0 91 obj.ttl = this.ttl;
michael@0 92 return obj;
michael@0 93 },
michael@0 94
michael@0 95 toString: function toString() {
michael@0 96 return "{ " +
michael@0 97 "id: " + this.id + " " +
michael@0 98 "index: " + this.sortindex + " " +
michael@0 99 "modified: " + this.modified + " " +
michael@0 100 "ttl: " + this.ttl + " " +
michael@0 101 "payload: " + JSON.stringify(this.payload) +
michael@0 102 " }";
michael@0 103 }
michael@0 104 };
michael@0 105
michael@0 106 Utils.deferGetSet(WBORecord, "data", ["id", "modified", "sortindex", "payload"]);
michael@0 107
michael@0 108 /**
michael@0 109 * An interface and caching layer for records.
michael@0 110 */
michael@0 111 this.RecordManager = function RecordManager(service) {
michael@0 112 this.service = service;
michael@0 113
michael@0 114 this._log = Log.repository.getLogger(this._logName);
michael@0 115 this._records = {};
michael@0 116 }
michael@0 117 RecordManager.prototype = {
michael@0 118 _recordType: WBORecord,
michael@0 119 _logName: "Sync.RecordManager",
michael@0 120
michael@0 121 import: function RecordMgr_import(url) {
michael@0 122 this._log.trace("Importing record: " + (url.spec ? url.spec : url));
michael@0 123 try {
michael@0 124 // Clear out the last response with empty object if GET fails
michael@0 125 this.response = {};
michael@0 126 this.response = this.service.resource(url).get();
michael@0 127
michael@0 128 // Don't parse and save the record on failure
michael@0 129 if (!this.response.success)
michael@0 130 return null;
michael@0 131
michael@0 132 let record = new this._recordType(url);
michael@0 133 record.deserialize(this.response);
michael@0 134
michael@0 135 return this.set(url, record);
michael@0 136 } catch(ex) {
michael@0 137 this._log.debug("Failed to import record: " + Utils.exceptionStr(ex));
michael@0 138 return null;
michael@0 139 }
michael@0 140 },
michael@0 141
michael@0 142 get: function RecordMgr_get(url) {
michael@0 143 // Use a url string as the key to the hash
michael@0 144 let spec = url.spec ? url.spec : url;
michael@0 145 if (spec in this._records)
michael@0 146 return this._records[spec];
michael@0 147 return this.import(url);
michael@0 148 },
michael@0 149
michael@0 150 set: function RecordMgr_set(url, record) {
michael@0 151 let spec = url.spec ? url.spec : url;
michael@0 152 return this._records[spec] = record;
michael@0 153 },
michael@0 154
michael@0 155 contains: function RecordMgr_contains(url) {
michael@0 156 if ((url.spec || url) in this._records)
michael@0 157 return true;
michael@0 158 return false;
michael@0 159 },
michael@0 160
michael@0 161 clearCache: function recordMgr_clearCache() {
michael@0 162 this._records = {};
michael@0 163 },
michael@0 164
michael@0 165 del: function RecordMgr_del(url) {
michael@0 166 delete this._records[url];
michael@0 167 }
michael@0 168 };
michael@0 169
michael@0 170 this.CryptoWrapper = function CryptoWrapper(collection, id) {
michael@0 171 this.cleartext = {};
michael@0 172 WBORecord.call(this, collection, id);
michael@0 173 this.ciphertext = null;
michael@0 174 this.id = id;
michael@0 175 }
michael@0 176 CryptoWrapper.prototype = {
michael@0 177 __proto__: WBORecord.prototype,
michael@0 178 _logName: "Sync.Record.CryptoWrapper",
michael@0 179
michael@0 180 ciphertextHMAC: function ciphertextHMAC(keyBundle) {
michael@0 181 let hasher = keyBundle.sha256HMACHasher;
michael@0 182 if (!hasher) {
michael@0 183 throw "Cannot compute HMAC without an HMAC key.";
michael@0 184 }
michael@0 185
michael@0 186 return Utils.bytesAsHex(Utils.digestUTF8(this.ciphertext, hasher));
michael@0 187 },
michael@0 188
michael@0 189 /*
michael@0 190 * Don't directly use the sync key. Instead, grab a key for this
michael@0 191 * collection, which is decrypted with the sync key.
michael@0 192 *
michael@0 193 * Cache those keys; invalidate the cache if the time on the keys collection
michael@0 194 * changes, or other auth events occur.
michael@0 195 *
michael@0 196 * Optional key bundle overrides the collection key lookup.
michael@0 197 */
michael@0 198 encrypt: function encrypt(keyBundle) {
michael@0 199 if (!keyBundle) {
michael@0 200 throw new Error("A key bundle must be supplied to encrypt.");
michael@0 201 }
michael@0 202
michael@0 203 this.IV = Svc.Crypto.generateRandomIV();
michael@0 204 this.ciphertext = Svc.Crypto.encrypt(JSON.stringify(this.cleartext),
michael@0 205 keyBundle.encryptionKeyB64, this.IV);
michael@0 206 this.hmac = this.ciphertextHMAC(keyBundle);
michael@0 207 this.cleartext = null;
michael@0 208 },
michael@0 209
michael@0 210 // Optional key bundle.
michael@0 211 decrypt: function decrypt(keyBundle) {
michael@0 212 if (!this.ciphertext) {
michael@0 213 throw "No ciphertext: nothing to decrypt?";
michael@0 214 }
michael@0 215
michael@0 216 if (!keyBundle) {
michael@0 217 throw new Error("A key bundle must be supplied to decrypt.");
michael@0 218 }
michael@0 219
michael@0 220 // Authenticate the encrypted blob with the expected HMAC
michael@0 221 let computedHMAC = this.ciphertextHMAC(keyBundle);
michael@0 222
michael@0 223 if (computedHMAC != this.hmac) {
michael@0 224 Utils.throwHMACMismatch(this.hmac, computedHMAC);
michael@0 225 }
michael@0 226
michael@0 227 // Handle invalid data here. Elsewhere we assume that cleartext is an object.
michael@0 228 let cleartext = Svc.Crypto.decrypt(this.ciphertext,
michael@0 229 keyBundle.encryptionKeyB64, this.IV);
michael@0 230 let json_result = JSON.parse(cleartext);
michael@0 231
michael@0 232 if (json_result && (json_result instanceof Object)) {
michael@0 233 this.cleartext = json_result;
michael@0 234 this.ciphertext = null;
michael@0 235 } else {
michael@0 236 throw "Decryption failed: result is <" + json_result + ">, not an object.";
michael@0 237 }
michael@0 238
michael@0 239 // Verify that the encrypted id matches the requested record's id.
michael@0 240 if (this.cleartext.id != this.id)
michael@0 241 throw "Record id mismatch: " + this.cleartext.id + " != " + this.id;
michael@0 242
michael@0 243 return this.cleartext;
michael@0 244 },
michael@0 245
michael@0 246 toString: function toString() {
michael@0 247 let payload = this.deleted ? "DELETED" : JSON.stringify(this.cleartext);
michael@0 248
michael@0 249 return "{ " +
michael@0 250 "id: " + this.id + " " +
michael@0 251 "index: " + this.sortindex + " " +
michael@0 252 "modified: " + this.modified + " " +
michael@0 253 "ttl: " + this.ttl + " " +
michael@0 254 "payload: " + payload + " " +
michael@0 255 "collection: " + (this.collection || "undefined") +
michael@0 256 " }";
michael@0 257 },
michael@0 258
michael@0 259 // The custom setter below masks the parent's getter, so explicitly call it :(
michael@0 260 get id() WBORecord.prototype.__lookupGetter__("id").call(this),
michael@0 261
michael@0 262 // Keep both plaintext and encrypted versions of the id to verify integrity
michael@0 263 set id(val) {
michael@0 264 WBORecord.prototype.__lookupSetter__("id").call(this, val);
michael@0 265 return this.cleartext.id = val;
michael@0 266 },
michael@0 267 };
michael@0 268
michael@0 269 Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "IV", "hmac"]);
michael@0 270 Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted");
michael@0 271
michael@0 272
michael@0 273 /**
michael@0 274 * Keeps track of mappings between collection names ('tabs') and KeyBundles.
michael@0 275 *
michael@0 276 * You can update this thing simply by giving it /info/collections. It'll
michael@0 277 * use the last modified time to bring itself up to date.
michael@0 278 */
michael@0 279 this.CollectionKeyManager = function CollectionKeyManager() {
michael@0 280 this.lastModified = 0;
michael@0 281 this._collections = {};
michael@0 282 this._default = null;
michael@0 283
michael@0 284 this._log = Log.repository.getLogger("Sync.CollectionKeyManager");
michael@0 285 }
michael@0 286
michael@0 287 // TODO: persist this locally as an Identity. Bug 610913.
michael@0 288 // Note that the last modified time needs to be preserved.
michael@0 289 CollectionKeyManager.prototype = {
michael@0 290
michael@0 291 // Return information about old vs new keys:
michael@0 292 // * same: true if two collections are equal
michael@0 293 // * changed: an array of collection names that changed.
michael@0 294 _compareKeyBundleCollections: function _compareKeyBundleCollections(m1, m2) {
michael@0 295 let changed = [];
michael@0 296
michael@0 297 function process(m1, m2) {
michael@0 298 for (let k1 in m1) {
michael@0 299 let v1 = m1[k1];
michael@0 300 let v2 = m2[k1];
michael@0 301 if (!(v1 && v2 && v1.equals(v2)))
michael@0 302 changed.push(k1);
michael@0 303 }
michael@0 304 }
michael@0 305
michael@0 306 // Diffs both ways.
michael@0 307 process(m1, m2);
michael@0 308 process(m2, m1);
michael@0 309
michael@0 310 // Return a sorted, unique array.
michael@0 311 changed.sort();
michael@0 312 let last;
michael@0 313 changed = [x for each (x in changed) if ((x != last) && (last = x))];
michael@0 314 return {same: changed.length == 0,
michael@0 315 changed: changed};
michael@0 316 },
michael@0 317
michael@0 318 get isClear() {
michael@0 319 return !this._default;
michael@0 320 },
michael@0 321
michael@0 322 clear: function clear() {
michael@0 323 this._log.info("Clearing collection keys...");
michael@0 324 this.lastModified = 0;
michael@0 325 this._collections = {};
michael@0 326 this._default = null;
michael@0 327 },
michael@0 328
michael@0 329 keyForCollection: function(collection) {
michael@0 330 if (collection && this._collections[collection])
michael@0 331 return this._collections[collection];
michael@0 332
michael@0 333 return this._default;
michael@0 334 },
michael@0 335
michael@0 336 /**
michael@0 337 * If `collections` (an array of strings) is provided, iterate
michael@0 338 * over it and generate random keys for each collection.
michael@0 339 * Create a WBO for the given data.
michael@0 340 */
michael@0 341 _makeWBO: function(collections, defaultBundle) {
michael@0 342 let wbo = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
michael@0 343 let c = {};
michael@0 344 for (let k in collections) {
michael@0 345 c[k] = collections[k].keyPairB64;
michael@0 346 }
michael@0 347 wbo.cleartext = {
michael@0 348 "default": defaultBundle ? defaultBundle.keyPairB64 : null,
michael@0 349 "collections": c,
michael@0 350 "collection": CRYPTO_COLLECTION,
michael@0 351 "id": KEYS_WBO
michael@0 352 };
michael@0 353 return wbo;
michael@0 354 },
michael@0 355
michael@0 356 /**
michael@0 357 * Create a WBO for the current keys.
michael@0 358 */
michael@0 359 asWBO: function(collection, id)
michael@0 360 this._makeWBO(this._collections, this._default),
michael@0 361
michael@0 362 /**
michael@0 363 * Compute a new default key, and new keys for any specified collections.
michael@0 364 */
michael@0 365 newKeys: function(collections) {
michael@0 366 let newDefaultKey = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
michael@0 367 newDefaultKey.generateRandom();
michael@0 368
michael@0 369 let newColls = {};
michael@0 370 if (collections) {
michael@0 371 collections.forEach(function (c) {
michael@0 372 let b = new BulkKeyBundle(c);
michael@0 373 b.generateRandom();
michael@0 374 newColls[c] = b;
michael@0 375 });
michael@0 376 }
michael@0 377 return [newDefaultKey, newColls];
michael@0 378 },
michael@0 379
michael@0 380 /**
michael@0 381 * Generates new keys, but does not replace our local copy. Use this to
michael@0 382 * verify an upload before storing.
michael@0 383 */
michael@0 384 generateNewKeysWBO: function(collections) {
michael@0 385 let newDefaultKey, newColls;
michael@0 386 [newDefaultKey, newColls] = this.newKeys(collections);
michael@0 387
michael@0 388 return this._makeWBO(newColls, newDefaultKey);
michael@0 389 },
michael@0 390
michael@0 391 // Take the fetched info/collections WBO, checking the change
michael@0 392 // time of the crypto collection.
michael@0 393 updateNeeded: function(info_collections) {
michael@0 394
michael@0 395 this._log.info("Testing for updateNeeded. Last modified: " + this.lastModified);
michael@0 396
michael@0 397 // No local record of modification time? Need an update.
michael@0 398 if (!this.lastModified)
michael@0 399 return true;
michael@0 400
michael@0 401 // No keys on the server? We need an update, though our
michael@0 402 // update handling will be a little more drastic...
michael@0 403 if (!(CRYPTO_COLLECTION in info_collections))
michael@0 404 return true;
michael@0 405
michael@0 406 // Otherwise, we need an update if our modification time is stale.
michael@0 407 return (info_collections[CRYPTO_COLLECTION] > this.lastModified);
michael@0 408 },
michael@0 409
michael@0 410 //
michael@0 411 // Set our keys and modified time to the values fetched from the server.
michael@0 412 // Returns one of three values:
michael@0 413 //
michael@0 414 // * If the default key was modified, return true.
michael@0 415 // * If the default key was not modified, but per-collection keys were,
michael@0 416 // return an array of such.
michael@0 417 // * Otherwise, return false -- we were up-to-date.
michael@0 418 //
michael@0 419 setContents: function setContents(payload, modified) {
michael@0 420
michael@0 421 if (!modified)
michael@0 422 throw "No modified time provided to setContents.";
michael@0 423
michael@0 424 let self = this;
michael@0 425
michael@0 426 this._log.info("Setting collection keys contents. Our last modified: " +
michael@0 427 this.lastModified + ", input modified: " + modified + ".");
michael@0 428
michael@0 429 if (!payload)
michael@0 430 throw "No payload in CollectionKeyManager.setContents().";
michael@0 431
michael@0 432 if (!payload.default) {
michael@0 433 this._log.warn("No downloaded default key: this should not occur.");
michael@0 434 this._log.warn("Not clearing local keys.");
michael@0 435 throw "No default key in CollectionKeyManager.setContents(). Cannot proceed.";
michael@0 436 }
michael@0 437
michael@0 438 // Process the incoming default key.
michael@0 439 let b = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
michael@0 440 b.keyPairB64 = payload.default;
michael@0 441 let newDefault = b;
michael@0 442
michael@0 443 // Process the incoming collections.
michael@0 444 let newCollections = {};
michael@0 445 if ("collections" in payload) {
michael@0 446 this._log.info("Processing downloaded per-collection keys.");
michael@0 447 let colls = payload.collections;
michael@0 448 for (let k in colls) {
michael@0 449 let v = colls[k];
michael@0 450 if (v) {
michael@0 451 let keyObj = new BulkKeyBundle(k);
michael@0 452 keyObj.keyPairB64 = v;
michael@0 453 if (keyObj) {
michael@0 454 newCollections[k] = keyObj;
michael@0 455 }
michael@0 456 }
michael@0 457 }
michael@0 458 }
michael@0 459
michael@0 460 // Check to see if these are already our keys.
michael@0 461 let sameDefault = (this._default && this._default.equals(newDefault));
michael@0 462 let collComparison = this._compareKeyBundleCollections(newCollections, this._collections);
michael@0 463 let sameColls = collComparison.same;
michael@0 464
michael@0 465 if (sameDefault && sameColls) {
michael@0 466 self._log.info("New keys are the same as our old keys! Bumped local modified time.");
michael@0 467 self.lastModified = modified;
michael@0 468 return false;
michael@0 469 }
michael@0 470
michael@0 471 // Make sure things are nice and tidy before we set.
michael@0 472 this.clear();
michael@0 473
michael@0 474 this._log.info("Saving downloaded keys.");
michael@0 475 this._default = newDefault;
michael@0 476 this._collections = newCollections;
michael@0 477
michael@0 478 // Always trust the server.
michael@0 479 self._log.info("Bumping last modified to " + modified);
michael@0 480 self.lastModified = modified;
michael@0 481
michael@0 482 return sameDefault ? collComparison.changed : true;
michael@0 483 },
michael@0 484
michael@0 485 updateContents: function updateContents(syncKeyBundle, storage_keys) {
michael@0 486 let log = this._log;
michael@0 487 log.info("Updating collection keys...");
michael@0 488
michael@0 489 // storage_keys is a WBO, fetched from storage/crypto/keys.
michael@0 490 // Its payload is the default key, and a map of collections to keys.
michael@0 491 // We lazily compute the key objects from the strings we're given.
michael@0 492
michael@0 493 let payload;
michael@0 494 try {
michael@0 495 payload = storage_keys.decrypt(syncKeyBundle);
michael@0 496 } catch (ex) {
michael@0 497 log.warn("Got exception \"" + ex + "\" decrypting storage keys with sync key.");
michael@0 498 log.info("Aborting updateContents. Rethrowing.");
michael@0 499 throw ex;
michael@0 500 }
michael@0 501
michael@0 502 let r = this.setContents(payload, storage_keys.modified);
michael@0 503 log.info("Collection keys updated.");
michael@0 504 return r;
michael@0 505 }
michael@0 506 }
michael@0 507
michael@0 508 this.Collection = function Collection(uri, recordObj, service) {
michael@0 509 if (!service) {
michael@0 510 throw new Error("Collection constructor requires a service.");
michael@0 511 }
michael@0 512
michael@0 513 Resource.call(this, uri);
michael@0 514
michael@0 515 // This is a bit hacky, but gets the job done.
michael@0 516 let res = service.resource(uri);
michael@0 517 this.authenticator = res.authenticator;
michael@0 518
michael@0 519 this._recordObj = recordObj;
michael@0 520 this._service = service;
michael@0 521
michael@0 522 this._full = false;
michael@0 523 this._ids = null;
michael@0 524 this._limit = 0;
michael@0 525 this._older = 0;
michael@0 526 this._newer = 0;
michael@0 527 this._data = [];
michael@0 528 }
michael@0 529 Collection.prototype = {
michael@0 530 __proto__: Resource.prototype,
michael@0 531 _logName: "Sync.Collection",
michael@0 532
michael@0 533 _rebuildURL: function Coll__rebuildURL() {
michael@0 534 // XXX should consider what happens if it's not a URL...
michael@0 535 this.uri.QueryInterface(Ci.nsIURL);
michael@0 536
michael@0 537 let args = [];
michael@0 538 if (this.older)
michael@0 539 args.push('older=' + this.older);
michael@0 540 else if (this.newer) {
michael@0 541 args.push('newer=' + this.newer);
michael@0 542 }
michael@0 543 if (this.full)
michael@0 544 args.push('full=1');
michael@0 545 if (this.sort)
michael@0 546 args.push('sort=' + this.sort);
michael@0 547 if (this.ids != null)
michael@0 548 args.push("ids=" + this.ids);
michael@0 549 if (this.limit > 0 && this.limit != Infinity)
michael@0 550 args.push("limit=" + this.limit);
michael@0 551
michael@0 552 this.uri.query = (args.length > 0)? '?' + args.join('&') : '';
michael@0 553 },
michael@0 554
michael@0 555 // get full items
michael@0 556 get full() { return this._full; },
michael@0 557 set full(value) {
michael@0 558 this._full = value;
michael@0 559 this._rebuildURL();
michael@0 560 },
michael@0 561
michael@0 562 // Apply the action to a certain set of ids
michael@0 563 get ids() this._ids,
michael@0 564 set ids(value) {
michael@0 565 this._ids = value;
michael@0 566 this._rebuildURL();
michael@0 567 },
michael@0 568
michael@0 569 // Limit how many records to get
michael@0 570 get limit() this._limit,
michael@0 571 set limit(value) {
michael@0 572 this._limit = value;
michael@0 573 this._rebuildURL();
michael@0 574 },
michael@0 575
michael@0 576 // get only items modified before some date
michael@0 577 get older() { return this._older; },
michael@0 578 set older(value) {
michael@0 579 this._older = value;
michael@0 580 this._rebuildURL();
michael@0 581 },
michael@0 582
michael@0 583 // get only items modified since some date
michael@0 584 get newer() { return this._newer; },
michael@0 585 set newer(value) {
michael@0 586 this._newer = value;
michael@0 587 this._rebuildURL();
michael@0 588 },
michael@0 589
michael@0 590 // get items sorted by some criteria. valid values:
michael@0 591 // oldest (oldest first)
michael@0 592 // newest (newest first)
michael@0 593 // index
michael@0 594 get sort() { return this._sort; },
michael@0 595 set sort(value) {
michael@0 596 this._sort = value;
michael@0 597 this._rebuildURL();
michael@0 598 },
michael@0 599
michael@0 600 pushData: function Coll_pushData(data) {
michael@0 601 this._data.push(data);
michael@0 602 },
michael@0 603
michael@0 604 clearRecords: function Coll_clearRecords() {
michael@0 605 this._data = [];
michael@0 606 },
michael@0 607
michael@0 608 set recordHandler(onRecord) {
michael@0 609 // Save this because onProgress is called with this as the ChannelListener
michael@0 610 let coll = this;
michael@0 611
michael@0 612 // Switch to newline separated records for incremental parsing
michael@0 613 coll.setHeader("Accept", "application/newlines");
michael@0 614
michael@0 615 this._onProgress = function() {
michael@0 616 let newline;
michael@0 617 while ((newline = this._data.indexOf("\n")) > 0) {
michael@0 618 // Split the json record from the rest of the data
michael@0 619 let json = this._data.slice(0, newline);
michael@0 620 this._data = this._data.slice(newline + 1);
michael@0 621
michael@0 622 // Deserialize a record from json and give it to the callback
michael@0 623 let record = new coll._recordObj();
michael@0 624 record.deserialize(json);
michael@0 625 onRecord(record);
michael@0 626 }
michael@0 627 };
michael@0 628 },
michael@0 629 };

mercurial