sharedb 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/agent.js CHANGED
@@ -31,23 +31,23 @@ function Agent(backend, stream) {
31
31
 
32
32
  // We need to track which documents are subscribed by the client. This is a
33
33
  // map of collection -> id -> stream
34
- this.subscribedDocs = {};
34
+ this.subscribedDocs = Object.create(null);
35
35
 
36
36
  // Map from queryId -> emitter
37
- this.subscribedQueries = {};
37
+ this.subscribedQueries = Object.create(null);
38
38
 
39
39
  // Track which documents are subscribed to presence by the client. This is a
40
40
  // map of channel -> stream
41
- this.subscribedPresences = {};
41
+ this.subscribedPresences = Object.create(null);
42
42
  // Highest seq received for a subscription request. Any seq lower than this
43
43
  // value is stale, and should be ignored. Used for keeping the subscription
44
44
  // state in sync with the client's desired state. Map of channel -> seq
45
- this.presenceSubscriptionSeq = {};
45
+ this.presenceSubscriptionSeq = Object.create(null);
46
46
  // Keep track of the last request that has been sent by each local presence
47
47
  // belonging to this agent. This is used to generate a new disconnection
48
48
  // request if the client disconnects ungracefully. This is a
49
49
  // map of channel -> id -> request
50
- this.presenceRequests = {};
50
+ this.presenceRequests = Object.create(null);
51
51
 
52
52
  // We need to track this manually to make sure we don't reply to messages
53
53
  // after the stream was closed.
@@ -56,7 +56,7 @@ function Agent(backend, stream) {
56
56
  // For custom use in middleware. The agent is a convenient place to cache
57
57
  // session state in memory. It is in memory only as long as the session is
58
58
  // active, and it is passed to each middleware call
59
- this.custom = {};
59
+ this.custom = Object.create(null);
60
60
 
61
61
  // The first message received over the connection. Stored to warn if messages
62
62
  // are being sent before the handshake.
@@ -95,19 +95,19 @@ Agent.prototype._cleanup = function() {
95
95
  stream.destroy();
96
96
  }
97
97
  }
98
- this.subscribedDocs = {};
98
+ this.subscribedDocs = Object.create(null);
99
99
 
100
100
  for (var channel in this.subscribedPresences) {
101
101
  this.subscribedPresences[channel].destroy();
102
102
  }
103
- this.subscribedPresences = {};
103
+ this.subscribedPresences = Object.create(null);
104
104
 
105
105
  // Clean up query subscription streams
106
106
  for (var id in this.subscribedQueries) {
107
107
  var emitter = this.subscribedQueries[id];
108
108
  emitter.destroy();
109
109
  }
110
- this.subscribedQueries = {};
110
+ this.subscribedQueries = Object.create(null);
111
111
  };
112
112
 
113
113
  /**
@@ -117,7 +117,7 @@ Agent.prototype._cleanup = function() {
117
117
  Agent.prototype._subscribeToStream = function(collection, id, stream) {
118
118
  if (this.closed) return stream.destroy();
119
119
 
120
- var streams = this.subscribedDocs[collection] || (this.subscribedDocs[collection] = {});
120
+ var streams = this.subscribedDocs[collection] || (this.subscribedDocs[collection] = Object.create(null));
121
121
 
122
122
  // If already subscribed to this document, destroy the previously subscribed stream
123
123
  var previous = streams[id];
@@ -373,15 +373,25 @@ Agent.prototype._checkRequest = function(request) {
373
373
  request.a === ACTIONS.unsubscribe ||
374
374
  request.a === ACTIONS.presence) {
375
375
  // Doc-based request.
376
- if (request.c != null && typeof request.c !== 'string') return 'Invalid collection';
377
- if (request.d != null && typeof request.d !== 'string') return 'Invalid id';
376
+ if (request.c != null) {
377
+ if (typeof request.c !== 'string' || util.isDangerousProperty(request.c)) {
378
+ return 'Invalid collection';
379
+ }
380
+ }
381
+ if (request.d != null) {
382
+ if (typeof request.d !== 'string' || util.isDangerousProperty(request.d)) {
383
+ return 'Invalid id';
384
+ }
385
+ }
378
386
 
379
387
  if (request.a === ACTIONS.op || request.a === ACTIONS.presence) {
380
388
  if (request.v != null && (typeof request.v !== 'number' || request.v < 0)) return 'Invalid version';
381
389
  }
382
390
 
383
391
  if (request.a === ACTIONS.presence) {
384
- if (typeof request.id !== 'string') return 'Missing presence ID';
392
+ if (typeof request.id !== 'string' || util.isDangerousProperty(request.id)) {
393
+ return 'Invalid presence ID';
394
+ }
385
395
  }
386
396
  } else if (
387
397
  request.a === ACTIONS.bulkFetch ||
@@ -389,9 +399,18 @@ Agent.prototype._checkRequest = function(request) {
389
399
  request.a === ACTIONS.bulkUnsubscribe
390
400
  ) {
391
401
  // Bulk request
392
- if (request.c != null && typeof request.c !== 'string') return 'Invalid collection';
402
+ if (request.c != null) {
403
+ if (typeof request.c !== 'string' || util.isDangerousProperty(request.c)) {
404
+ return 'Invalid collection';
405
+ }
406
+ }
393
407
  if (typeof request.b !== 'object') return 'Invalid bulk subscribe data';
394
408
  }
409
+ if (request.ch != null) {
410
+ if (typeof request.ch !== 'string' || util.isDangerousProperty(request.ch)) {
411
+ return 'Invalid presence channel';
412
+ }
413
+ }
395
414
  };
396
415
 
397
416
  // Handle an incoming message from the client
@@ -453,6 +472,8 @@ Agent.prototype._handleMessage = function(request, callback) {
453
472
  return this._subscribePresence(request.ch, request.seq, callback);
454
473
  case ACTIONS.presenceUnsubscribe:
455
474
  return this._unsubscribePresence(request.ch, request.seq, callback);
475
+ case ACTIONS.presenceRequest:
476
+ return this._requestPresence(request.ch, callback);
456
477
  case ACTIONS.pingPong:
457
478
  return this._pingPong(callback);
458
479
  default:
@@ -481,7 +502,7 @@ function getQueryOptions(request) {
481
502
  fetch = [id];
482
503
  }
483
504
  } else {
484
- if (!fetchOps) fetchOps = {};
505
+ if (!fetchOps) fetchOps = Object.create(null);
485
506
  fetchOps[id] = version;
486
507
  }
487
508
  }
@@ -568,7 +589,7 @@ function getResultsData(results) {
568
589
  }
569
590
 
570
591
  function getMapResult(snapshotMap) {
571
- var data = {};
592
+ var data = Object.create(null);
572
593
  for (var id in snapshotMap) {
573
594
  var mapValue = snapshotMap[id];
574
595
  // fetchBulk / subscribeBulk map data can have either a Snapshot or an object
@@ -767,11 +788,7 @@ Agent.prototype._src = function() {
767
788
  Agent.prototype._broadcastPresence = function(presence, callback) {
768
789
  var agent = this;
769
790
  var backend = this.backend;
770
- var requests = this.presenceRequests[presence.ch] || (this.presenceRequests[presence.ch] = {});
771
- var previousRequest = requests[presence.id];
772
- if (!previousRequest || previousRequest.pv < presence.pv) {
773
- this.presenceRequests[presence.ch][presence.id] = presence;
774
- }
791
+ var presenceRequests = this.presenceRequests;
775
792
  var context = {
776
793
  presence: presence,
777
794
  collection: presence.c
@@ -779,6 +796,11 @@ Agent.prototype._broadcastPresence = function(presence, callback) {
779
796
  var start = Date.now();
780
797
  backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, this, context, function(error) {
781
798
  if (error) return callback(error);
799
+ var requests = presenceRequests[presence.ch] || (presenceRequests[presence.ch] = Object.create(null));
800
+ var previousRequest = requests[presence.id];
801
+ if (!previousRequest || previousRequest.pv < presence.pv) {
802
+ presenceRequests[presence.ch][presence.id] = presence;
803
+ }
782
804
  backend.transformPresenceToLatestVersion(agent, presence, function(error, presence) {
783
805
  if (error) return callback(error);
784
806
  var channel = agent._getPresenceChannel(presence.ch);
@@ -807,19 +829,17 @@ Agent.prototype._createPresence = function(request) {
807
829
  };
808
830
  };
809
831
 
810
- Agent.prototype._subscribePresence = function(channel, seq, callback) {
832
+ Agent.prototype._subscribePresence = function(channel, seq, cb) {
811
833
  var agent = this;
812
834
 
813
- function requestPresence() {
814
- agent._requestPresence(channel, function(error) {
815
- callback(error, {ch: channel, seq: seq});
816
- });
835
+ function callback(error) {
836
+ cb(error, {ch: channel, seq: seq});
817
837
  }
818
838
 
819
839
  var existingStream = agent.subscribedPresences[channel];
820
840
  if (existingStream) {
821
841
  agent.presenceSubscriptionSeq[channel] = seq;
822
- return requestPresence();
842
+ return callback();
823
843
  }
824
844
 
825
845
  var presenceChannel = this._getPresenceChannel(channel);
@@ -827,12 +847,14 @@ Agent.prototype._subscribePresence = function(channel, seq, callback) {
827
847
  if (error) return callback(error);
828
848
  if (seq < agent.presenceSubscriptionSeq[channel]) {
829
849
  stream.destroy();
830
- return callback(null, {ch: channel, seq: seq});
850
+ return callback();
831
851
  }
832
852
  agent.presenceSubscriptionSeq[channel] = seq;
833
853
  agent.subscribedPresences[channel] = stream;
834
854
  agent._subscribeToPresenceStream(channel, stream);
835
- requestPresence();
855
+ agent._requestPresence(channel, function(error) {
856
+ callback(error);
857
+ });
836
858
  });
837
859
  };
838
860
 
@@ -902,7 +924,9 @@ function createClientOp(request, clientId) {
902
924
  function shallowCopy(object) {
903
925
  var out = {};
904
926
  for (var key in object) {
905
- out[key] = object[key];
927
+ if (util.hasOwn(object, key)) {
928
+ out[key] = object[key];
929
+ }
906
930
  }
907
931
  return out;
908
932
  }
package/lib/backend.js CHANGED
@@ -30,7 +30,7 @@ function Backend(options) {
30
30
  this.milestoneDb = options.milestoneDb || new NoOpMilestoneDB();
31
31
 
32
32
  // Map from projected collection -> {type, fields}
33
- this.projections = {};
33
+ this.projections = Object.create(null);
34
34
 
35
35
  this.suppressPublish = !!options.suppressPublish;
36
36
  this.maxSubmitRetries = options.maxSubmitRetries || null;
@@ -45,7 +45,7 @@ function Backend(options) {
45
45
  }
46
46
 
47
47
  // Map from event name to a list of middleware
48
- this.middleware = {};
48
+ this.middleware = Object.create(null);
49
49
 
50
50
  // The number of open agents for monitoring and testing memory leaks
51
51
  this.agentsCount = 0;
@@ -569,7 +569,7 @@ Backend.prototype.subscribeBulk = function(agent, index, versions, callback) {
569
569
  var projection = this.projections[index];
570
570
  var collection = (projection) ? projection.target : index;
571
571
  var backend = this;
572
- var streams = {};
572
+ var streams = Object.create(null);
573
573
  var doFetch = Array.isArray(versions);
574
574
  var ids = (doFetch) ? versions : Object.keys(versions);
575
575
  var request = {
@@ -40,7 +40,7 @@ function Connection(socket) {
40
40
 
41
41
  // Map of collection -> id -> doc object for created documents.
42
42
  // (created documents MUST BE UNIQUE)
43
- this.collections = {};
43
+ this.collections = Object.create(null);
44
44
 
45
45
  // Each query and snapshot request is created with an id that the server uses when it sends us
46
46
  // info about the request (updates, etc)
@@ -48,14 +48,14 @@ function Connection(socket) {
48
48
  this.nextSnapshotRequestId = 1;
49
49
 
50
50
  // Map from query ID -> query object.
51
- this.queries = {};
51
+ this.queries = Object.create(null);
52
52
 
53
53
  // Maps from channel -> presence objects
54
- this._presences = {};
54
+ this._presences = Object.create(null);
55
55
  this._docPresenceEmitter = new DocPresenceEmitter();
56
56
 
57
57
  // Map from snapshot request ID -> snapshot request
58
- this._snapshotRequests = {};
58
+ this._snapshotRequests = Object.create(null);
59
59
 
60
60
  // A unique message number for the given id
61
61
  this.seq = 1;
@@ -221,7 +221,7 @@ Connection.prototype.handleMessage = function(message) {
221
221
  if (!query) return;
222
222
  if (err) return query._handleError(err);
223
223
  if (message.diff) query._handleDiff(message.diff);
224
- if (message.hasOwnProperty('extra')) query._handleExtra(message.extra);
224
+ if (util.hasOwn(message, 'extra')) query._handleExtra(message.extra);
225
225
  return;
226
226
 
227
227
  case ACTIONS.bulkFetch:
@@ -376,7 +376,7 @@ Connection.prototype._setState = function(newState, reason) {
376
376
  };
377
377
 
378
378
  Connection.prototype.startBulk = function() {
379
- if (!this.bulk) this.bulk = {};
379
+ if (!this.bulk) this.bulk = Object.create(null);
380
380
  };
381
381
 
382
382
  Connection.prototype.endBulk = function() {
@@ -394,7 +394,7 @@ Connection.prototype.endBulk = function() {
394
394
  Connection.prototype._sendBulk = function(action, collection, values) {
395
395
  if (!values) return;
396
396
  var ids = [];
397
- var versions = {};
397
+ var versions = Object.create(null);
398
398
  var versionsCount = 0;
399
399
  var versionId;
400
400
  for (var id in values) {
@@ -426,9 +426,9 @@ Connection.prototype._sendActions = function(action, doc, version) {
426
426
  this._addDoc(doc);
427
427
  if (this.bulk) {
428
428
  // Bulk subscribe
429
- var actions = this.bulk[doc.collection] || (this.bulk[doc.collection] = {});
430
- var versions = actions[action] || (actions[action] = {});
431
- var isDuplicate = versions.hasOwnProperty(doc.id);
429
+ var actions = this.bulk[doc.collection] || (this.bulk[doc.collection] = Object.create(null));
430
+ var versions = actions[action] || (actions[action] = Object.create(null));
431
+ var isDuplicate = util.hasOwn(versions, doc.id);
432
432
  versions[doc.id] = version;
433
433
  return isDuplicate;
434
434
  } else {
@@ -515,7 +515,7 @@ Connection.prototype.getExisting = function(collection, id) {
515
515
  */
516
516
  Connection.prototype.get = function(collection, id) {
517
517
  var docs = this.collections[collection] ||
518
- (this.collections[collection] = {});
518
+ (this.collections[collection] = Object.create(null));
519
519
 
520
520
  var doc = docs[id];
521
521
  if (!doc) {
@@ -542,7 +542,7 @@ Connection.prototype._destroyDoc = function(doc) {
542
542
  Connection.prototype._addDoc = function(doc) {
543
543
  var docs = this.collections[doc.collection];
544
544
  if (!docs) {
545
- docs = this.collections[doc.collection] = {};
545
+ docs = this.collections[doc.collection] = Object.create(null);
546
546
  }
547
547
  if (docs[doc.id] !== doc) {
548
548
  docs[doc.id] = doc;
@@ -809,6 +809,10 @@ Connection.prototype._addPresence = function(presence) {
809
809
  });
810
810
  };
811
811
 
812
+ Connection.prototype._requestRemotePresence = function(channel) {
813
+ this.send({a: ACTIONS.presenceRequest, ch: channel});
814
+ };
815
+
812
816
  Connection.prototype._handlePresenceSubscribe = function(error, message) {
813
817
  var presence = util.dig(this._presences, message.ch);
814
818
  if (presence) presence._handleSubscribe(error, message.seq);
@@ -12,9 +12,9 @@ var EVENTS = [
12
12
  module.exports = DocPresenceEmitter;
13
13
 
14
14
  function DocPresenceEmitter() {
15
- this._docs = {};
16
- this._forwarders = {};
17
- this._emitters = {};
15
+ this._docs = Object.create(null);
16
+ this._forwarders = Object.create(null);
17
+ this._emitters = Object.create(null);
18
18
  }
19
19
 
20
20
  DocPresenceEmitter.prototype.addEventListener = function(doc, event, listener) {
@@ -13,7 +13,7 @@ function LocalDocPresence(presence, presenceId) {
13
13
  this._doc = this.connection.get(this.collection, this.id);
14
14
  this._emitter = this.connection._docPresenceEmitter;
15
15
  this._isSending = false;
16
- this._docDataVersionByPresenceVersion = {};
16
+ this._docDataVersionByPresenceVersion = Object.create(null);
17
17
 
18
18
  this._opHandler = this._transformAgainstOp.bind(this);
19
19
  this._createOrDelHandler = this._handleCreateOrDel.bind(this);
@@ -68,7 +68,7 @@ LocalDocPresence.prototype._sendPending = function() {
68
68
  });
69
69
 
70
70
  presence._pendingMessages = [];
71
- presence._docDataVersionByPresenceVersion = {};
71
+ presence._docDataVersionByPresenceVersion = Object.create(null);
72
72
  });
73
73
  };
74
74
 
@@ -118,7 +118,7 @@ LocalDocPresence.prototype._handleCreateOrDel = function() {
118
118
  LocalDocPresence.prototype._handleLoad = function() {
119
119
  this.value = null;
120
120
  this._pendingMessages = [];
121
- this._docDataVersionByPresenceVersion = {};
121
+ this._docDataVersionByPresenceVersion = Object.create(null);
122
122
  };
123
123
 
124
124
  LocalDocPresence.prototype._message = function() {
@@ -18,7 +18,7 @@ function LocalPresence(presence, presenceId) {
18
18
  this.value = null;
19
19
 
20
20
  this._pendingMessages = [];
21
- this._callbacksByPresenceVersion = {};
21
+ this._callbacksByPresenceVersion = Object.create(null);
22
22
  }
23
23
  emitter.mixin(LocalPresence);
24
24
 
@@ -19,21 +19,21 @@ function Presence(connection, channel) {
19
19
 
20
20
  this.wantSubscribe = false;
21
21
  this.subscribed = false;
22
- this.remotePresences = {};
23
- this.localPresences = {};
22
+ this.remotePresences = Object.create(null);
23
+ this.localPresences = Object.create(null);
24
24
 
25
- this._remotePresenceInstances = {};
26
- this._subscriptionCallbacksBySeq = {};
25
+ this._remotePresenceInstances = Object.create(null);
26
+ this._subscriptionCallbacksBySeq = Object.create(null);
27
27
  this._wantsDestroy = false;
28
28
  }
29
29
  emitter.mixin(Presence);
30
30
 
31
- Presence.prototype.subscribe = function(options, callback) {
32
- this._sendSubscriptionAction(true, options, callback);
31
+ Presence.prototype.subscribe = function(callback) {
32
+ this._sendSubscriptionAction(true, callback);
33
33
  };
34
34
 
35
- Presence.prototype.unsubscribe = function(options, callback) {
36
- this._sendSubscriptionAction(false, options, callback);
35
+ Presence.prototype.unsubscribe = function(callback) {
36
+ this._sendSubscriptionAction(false, callback);
37
37
  };
38
38
 
39
39
  Presence.prototype.create = function(id) {
@@ -83,14 +83,9 @@ Presence.prototype.destroy = function(callback) {
83
83
  });
84
84
  };
85
85
 
86
- Presence.prototype._sendSubscriptionAction = function(wantSubscribe, options, callback) {
87
- if (typeof options === 'function') {
88
- callback = options;
89
- options = {};
90
- }
91
- options = options || {};
86
+ Presence.prototype._sendSubscriptionAction = function(wantSubscribe, callback) {
92
87
  wantSubscribe = !!wantSubscribe;
93
- if (!options._force && wantSubscribe === this.wantSubscribe) {
88
+ if (wantSubscribe === this.wantSubscribe) {
94
89
  if (!callback) return;
95
90
  if (wantSubscribe === this.subscribed) return util.nextTick(callback);
96
91
  if (Object.keys(this._subscriptionCallbacksBySeq).length) {
@@ -106,6 +101,10 @@ Presence.prototype._sendSubscriptionAction = function(wantSubscribe, options, ca
106
101
  }
107
102
  };
108
103
 
104
+ Presence.prototype._requestRemotePresence = function() {
105
+ this.connection._requestRemotePresence(this.channel);
106
+ };
107
+
109
108
  Presence.prototype._handleSubscribe = function(error, seq) {
110
109
  if (this.wantSubscribe) this.subscribed = true;
111
110
  var callback = this._subscriptionCallback(seq);
@@ -105,9 +105,7 @@ RemoteDocPresence.prototype._catchUpStalePresence = function() {
105
105
  if (!this._opCache) {
106
106
  this._startCachingOps();
107
107
  this._doc.fetch();
108
- // We're already subscribed, but we send another subscribe message
109
- // to force presence updates from other clients
110
- this.presence.subscribe({_force: true});
108
+ this.presence._requestRemotePresence();
111
109
  return false;
112
110
  }
113
111
 
package/lib/db/index.js CHANGED
@@ -26,7 +26,7 @@ DB.prototype.getSnapshot = function(collection, id, fields, options, callback) {
26
26
  };
27
27
 
28
28
  DB.prototype.getSnapshotBulk = function(collection, ids, fields, options, callback) {
29
- var results = {};
29
+ var results = Object.create(null);
30
30
  var db = this;
31
31
  async.each(ids, function(id, eachCb) {
32
32
  db.getSnapshot(collection, id, fields, options, function(err, snapshot) {
@@ -50,7 +50,7 @@ DB.prototype.getOpsToSnapshot = function(collection, id, from, snapshot, options
50
50
  };
51
51
 
52
52
  DB.prototype.getOpsBulk = function(collection, fromMap, toMap, options, callback) {
53
- var results = {};
53
+ var results = Object.create(null);
54
54
  var db = this;
55
55
  async.forEachOf(fromMap, function(from, id, eachCb) {
56
56
  var to = toMap && toMap[id];
@@ -83,7 +83,7 @@ DB.prototype.query = function(collection, query, fields, options, callback) {
83
83
  };
84
84
 
85
85
  DB.prototype.queryPoll = function(collection, query, options, callback) {
86
- var fields = {};
86
+ var fields = Object.create(null);
87
87
  this.query(collection, query, fields, options, function(err, snapshots, extra) {
88
88
  if (err) return callback(err);
89
89
  var ids = [];
package/lib/db/memory.js CHANGED
@@ -17,12 +17,12 @@ function MemoryDB(options) {
17
17
  DB.call(this, options);
18
18
 
19
19
  // Map from collection name -> doc id -> doc snapshot ({v:, type:, data:})
20
- this.docs = {};
20
+ this.docs = Object.create(null);
21
21
 
22
22
  // Map from collection name -> doc id -> list of operations. Operations
23
23
  // don't store their version - instead their version is simply the index in
24
24
  // the list.
25
- this.ops = {};
25
+ this.ops = Object.create(null);
26
26
 
27
27
  this.closed = false;
28
28
  };
@@ -139,7 +139,7 @@ MemoryDB.prototype._writeOpSync = function(collection, id, op) {
139
139
  // object will be passed in with a type property. If there is no type property,
140
140
  // it should be considered a delete
141
141
  MemoryDB.prototype._writeSnapshotSync = function(collection, id, snapshot) {
142
- var collectionDocs = this.docs[collection] || (this.docs[collection] = {});
142
+ var collectionDocs = this.docs[collection] || (this.docs[collection] = Object.create(null));
143
143
  if (!snapshot.type) {
144
144
  delete collectionDocs[id];
145
145
  } else {
@@ -165,7 +165,7 @@ MemoryDB.prototype._getSnapshotSync = function(collection, id, includeMetadata)
165
165
  };
166
166
 
167
167
  MemoryDB.prototype._getOpLogSync = function(collection, id) {
168
- var collectionOps = this.ops[collection] || (this.ops[collection] = {});
168
+ var collectionOps = this.ops[collection] || (this.ops[collection] = Object.create(null));
169
169
  return collectionOps[id] || (collectionOps[id] = []);
170
170
  };
171
171
 
@@ -5,7 +5,7 @@ var SUPPORTED_METHODS = [
5
5
  ];
6
6
 
7
7
  function Logger() {
8
- var defaultMethods = {};
8
+ var defaultMethods = Object.create(null);
9
9
  SUPPORTED_METHODS.forEach(function(method) {
10
10
  // Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628
11
11
  defaultMethods[method] = console[method].bind(console);
@@ -20,7 +20,7 @@ function MemoryMilestoneDB(options) {
20
20
  MilestoneDB.call(this, options);
21
21
 
22
22
  // Map from collection name -> doc id -> array of milestone snapshots
23
- this._milestoneSnapshots = {};
23
+ this._milestoneSnapshots = Object.create(null);
24
24
  }
25
25
 
26
26
  MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype);
@@ -103,7 +103,8 @@ MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, br
103
103
  };
104
104
 
105
105
  MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function(collection, id) {
106
- var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {});
106
+ var collectionSnapshots = this._milestoneSnapshots[collection] ||
107
+ (this._milestoneSnapshots[collection] = Object.create(null));
107
108
  return collectionSnapshots[id] || (collectionSnapshots[id] = []);
108
109
  };
109
110
 
package/lib/ot.js CHANGED
@@ -108,6 +108,20 @@ function applyOpEdit(snapshot, edit) {
108
108
  var type = types.map[snapshot.type];
109
109
  if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type');
110
110
 
111
+ if (type.name === 'json0' && Array.isArray(edit)) {
112
+ for (var i = 0; i < edit.length; i++) {
113
+ var opComponent = edit[i];
114
+ if (Array.isArray(opComponent.p)) {
115
+ for (var j = 0; j < opComponent.p.length; j++) {
116
+ var pathSegment = opComponent.p[j];
117
+ if (util.isDangerousProperty(pathSegment)) {
118
+ return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_APPLIED, 'Invalid path segment');
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+
111
125
  try {
112
126
  snapshot.data = type.apply(snapshot.data, edit);
113
127
  } catch (err) {
@@ -1,5 +1,6 @@
1
1
  var json0 = require('ot-json0').type;
2
2
  var ShareDBError = require('./error');
3
+ var util = require('./util');
3
4
 
4
5
  var ERROR_CODE = ShareDBError.CODES;
5
6
 
@@ -119,7 +120,7 @@ function projectData(fields, data) {
119
120
  // Shallow copy of each field
120
121
  var result = {};
121
122
  for (var key in fields) {
122
- if (data.hasOwnProperty(key)) {
123
+ if (util.hasOwn(data, key)) {
123
124
  result[key] = data[key];
124
125
  }
125
126
  }
@@ -13,12 +13,12 @@ function PubSub(options) {
13
13
  this.nextStreamId = 1;
14
14
  this.streamsCount = 0;
15
15
  // Maps channel -> id -> stream
16
- this.streams = {};
16
+ this.streams = Object.create(null);
17
17
  // State for tracking subscriptions. We track this.subscribed separately from
18
18
  // the streams, since the stream gets added synchronously, and the subscribe
19
19
  // isn't complete until the callback returns from Redis
20
20
  // Maps channel -> true
21
- this.subscribed = {};
21
+ this.subscribed = Object.create(null);
22
22
 
23
23
  var pubsub = this;
24
24
  this._defaultCallback = function(err) {
@@ -111,7 +111,7 @@ PubSub.prototype._createStream = function(channel) {
111
111
  });
112
112
 
113
113
  this.streamsCount++;
114
- var map = this.streams[channel] || (this.streams[channel] = {});
114
+ var map = this.streams[channel] || (this.streams[channel] = Object.create(null));
115
115
  stream.id = this.nextStreamId++;
116
116
  map[stream.id] = stream;
117
117
 
@@ -43,7 +43,7 @@ function ReadSnapshotsRequest(collection, snapshots, snapshotType) {
43
43
  */
44
44
  ReadSnapshotsRequest.prototype.rejectSnapshotRead = function(snapshot, error) {
45
45
  if (!this._idToError) {
46
- this._idToError = {};
46
+ this._idToError = Object.create(null);
47
47
  }
48
48
  this._idToError[snapshot.id] = error;
49
49
  };
@@ -26,7 +26,7 @@ function SubmitRequest(backend, agent, index, id, op, options) {
26
26
  // Set as this request is sent through middleware
27
27
  this.action = null;
28
28
  // For custom use in middleware
29
- this.custom = {};
29
+ this.custom = Object.create(null);
30
30
 
31
31
  // Whether or not to store a milestone snapshot. If left as null, the milestone
32
32
  // snapshots are saved according to the interval provided to the milestone db
package/lib/types.js CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  exports.defaultType = require('ot-json0').type;
3
3
 
4
- exports.map = {};
4
+ exports.map = Object.create(null);
5
5
 
6
6
  exports.register = function(type) {
7
7
  if (type.name) exports.map[type.name] = type;
package/lib/util.js CHANGED
@@ -7,6 +7,9 @@ exports.hasKeys = function(object) {
7
7
  return false;
8
8
  };
9
9
 
10
+ var hasOwn;
11
+ exports.hasOwn = hasOwn = Object.hasOwn || Object.prototype.hasOwnProperty.call;
12
+
10
13
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill
11
14
  exports.isInteger = Number.isInteger || function(value) {
12
15
  return typeof value === 'number' &&
@@ -29,7 +32,7 @@ exports.dig = function() {
29
32
  var obj = arguments[0];
30
33
  for (var i = 1; i < arguments.length; i++) {
31
34
  var key = arguments[i];
32
- obj = obj[key] || (i === arguments.length - 1 ? undefined : {});
35
+ obj = hasOwn(obj, key) ? obj[key] : (i === arguments.length - 1 ? undefined : Object.create(null));
33
36
  }
34
37
  return obj;
35
38
  };
@@ -39,8 +42,8 @@ exports.digOrCreate = function() {
39
42
  var createCallback = arguments[arguments.length - 1];
40
43
  for (var i = 1; i < arguments.length - 1; i++) {
41
44
  var key = arguments[i];
42
- obj = obj[key] ||
43
- (obj[key] = i === arguments.length - 2 ? createCallback() : {});
45
+ obj = hasOwn(obj, key) ? obj[key] :
46
+ (obj[key] = i === arguments.length - 2 ? createCallback() : Object.create(null));
44
47
  }
45
48
  return obj;
46
49
  };
@@ -50,7 +53,7 @@ exports.digAndRemove = function() {
50
53
  var objects = [obj];
51
54
  for (var i = 1; i < arguments.length - 1; i++) {
52
55
  var key = arguments[i];
53
- if (!obj.hasOwnProperty(key)) break;
56
+ if (!hasOwn(obj, key)) break;
54
57
  obj = obj[key];
55
58
  objects.push(obj);
56
59
  };
@@ -101,3 +104,12 @@ exports.clone = function(obj) {
101
104
  return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
102
105
  };
103
106
 
107
+ var objectProtoPropNames = Object.create(null);
108
+ Object.getOwnPropertyNames(Object.prototype).forEach(function(prop) {
109
+ if (prop !== '__proto__') {
110
+ objectProtoPropNames[prop] = true;
111
+ }
112
+ });
113
+ exports.isDangerousProperty = function(propName) {
114
+ return propName === '__proto__' || objectProtoPropNames[propName];
115
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -617,6 +617,106 @@ describe('Doc', function() {
617
617
  });
618
618
  });
619
619
 
620
+ describe('errors on ops that could cause prototype corruption', function() {
621
+ function expectReceiveError(
622
+ connection,
623
+ collectionName,
624
+ docId,
625
+ expectedError,
626
+ done
627
+ ) {
628
+ connection.on('receive', function(request) {
629
+ var message = request.data;
630
+ if (message.c === collectionName && message.d === docId) {
631
+ if ('error' in message) {
632
+ request.data = null; // Stop further processing of the message
633
+ if (message.error.message === expectedError) {
634
+ return done();
635
+ } else {
636
+ return done('Unexpected ShareDB error: ' + message.error.message);
637
+ }
638
+ } else {
639
+ return done('Expected error on ' + collectionName + '.' + docId + ' but got no error');
640
+ }
641
+ }
642
+ });
643
+ }
644
+
645
+ ['__proto__', 'constructor'].forEach(function(badProp) {
646
+ it('Rejects ops with collection ' + badProp, function(done) {
647
+ var collectionName = badProp;
648
+ var docId = 'test-doc';
649
+ expectReceiveError(this.connection, collectionName, docId, 'Invalid collection', done);
650
+ this.connection.send({
651
+ a: 'op',
652
+ c: collectionName,
653
+ d: docId,
654
+ v: 0,
655
+ seq: this.connection.seq++,
656
+ x: {},
657
+ create: {type: 'http://sharejs.org/types/JSONv0', data: {name: 'Test doc'}}
658
+ });
659
+ });
660
+
661
+ it('Rejects ops with doc id ' + badProp, function(done) {
662
+ var collectionName = 'test-collection';
663
+ var docId = badProp;
664
+ expectReceiveError(this.connection, collectionName, docId, 'Invalid id', done);
665
+ this.connection.send({
666
+ a: 'op',
667
+ c: collectionName,
668
+ d: docId,
669
+ v: 0,
670
+ seq: this.connection.seq++,
671
+ x: {},
672
+ create: {type: 'http://sharejs.org/types/JSONv0', data: {name: 'Some doc'}}
673
+ });
674
+ });
675
+
676
+ it('Rejects ops with ' + badProp + ' as first path segment', function(done) {
677
+ var connection = this.connection;
678
+ var collectionName = 'test-collection';
679
+ var docId = 'test-doc';
680
+ connection.get(collectionName, docId).create({id: docId}, function(err) {
681
+ if (err) {
682
+ return done(err);
683
+ }
684
+ expectReceiveError(connection, collectionName, docId, 'Invalid path segment', done);
685
+ connection.send({
686
+ a: 'op',
687
+ c: collectionName,
688
+ d: docId,
689
+ v: 1,
690
+ seq: connection.seq++,
691
+ x: {},
692
+ op: [{p: [badProp, 'toString'], oi: 'oops'}]
693
+ });
694
+ });
695
+ });
696
+
697
+ it('Rejects ops with ' + badProp + ' as later path segment', function(done) {
698
+ var connection = this.connection;
699
+ var collectionName = 'test-collection';
700
+ var docId = 'test-doc';
701
+ connection.get(collectionName, docId).create({id: docId}, function(err) {
702
+ if (err) {
703
+ return done(err);
704
+ }
705
+ expectReceiveError(connection, collectionName, docId, 'Invalid path segment', done);
706
+ connection.send({
707
+ a: 'op',
708
+ c: collectionName,
709
+ d: docId,
710
+ v: 1,
711
+ seq: connection.seq++,
712
+ x: {},
713
+ op: [{p: ['foo', badProp], oi: 'oops'}]
714
+ });
715
+ });
716
+ });
717
+ });
718
+ });
719
+
620
720
  describe('toSnapshot', function() {
621
721
  var doc;
622
722
  beforeEach(function(done) {
@@ -645,20 +645,14 @@ describe('DocPresence', function() {
645
645
  presence1.subscribe.bind(presence1),
646
646
  presence2.subscribe.bind(presence2),
647
647
  function(next) {
648
- connection1._setState('disconnected');
649
- next();
650
- },
651
- function(next) {
652
- localPresence2.submit({index: 0}, errorHandler(done));
653
- // We've not _actually_ disconnected the connection, so this
654
- // event will still fire.
655
- presence1.once('receive', function() {
648
+ connection1.once('closed', function() {
656
649
  next();
657
650
  });
651
+ connection1.close();
658
652
  },
653
+ localPresence2.submit.bind(localPresence2, {index: 0}),
659
654
  function(next) {
660
- connection1._setState('connecting');
661
- connection1._setState('connected');
655
+ backend.connect(connection1);
662
656
  presence1.once('receive', function(id, presence) {
663
657
  expect(presence).to.eql({index: 0});
664
658
  next();
@@ -344,9 +344,11 @@ describe('Presence', function() {
344
344
  presence1.subscribe.bind(presence1),
345
345
  presence2.subscribe.bind(presence2),
346
346
  function(next) {
347
- connection2._setState('disconnected');
347
+ connection2.once('closed', function() {
348
+ next();
349
+ });
350
+ connection2.close();
348
351
  expect(connection2.canSend).to.be.false;
349
- next();
350
352
  },
351
353
  localPresence1.submit.bind(localPresence1, {index: 1}),
352
354
  function(next) {
@@ -355,8 +357,7 @@ describe('Presence', function() {
355
357
  expect(presence).to.eql({index: 1});
356
358
  next();
357
359
  });
358
- connection2._setState('connecting');
359
- connection2._setState('connected');
360
+ backend.connect(connection2);
360
361
  }
361
362
  ], done);
362
363
  });
@@ -444,12 +445,14 @@ describe('Presence', function() {
444
445
  it('does not leak Streams when subscribing the same presence multiple times', function(done) {
445
446
  var streamsCount;
446
447
  async.series([
447
- presence1.subscribe.bind(presence1, {force: true}),
448
+ presence1.subscribe.bind(presence1),
448
449
  function(next) {
449
450
  streamsCount = backend.pubsub.streamsCount;
451
+ // Trick it into sending a duplicate request
452
+ presence1.wantSubscribe = false;
450
453
  next();
451
454
  },
452
- presence1.subscribe.bind(presence1, {force: true}),
455
+ presence1.subscribe.bind(presence1),
453
456
  function(next) {
454
457
  expect(backend.pubsub.streamsCount).to.equal(streamsCount);
455
458
  next();
@@ -463,6 +466,16 @@ describe('Presence', function() {
463
466
  }).to.throw();
464
467
  });
465
468
 
469
+ ['__proto__', 'constructor'].forEach(function(badProp) {
470
+ it('Rejects presence with channel ' + badProp, function(done) {
471
+ var presence = connection1.getPresence(badProp);
472
+ presence.subscribe(function(err) {
473
+ expect(err).to.be.an('error').to.haveOwnProperty('message', 'Invalid presence channel');
474
+ done();
475
+ });
476
+ });
477
+ });
478
+
466
479
  it('assigns an ID if one is not provided', function() {
467
480
  var localPresence = presence1.create();
468
481
  expect(localPresence.presenceId).to.be.ok;
@@ -656,6 +669,7 @@ describe('Presence', function() {
656
669
  function(next) {
657
670
  presence2.on('receive', function(id, value) {
658
671
  expect(value).to.eql({index: 6});
672
+ expect(connection1.agent.presenceRequests['test-channel']['presence-1'].p).to.eql({index: 6});
659
673
  next();
660
674
  });
661
675
  localPresence1.submit({index: 5}, errorHandler(done));
@@ -678,6 +692,7 @@ describe('Presence', function() {
678
692
  });
679
693
  localPresence1.submit({index: 5}, function(error) {
680
694
  expect(error.message).to.contain('bad!');
695
+ expect(connection1.agent.presenceRequests['test-channel']).not.to.be.ok;
681
696
  next();
682
697
  });
683
698
  }