From dc887a40c8303eb5ec1a20ac20ba9cebd432d9e5 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 15 Apr 2013 14:12:56 -0700 Subject: [PATCH 001/102] Update tools tests for stream -> livedata package merging --- tools/tests/test_bundler_options.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_bundler_options.js b/tools/tests/test_bundler_options.js index 434bd1828df..c0dd1de26de 100644 --- a/tools/tests/test_bundler_options.js +++ b/tools/tests/test_bundler_options.js @@ -23,7 +23,7 @@ assert.doesNotThrow(function () { assert(!fs.existsSync(path.join(tmpOutputDir, "server", "node_modules"))); // yes package node_modules directory assert(fs.lstatSync(path.join( - tmpOutputDir, "app", "packages", "stream", "node_modules")) + tmpOutputDir, "app", "packages", "livedata", "node_modules")) .isDirectory()); // verify that contents are minified @@ -96,6 +96,6 @@ assert.doesNotThrow(function () { // package node_modules directory also a symlink assert(fs.lstatSync(path.join( - tmpOutputDir, "app", "packages", "stream", "node_modules")) + tmpOutputDir, "app", "packages", "livedata", "node_modules")) .isSymbolicLink()); }); From 7825ae7845026302ff0d6cb47d8b60ca63f2d43b Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 15 Apr 2013 14:59:03 -0700 Subject: [PATCH 002/102] Use npm instal --force to get around NPM cache corruption bug. --- tools/meteor_npm.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tools/meteor_npm.js b/tools/meteor_npm.js index 3284db4dc47..5ecfa17f5aa 100644 --- a/tools/meteor_npm.js +++ b/tools/meteor_npm.js @@ -335,8 +335,17 @@ _.extend(exports, { // We don't use npm.commands.install since we couldn't // figure out how to silence all output (specifically the // installed tree which is printed out with `console.log`) + // + // We use --force, because the NPM cache is broken! See + // https://github.com/isaacs/npm/issues/3265 Basically, switching back and + // forth between a tarball fork of version X and the real version X can + // confuse NPM. But the main reason to use tarball URLs is to get a fork of + // the latest version with some fix, so it's easy to trigger this! So + // instead, always use --force. (Even with --force, we still WRITE to the + // cache, so we can corrupt the cache for other invocations of npm... ah + // well.) this._execFileSync(path.join(files.get_dev_bundle(), "bin", "npm"), - ["install", installArg], + ["install", "--force", installArg], {cwd: dir}); }, @@ -346,9 +355,10 @@ _.extend(exports, { this._ensureConnected(); - // `npm install`, which reads npm-shrinkwrap.json + // `npm install`, which reads npm-shrinkwrap.json. + // see above for why --force. this._execFileSync(path.join(files.get_dev_bundle(), "bin", "npm"), - ["install"], + ["install", "--force"], {cwd: dir}); }, From 49822939c2e7050f0e9eb9d5f9c12608c16d5842 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 15 Apr 2013 16:54:48 -0700 Subject: [PATCH 003/102] Revert "Use npm instal --force to get around NPM cache corruption bug." This reverts commit 7825ae7845026302ff0d6cb47d8b60ca63f2d43b. test_bundler_npm.js (even after an obvious fix is applied to an unrelated problem) fails a noticeable percentage of the time in the "bundle multiple apps in parallel" test. It does not appear to fail on the node-0.10 branch, so will apply this there instead. --- tools/meteor_npm.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/tools/meteor_npm.js b/tools/meteor_npm.js index 5ecfa17f5aa..3284db4dc47 100644 --- a/tools/meteor_npm.js +++ b/tools/meteor_npm.js @@ -335,17 +335,8 @@ _.extend(exports, { // We don't use npm.commands.install since we couldn't // figure out how to silence all output (specifically the // installed tree which is printed out with `console.log`) - // - // We use --force, because the NPM cache is broken! See - // https://github.com/isaacs/npm/issues/3265 Basically, switching back and - // forth between a tarball fork of version X and the real version X can - // confuse NPM. But the main reason to use tarball URLs is to get a fork of - // the latest version with some fix, so it's easy to trigger this! So - // instead, always use --force. (Even with --force, we still WRITE to the - // cache, so we can corrupt the cache for other invocations of npm... ah - // well.) this._execFileSync(path.join(files.get_dev_bundle(), "bin", "npm"), - ["install", "--force", installArg], + ["install", installArg], {cwd: dir}); }, @@ -355,10 +346,9 @@ _.extend(exports, { this._ensureConnected(); - // `npm install`, which reads npm-shrinkwrap.json. - // see above for why --force. + // `npm install`, which reads npm-shrinkwrap.json this._execFileSync(path.join(files.get_dev_bundle(), "bin", "npm"), - ["install", "--force"], + ["install"], {cwd: dir}); }, From 583508e10d726c75a375825877afcec541516616 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 15 Apr 2013 17:24:05 -0700 Subject: [PATCH 004/102] Support EJSON.clone(arguments). This enables (eg) Meteor.apply('foo', arguments). Fixes #946. --- packages/ejson/ejson.js | 8 +++----- packages/ejson/ejson_test.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 3774169aaad..02c7a8f6039 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -301,11 +301,9 @@ EJSON.clone = function (v) { } return ret; } - if (_.isArray(v)) { - ret = v.slice(0); - for (var i = 0; i < v.length; i++) - ret[i] = EJSON.clone(ret[i]); - return ret; + // Clone arrays (and turn 'arguments' into an array). + if (_.isArray(v) || _.isArguments(v)) { + return _.map(v, EJSON.clone); } // handle general user-defined typed Objects if they have a clone method if (typeof v.clone === 'function') { diff --git a/packages/ejson/ejson_test.js b/packages/ejson/ejson_test.js index 4f1e6b6ac65..bd3e5d63104 100644 --- a/packages/ejson/ejson_test.js +++ b/packages/ejson/ejson_test.js @@ -51,3 +51,24 @@ Tinytest.add("ejson - equality and falsiness", function (test) { test.isFalse(EJSON.equals(undefined, {foo: "foo"})); test.isFalse(EJSON.equals({foo: "foo"}, undefined)); }); + +Tinytest.add("ejson - clone", function (test) { + var cloneTest = function (x, identical) { + var y = EJSON.clone(x); + test.isTrue(EJSON.equals(x, y)); + test.equal(x === y, !!identical); + }; + cloneTest(null, true); + cloneTest(undefined, true); + cloneTest(42, true); + cloneTest("asdf", true); + cloneTest([1, 2, 3]); + cloneTest([1, "fasdf", {foo: 42}]); + cloneTest({x: 42, y: "asdf"}); + + var testCloneArgs = function (/*arguments*/) { + var clonedArgs = EJSON.clone(arguments); + test.equal(clonedArgs, [1, 2, "foo", [4]]); + }; + testCloneArgs(1, 2, "foo", [4]); +}); From 2294da1297423147011ffd8441092026abdcd26a Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 16 Apr 2013 11:13:32 -0700 Subject: [PATCH 005/102] Only define Collection.prototype.allow/deny on the server. Motivated by finding a user created package which added a file only on the client, in which there were calls to .allow. This way they would get an error. --- packages/mongo-livedata/collection.js | 77 ++++++++++++++------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index e499dd69fcf..c91c817f5c9 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -421,50 +421,51 @@ Meteor.Collection.ObjectID = LocalCollection._ObjectID; // call all of them if it is able to make a decision without calling them all // (so don't include side effects). -(function () { - var addValidator = function(allowOrDeny, options) { - // validate keys - var VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform']; - _.each(_.keys(options), function (key) { - if (!_.contains(VALID_KEYS, key)) - throw new Error(allowOrDeny + ": Invalid key: " + key); - }); +if (Meteor.isServer) { // ACLs only run on the server. + (function () { + var addValidator = function(allowOrDeny, options) { + // validate keys + var VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform']; + _.each(_.keys(options), function (key) { + if (!_.contains(VALID_KEYS, key)) + throw new Error(allowOrDeny + ": Invalid key: " + key); + }); - var self = this; - self._restricted = true; + var self = this; + self._restricted = true; - _.each(['insert', 'update', 'remove'], function (name) { - if (options[name]) { - if (!(options[name] instanceof Function)) { - throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); + _.each(['insert', 'update', 'remove'], function (name) { + if (options[name]) { + if (!(options[name] instanceof Function)) { + throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); + } + if (self._transform) + options[name].transform = self._transform; + if (options.transform) + options[name].transform = Deps._makeNonreactive(options.transform); + self._validators[name][allowOrDeny].push(options[name]); } - if (self._transform) - options[name].transform = self._transform; - if (options.transform) - options[name].transform = Deps._makeNonreactive(options.transform); - self._validators[name][allowOrDeny].push(options[name]); - } - }); + }); - // Only update the fetch fields if we're passed things that affect - // fetching. This way allow({}) and allow({insert: f}) don't result in - // setting fetchAllFields - if (options.update || options.remove || options.fetch) { - if (options.fetch && !(options.fetch instanceof Array)) { - throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); + // Only update the fetch fields if we're passed things that affect + // fetching. This way allow({}) and allow({insert: f}) don't result in + // setting fetchAllFields + if (options.update || options.remove || options.fetch) { + if (options.fetch && !(options.fetch instanceof Array)) { + throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); + } + self._updateFetch(options.fetch); } - self._updateFetch(options.fetch); - } - }; - - Meteor.Collection.prototype.allow = function(options) { - addValidator.call(this, 'allow', options); - }; - Meteor.Collection.prototype.deny = function(options) { - addValidator.call(this, 'deny', options); - }; -})(); + }; + Meteor.Collection.prototype.allow = function(options) { + addValidator.call(this, 'allow', options); + }; + Meteor.Collection.prototype.deny = function(options) { + addValidator.call(this, 'deny', options); + }; + })(); +} Meteor.Collection.prototype._defineMutationMethods = function() { var self = this; From eb90df2f8dabd51d64270ddfa005a69a987963cb Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Tue, 16 Apr 2013 13:34:22 -0700 Subject: [PATCH 006/102] Revert "Only define Collection.prototype.allow/deny on the server." This reverts commit 2294da1297423147011ffd8441092026abdcd26a. --- packages/mongo-livedata/collection.js | 77 +++++++++++++-------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index c91c817f5c9..e499dd69fcf 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -421,51 +421,50 @@ Meteor.Collection.ObjectID = LocalCollection._ObjectID; // call all of them if it is able to make a decision without calling them all // (so don't include side effects). -if (Meteor.isServer) { // ACLs only run on the server. - (function () { - var addValidator = function(allowOrDeny, options) { - // validate keys - var VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform']; - _.each(_.keys(options), function (key) { - if (!_.contains(VALID_KEYS, key)) - throw new Error(allowOrDeny + ": Invalid key: " + key); - }); +(function () { + var addValidator = function(allowOrDeny, options) { + // validate keys + var VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform']; + _.each(_.keys(options), function (key) { + if (!_.contains(VALID_KEYS, key)) + throw new Error(allowOrDeny + ": Invalid key: " + key); + }); - var self = this; - self._restricted = true; + var self = this; + self._restricted = true; - _.each(['insert', 'update', 'remove'], function (name) { - if (options[name]) { - if (!(options[name] instanceof Function)) { - throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); - } - if (self._transform) - options[name].transform = self._transform; - if (options.transform) - options[name].transform = Deps._makeNonreactive(options.transform); - self._validators[name][allowOrDeny].push(options[name]); + _.each(['insert', 'update', 'remove'], function (name) { + if (options[name]) { + if (!(options[name] instanceof Function)) { + throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); } - }); + if (self._transform) + options[name].transform = self._transform; + if (options.transform) + options[name].transform = Deps._makeNonreactive(options.transform); + self._validators[name][allowOrDeny].push(options[name]); + } + }); - // Only update the fetch fields if we're passed things that affect - // fetching. This way allow({}) and allow({insert: f}) don't result in - // setting fetchAllFields - if (options.update || options.remove || options.fetch) { - if (options.fetch && !(options.fetch instanceof Array)) { - throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); - } - self._updateFetch(options.fetch); + // Only update the fetch fields if we're passed things that affect + // fetching. This way allow({}) and allow({insert: f}) don't result in + // setting fetchAllFields + if (options.update || options.remove || options.fetch) { + if (options.fetch && !(options.fetch instanceof Array)) { + throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); } - }; + self._updateFetch(options.fetch); + } + }; + + Meteor.Collection.prototype.allow = function(options) { + addValidator.call(this, 'allow', options); + }; + Meteor.Collection.prototype.deny = function(options) { + addValidator.call(this, 'deny', options); + }; +})(); - Meteor.Collection.prototype.allow = function(options) { - addValidator.call(this, 'allow', options); - }; - Meteor.Collection.prototype.deny = function(options) { - addValidator.call(this, 'deny', options); - }; - })(); -} Meteor.Collection.prototype._defineMutationMethods = function() { var self = this; From 311837cdf4c3daa98182e58e8606100839825bfc Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Fri, 12 Apr 2013 08:17:57 -0700 Subject: [PATCH 007/102] added file for fiber_helper style stubs on the client --- packages/meteor/fiber_stubs_client.js | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/meteor/fiber_stubs_client.js diff --git a/packages/meteor/fiber_stubs_client.js b/packages/meteor/fiber_stubs_client.js new file mode 100644 index 00000000000..ebbebfb61ef --- /dev/null +++ b/packages/meteor/fiber_stubs_client.js @@ -0,0 +1,60 @@ +// This file is a partial analogue to fiber_helpers.js, which allows the client +// to use a queue too, and also to call noYieldsAllowed. + +// The client has no ability to yield, so noYieldsAllowed is a noop. +Meteor._noYieldsAllowed = function (f) { + return f(); +}; + +// An even simpler queue of tasks than the fiber-enabled one. This one just +// runs all the tasks when you call runTask or flush, synchronously. +Meteor._SynchronousQueue = function () { + var self = this; + self._tasks = []; + self._running = false; +}; + +_.extend(Meteor._SynchronousQueue.prototype, { + runTask: function (task) { + var self = this; + self._tasks.push(task); + self._running = true; + try { + while (!_.isEmpty(self._tasks)) { + var t = self._tasks.shift(); + try { + t(); + } catch (e) { + if (_.isEmpty(self._tasks)) { + // this was the last task, that is, the one we're calling runTask + // for. + throw e; + } else { + Meteor._debug("Exception in queued task: " + e.stack); + } + } + } + } finally { + self._running = false; + } + }, + + queueTask: function (task) { + var self = this; + self._tasks.push(task); + }, + + flush: function () { + var self = this; + self.runTask(function () {}); + }, + + taskRunning: function () { + var self = this; + return self._running; + }, + + safeToRunTask: function () { + return true; + } +}); From 4ca4859c45bb53304fefc74663677dad86bd0e42 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Fri, 12 Apr 2013 08:57:13 -0700 Subject: [PATCH 008/102] Test for observe ordering in minimongo on server. Currently failing. Test first wheeeee! --- .../mongo-livedata/mongo_livedata_tests.js | 147 +++++++++++++----- 1 file changed, 105 insertions(+), 42 deletions(-) diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index 3c1eb335384..f4d8850716e 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -820,52 +820,115 @@ testAsyncMulti('mongo-livedata - specified _id', [ if (Meteor.isServer) { - (function () { - - testAsyncMulti("mongo-livedata - minimongo on server to server connection", [ - function (test, expect) { - var self = this; - self.id = Random.id(); - var C = new Meteor.Collection("ServerMinimongo_" + self.id); - - C.insert({a: 0, b: 1}); - C.insert({a: 0, b: 2}); - C.insert({a: 1, b: 3}); - Meteor.publish(self.id, function () { - return C.find({a: 0}); + + testAsyncMulti("mongo-livedata - minimongo on server to server connection", [ + function (test, expect) { + var self = this; + Meteor._debug("connection setup"); + self.id = Random.id(); + var C = new Meteor.Collection("ServerMinimongo_" + self.id); + + C.insert({a: 0, b: 1}); + C.insert({a: 0, b: 2}); + C.insert({a: 1, b: 3}); + Meteor.publish(self.id, function () { + return C.find({a: 0}); + }); + + self.conn = Meteor.connect(Meteor.absoluteUrl()); + pollUntil(expect, function () { + return self.conn.status().connected; + }, 10000); + }, + + function (test, expect) { + var self = this; + if (self.conn.status().connected) { + self.miniC = new Meteor.Collection("ServerMinimongo_" + self.id, { + manager: self.conn + }); + var exp = expect(function (err) { + test.isFalse(err); }); + self.conn.subscribe(self.id, { + onError: exp, + onReady: exp + }); + } + }, + + function (test, expect) { + var self = this; + if (self.miniC) { + var contents = self.miniC.find().fetch(); + test.equal(contents.length, 2); + test.equal(contents[0].a, 0); + } + } + ]); + + testAsyncMulti("mongo-livedata - minimongo observe on server", [ + function (test, expect) { + var self = this; + self.id = Random.id(); + self.C = new Meteor.Collection("ServerMinimongoObserve_" + self.id); + self.events = []; + + Meteor.publish(self.id, function () { + return self.C.find(); + }); - self.conn = Meteor.connect(Meteor.absoluteUrl()); + self.conn = Meteor.connect(Meteor.absoluteUrl()); + pollUntil(expect, function () { + return self.conn.status().connected; + }, 10000); + }, + + function (test, expect) { + var self = this; + if (self.conn.status().connected) { + self.miniC = new Meteor.Collection("ServerMinimongoObserve_" + self.id, { + manager: self.conn + }); + var exp = expect(function (err) { + test.isFalse(err); + }); + self.conn.subscribe(self.id, { + onError: exp, + onReady: exp + }); + } + }, + + function (test, expect) { + var self = this; + if (self.miniC) { + self.obs = self.miniC.find().observeChanges({ + added: function (id, fields) { + self.events.push({evt: "a", id: id}); + Meteor._sleepForMs(200); + self.events.push({evt: "b", id: id}); + } + }); + self.one = self.C.insert({}); + self.two = self.C.insert({}); pollUntil(expect, function () { - return self.conn.status().connected; + return self.events.length === 4; }, 10000); - }, - - function (test, expect) { - var self = this; - if (self.conn.status().connected) { - self.miniC = new Meteor.Collection("ServerMinimongo_" + self.id, { - manager: self.conn - }); - var exp = expect(function (err) { - test.isFalse(err); - }); - self.conn.subscribe(self.id, { - onError: exp, - onReady: exp - }); - } - }, - - function (test, expect) { - var self = this; - if (self.miniC) { - var contents = self.miniC.find().fetch(); - test.equal(contents.length, 2); - test.equal(contents[0].a, 0); - } } - ]); - })(); + }, + function (test, expect) { + var self = this; + if (self.miniC) { + test.equal(self.events, [ + {evt: "a", id: self.one}, + {evt: "b", id: self.one}, + {evt: "a", id: self.two}, + {evt: "b", id: self.two} + ]); + } + self.obs && self.obs.stop(); + } + ]); } From 6ebee578d610aa1a3ee9d19ebc2ebd9d68f5c6b8 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Sun, 14 Apr 2013 20:10:39 -0500 Subject: [PATCH 009/102] Minimongo now uses a queue to manage observes What this means for the user is that now, both on the server and the client, if you look at a minimongo-backed collection from an observe/observeChanges callback, you'll see a consistent view of it -- you'll never see it in the middle of an update, for instance. Furthermore, server!minimongo no longer has stubs, just future waits or callbacks, your choice. --- .../livedata/livedata_connection_tests.js | 1010 +++++++++-------- packages/meteor/package.js | 1 + packages/minimongo/minimongo.js | 37 +- packages/mongo-livedata/collection.js | 3 + 4 files changed, 542 insertions(+), 509 deletions(-) diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 839a5b1fbb1..39dc5e66d46 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -287,106 +287,108 @@ Tinytest.add("livedata stub - this", function (test) { }); -Tinytest.add("livedata stub - methods", function (test) { - var stream = new Meteor._StubStream(); - var conn = newConnection(stream); +if (Meteor.isClient) { + Tinytest.add("livedata stub - methods", function (test) { + var stream = new Meteor._StubStream(); + var conn = newConnection(stream); + + startAndConnect(test, stream); + + var collName = Random.id(); + var coll = new Meteor.Collection(collName, {manager: conn}); + + // setup method + conn.methods({do_something: function (x) { + coll.insert({value: x}); + }}); + + // setup observers + var counts = {added: 0, removed: 0, changed: 0, moved: 0}; + var handle = coll.find({}).observe( + { addedAt: function () { counts.added += 1; }, + removedAt: function () { counts.removed += 1; }, + changedAt: function () { counts.changed += 1; }, + movedTo: function () { counts.moved += 1; } + }); - startAndConnect(test, stream); - var collName = Random.id(); - var coll = new Meteor.Collection(collName, {manager: conn}); - - // setup method - conn.methods({do_something: function (x) { - coll.insert({value: x}); - }}); - - // setup observers - var counts = {added: 0, removed: 0, changed: 0, moved: 0}; - var handle = coll.find({}).observe( - { addedAt: function () { counts.added += 1; }, - removedAt: function () { counts.removed += 1; }, - changedAt: function () { counts.changed += 1; }, - movedTo: function () { counts.moved += 1; } + // call method with results callback + var callback1Fired = false; + conn.call('do_something', 'friday!', function (err, res) { + test.isUndefined(err); + test.equal(res, '1234'); + callback1Fired = true; }); - - - // call method with results callback - var callback1Fired = false; - conn.call('do_something', 'friday!', function (err, res) { - test.isUndefined(err); - test.equal(res, '1234'); - callback1Fired = true; - }); - test.isFalse(callback1Fired); - - // observers saw the method run. - test.equal(counts, {added: 1, removed: 0, changed: 0, moved: 0}); - - // get response from server - var message = JSON.parse(stream.sent.shift()); - test.equal(message, {msg: 'method', method: 'do_something', - params: ['friday!'], id:message.id}); - - test.equal(coll.find({}).count(), 1); - test.equal(coll.find({value: 'friday!'}).count(), 1); - var docId = coll.findOne({value: 'friday!'})._id; - - // results does not yet result in callback, because data is not - // ready. - stream.receive({msg: 'result', id:message.id, result: "1234"}); - test.isFalse(callback1Fired); - - // result message doesn't affect data - test.equal(coll.find({}).count(), 1); - test.equal(coll.find({value: 'friday!'}).count(), 1); - test.equal(counts, {added: 1, removed: 0, changed: 0, moved: 0}); - - // data methods do not show up (not quiescent yet) - stream.receive({msg: 'added', collection: collName, id: Meteor.idStringify(docId), - fields: {value: 'tuesday'}}); - test.equal(coll.find({}).count(), 1); - test.equal(coll.find({value: 'friday!'}).count(), 1); - test.equal(counts, {added: 1, removed: 0, changed: 0, moved: 0}); - - // send another methods (unknown on client) - var callback2Fired = false; - conn.call('do_something_else', 'monday', function (err, res) { - callback2Fired = true; + test.isFalse(callback1Fired); + + // observers saw the method run. + test.equal(counts, {added: 1, removed: 0, changed: 0, moved: 0}); + + // get response from server + var message = JSON.parse(stream.sent.shift()); + test.equal(message, {msg: 'method', method: 'do_something', + params: ['friday!'], id:message.id}); + + test.equal(coll.find({}).count(), 1); + test.equal(coll.find({value: 'friday!'}).count(), 1); + var docId = coll.findOne({value: 'friday!'})._id; + + // results does not yet result in callback, because data is not + // ready. + stream.receive({msg: 'result', id:message.id, result: "1234"}); + test.isFalse(callback1Fired); + + // result message doesn't affect data + test.equal(coll.find({}).count(), 1); + test.equal(coll.find({value: 'friday!'}).count(), 1); + test.equal(counts, {added: 1, removed: 0, changed: 0, moved: 0}); + + // data methods do not show up (not quiescent yet) + stream.receive({msg: 'added', collection: collName, id: Meteor.idStringify(docId), + fields: {value: 'tuesday'}}); + test.equal(coll.find({}).count(), 1); + test.equal(coll.find({value: 'friday!'}).count(), 1); + test.equal(counts, {added: 1, removed: 0, changed: 0, moved: 0}); + + // send another methods (unknown on client) + var callback2Fired = false; + conn.call('do_something_else', 'monday', function (err, res) { + callback2Fired = true; + }); + test.isFalse(callback1Fired); + test.isFalse(callback2Fired); + + // test we still send a method request to server + var message2 = JSON.parse(stream.sent.shift()); + test.equal(message2, {msg: 'method', method: 'do_something_else', + params: ['monday'], id: message2.id}); + + // get the first data satisfied message. changes are applied to database even + // though another method is outstanding, because the other method didn't have + // a stub. and its callback is called. + stream.receive({msg: 'updated', 'methods': [message.id]}); + test.isTrue(callback1Fired); + test.isFalse(callback2Fired); + + test.equal(coll.find({}).count(), 1); + test.equal(coll.find({value: 'tuesday'}).count(), 1); + test.equal(counts, {added: 1, removed: 0, changed: 1, moved: 0}); + + // second result + stream.receive({msg: 'result', id:message2.id, result:"bupkis"}); + test.isFalse(callback2Fired); + + // get second satisfied; no new changes are applied. + stream.receive({msg: 'updated', 'methods': [message2.id]}); + test.isTrue(callback2Fired); + + test.equal(coll.find({}).count(), 1); + test.equal(coll.find({value: 'tuesday', _id: docId}).count(), 1); + test.equal(counts, {added: 1, removed: 0, changed: 1, moved: 0}); + + handle.stop(); }); - test.isFalse(callback1Fired); - test.isFalse(callback2Fired); - - // test we still send a method request to server - var message2 = JSON.parse(stream.sent.shift()); - test.equal(message2, {msg: 'method', method: 'do_something_else', - params: ['monday'], id: message2.id}); - - // get the first data satisfied message. changes are applied to database even - // though another method is outstanding, because the other method didn't have - // a stub. and its callback is called. - stream.receive({msg: 'updated', 'methods': [message.id]}); - test.isTrue(callback1Fired); - test.isFalse(callback2Fired); - - test.equal(coll.find({}).count(), 1); - test.equal(coll.find({value: 'tuesday'}).count(), 1); - test.equal(counts, {added: 1, removed: 0, changed: 1, moved: 0}); - - // second result - stream.receive({msg: 'result', id:message2.id, result:"bupkis"}); - test.isFalse(callback2Fired); - - // get second satisfied; no new changes are applied. - stream.receive({msg: 'updated', 'methods': [message2.id]}); - test.isTrue(callback2Fired); - - test.equal(coll.find({}).count(), 1); - test.equal(coll.find({value: 'tuesday', _id: docId}).count(), 1); - test.equal(counts, {added: 1, removed: 0, changed: 1, moved: 0}); - - handle.stop(); -}); +} Tinytest.add("livedata stub - mutating method args", function (test) { var stream = new Meteor._StubStream(); @@ -428,69 +430,70 @@ var observeCursor = function (test, cursor) { }; // method calls another method in simulation. see not sent. -Tinytest.add("livedata stub - methods calling methods", function (test) { - var stream = new Meteor._StubStream(); - var conn = newConnection(stream); - - startAndConnect(test, stream); - - var coll_name = Random.id(); - var coll = new Meteor.Collection(coll_name, {manager: conn}); +if (Meteor.isClient) { + Tinytest.add("livedata stub - methods calling methods", function (test) { + var stream = new Meteor._StubStream(); + var conn = newConnection(stream); + + startAndConnect(test, stream); + + var coll_name = Random.id(); + var coll = new Meteor.Collection(coll_name, {manager: conn}); + + // setup methods + conn.methods({ + do_something: function () { + conn.call('do_something_else'); + }, + do_something_else: function () { + coll.insert({a: 1}); + } + }); - // setup methods - conn.methods({ - do_something: function () { - conn.call('do_something_else'); - }, - do_something_else: function () { - coll.insert({a: 1}); - } + var o = observeCursor(test, coll.find()); + + // call method. + conn.call('do_something', _.identity); + + // see we only send message for outer methods + var message = JSON.parse(stream.sent.shift()); + test.equal(message, {msg: 'method', method: 'do_something', + params: [], id:message.id}); + test.length(stream.sent, 0); + + // but inner method runs locally. + o.expectCallbacks({added: 1}); + test.equal(coll.find().count(), 1); + var docId = coll.findOne()._id; + test.equal(coll.findOne(), {_id: docId, a: 1}); + + // we get the results + stream.receive({msg: 'result', id:message.id, result:"1234"}); + + // get data from the method. data from this doc does not show up yet, but data + // from another doc does. + stream.receive({msg: 'added', collection: coll_name, id: Meteor.idStringify(docId), + fields: {value: 'tuesday'}}); + o.expectCallbacks(); + test.equal(coll.findOne(docId), {_id: docId, a: 1}); + stream.receive({msg: 'added', collection: coll_name, id: 'monkey', + fields: {value: 'bla'}}); + o.expectCallbacks({added: 1}); + test.equal(coll.findOne(docId), {_id: docId, a: 1}); + var newDoc = coll.findOne({value: 'bla'}); + test.isTrue(newDoc); + test.equal(newDoc, {_id: newDoc._id, value: 'bla'}); + + // get method satisfied. all data shows up. the 'a' field is reverted and + // 'value' field is set. + stream.receive({msg: 'updated', 'methods': [message.id]}); + o.expectCallbacks({changed: 1}); + test.equal(coll.findOne(docId), {_id: docId, value: 'tuesday'}); + test.equal(coll.findOne(newDoc._id), {_id: newDoc._id, value: 'bla'}); + + o.stop(); }); - - var o = observeCursor(test, coll.find()); - - // call method. - conn.call('do_something', _.identity); - - // see we only send message for outer methods - var message = JSON.parse(stream.sent.shift()); - test.equal(message, {msg: 'method', method: 'do_something', - params: [], id:message.id}); - test.length(stream.sent, 0); - - // but inner method runs locally. - o.expectCallbacks({added: 1}); - test.equal(coll.find().count(), 1); - var docId = coll.findOne()._id; - test.equal(coll.findOne(), {_id: docId, a: 1}); - - // we get the results - stream.receive({msg: 'result', id:message.id, result:"1234"}); - - // get data from the method. data from this doc does not show up yet, but data - // from another doc does. - stream.receive({msg: 'added', collection: coll_name, id: Meteor.idStringify(docId), - fields: {value: 'tuesday'}}); - o.expectCallbacks(); - test.equal(coll.findOne(docId), {_id: docId, a: 1}); - stream.receive({msg: 'added', collection: coll_name, id: 'monkey', - fields: {value: 'bla'}}); - o.expectCallbacks({added: 1}); - test.equal(coll.findOne(docId), {_id: docId, a: 1}); - var newDoc = coll.findOne({value: 'bla'}); - test.isTrue(newDoc); - test.equal(newDoc, {_id: newDoc._id, value: 'bla'}); - - // get method satisfied. all data shows up. the 'a' field is reverted and - // 'value' field is set. - stream.receive({msg: 'updated', 'methods': [message.id]}); - o.expectCallbacks({changed: 1}); - test.equal(coll.findOne(docId), {_id: docId, value: 'tuesday'}); - test.equal(coll.findOne(newDoc._id), {_id: newDoc._id, value: 'bla'}); - - o.stop(); -}); - +} Tinytest.add("livedata stub - method call before connect", function (test) { var stream = new Meteor._StubStream; var conn = newConnection(stream); @@ -640,179 +643,180 @@ Tinytest.add("livedata stub - reconnect", function (test) { }); -Tinytest.add("livedata stub - reconnect method which only got result", function (test) { - var stream = new Meteor._StubStream; - var conn = newConnection(stream); - startAndConnect(test, stream); - - var collName = Random.id(); - var coll = new Meteor.Collection(collName, {manager: conn}); - var o = observeCursor(test, coll.find()); - - conn.methods({writeSomething: function () { - // stub write - coll.insert({foo: 'bar'}); - }}); - - test.equal(coll.find({foo: 'bar'}).count(), 0); - - // Call a method. We'll get the result but not data-done before reconnect. - var callbackOutput = []; - var onResultReceivedOutput = []; - conn.apply('writeSomething', [], - {onResultReceived: function (err, result) { - onResultReceivedOutput.push(result); - }}, - function (err, result) { - callbackOutput.push(result); - }); - // Stub write is visible. - test.equal(coll.find({foo: 'bar'}).count(), 1); - var stubWrittenId = coll.findOne({foo: 'bar'})._id; - o.expectCallbacks({added: 1}); - // Callback not called. - test.equal(callbackOutput, []); - test.equal(onResultReceivedOutput, []); - // Method sent. - var methodId = testGotMessage( - test, stream, {msg: 'method', method: 'writeSomething', - params: [], id: '*'}); - test.equal(stream.sent.length, 0); - - // Get some data. - stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId), fields: {baz: 42}}); - // It doesn't show up yet. - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, foo: 'bar'}); - o.expectCallbacks(); - - // Get the result. - stream.receive({msg: 'result', id: methodId, result: 'bla'}); - // Data unaffected. - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, foo: 'bar'}); - o.expectCallbacks(); - // Callback not called, but onResultReceived is. - test.equal(callbackOutput, []); - test.equal(onResultReceivedOutput, ['bla']); - - // Reset stream. Method does NOT get resent, because its result is already - // in. Reconnect quiescence happens as soon as 'connected' is received because - // there are no pending methods or subs in need of revival. - stream.reset(); - testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); - // Still holding out hope for session resumption, so nothing updated yet. - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, foo: 'bar'}); - o.expectCallbacks(); - test.equal(callbackOutput, []); - - // Receive 'connected': time for reconnect quiescence! Data gets updated - // locally (ie, data is reset) and callback gets called. - stream.receive({msg: 'connected', session: SESSION_ID + 1}); - test.equal(coll.find().count(), 0); - o.expectCallbacks({removed: 1}); - test.equal(callbackOutput, ['bla']); - test.equal(onResultReceivedOutput, ['bla']); - stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId), fields: {baz: 42}}); - test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, baz: 42}); - o.expectCallbacks({added: 1}); - - - - - // Run method again. We're going to do the same thing this time, except we're - // also going to use an onReconnect to insert another method at reconnect - // time, which will delay reconnect quiescence. - conn.apply('writeSomething', [], - {onResultReceived: function (err, result) { - onResultReceivedOutput.push(result); - }}, - function (err, result) { - callbackOutput.push(result); - }); - // Stub write is visible. - test.equal(coll.find({foo: 'bar'}).count(), 1); - var stubWrittenId2 = coll.findOne({foo: 'bar'})._id; - o.expectCallbacks({added: 1}); - // Callback not called. - test.equal(callbackOutput, ['bla']); - test.equal(onResultReceivedOutput, ['bla']); - // Method sent. - var methodId2 = testGotMessage( - test, stream, {msg: 'method', method: 'writeSomething', - params: [], id: '*'}); - test.equal(stream.sent.length, 0); - - // Get some data. - stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId2), fields: {baz: 42}}); - // It doesn't show up yet. - test.equal(coll.find().count(), 2); - test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); - o.expectCallbacks(); - - // Get the result. - stream.receive({msg: 'result', id: methodId2, result: 'blab'}); - // Data unaffected. - test.equal(coll.find().count(), 2); - test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); - o.expectCallbacks(); - // Callback not called, but onResultReceived is. - test.equal(callbackOutput, ['bla']); - test.equal(onResultReceivedOutput, ['bla', 'blab']); - conn.onReconnect = function () { - conn.call('slowMethod', function (err, result) { - callbackOutput.push(result); - }); - }; +if (Meteor.isClient) { + Tinytest.add("livedata stub - reconnect method which only got result", function (test) { + var stream = new Meteor._StubStream; + var conn = newConnection(stream); + startAndConnect(test, stream); - // Reset stream. Method does NOT get resent, because its result is already in, - // but slowMethod gets called via onReconnect. Reconnect quiescence is now - // blocking on slowMethod. - stream.reset(); - testGotMessage(test, stream, makeConnectMessage(SESSION_ID + 1)); - var slowMethodId = testGotMessage( - test, stream, - {msg: 'method', method: 'slowMethod', params: [], id: '*'}); - // Still holding out hope for session resumption, so nothing updated yet. - test.equal(coll.find().count(), 2); - test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); - o.expectCallbacks(); - test.equal(callbackOutput, ['bla']); - - // Receive 'connected'... but no reconnect quiescence yet due to slowMethod. - stream.receive({msg: 'connected', session: SESSION_ID + 2}); - test.equal(coll.find().count(), 2); - test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); - o.expectCallbacks(); - test.equal(callbackOutput, ['bla']); - - // Receive data matching our stub. It doesn't take effect yet. - stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId2), fields: {foo: 'bar'}}); - o.expectCallbacks(); + var collName = Random.id(); + var coll = new Meteor.Collection(collName, {manager: conn}); + var o = observeCursor(test, coll.find()); - // slowMethod is done writing, so we get full reconnect quiescence (but no - // slowMethod callback)... ie, a reset followed by applying the data we just - // got, as well as calling the callback from the method that half-finished - // before reset. The net effect is deleting doc 'stubWrittenId'. - stream.receive({msg: 'updated', methods: [slowMethodId]}); - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); - o.expectCallbacks({removed: 1}); - test.equal(callbackOutput, ['bla', 'blab']); - - // slowMethod returns a value now. - stream.receive({msg: 'result', id: slowMethodId, result: 'slow'}); - o.expectCallbacks(); - test.equal(callbackOutput, ['bla', 'blab', 'slow']); - - o.stop(); -}); + conn.methods({writeSomething: function () { + // stub write + coll.insert({foo: 'bar'}); + }}); + + test.equal(coll.find({foo: 'bar'}).count(), 0); + + // Call a method. We'll get the result but not data-done before reconnect. + var callbackOutput = []; + var onResultReceivedOutput = []; + conn.apply('writeSomething', [], + {onResultReceived: function (err, result) { + onResultReceivedOutput.push(result); + }}, + function (err, result) { + callbackOutput.push(result); + }); + // Stub write is visible. + test.equal(coll.find({foo: 'bar'}).count(), 1); + var stubWrittenId = coll.findOne({foo: 'bar'})._id; + o.expectCallbacks({added: 1}); + // Callback not called. + test.equal(callbackOutput, []); + test.equal(onResultReceivedOutput, []); + // Method sent. + var methodId = testGotMessage( + test, stream, {msg: 'method', method: 'writeSomething', + params: [], id: '*'}); + test.equal(stream.sent.length, 0); + + // Get some data. + stream.receive({msg: 'added', collection: collName, + id: Meteor.idStringify(stubWrittenId), fields: {baz: 42}}); + // It doesn't show up yet. + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, foo: 'bar'}); + o.expectCallbacks(); + + // Get the result. + stream.receive({msg: 'result', id: methodId, result: 'bla'}); + // Data unaffected. + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, foo: 'bar'}); + o.expectCallbacks(); + // Callback not called, but onResultReceived is. + test.equal(callbackOutput, []); + test.equal(onResultReceivedOutput, ['bla']); + + // Reset stream. Method does NOT get resent, because its result is already + // in. Reconnect quiescence happens as soon as 'connected' is received because + // there are no pending methods or subs in need of revival. + stream.reset(); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID)); + // Still holding out hope for session resumption, so nothing updated yet. + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, foo: 'bar'}); + o.expectCallbacks(); + test.equal(callbackOutput, []); + + // Receive 'connected': time for reconnect quiescence! Data gets updated + // locally (ie, data is reset) and callback gets called. + stream.receive({msg: 'connected', session: SESSION_ID + 1}); + test.equal(coll.find().count(), 0); + o.expectCallbacks({removed: 1}); + test.equal(callbackOutput, ['bla']); + test.equal(onResultReceivedOutput, ['bla']); + stream.receive({msg: 'added', collection: collName, + id: Meteor.idStringify(stubWrittenId), fields: {baz: 42}}); + test.equal(coll.findOne(stubWrittenId), {_id: stubWrittenId, baz: 42}); + o.expectCallbacks({added: 1}); + + + + + // Run method again. We're going to do the same thing this time, except we're + // also going to use an onReconnect to insert another method at reconnect + // time, which will delay reconnect quiescence. + conn.apply('writeSomething', [], + {onResultReceived: function (err, result) { + onResultReceivedOutput.push(result); + }}, + function (err, result) { + callbackOutput.push(result); + }); + // Stub write is visible. + test.equal(coll.find({foo: 'bar'}).count(), 1); + var stubWrittenId2 = coll.findOne({foo: 'bar'})._id; + o.expectCallbacks({added: 1}); + // Callback not called. + test.equal(callbackOutput, ['bla']); + test.equal(onResultReceivedOutput, ['bla']); + // Method sent. + var methodId2 = testGotMessage( + test, stream, {msg: 'method', method: 'writeSomething', + params: [], id: '*'}); + test.equal(stream.sent.length, 0); + + // Get some data. + stream.receive({msg: 'added', collection: collName, + id: Meteor.idStringify(stubWrittenId2), fields: {baz: 42}}); + // It doesn't show up yet. + test.equal(coll.find().count(), 2); + test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); + o.expectCallbacks(); + + // Get the result. + stream.receive({msg: 'result', id: methodId2, result: 'blab'}); + // Data unaffected. + test.equal(coll.find().count(), 2); + test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); + o.expectCallbacks(); + // Callback not called, but onResultReceived is. + test.equal(callbackOutput, ['bla']); + test.equal(onResultReceivedOutput, ['bla', 'blab']); + conn.onReconnect = function () { + conn.call('slowMethod', function (err, result) { + callbackOutput.push(result); + }); + }; + // Reset stream. Method does NOT get resent, because its result is already in, + // but slowMethod gets called via onReconnect. Reconnect quiescence is now + // blocking on slowMethod. + stream.reset(); + testGotMessage(test, stream, makeConnectMessage(SESSION_ID + 1)); + var slowMethodId = testGotMessage( + test, stream, + {msg: 'method', method: 'slowMethod', params: [], id: '*'}); + // Still holding out hope for session resumption, so nothing updated yet. + test.equal(coll.find().count(), 2); + test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); + o.expectCallbacks(); + test.equal(callbackOutput, ['bla']); + + // Receive 'connected'... but no reconnect quiescence yet due to slowMethod. + stream.receive({msg: 'connected', session: SESSION_ID + 2}); + test.equal(coll.find().count(), 2); + test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); + o.expectCallbacks(); + test.equal(callbackOutput, ['bla']); + + // Receive data matching our stub. It doesn't take effect yet. + stream.receive({msg: 'added', collection: collName, + id: Meteor.idStringify(stubWrittenId2), fields: {foo: 'bar'}}); + o.expectCallbacks(); + + // slowMethod is done writing, so we get full reconnect quiescence (but no + // slowMethod callback)... ie, a reset followed by applying the data we just + // got, as well as calling the callback from the method that half-finished + // before reset. The net effect is deleting doc 'stubWrittenId'. + stream.receive({msg: 'updated', methods: [slowMethodId]}); + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId2), {_id: stubWrittenId2, foo: 'bar'}); + o.expectCallbacks({removed: 1}); + test.equal(callbackOutput, ['bla', 'blab']); + + // slowMethod returns a value now. + stream.receive({msg: 'result', id: slowMethodId, result: 'slow'}); + o.expectCallbacks(); + test.equal(callbackOutput, ['bla', 'blab', 'slow']); + + o.stop(); + }); +} Tinytest.add("livedata stub - reconnect method which only got data", function (test) { var stream = new Meteor._StubStream; var conn = newConnection(stream); @@ -898,156 +902,158 @@ Tinytest.add("livedata stub - reconnect method which only got data", function (t o.stop(); }); +if (Meteor.isClient) { + Tinytest.add("livedata stub - multiple stubs same doc", function (test) { + var stream = new Meteor._StubStream; + var conn = newConnection(stream); + startAndConnect(test, stream); + + var collName = Random.id(); + var coll = new Meteor.Collection(collName, {manager: conn}); + var o = observeCursor(test, coll.find()); + + conn.methods({ + insertSomething: function () { + // stub write + coll.insert({foo: 'bar'}); + }, + updateIt: function (id) { + coll.update(id, {$set: {baz: 42}}); + } + }); -Tinytest.add("livedata stub - multiple stubs same doc", function (test) { - var stream = new Meteor._StubStream; - var conn = newConnection(stream); - startAndConnect(test, stream); - - var collName = Random.id(); - var coll = new Meteor.Collection(collName, {manager: conn}); - var o = observeCursor(test, coll.find()); - - conn.methods({ - insertSomething: function () { - // stub write - coll.insert({foo: 'bar'}); - }, - updateIt: function (id) { - coll.update(id, {$set: {baz: 42}}); - } + test.equal(coll.find().count(), 0); + + // Call the insert method. + conn.call('insertSomething', _.identity); + // Stub write is visible. + test.equal(coll.find({foo: 'bar'}).count(), 1); + var stubWrittenId = coll.findOne({foo: 'bar'})._id; + o.expectCallbacks({added: 1}); + // Method sent. + var insertMethodId = testGotMessage( + test, stream, {msg: 'method', method: 'insertSomething', + params: [], id: '*'}); + test.equal(stream.sent.length, 0); + + // Call update method. + conn.call('updateIt', stubWrittenId, _.identity); + // This stub write is visible too. + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId), + {_id: stubWrittenId, foo: 'bar', baz: 42}); + o.expectCallbacks({changed: 1}); + // Method sent. + var updateMethodId = testGotMessage( + test, stream, {msg: 'method', method: 'updateIt', + params: [stubWrittenId], id: '*'}); + test.equal(stream.sent.length, 0); + + // Get some data... slightly different than what we wrote. + stream.receive({msg: 'added', collection: collName, + id: Meteor.idStringify(stubWrittenId), fields: {foo: 'barb', other: 'field', + other2: 'bla'}}); + // It doesn't show up yet. + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId), + {_id: stubWrittenId, foo: 'bar', baz: 42}); + o.expectCallbacks(); + + // And get the first method-done. Still no updates to minimongo: we can't + // quiesce the doc until the second method is done. + stream.receive({msg: 'updated', methods: [insertMethodId]}); + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId), + {_id: stubWrittenId, foo: 'bar', baz: 42}); + o.expectCallbacks(); + + // More data. Not quite what we wrote. Also ignored for now. + stream.receive({msg: 'changed', collection: collName, + id: Meteor.idStringify(stubWrittenId), fields: {baz: 43}, cleared: ['other']}); + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId), + {_id: stubWrittenId, foo: 'bar', baz: 42}); + o.expectCallbacks(); + + // Second data-ready. Now everything takes effect! + stream.receive({msg: 'updated', methods: [updateMethodId]}); + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(stubWrittenId), + {_id: stubWrittenId, foo: 'barb', other2: 'bla', + baz: 43}); + o.expectCallbacks({changed: 1}); + + o.stop(); }); +} - test.equal(coll.find().count(), 0); - - // Call the insert method. - conn.call('insertSomething', _.identity); - // Stub write is visible. - test.equal(coll.find({foo: 'bar'}).count(), 1); - var stubWrittenId = coll.findOne({foo: 'bar'})._id; - o.expectCallbacks({added: 1}); - // Method sent. - var insertMethodId = testGotMessage( - test, stream, {msg: 'method', method: 'insertSomething', - params: [], id: '*'}); - test.equal(stream.sent.length, 0); - - // Call update method. - conn.call('updateIt', stubWrittenId, _.identity); - // This stub write is visible too. - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId), - {_id: stubWrittenId, foo: 'bar', baz: 42}); - o.expectCallbacks({changed: 1}); - // Method sent. - var updateMethodId = testGotMessage( - test, stream, {msg: 'method', method: 'updateIt', - params: [stubWrittenId], id: '*'}); - test.equal(stream.sent.length, 0); - - // Get some data... slightly different than what we wrote. - stream.receive({msg: 'added', collection: collName, - id: Meteor.idStringify(stubWrittenId), fields: {foo: 'barb', other: 'field', - other2: 'bla'}}); - // It doesn't show up yet. - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId), - {_id: stubWrittenId, foo: 'bar', baz: 42}); - o.expectCallbacks(); - - // And get the first method-done. Still no updates to minimongo: we can't - // quiesce the doc until the second method is done. - stream.receive({msg: 'updated', methods: [insertMethodId]}); - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId), - {_id: stubWrittenId, foo: 'bar', baz: 42}); - o.expectCallbacks(); - - // More data. Not quite what we wrote. Also ignored for now. - stream.receive({msg: 'changed', collection: collName, - id: Meteor.idStringify(stubWrittenId), fields: {baz: 43}, cleared: ['other']}); - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId), - {_id: stubWrittenId, foo: 'bar', baz: 42}); - o.expectCallbacks(); - - // Second data-ready. Now everything takes effect! - stream.receive({msg: 'updated', methods: [updateMethodId]}); - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(stubWrittenId), - {_id: stubWrittenId, foo: 'barb', other2: 'bla', - baz: 43}); - o.expectCallbacks({changed: 1}); +if (Meteor.isClient) { + Tinytest.add("livedata stub - unsent methods don't block quiescence", function (test) { + // This test is for https://github.com/meteor/meteor/issues/555 - o.stop(); -}); + var stream = new Meteor._StubStream; + var conn = newConnection(stream); + startAndConnect(test, stream); -Tinytest.add("livedata stub - unsent methods don't block quiescence", function (test) { - // This test is for https://github.com/meteor/meteor/issues/555 + var collName = Random.id(); + var coll = new Meteor.Collection(collName, {manager: conn}); - var stream = new Meteor._StubStream; - var conn = newConnection(stream); - startAndConnect(test, stream); - - var collName = Random.id(); - var coll = new Meteor.Collection(collName, {manager: conn}); + conn.methods({ + insertSomething: function () { + // stub write + coll.insert({foo: 'bar'}); + } + }); - conn.methods({ - insertSomething: function () { - // stub write - coll.insert({foo: 'bar'}); - } - }); + test.equal(coll.find().count(), 0); - test.equal(coll.find().count(), 0); + // Call a random method (no-op) + conn.call('no-op', _.identity); + // Call a wait method + conn.apply('no-op', [], {wait: true}, _.identity); + // Call a method with a stub that writes. + conn.call('insertSomething', _.identity); - // Call a random method (no-op) - conn.call('no-op', _.identity); - // Call a wait method - conn.apply('no-op', [], {wait: true}, _.identity); - // Call a method with a stub that writes. - conn.call('insertSomething', _.identity); + // Stub write is visible. + test.equal(coll.find({foo: 'bar'}).count(), 1); + var stubWrittenId = coll.findOne({foo: 'bar'})._id; - // Stub write is visible. - test.equal(coll.find({foo: 'bar'}).count(), 1); - var stubWrittenId = coll.findOne({foo: 'bar'})._id; + // first method sent + var firstMethodId = testGotMessage( + test, stream, {msg: 'method', method: 'no-op', + params: [], id: '*'}); + test.equal(stream.sent.length, 0); - // first method sent - var firstMethodId = testGotMessage( - test, stream, {msg: 'method', method: 'no-op', - params: [], id: '*'}); - test.equal(stream.sent.length, 0); + // ack the first method + stream.receive({msg: 'updated', methods: [firstMethodId]}); + stream.receive({msg: 'result', id: firstMethodId}); - // ack the first method - stream.receive({msg: 'updated', methods: [firstMethodId]}); - stream.receive({msg: 'result', id: firstMethodId}); + // Wait method sent. + var waitMethodId = testGotMessage( + test, stream, {msg: 'method', method: 'no-op', + params: [], id: '*'}); + test.equal(stream.sent.length, 0); - // Wait method sent. - var waitMethodId = testGotMessage( - test, stream, {msg: 'method', method: 'no-op', - params: [], id: '*'}); - test.equal(stream.sent.length, 0); + // ack the wait method + stream.receive({msg: 'updated', methods: [waitMethodId]}); + stream.receive({msg: 'result', id: waitMethodId}); - // ack the wait method - stream.receive({msg: 'updated', methods: [waitMethodId]}); - stream.receive({msg: 'result', id: waitMethodId}); + // insert method sent. + var insertMethodId = testGotMessage( + test, stream, {msg: 'method', method: 'insertSomething', + params: [], id: '*'}); + test.equal(stream.sent.length, 0); - // insert method sent. - var insertMethodId = testGotMessage( - test, stream, {msg: 'method', method: 'insertSomething', - params: [], id: '*'}); - test.equal(stream.sent.length, 0); + // ack the insert method + stream.receive({msg: 'updated', methods: [insertMethodId]}); + stream.receive({msg: 'result', id: insertMethodId}); - // ack the insert method - stream.receive({msg: 'updated', methods: [insertMethodId]}); - stream.receive({msg: 'result', id: insertMethodId}); - - // simulation reverted. - test.equal(coll.find({foo: 'bar'}).count(), 0); - -}); + // simulation reverted. + test.equal(coll.find({foo: 'bar'}).count(), 0); + }); +} Tinytest.add("livedata stub - reactive resub", function (test) { var stream = new Meteor._StubStream(); var conn = newConnection(stream); @@ -1282,10 +1288,17 @@ Tinytest.add("livedata connection - onReconnect prepends messages correctly with ]); }); +var getSelfConnectionUrl = function () { + if (Meteor.isClient) { + return "/"; + } else { + return Meteor.absoluteUrl(); + } +}; Tinytest.addAsync("livedata connection - version negotiation requires renegotiating", function (test, onComplete) { - var connection = new Meteor._LivedataConnection(Meteor.absoluteUrl(), { + var connection = new Meteor._LivedataConnection(getSelfConnectionUrl(), { reloadWithOutstanding: true, supportedDDPVersions: ["garbled", Meteor._SUPPORTED_DDP_VERSIONS[0]], onConnectionFailure: function () { test.fail(); onComplete(); }, @@ -1299,7 +1312,7 @@ Tinytest.addAsync("livedata connection - version negotiation requires renegotiat Tinytest.addAsync("livedata connection - version negotiation error", function (test, onComplete) { - var connection = new Meteor._LivedataConnection(Meteor.absoluteUrl(), { + var connection = new Meteor._LivedataConnection(getSelfConnectionUrl(), { reloadWithOutstanding: true, supportedDDPVersions: ["garbled", "more garbled"], onConnectionFailure: function () { @@ -1507,43 +1520,44 @@ Tinytest.add("livedata stub - subscribe errors", function (test) { test.isFalse(onReadyFired); }); -Tinytest.add("livedata stub - stubs before connected", function (test) { - var stream = new Meteor._StubStream(); - var conn = newConnection(stream); - - var collName = Random.id(); - var coll = new Meteor.Collection(collName, {manager: conn}); - - // Start and send "connect", but DON'T get 'connected' quite yet. - stream.reset(); // initial connection start. - - testGotMessage(test, stream, makeConnectMessage()); - test.length(stream.sent, 0); - - // Insert a document. The stub updates "conn" directly. - coll.insert({_id: "foo", bar: 42}, _.identity); - test.equal(coll.find().count(), 1); - test.equal(coll.findOne(), {_id: "foo", bar: 42}); - // It also sends the method message. - var methodMessage = JSON.parse(stream.sent.shift()); - test.equal(methodMessage, {msg: 'method', method: '/' + collName + '/insert', - params: [{_id: "foo", bar: 42}], - id: methodMessage.id}); - test.length(stream.sent, 0); - - // Now receive a connected message. This should not clear the - // _documentsWrittenByStub state! - stream.receive({msg: 'connected', session: SESSION_ID}); - test.length(stream.sent, 0); - test.equal(coll.find().count(), 1); - - // Now receive the "updated" message for the method. This should revert the - // insert. - stream.receive({msg: 'updated', methods: [methodMessage.id]}); - test.length(stream.sent, 0); - test.equal(coll.find().count(), 0); -}); - +if (Meteor.isClient) { + Tinytest.add("livedata stub - stubs before connected", function (test) { + var stream = new Meteor._StubStream(); + var conn = newConnection(stream); + + var collName = Random.id(); + var coll = new Meteor.Collection(collName, {manager: conn}); + + // Start and send "connect", but DON'T get 'connected' quite yet. + stream.reset(); // initial connection start. + + testGotMessage(test, stream, makeConnectMessage()); + test.length(stream.sent, 0); + + // Insert a document. The stub updates "conn" directly. + coll.insert({_id: "foo", bar: 42}, _.identity); + test.equal(coll.find().count(), 1); + test.equal(coll.findOne(), {_id: "foo", bar: 42}); + // It also sends the method message. + var methodMessage = JSON.parse(stream.sent.shift()); + test.equal(methodMessage, {msg: 'method', method: '/' + collName + '/insert', + params: [{_id: "foo", bar: 42}], + id: methodMessage.id}); + test.length(stream.sent, 0); + + // Now receive a connected message. This should not clear the + // _documentsWrittenByStub state! + stream.receive({msg: 'connected', session: SESSION_ID}); + test.length(stream.sent, 0); + test.equal(coll.find().count(), 1); + + // Now receive the "updated" message for the method. This should revert the + // insert. + stream.receive({msg: 'updated', methods: [methodMessage.id]}); + test.length(stream.sent, 0); + test.equal(coll.find().count(), 0); + }); +} // XXX also test: // - reconnect, with session resume. // - restart on update flag diff --git a/packages/meteor/package.js b/packages/meteor/package.js index 25251f99377..dafe07c0901 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -36,6 +36,7 @@ Package.on_use(function (api, where) { api.add_files('timers.js', ['client', 'server']); api.add_files('errors.js', ['client', 'server']); api.add_files('fiber_helpers.js', 'server'); + api.add_files('fiber_stubs_client.js', 'client'); // dynamic variables, bindEnvironment // XXX move into a separate package? diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 77115fa3c2c..4e33787bc26 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -10,6 +10,8 @@ LocalCollection = function () { this.docs = {}; // _id -> document (also containing id) + this._observeQueue = new Meteor._SynchronousQueue(); + this.next_qid = 1; // live query id generator // qid -> live query object. keys: @@ -259,21 +261,29 @@ _.extend(LocalCollection.Cursor.prototype, { // wrap callbacks we were passed. callbacks only fire when not paused and // are never undefined (except that query.moved is undefined for unordered // callbacks). - var if_not_paused = function (f) { + + // furthermore, callbacks enqueue until the operation we're working on is + // done. + var wrapCallback = function (f) { if (!f) return function () {}; return function (/*args*/) { - if (!self.collection.paused) - f.apply(this, arguments); + var collection = this; + var args = arguments; + if (!self.collection.paused) { + self.collection._observeQueue.queueTask(function () { + f.apply(collection, args); + }); + } }; }; - query.added = if_not_paused(options.added); - query.changed = if_not_paused(options.changed); - query.removed = if_not_paused(options.removed); + query.added = wrapCallback(options.added); + query.changed = wrapCallback(options.changed); + query.removed = wrapCallback(options.removed); if (ordered) { - query.moved = if_not_paused(options.moved); - query.addedBefore = if_not_paused(options.addedBefore); - query.movedBefore = if_not_paused(options.movedBefore); + query.moved = wrapCallback(options.moved); + query.addedBefore = wrapCallback(options.addedBefore); + query.movedBefore = wrapCallback(options.movedBefore); } if (!options._suppress_initial && !self.collection.paused) { @@ -305,6 +315,7 @@ _.extend(LocalCollection.Cursor.prototype, { handle.stop(); }); } + self.collection._observeQueue.flush(); return handle; } @@ -427,7 +438,7 @@ LocalCollection.prototype.insert = function (doc) { if (self.queries[qid]) LocalCollection._recomputeResults(self.queries[qid]); }); - + self._observeQueue.flush(); return doc._id; }; @@ -484,6 +495,7 @@ LocalCollection.prototype.remove = function (selector) { if (query) LocalCollection._recomputeResults(query); }); + self._observeQueue.flush(); }; // XXX atomicity: if multi is true, and one modification fails, do @@ -526,6 +538,7 @@ LocalCollection.prototype.update = function (selector, mod, options) { LocalCollection._recomputeResults(query, qidToOriginalResults[qid]); }); + self._observeQueue.flush(); }; LocalCollection.prototype._modifyAndNotify = function ( @@ -755,6 +768,7 @@ LocalCollection.prototype.pauseObservers = function () { // database. Note that this is not just replaying all the changes that // happened during the pause, it is a smarter 'coalesced' diff. LocalCollection.prototype.resumeObservers = function () { + var self = this; // No-op if not paused. if (!this.paused) return; @@ -764,13 +778,14 @@ LocalCollection.prototype.resumeObservers = function () { this.paused = false; for (var qid in this.queries) { - var query = this.queries[qid]; + var query = self.queries[qid]; // Diff the current results against the snapshot and send to observers. // pass the query object for its observer callbacks. LocalCollection._diffQueryChanges( query.ordered, query.results_snapshot, query.results, query); query.results_snapshot = null; } + self._observeQueue.flush(); }; diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index e499dd69fcf..1b7d4149225 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -502,6 +502,9 @@ Meteor.Collection.prototype._defineMutationMethods = function() { m[self._prefix + method] = function (/* ... */) { try { if (this.isSimulation) { + if (Meteor.isServer) + return; + // In a client simulation, you can do any mutation (even with a // complex selector). self._collection[method].apply( From 8432577f003c1cc510584b87a81a12d274d49c54 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Tue, 16 Apr 2013 10:33:49 -0700 Subject: [PATCH 010/102] Just don't provide stubs for collection methods on server; test. Test is for insert on server-to-server DDP minimongo side. --- packages/livedata/livedata_connection.js | 3 ++- packages/livedata/livedata_server.js | 4 +++- packages/mongo-livedata/collection.js | 5 ++--- .../mongo-livedata/mongo_livedata_tests.js | 18 ++++++++++++++++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 71a11d114e4..03162d5a02d 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -683,8 +683,9 @@ _.extend(Meteor._LivedataConnection.prototype, { callback = function (err, result) { if (err) future['throw'](err); - else + else { future['return'](result); + } }; } } diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index 61d8a61013e..0dfe7e691d1 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -32,7 +32,7 @@ _.extend(Meteor._SessionDocumentView.prototype, { // It's okay to clear fields that didn't exist. No need to throw // an error. - if (!precedenceList) + if (!precedenceList) return; var removedValue = undefined; @@ -423,6 +423,8 @@ _.extend(Meteor._LivedataSession.prototype, { // It should be a JSON object (it will be stringified.) send: function (msg) { var self = this; + if (Meteor._printSentDDP) + Meteor._debug("Sent DDP", Meteor._stringifyDDP(msg)); if (self.socket) self.socket.send(Meteor._stringifyDDP(msg)); else diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 1b7d4149225..05d5f130882 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -502,8 +502,6 @@ Meteor.Collection.prototype._defineMutationMethods = function() { m[self._prefix + method] = function (/* ... */) { try { if (this.isSimulation) { - if (Meteor.isServer) - return; // In a client simulation, you can do any mutation (even with a // complex selector). @@ -548,7 +546,8 @@ Meteor.Collection.prototype._defineMutationMethods = function() { } }; }); - self._manager.methods(m); + if (Meteor.isClient || self._manager === Meteor.default_server) + self._manager.methods(m); } }; diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index f4d8850716e..6e79b899d15 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -826,8 +826,12 @@ if (Meteor.isServer) { var self = this; Meteor._debug("connection setup"); self.id = Random.id(); - var C = new Meteor.Collection("ServerMinimongo_" + self.id); - + var C = self.C = new Meteor.Collection("ServerMinimongo_" + self.id); + C.allow({ + insert: function () {return true;}, + update: function () {return true;}, + remove: function () {return true;} + }); C.insert({a: 0, b: 1}); C.insert({a: 0, b: 2}); C.insert({a: 1, b: 3}); @@ -864,6 +868,16 @@ if (Meteor.isServer) { test.equal(contents.length, 2); test.equal(contents[0].a, 0); } + }, + + function (test, expect) { + var self = this; + if (!self.miniC) + return; + self.miniC.insert({a:0, b:3}); + var contents = self.miniC.find({b:3}).fetch(); + test.equal(contents.length, 1); + test.equal(contents[0].a, 0); } ]); From aa91dff1bf52a5af9bcf364cb0b2d0f1cb549a19 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Wed, 17 Apr 2013 14:13:19 -0700 Subject: [PATCH 011/102] Queue behavior v1, flush until mark --- packages/meteor/fiber_helpers.js | 5 +++-- packages/meteor/fiber_stubs_client.js | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/meteor/fiber_helpers.js b/packages/meteor/fiber_helpers.js index 9069a32525d..e1eb5ba8bd2 100644 --- a/packages/meteor/fiber_helpers.js +++ b/packages/meteor/fiber_helpers.js @@ -78,9 +78,10 @@ _.extend(Meteor._SynchronousQueue.prototype, { self._scheduleRun(); // No need to block. }, - taskRunning: function () { + + flush: function () { var self = this; - return self._taskRunning; + self.runTask(function () {}); }, safeToRunTask: function () { diff --git a/packages/meteor/fiber_stubs_client.js b/packages/meteor/fiber_stubs_client.js index ebbebfb61ef..79fb1c6a3c7 100644 --- a/packages/meteor/fiber_stubs_client.js +++ b/packages/meteor/fiber_stubs_client.js @@ -17,15 +17,19 @@ Meteor._SynchronousQueue = function () { _.extend(Meteor._SynchronousQueue.prototype, { runTask: function (task) { var self = this; + if (!self.safeToRunTask()) + throw new Error("Could not synchronously run a task from a running task"); self._tasks.push(task); + var tasks = self._tasks; + self._tasks = []; self._running = true; try { - while (!_.isEmpty(self._tasks)) { - var t = self._tasks.shift(); + while (!_.isEmpty(tasks)) { + var t = tasks.shift(); try { t(); } catch (e) { - if (_.isEmpty(self._tasks)) { + if (_.isEmpty(tasks)) { // this was the last task, that is, the one we're calling runTask // for. throw e; @@ -41,7 +45,12 @@ _.extend(Meteor._SynchronousQueue.prototype, { queueTask: function (task) { var self = this; + var wasEmpty = _.isEmpty(self._tasks); self._tasks.push(task); + // Intentionally not using Meteor.setTimeout, because it doesn't like runing + // in stubs for now. + if (wasEmpty) + setTimeout(_.bind(self.flush, self), 0); }, flush: function () { @@ -49,12 +58,8 @@ _.extend(Meteor._SynchronousQueue.prototype, { self.runTask(function () {}); }, - taskRunning: function () { - var self = this; - return self._running; - }, - safeToRunTask: function () { - return true; + var self = this; + return !self._running; } }); From 68e4ecfc184b7c0ef704d718a61bb8b8a0d8f57c Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Wed, 17 Apr 2013 16:15:44 -0700 Subject: [PATCH 012/102] New queueing semantics for observe events When modifications to a collection are nested inside observe events for the same collection, previously we would just throw an error. Now, instead, we schedule the new observe events for after the currently scheduled events, and make sure they all happen before the outer modification returns. I think this is going to be the least-unexpected behavior, even if it is a little difficult to explain at first blush. --- packages/meteor/fiber_helpers.js | 9 +++++++++ packages/meteor/fiber_stubs_client.js | 9 +++++++++ packages/minimongo/minimongo.js | 10 +++++----- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/meteor/fiber_helpers.js b/packages/meteor/fiber_helpers.js index e1eb5ba8bd2..e1b0778c316 100644 --- a/packages/meteor/fiber_helpers.js +++ b/packages/meteor/fiber_helpers.js @@ -89,6 +89,15 @@ _.extend(Meteor._SynchronousQueue.prototype, { return Fiber.current && self._currentTaskFiber !== Fiber.current; }, + drain: function () { + var self = this; + if (!self.safeToRunTask()) + return; + while (!_.isEmpty(self._taskHandles)) { + self.flush(); + } + }, + _scheduleRun: function () { var self = this; diff --git a/packages/meteor/fiber_stubs_client.js b/packages/meteor/fiber_stubs_client.js index 79fb1c6a3c7..4e419c07da3 100644 --- a/packages/meteor/fiber_stubs_client.js +++ b/packages/meteor/fiber_stubs_client.js @@ -58,6 +58,15 @@ _.extend(Meteor._SynchronousQueue.prototype, { self.runTask(function () {}); }, + drain: function () { + var self = this; + if (!self.safeToRunTask()) + return; + while (!_.isEmpty(self._tasks)) { + self.flush(); + } + }, + safeToRunTask: function () { var self = this; return !self._running; diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 4e33787bc26..0bea8fd953b 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -315,7 +315,7 @@ _.extend(LocalCollection.Cursor.prototype, { handle.stop(); }); } - self.collection._observeQueue.flush(); + self.collection._observeQueue.drain(); return handle; } @@ -438,7 +438,7 @@ LocalCollection.prototype.insert = function (doc) { if (self.queries[qid]) LocalCollection._recomputeResults(self.queries[qid]); }); - self._observeQueue.flush(); + self._observeQueue.drain(); return doc._id; }; @@ -495,7 +495,7 @@ LocalCollection.prototype.remove = function (selector) { if (query) LocalCollection._recomputeResults(query); }); - self._observeQueue.flush(); + self._observeQueue.drain(); }; // XXX atomicity: if multi is true, and one modification fails, do @@ -538,7 +538,7 @@ LocalCollection.prototype.update = function (selector, mod, options) { LocalCollection._recomputeResults(query, qidToOriginalResults[qid]); }); - self._observeQueue.flush(); + self._observeQueue.drain(); }; LocalCollection.prototype._modifyAndNotify = function ( @@ -785,7 +785,7 @@ LocalCollection.prototype.resumeObservers = function () { query.ordered, query.results_snapshot, query.results, query); query.results_snapshot = null; } - self._observeQueue.flush(); + self._observeQueue.drain(); }; From 11fb1714365d4a60c55fbcf68a632a7ec33249e0 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Wed, 17 Apr 2013 19:21:49 -0700 Subject: [PATCH 013/102] Glasser diff review comments --- packages/livedata/livedata_server.js | 5 +++++ packages/minimongo/minimongo.js | 6 ++++-- packages/mongo-livedata/collection.js | 3 +++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index 0dfe7e691d1..f616950da92 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -342,6 +342,8 @@ _.extend(Meteor._LivedataSession.prototype, { self.socket = socket; self.last_connect_time = +(new Date); _.each(self.out_queue, function (msg) { + if (Meteor._printSentDDP) + Meteor._debug("Sent DDP", Meteor._stringifyDDP(msg)); self.socket.send(Meteor._stringifyDDP(msg)); }); self.out_queue = []; @@ -1015,6 +1017,9 @@ Meteor._LivedataServer = function () { }; socket.on('data', function (raw_msg) { + if (Meteor._printReceivedDDP) { + Meteor._debug("Received DDP", raw_msg); + } try { try { var msg = Meteor._parseDDP(raw_msg); diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 0bea8fd953b..b8c45951fec 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -268,11 +268,11 @@ _.extend(LocalCollection.Cursor.prototype, { if (!f) return function () {}; return function (/*args*/) { - var collection = this; + var context = this; var args = arguments; if (!self.collection.paused) { self.collection._observeQueue.queueTask(function () { - f.apply(collection, args); + f.apply(context, args); }); } }; @@ -315,6 +315,8 @@ _.extend(LocalCollection.Cursor.prototype, { handle.stop(); }); } + // run the observe callbacks resulting from the initial contents + // before we leave the observe. self.collection._observeQueue.drain(); return handle; diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 05d5f130882..976a49f5331 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -546,6 +546,9 @@ Meteor.Collection.prototype._defineMutationMethods = function() { } }; }); + // Minimongo on the server gets no stubs; instead, by default + // it wait()s until its result is ready, yielding. + // This matches the behavior of macromongo on the server better. if (Meteor.isClient || self._manager === Meteor.default_server) self._manager.methods(m); } From f67db983c3d60aae446658cc4b203444ab03567a Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 10 Apr 2013 17:32:46 -0700 Subject: [PATCH 014/102] New check library. --- docs/client/api.html | 20 +- docs/client/api.js | 2 +- docs/client/concepts.html | 1 + examples/parties/model.js | 29 +- examples/todos/server/publish.js | 1 + examples/wordplay/model.js | 4 + examples/wordplay/server/game.js | 3 +- packages/accounts-base/accounts_server.js | 31 +- .../accounts-oauth-helper/oauth_server.js | 9 +- .../accounts-password/email_tests_setup.js | 3 + packages/accounts-password/password_server.js | 104 ++++--- .../audit_argument_checks.js | 1 + packages/audit-argument-checks/package.js | 8 + packages/check/match.js | 266 ++++++++++++++++++ packages/check/match_test.js | 198 +++++++++++++ packages/check/package.js | 16 ++ packages/livedata/livedata_connection.js | 2 + packages/livedata/livedata_server.js | 88 ++++-- packages/livedata/livedata_test_service.js | 35 ++- packages/livedata/livedata_tests.js | 16 +- packages/livedata/package.js | 2 +- packages/madewith/madewith.js | 1 + packages/mongo-livedata/allow_tests.js | 2 + packages/mongo-livedata/collection.js | 2 + .../mongo-livedata/mongo_livedata_tests.js | 6 + packages/srp/package.js | 2 +- packages/srp/srp.js | 6 + packages/test-in-console/reporter.js | 2 + packages/tinytest/tinytest_server.js | 4 + 29 files changed, 746 insertions(+), 118 deletions(-) create mode 100644 packages/audit-argument-checks/audit_argument_checks.js create mode 100644 packages/audit-argument-checks/package.js create mode 100644 packages/check/match.js create mode 100644 packages/check/match_test.js create mode 100644 packages/check/package.js diff --git a/docs/client/api.html b/docs/client/api.html index 2f9d40361ac..921c182596d 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -76,6 +76,7 @@

Publish and subscribe

// publish dependent documents and simulate joins Meteor.publish("roomAndMessages", function (roomId) { + check(roomId, String); return [ Rooms.find({_id: roomId}, {fields: {secretInfo: 0}}), Messages.find({roomId: roomId}) @@ -98,6 +99,7 @@

Publish and subscribe

// server: publish the current size of a collection Meteor.publish("counts-by-room", function (roomId) { var self = this; + check(roomId, String); var count = 0; var initializing = true; var handle = Messages.find({roomId: roomId}).observeChanges({ @@ -242,6 +244,8 @@

Methods

Meteor.methods({ foo: function (arg1, arg2) { + check(arg1, String); + check(arg2, [Number]); // .. do stuff .. if (you want to throw an error) throw new Meteor.Error(404, "Can't find my pants"); @@ -318,11 +322,14 @@

Methods

{{> api_box error}} -If you want to return an error from a method, throw an exception. -Methods can throw any kind of exception. But `Meteor.Error` is the only -kind of error that a server will send to the client. If a method -function throws a different exception, then it will be mapped to -`Meteor.Error(500, "Internal server error")` on the wire. +If you want to return an error from a method, throw an exception. Methods can +throw any kind of exception. But `Meteor.Error` is the only kind of error that +a server will send to the client. If a method function throws a different +exception, then it will be mapped to a sanitized version on the +wire. Specifically, if the `sanitizedError` field on the thrown error is set to +a `Meteor.Error`, then that error will be sent to the client. Otherwise, if no +sanitized version is available, the client gets +`Meteor.Error(500, 'Internal server error')`. {{> api_box meteor_call}} @@ -2755,6 +2762,7 @@

Meteor.http

Example server method: Meteor.methods({checkTwitter: function (userId) { + check(userId, String); this.unblock(); var result = Meteor.http.call("GET", "http://api.twitter.com/xyz", {params: {user: userId}}); @@ -2811,6 +2819,8 @@

Email

// In your server code: define a method that the client can call Meteor.methods({ sendEmail: function (to, from, subject, text) { + check([to, from, subject, text], [String]); + // Let other method calls from the same client start running, // without waiting for the email sending to complete. this.unblock(); diff --git a/docs/client/api.js b/docs/client/api.js index a3367f4ba5a..2a0b0b19445 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -275,7 +275,7 @@ Template.api.subscription_error = { id: "publish_error", name: "this.error(error)", locus: "Server", - descr: ["Call inside the publish function. Stops this client's subscription, triggering a call on the client to the `onError` callback passed to [`Meteor.subscribe`](#meteor_subscribe), if any. If `error` is not a [`Meteor.Error`](#meteor_error), it will be mapped to `Meteor.Error(500, \"Internal server error\")`."] + descr: ["Call inside the publish function. Stops this client's subscription, triggering a call on the client to the `onError` callback passed to [`Meteor.subscribe`](#meteor_subscribe), if any. If `error` is not a [`Meteor.Error`](#meteor_error), it will be [sanitized](#meteor_error)."] }; Template.api.subscription_stop = { diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 64d0692ed4d..4376a85854c 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -168,6 +168,7 @@

Data and security

// server: publish all messages for a given room Meteor.publish("messages", function (roomId) { + check(roomId, String); return Messages.find({room: roomId}); }); diff --git a/examples/parties/model.js b/examples/parties/model.js index ee432095052..6eb086aae60 100644 --- a/examples/parties/model.js +++ b/examples/parties/model.js @@ -42,16 +42,27 @@ attending = function (party) { return (_.groupBy(party.rsvps, 'rsvp').yes || []).length; }; +var NonEmptyString = Match.Where(function (x) { + check(x, String); + return x.length !== 0; +}); + +var Coordinate = Match.Where(function (x) { + check(x, Number); + return x >= 0 && x <= 1; +}); + Meteor.methods({ // options should include: title, description, x, y, public createParty: function (options) { - options = options || {}; - if (! (typeof options.title === "string" && options.title.length && - typeof options.description === "string" && - options.description.length && - typeof options.x === "number" && options.x >= 0 && options.x <= 1 && - typeof options.y === "number" && options.y >= 0 && options.y <= 1)) - throw new Meteor.Error(400, "Required parameter missing"); + check(options, { + title: NonEmptyString, + description: NonEmptyString, + x: Coordinate, + y: Coordinate, + public: Match.Optional(Boolean) + }); + if (options.title.length > 100) throw new Meteor.Error(413, "Title too long"); if (options.description.length > 1000) @@ -72,6 +83,8 @@ Meteor.methods({ }, invite: function (partyId, userId) { + check(partyId, String); + check(userId, String); var party = Parties.findOne(partyId); if (! party || party.owner !== this.userId) throw new Meteor.Error(404, "No such party"); @@ -100,6 +113,8 @@ Meteor.methods({ }, rsvp: function (partyId, rsvp) { + check(partyId, String); + check(rsvp, String); if (! this.userId) throw new Meteor.Error(403, "You must be logged in to RSVP"); if (! _.contains(['yes', 'no', 'maybe'], rsvp)) diff --git a/examples/todos/server/publish.js b/examples/todos/server/publish.js index 76c3630ee21..6b8cf1c43bd 100644 --- a/examples/todos/server/publish.js +++ b/examples/todos/server/publish.js @@ -16,6 +16,7 @@ Todos = new Meteor.Collection("todos"); // Publish all items for requested list_id. Meteor.publish('todos', function (list_id) { + check(list_id, String); return Todos.find({list_id: list_id}); }); diff --git a/examples/wordplay/model.js b/examples/wordplay/model.js index 68b240d846f..e9ee58b7d9f 100644 --- a/examples/wordplay/model.js +++ b/examples/wordplay/model.js @@ -96,6 +96,7 @@ paths_for_word = function (board, word) { Meteor.methods({ score_word: function (word_id) { + check(word_id, String); var word = Words.findOne(word_id); var game = Games.findOne(word.game_id); @@ -129,12 +130,15 @@ if (Meteor.isServer) { // publish single games Meteor.publish('games', function (id) { + check(id, String); return Games.find({_id: id}); }); // publish all my words and opponents' words that the server has // scored as good. Meteor.publish('words', function (game_id, player_id) { + check(game_id, String); + check(player_id, String); return Words.find({$or: [{game_id: game_id, state: 'good'}, {player_id: player_id}]}); }); diff --git a/examples/wordplay/server/game.js b/examples/wordplay/server/game.js index 31e2010dadf..94ac4f56691 100644 --- a/examples/wordplay/server/game.js +++ b/examples/wordplay/server/game.js @@ -1,7 +1,7 @@ ////////// Server only logic ////////// Meteor.methods({ - start_new_game: function (evt) { + start_new_game: function () { // create a new game w/ fresh board var game_id = Games.insert({board: new_board(), clock: 120}); @@ -49,6 +49,7 @@ Meteor.methods({ keepalive: function (player_id) { + check(player_id, String); Players.update({_id: player_id}, {$set: {last_keepalive: (new Date()).getTime(), idle: false}}); diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 0852e8a3027..6795423ad8f 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -48,24 +48,14 @@ Accounts._loginHandlers = []; // return `undefined`, meaning it handled this call to `login`. Return // that return value, which ought to be a {id/token} pair. var tryAllLoginHandlers = function (options) { - var result = undefined; - - _.find(Accounts._loginHandlers, function(handler) { - - var maybeResult = handler(options); - if (maybeResult !== undefined) { - result = maybeResult; - return true; - } else { - return false; - } - }); - - if (result === undefined) { - throw new Meteor.Error(400, "Unrecognized options for login request"); - } else { - return result; + for (var i = 0; i < Accounts._loginHandlers.length; ++i) { + var handler = Accounts._loginHandlers[i]; + var result = handler(options); + if (result !== undefined) + return result; } + + throw new Meteor.Error(400, "Unrecognized options for login request"); }; @@ -77,6 +67,9 @@ Meteor.methods({ // If unsuccessful (for example, if the user closed the oauth login popup), // returns null login: function(options) { + // Login handlers should really also check whatever field they look at in + // options, but we don't enforce it. + check(options, Object); var result = tryAllLoginHandlers(options); if (result !== null) this.setUserId(result.id); @@ -98,6 +91,7 @@ Accounts.registerLoginHandler(function(options) { if (!options.resume) return undefined; + check(options.resume, String); var user = Meteor.users.findOne({ "services.resume.loginTokens.token": ""+options.resume }); @@ -312,7 +306,8 @@ Meteor.publish("meteor.loginServiceConfiguration", function () { // Allow a one-time configuration for a login service. Modifications // to this collection are also allowed in insecure mode. Meteor.methods({ - "configureLoginService": function(options) { + "configureLoginService": function (options) { + check(options, Match.ObjectIncluding({service: String})); // Don't let random users configure a service we haven't added yet (so // that when we do later add it, it's set up with their configuration // instead of ours). diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index b6417299cc1..b6303ab45ec 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -60,8 +60,9 @@ Accounts.registerLoginHandler(function (options) { if (!options.oauth) return undefined; // don't handle - var result = Accounts.oauth._loginResultForState[options.oauth.state]; - if (!result) { + check(options.oauth, {state: String}); + + if (!_.has(Accounts.oauth._loginResultForState, options.oauth.state)) { // OAuth state is not recognized, which could be either because the popup // was closed by the user before completion, or some sort of error where // the oauth provider didn't talk to our server correctly and closed the @@ -73,7 +74,9 @@ Accounts.registerLoginHandler(function (options) { // request but does close the window. This seems unlikely. throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'No matching login attempt found'); - } else if (result instanceof Error) + } + var result = Accounts.oauth._loginResultForState[options.oauth.state]; + if (result instanceof Error) // We tried to login, but there was a fatal error. Report it back // to the user. throw result; diff --git a/packages/accounts-password/email_tests_setup.js b/packages/accounts-password/email_tests_setup.js index db5c8842835..1745e3a9091 100644 --- a/packages/accounts-password/email_tests_setup.js +++ b/packages/accounts-password/email_tests_setup.js @@ -20,10 +20,12 @@ Email.send = function (options) { Meteor.methods({ getInterceptedEmails: function (email) { + check(email, String); return interceptedEmails[email]; }, addEmailForTestAndVerify: function (email) { + check(email, String); Meteor.users.update( {_id: this.userId}, {$push: {emails: {address: email, verified: false}}}); @@ -31,6 +33,7 @@ Meteor.methods({ }, createUserOnServer: function (email) { + check(email, String); var userId = Accounts.createUser({email: email}); Accounts.sendEnrollmentEmail(userId); return Meteor.users.findOne(userId); diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 3561a6e9af9..a3d46d528a1 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -5,25 +5,28 @@ // Users can specify various keys to identify themselves with. // @param user {Object} with one of `id`, `username`, or `email`. // @returns A selector to pass to mongo to get the user record. -var selectorFromUserQuery = function (user) { - if (!user) - throw new Meteor.Error(400, "Must pass a user property in request"); - if (_.keys(user).length !== 1) - throw new Meteor.Error(400, "User property must have exactly one field"); - var selector; +var selectorFromUserQuery = function (user) { if (user.id) - selector = {_id: user.id}; + return {_id: user.id}; else if (user.username) - selector = {username: user.username}; + return {username: user.username}; else if (user.email) - selector = {"emails.address": user.email}; - else - throw new Meteor.Error(400, "Must pass username, email, or id in request.user"); - - return selector; + return {"emails.address": user.email}; + throw new Error("shouldn't happen (validation missed something)"); }; +var userQueryValidator = Match.Where(function (user) { + check(user, { + id: Match.Optional(String), + username: Match.Optional(String), + email: Match.Optional(String) + }); + if (_.keys(user).length !== 1) + throw new Match.Error("User property must have exactly one field"); + return true; +}); + // Step 1 of SRP password exchange. This puts an `M` value in the // session data for this connection. If a client later sends the same // `M` value to a method on this connection, it proves they know the @@ -38,6 +41,10 @@ var selectorFromUserQuery = function (user) { // salt: random string ID // B: hex encoded int. server's public key for this exchange Meteor.methods({beginPasswordExchange: function (request) { + check(request, { + user: userQueryValidator, + A: String + }); var selector = selectorFromUserQuery(request.user); var user = Meteor.users.findOne(selector); @@ -65,8 +72,7 @@ Meteor.methods({beginPasswordExchange: function (request) { Accounts.registerLoginHandler(function (options) { if (!options.srp) return undefined; // don't handle - if (!options.srp.M) - throw new Meteor.Error(400, "Must pass M in options.srp"); + check(options.srp, {M: String}); // we're always called from within a 'login' method, so this should // be safe. @@ -101,6 +107,8 @@ Accounts.registerLoginHandler(function (options) { if (!options.password || !options.user) return undefined; // don't handle + check(options, {user: userQueryValidator, password: String}); + var selector = selectorFromUserQuery(options.user); var user = Meteor.users.findOne(selector); if (!user) @@ -136,31 +144,29 @@ Accounts.registerLoginHandler(function (options) { Meteor.methods({changePassword: function (options) { if (!this.userId) throw new Meteor.Error(401, "Must be logged in"); + check(options, { + // If options.M is set, it means we went through a challenge with the old + // password. For now, we don't allow changePassword without knowing the old + // password. + M: String, + srp: Match.Optional(Meteor._srp.matchVerifier), + password: Match.Optional(String) + }); - // If options.M is set, it means we went through a challenge with - // the old password. - - if (!options.M /* could allow unsafe password changes here */) { - throw new Meteor.Error(403, "Old password required."); - } - - if (options.M) { - var serialized = this._sessionData.srpChallenge; - if (!serialized || serialized.M !== options.M) - throw new Meteor.Error(403, "Incorrect password"); - if (serialized.userId !== this.userId) - // No monkey business! - throw new Meteor.Error(403, "Incorrect password"); - // Only can use challenges once. - delete this._sessionData.srpChallenge; - } + var serialized = this._sessionData.srpChallenge; + if (!serialized || serialized.M !== options.M) + throw new Meteor.Error(403, "Incorrect password"); + if (serialized.userId !== this.userId) + // No monkey business! + throw new Meteor.Error(403, "Incorrect password"); + // Only can use challenges once. + delete this._sessionData.srpChallenge; var verifier = options.srp; if (!verifier && options.password) { verifier = Meteor._srp.generateVerifier(options.password); } - if (!verifier || !verifier.identity || !verifier.salt || - !verifier.verifier) + if (!verifier) throw new Meteor.Error(400, "Invalid verifier"); // XXX this should invalidate all login tokens other than the current one @@ -194,15 +200,13 @@ Accounts.setPassword = function (userId, newPassword) { // Method called by a user to request a password reset email. This is // the start of the reset process. Meteor.methods({forgotPassword: function (options) { - var email = options.email; - if (!email) - throw new Meteor.Error(400, "Need to set options.email"); + check(options, {email: String}); - var user = Meteor.users.findOne({"emails.address": email}); + var user = Meteor.users.findOne({"emails.address": options.email}); if (!user) throw new Meteor.Error(403, "User not found"); - Accounts.sendResetPasswordEmail(user._id, email); + Accounts.sendResetPasswordEmail(user._id, options.email); }}); // send the user an email with a link that when opened allows the user @@ -282,10 +286,8 @@ Accounts.sendEnrollmentEmail = function (userId, email) { // Take token from sendResetPasswordEmail or sendEnrollmentEmail, change // the users password, and log them in. Meteor.methods({resetPassword: function (token, newVerifier) { - if (!token) - throw new Meteor.Error(400, "Need to pass token"); - if (!newVerifier) - throw new Meteor.Error(400, "Need to pass newVerifier"); + check(token, String); + check(newVerifier, Meteor._srp.matchVerifier); var user = Meteor.users.findOne({ "services.password.reset.token": ""+token}); @@ -361,8 +363,7 @@ Accounts.sendVerificationEmail = function (userId, address) { // Take token from sendVerificationEmail, mark the email as verified, // and log them in. Meteor.methods({verifyEmail: function (token) { - if (!token) - throw new Meteor.Error(400, "Need to pass token"); + check(token, String); var user = Meteor.users.findOne( {'services.email.verificationTokens.token': token}); @@ -414,6 +415,16 @@ Meteor.methods({verifyEmail: function (token) { // returns an object with id: userId, and (if options.generateLoginToken is // set) token: loginToken. var createUser = function (options) { + // Unknown keys allowed, because a onCreateUserHook can take arbitrary + // options. + check(options, Match.ObjectIncluding({ + generateLoginToken: Boolean, + username: Match.Optional(String), + email: Match.Optional(String), + password: Match.Optional(String), + srp: Match.Optional(Meteor._srp.matchVerifier) + })); + var username = options.username; var email = options.email; if (!username && !email) @@ -441,7 +452,8 @@ var createUser = function (options) { // method for create user. Requests come from the client. Meteor.methods({createUser: function (options) { - options = _.clone(options); + // createUser() above does more checking. + check(options, Object); options.generateLoginToken = true; if (Accounts._options.forbidClientAccountCreation) throw new Meteor.Error(403, "Signups forbidden"); diff --git a/packages/audit-argument-checks/audit_argument_checks.js b/packages/audit-argument-checks/audit_argument_checks.js new file mode 100644 index 00000000000..a1a2afe6023 --- /dev/null +++ b/packages/audit-argument-checks/audit_argument_checks.js @@ -0,0 +1 @@ +Meteor._LivedataServer._auditArgumentChecks = true; diff --git a/packages/audit-argument-checks/package.js b/packages/audit-argument-checks/package.js new file mode 100644 index 00000000000..9252ea10a13 --- /dev/null +++ b/packages/audit-argument-checks/package.js @@ -0,0 +1,8 @@ +Package.describe({ + summary: "Try to detect inadequate input sanitization" +}); + +Package.on_use(function (api) { + api.use(['livedata'], ['server']); + api.add_files(['audit_argument_checks.js'], 'server'); +}); diff --git a/packages/check/match.js b/packages/check/match.js new file mode 100644 index 00000000000..c2af5afef55 --- /dev/null +++ b/packages/check/match.js @@ -0,0 +1,266 @@ +// XXX docs +// XXX on linker branch, export Match and check + +// Things we explicitly do NOT support: +// - heterogenous arrays + +var currentArgumentChecker = new Meteor.EnvironmentVariable; + +check = function (value, pattern) { + // Record that check got called, if somebody cared. + var argChecker = currentArgumentChecker.get(); + if (argChecker) + argChecker.checking(value); + checkSubtree(value, pattern); +}; + +Match = { + Optional: function (pattern) { + return new Optional(pattern); + }, + OneOf: function (/*arguments*/) { + return new OneOf(_.toArray(arguments)); + }, + Any: ['__any__'], + Where: function (condition) { + return new Where(condition); + }, + ObjectIncluding: function (pattern) { + return new ObjectIncluding(pattern); + }, + + // XXX should we record the path down the tree in the error message? + // XXX matchers should know how to describe themselves for errors + Error: Meteor.makeErrorType("Match.Error", function (msg) { + this.message = "Match error: " + msg; + // If this gets sent over DDP, don't give full internal details but at least + // provide something better than 500 Internal server error. + this.sanitizedError = new Meteor.Error(400, "Match failed"); + }), + + // Tests to see if value matches pattern. Unlike check, it merely returns true + // or false (unless an error other than Match.Error was thrown). It does not + // interact with _failIfArgumentsAreNotAllChecked. + // XXX maybe also implement a Match.match which returns more information about + // failures but without using exception handling or doing what check() + // does with _failIfArgumentsAreNotAllChecked and Meteor.Error conversion + test: function (value, pattern) { + try { + checkSubtree(value, pattern); + return true; + } catch (e) { + if (e instanceof Match.Error) + return false; + // Rethrow other errors. + throw e; + } + }, + + // Runs `f.apply(context, args)`. If check() is not called on every element of + // `args` (either directly or in the first level of an array), throws an error + // (using `description` in the message). + _failIfArgumentsAreNotAllChecked: function (f, context, args, description) { + var argChecker = new ArgumentChecker(args, description); + var result = currentArgumentChecker.withValue(argChecker, function () { + return f.apply(context, args); + }); + // If f didn't itself throw, make sure it checked all of its arguments. + argChecker.throwUnlessAllArgumentsHaveBeenChecked(); + return result; + } +}; + +var Optional = function (pattern) { + this.pattern = pattern; +}; + +var OneOf = function (choices) { + this.choices = choices; +}; + +var Where = function (condition) { + this.condition = condition; +}; + +var ObjectIncluding = function (pattern) { + this.pattern = pattern; +}; + +var typeofChecks = [ + [String, "string"], + [Number, "number"], + [Boolean, "boolean"], + // While we don't allow undefined in EJSON, this is good for optional + // arguments with OneOf. + [undefined, "undefined"] +]; + +var checkSubtree = function (value, pattern) { + // Match anything! + if (pattern === Match.Any) + return; + + // Basic atomic types. + // XXX do we have to worry about if value is boxed (eg String)? will that + // happen? + for (var i = 0; i < typeofChecks.length; ++i) { + if (pattern === typeofChecks[i][0]) { + if (typeof value === typeofChecks[i][1]) + return; + throw new Match.Error("Expected " + typeofChecks[i][1] + ", got " + + typeof value); + } + } + if (pattern === null) { + if (value === null) + return; + throw new Match.Error("Expected null, got " + EJSON.stringify(value)); + } + + // "Object" is shorthand for Match.ObjectIncluding({}); + if (pattern === Object) + pattern = Match.ObjectIncluding({}); + + // Array (checked AFTER Any, which is implemented as an Array). + if (pattern instanceof Array) { + if (pattern.length !== 1) + throw Error("Bad pattern: arrays must have one type element" + + EJSON.stringify(pattern)); + if (!_.isArray(value) && !_.isArguments(value)) { + throw new Match.Error("Expected array, got " + EJSON.stringify(value)); + } + + _.each(value, function (valueElement) { + checkSubtree(valueElement, pattern[0]); + }); + return; + } + + // Arbitrary validation checks. The condition can return false or throw a + // Match.Error (ie, it can internally use check()) to fail. + if (pattern instanceof Where) { + if (pattern.condition(value)) + return; + // XXX this error is terrible + throw new Match.Error("Failed Match.Where validation"); + } + + + if (pattern instanceof Optional) + pattern = Match.OneOf(undefined, pattern.pattern); + + if (pattern instanceof OneOf) { + for (var i = 0; i < pattern.choices.length; ++i) { + try { + checkSubtree(value, pattern.choices[i]); + // No error? Yay, return. + return; + } catch (err) { + // Other errors should be thrown. Match errors just mean try another + // choice. + if (!(err instanceof Match.Error)) + throw err; + } + } + // XXX this error is terrible, esp if it was converted from Optional + throw new Match.Error("Failed Match.OneOf validation"); + } + + // A function that isn't something we special-case is assumed to be a + // constructor. + if (pattern instanceof Function) { + if (value instanceof pattern) + return; + // XXX what if .name isn't defined + throw new Match.Error("Expected " + pattern.name); + } + + var unknownKeysAllowed = false; + if (pattern instanceof ObjectIncluding) { + unknownKeysAllowed = true; + pattern = pattern.pattern; + } + + if (typeof pattern !== "object") + throw Error("Bad pattern: unknown pattern type"); + + // An object, with required and optional keys. Note that this does NOT do + // structural matches against objects of special types that happen to match + // the pattern: this really needs to be a plain old {Object}! + if (typeof value !== 'object') + throw new Match.Error("Expected object, got " + typeof value); + if (value === null) + throw new Match.Error("Expected object, got null"); + if (value.constructor !== Object) + throw new Match.Error("Expected plain object"); + + var requiredPatterns = {}; + var optionalPatterns = {}; + _.each(pattern, function (subPattern, key) { + if (subPattern instanceof Optional) + optionalPatterns[key] = subPattern.pattern; + else + requiredPatterns[key] = subPattern; + }); + + _.each(value, function (subValue, key) { + if (_.has(requiredPatterns, key)) { + checkSubtree(subValue, requiredPatterns[key]); + delete requiredPatterns[key]; + } else if (_.has(optionalPatterns, key)) { + checkSubtree(subValue, optionalPatterns[key]); + } else { + if (!unknownKeysAllowed) + throw new Match.Error("Unknown key '" + key + "'"); + } + }); + + _.each(requiredPatterns, function (subPattern, key) { + throw new Match.Error("Missing key '" + key + "'"); + }); +}; + +var ArgumentChecker = function (args, description) { + var self = this; + // Make a SHALLOW copy of the arguments. (We'll be doing identity checks + // against its contents.) + self.args = _.clone(args); + // Since the common case will be to check arguments in order, and we splice + // out arguments when we check them, make it so we splice out from the end + // rather than the beginning. + self.args.reverse(); + self.description = description; +}; + +_.extend(ArgumentChecker.prototype, { + checking: function (value) { + var self = this; + if (self._checkingOneValue(value)) + return; + // Allow check(arguments, [String]) or check(arguments.slice(1), [String]) + // or check([foo, bar], [String]) to count... but only if value wasn't + // itself an argument. + if (_.isArray(value) || _.isArguments(value)) { + _.each(value, _.bind(self._checkingOneValue, self)); + } + }, + _checkingOneValue: function (value) { + var self = this; + for (var i = 0; i < self.args.length; ++i) { + // Is this value one of the arguments? (This can have a false positive if + // the argument is an interned primitive, but it's still a good enough + // check.) + if (value === self.args[i]) { + self.args.splice(i, 1); + return true; + } + } + return false; + }, + throwUnlessAllArgumentsHaveBeenChecked: function () { + var self = this; + if (!_.isEmpty(self.args)) + throw new Error("Did not check() all arguments during " + + self.description); + } +}); diff --git a/packages/check/match_test.js b/packages/check/match_test.js new file mode 100644 index 00000000000..e22ff9405c9 --- /dev/null +++ b/packages/check/match_test.js @@ -0,0 +1,198 @@ +Tinytest.add("check - check", function (test) { + var matches = function (value, pattern) { + var error; + try { + check(value, pattern); + } catch (e) { + error = e; + } + test.isFalse(error); + test.isTrue(Match.test(value, pattern)); + }; + var fails = function (value, pattern) { + var error; + try { + check(value, pattern); + } catch (e) { + error = e; + } + test.isTrue(error); + test.instanceOf(error, Match.Error); + test.isFalse(Match.test(value, pattern)); + }; + + // Atoms. + var pairs = [ + ["foo", String], + ["", String], + [0, Number], + [42.59, Number], + [NaN, Number], + [Infinity, Number], + [true, Boolean], + [false, Boolean], + [undefined, undefined], + [null, null] + ]; + _.each(pairs, function (pair) { + matches(pair[0], Match.Any); + _.each([String, Number, Boolean, undefined, null], function (type) { + if (type === pair[1]) { + matches(pair[0], type); + matches(pair[0], Match.Optional(type)); + matches(undefined, Match.Optional(type)); + matches(pair[0], Match.Where(function () { + check(pair[0], type); + return true; + })); + matches(pair[0], Match.Where(function () { + try { + check(pair[0], type); + return true; + } catch (e) { + return false; + } + })); + } else { + fails(pair[0], type); + matches(pair[0], Match.OneOf(type, pair[1])); + matches(pair[0], Match.OneOf(pair[1], type)); + fails(pair[0], Match.Where(function () { + check(pair[0], type); + return true; + })); + fails(pair[0], Match.Where(function () { + try { + check(pair[0], type); + return true; + } catch (e) { + return false; + } + })); + } + fails(pair[0], [type]); + fails(pair[0], Object); + }); + }); + fails(true, Match.OneOf(String, Number, undefined, null, [Boolean])); + + matches([1, 2, 3], [Number]); + matches([], [Number]); + fails([1, 2, 3, "4"], [Number]); + fails([1, 2, 3, [4]], [Number]); + matches([1, 2, 3, "4"], [Match.OneOf(Number, String)]); + + matches({}, Object); + matches({}, {}); + matches({foo: 42}, Object); + fails({foo: 42}, {}); + matches({a: 1, b:2}, {b: Number, a: Number}); + fails({a: 1, b:2}, {b: Number}); + matches({a: 1, b:2}, Match.ObjectIncluding({b: Number})); + fails({a: 1, b:2}, Match.ObjectIncluding({b: String})); + fails({a: 1, b:2}, Match.ObjectIncluding({c: String})); + fails({}, {a: Number}); + matches({}, {a: Match.Optional(Number)}); + matches({a: 1}, {a: Match.Optional(Number)}); + fails({a: true}, {a: Match.Optional(Number)}); + // Match.Optional means "or undefined" at the top level but "or absent" in + // objects. + fails({a: undefined}, {a: Match.Optional(Number)}); + + matches(/foo/, RegExp); + fails(/foo/, String); + matches(new Date, Date); + fails(new Date, Number); + matches(EJSON.newBinary(42), Match.Where(EJSON.isBinary)); + fails([], Match.Where(EJSON.isBinary)); + + matches(42, Match.Where(function (x) { return x % 2 === 0; })); + fails(43, Match.Where(function (x) { return x % 2 === 0; })); + + matches({ + a: "something", + b: [ + {x: 42, k: null}, + {x: 43, k: true, p: ["yay"]} + ] + }, {a: String, b: [Match.ObjectIncluding({ + x: Number, + k: Match.OneOf(null, Boolean)})]}); + + // Test that "arguments" is treated like an array. + var argumentsMatches = function () { + matches(arguments, [Number]); + }; + argumentsMatches(); + argumentsMatches(1); + argumentsMatches(1, 2); + var argumentsFails = function () { + fails(arguments, [Number]); + }; + argumentsFails("123"); + argumentsFails(1, "23"); +}); + +Tinytest.add("check - argument checker", function (test) { + var checksAllArguments = function (f /*arguments*/) { + Match._failIfArgumentsAreNotAllChecked( + f, {}, _.toArray(arguments).slice(1), "test"); + }; + checksAllArguments(function () {}); + checksAllArguments(function (x) {check(x, Match.Any);}, undefined); + checksAllArguments(function (x) {check(x, Match.Any);}, null); + checksAllArguments(function (x) {check(x, Match.Any);}, false); + checksAllArguments(function (x) {check(x, Match.Any);}, true); + checksAllArguments(function (x) {check(x, Match.Any);}, 0); + checksAllArguments(function (a, b, c) { + check(a, String); + check(b, Boolean); + check(c, Match.Optional(Number)); + }, "foo", true); + checksAllArguments(function () { + check(arguments, [Number]); + }, 1, 2, 4); + checksAllArguments(function(x) { + check(x, Number); + check(_.toArray(arguments).slice(1), [String]); + }, 1, "foo", "bar", "baz"); + + var doesntCheckAllArguments = function (f /*arguments*/) { + try { + Match._failIfArgumentsAreNotAllChecked( + f, {}, _.toArray(arguments).slice(1), "test"); + test.fail({message: "expected _failIfArgumentsAreNotAllChecked to throw"}); + } catch (e) { + test.equal(e.message, "Did not check() all arguments during test"); + } + }; + + doesntCheckAllArguments(function () {}, undefined); + doesntCheckAllArguments(function () {}, null); + doesntCheckAllArguments(function () {}, 1); + doesntCheckAllArguments(function () { + check(_.toArray(arguments).slice(1), [String]); + }, 1, "asdf", "foo"); + doesntCheckAllArguments(function (x, y) { + check(x, Boolean); + }, true, false); + // One "true" check doesn't count for all. + doesntCheckAllArguments(function (x, y) { + check(x, Boolean); + }, true, true); + // For non-primitives, we really do require that each arg gets checked. + doesntCheckAllArguments(function (x, y) { + check(x, [Boolean]); + check(x, [Boolean]); + }, [true], [true]); + + + // In an ideal world this test would fail, but we currently can't + // differentiate between "two calls to check x, both of which are true" and + // "check x and check y, both of which are true" (for any interned primitive + // type). + checksAllArguments(function (x, y) { + check(x, Boolean); + check(x, Boolean); + }, true, true); +}); diff --git a/packages/check/package.js b/packages/check/package.js new file mode 100644 index 00000000000..2a58b7c4006 --- /dev/null +++ b/packages/check/package.js @@ -0,0 +1,16 @@ +Package.describe({ + summary: "Check whether a value matches a pattern", + internal: true +}); + +Package.on_use(function (api) { + api.use(['underscore', 'ejson'], ['client', 'server']); + + api.add_files('match.js', ['client', 'server']); +}); + +Package.on_test(function (api) { + api.use(['check', 'tinytest', 'underscore', 'ejson'], ['client', 'server']); + + api.add_files('match_test.js', ['client', 'server']); +}); diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 03162d5a02d..76a421f86c7 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -622,6 +622,8 @@ _.extend(Meteor._LivedataConnection.prototype, { self._saveOriginals(); try { + // Note that unlike in the corresponding server code, we never audit + // that stubs check() their arguments. var ret = Meteor._CurrentInvocation.withValue(invocation,function () { if (Meteor.isServer) { // Because saveOriginals and retrieveOriginals aren't reentrant, diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index f616950da92..ea5dde0cdab 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -521,8 +521,8 @@ _.extend(Meteor._LivedataSession.prototype, { return; var handler = self.server.publish_handlers[msg.name]; - self._startSubscription(handler, - msg.id, msg.params); + self._startSubscription(handler, msg.id, msg.params, msg.name); + }, unsub: function (msg) { @@ -591,25 +591,26 @@ _.extend(Meteor._LivedataSession.prototype, { sessionData: self.sessionData }); try { - var ret = - Meteor._CurrentWriteFence.withValue(fence, function () { - return Meteor._CurrentInvocation.withValue(invocation, function () { - return handler.apply(invocation, msg.params || []); - }); + var result = Meteor._CurrentWriteFence.withValue(fence, function () { + return Meteor._CurrentInvocation.withValue(invocation, function () { + return maybeAuditArgumentChecks( + handler, invocation, msg.params, "call to '" + msg.method + "'"); }); + }); } catch (e) { var exception = e; } fence.arm(); // we're done adding writes to the fence unblock(); // unblock, if the method hasn't done it already + exception = wrapInternalException( exception, "while invoking method '" + msg.method + "'"); // send response and add to cache var payload = - exception ? {error: exception} : (ret !== undefined ? - {result: ret} : {}); + exception ? {error: exception} : (result !== undefined ? + {result: result} : {}); self.result_cache[msg.id] = _.extend({when: +(new Date)}, payload); self.send(_.extend({msg: 'result', id: msg.id}, payload)); } @@ -645,6 +646,10 @@ _.extend(Meteor._LivedataSession.prototype, { _setUserId: function(userId) { var self = this; + if (userId !== null && typeof userId !== "string") + throw new Error("setUserId must be called on string or null, not " + + typeof userId); + // Prevent newly-created universal subscriptions from being added to our // session; they will be found below when we call startUniversalSubs. // @@ -706,10 +711,11 @@ _.extend(Meteor._LivedataSession.prototype, { // should make it automatic. }, - _startSubscription: function (handler, subId, params) { + _startSubscription: function (handler, subId, params, name) { var self = this; - var sub = new Meteor._LivedataSubscription(self, handler, subId, params); + var sub = new Meteor._LivedataSubscription( + self, handler, subId, params, name); if (subId) self._namedSubs[subId] = sub; else @@ -760,7 +766,7 @@ _.extend(Meteor._LivedataSession.prototype, { // ctor for a sub handle: the input to each publish function Meteor._LivedataSubscription = function ( - session, handler, subscriptionId, params) { + session, handler, subscriptionId, params, name) { var self = this; // LivedataSession self._session = session; @@ -769,6 +775,8 @@ Meteor._LivedataSubscription = function ( // my subscription ID (generated by client, undefined for universal subs). self._subscriptionId = subscriptionId; + // undefined for universal subs + self._name = name; self._params = params || []; @@ -816,12 +824,18 @@ _.extend(Meteor._LivedataSubscription.prototype, { _runHandler: function () { var self = this; try { - var res = self._handler.apply(self, EJSON.clone(self._params)); + var res = maybeAuditArgumentChecks( + self._handler, self, EJSON.clone(self._params), + "publisher '" + self._name + "'"); } catch (e) { self.error(e); return; } + // Did the handler call this.error or this.stop? + if (self._deactivated) + return; + // SPECIAL CASE: Instead of writing their own callbacks that invoke // this.added/changed/ready/etc, the user can just return a collection // cursor or array of cursors from the publish function; we call their @@ -924,6 +938,8 @@ _.extend(Meteor._LivedataSubscription.prototype, { error: function (error) { var self = this; + if (self._deactivated) + return; self._session._stopSubscription(self._subscriptionId, error); }, @@ -933,6 +949,8 @@ _.extend(Meteor._LivedataSubscription.prototype, { // triggers if there is an error). stop: function () { var self = this; + if (self._deactivated) + return; self._session._stopSubscription(self._subscriptionId); }, @@ -1236,9 +1254,10 @@ _.extend(Meteor._LivedataServer.prototype, { // Run the handler var handler = self.method_handlers[name]; - if (!handler) - var exception = new Meteor.Error(404, "Method not found"); - else { + var exception; + if (!handler) { + exception = new Meteor.Error(404, "Method not found"); + } else { // If this is a method call from within another method, get the // user state from the outer method, otherwise don't allow // setUserId to be called @@ -1260,11 +1279,12 @@ _.extend(Meteor._LivedataServer.prototype, { sessionData: self.sessionData }); try { - var ret = Meteor._CurrentInvocation.withValue(invocation, function () { - return handler.apply(invocation, args); + var result = Meteor._CurrentInvocation.withValue(invocation, function () { + return maybeAuditArgumentChecks( + handler, invocation, args, "internal call to '" + name + "'"); }); } catch (e) { - var exception = e; + exception = e; } } @@ -1274,12 +1294,12 @@ _.extend(Meteor._LivedataServer.prototype, { // cursor observe callbacks have fired when your callback is invoked. (We // can change this if there's a real use case.) if (callback) { - callback(exception, ret); - return; + callback(exception, result); + return undefined; } if (exception) throw exception; - return ret; + return result; }, // A much more elegant way to do this would be: let any autopublish @@ -1319,9 +1339,31 @@ var wrapInternalException = function (exception, context) { if (!exception || exception instanceof Meteor.Error) return exception; + // Did the error contain more details that could have been useful if caught in + // server code (or if thrown from non-client-originated code), but also + // provided a "sanitized" version with more context than 500 Internal server + // error? Use that. + if (exception.sanitizedError) { + if (exception.sanitizedError instanceof Meteor.Error) + return exception.sanitizedError; + Meteor._debug("Exception " + context + " provides a sanitizedError that " + + "is not a Meteor.Error; ignoring"); + } + // tests can set the 'expected' flag on an exception so it won't go to the // server log if (!exception.expected) - Meteor._debug("Exception " + context, exception.stack); + Meteor._debug("Exception " + context, exception.toString(), + exception.stack); + return new Meteor.Error(500, "Internal server error"); }; + +var maybeAuditArgumentChecks = function (f, context, args, description) { + args = args || []; + if (Meteor._LivedataServer._auditArgumentChecks) { + return Match._failIfArgumentsAreNotAllChecked( + f, context, args, description); + } + return f.apply(context, args); +}; diff --git a/packages/livedata/livedata_test_service.js b/packages/livedata/livedata_test_service.js index 3137ab2eddc..6c472f391bf 100644 --- a/packages/livedata/livedata_test_service.js +++ b/packages/livedata/livedata_test_service.js @@ -1,13 +1,18 @@ Meteor.methods({ nothing: function () { + // No need to check if there are no arguments. }, echo: function (/* arguments */) { + check(arguments, [Match.Any]); return _.toArray(arguments); }, echoOne: function (/*arguments*/) { + check(arguments, [Match.Any]); return arguments[0]; }, exception: function (where, intended) { + check(where, String); + check(intended, Match.Optional(Boolean)); var shouldThrow = (Meteor.isServer && where === "server") || (Meteor.isClient && where === "client") || @@ -24,6 +29,7 @@ Meteor.methods({ } }, setUserId: function(userId) { + check(userId, String); this.setUserId(userId); } }); @@ -52,6 +58,7 @@ if (Meteor.isServer) { Meteor.methods({ delayedTrue: function(token) { + check(token, String); var record = waiters[token] = { future: new Future(), timer: Meteor.setTimeout(function() { @@ -63,6 +70,7 @@ if (Meteor.isServer) { return record.future.wait(); }, makeDelayedTrueImmediatelyReturnFalse: function(token) { + check(token, String); var record = waiters[token]; if (!record) return; // since delayedTrue's timeout had already run @@ -89,11 +97,17 @@ Meteor.startup(function () { if (Meteor.isServer) Meteor.publish('ledger', function (world) { + check(world, String); return Ledger.find({world: world}); }); Meteor.methods({ 'ledger/transfer': function (world, from_name, to_name, amount, cheat) { + check(world, String); + check(from_name, String); + check(to_name, String); + check(amount, Number); + check(cheat, Match.Optional(Boolean)); var from = Ledger.findOne({name: from_name, world: world}); var to = Ledger.findOne({name: to_name, world: world}); @@ -125,11 +139,11 @@ objectsWithUsers = new Meteor.Collection("objectsWithUsers"); if (Meteor.isServer) { objectsWithUsers.remove({}); objectsWithUsers.insert({name: "owned by none", ownerUserIds: [null]}); - objectsWithUsers.insert({name: "owned by one - a", ownerUserIds: [1]}); - objectsWithUsers.insert({name: "owned by one/two - a", ownerUserIds: [1, 2]}); - objectsWithUsers.insert({name: "owned by one/two - b", ownerUserIds: [1, 2]}); - objectsWithUsers.insert({name: "owned by two - a", ownerUserIds: [2]}); - objectsWithUsers.insert({name: "owned by two - b", ownerUserIds: [2]}); + objectsWithUsers.insert({name: "owned by one - a", ownerUserIds: ["1"]}); + objectsWithUsers.insert({name: "owned by one/two - a", ownerUserIds: ["1", "2"]}); + objectsWithUsers.insert({name: "owned by one/two - b", ownerUserIds: ["1", "2"]}); + objectsWithUsers.insert({name: "owned by two - a", ownerUserIds: ["2"]}); + objectsWithUsers.insert({name: "owned by two - b", ownerUserIds: ["2"]}); Meteor.publish("objectsWithUsers", function() { return objectsWithUsers.find({ownerUserIds: this.userId}, @@ -139,6 +153,7 @@ if (Meteor.isServer) { (function () { var userIdWhenStopped = {}; Meteor.publish("recordUserIdOnStop", function (key) { + check(key, String); var self = this; self.onStop(function() { userIdWhenStopped[key] = self.userId; @@ -147,6 +162,7 @@ if (Meteor.isServer) { Meteor.methods({ userIdWhenStopped: function (key) { + check(key, String); return userIdWhenStopped[key]; } }); @@ -161,7 +177,7 @@ if (Meteor.isServer) { Meteor.startup(function() { errorThrownWhenCallingSetUserIdDirectlyOnServer = null; try { - Meteor.call("setUserId", 1000); + Meteor.call("setUserId", "1000"); } catch (e) { errorThrownWhenCallingSetUserIdDirectlyOnServer = e; } @@ -210,6 +226,7 @@ if (Meteor.isServer) { Meteor.methods({ testOverlappingSubs: function (token) { + check(token, String); _.each(universalSubscribers[0], function (sub) { sub.added(collName, token, {}); }); @@ -229,6 +246,7 @@ if (Meteor.isServer) { if (Meteor.isServer) { Meteor.methods({ runtimeUniversalSubCreation: function (token) { + check(token, String); Meteor.publish(null, function () { this.added("runtimeSubCreation", token, {}); }); @@ -240,6 +258,9 @@ if (Meteor.isServer) { if (Meteor.isServer) { Meteor.publish("publisherErrors", function (collName, options) { + check(collName, String); + // See below to see what options are accepted. + check(options, Object); var sub = this; // First add a random item, which should be cleaned up. We use ready/onReady @@ -293,6 +314,8 @@ if (Meteor.isServer) { Two.insert({name: "value5"}); Meteor.publish("multiPublish", function (options) { + // See below to see what options are accepted. + check(options, Object); if (options.normal) { return [ One.find(), diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index f9e234aa849..88bc2c94d8c 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -350,7 +350,7 @@ if (Meteor.isClient) { Meteor.subscribe("objectsWithUsers", expect(function() { expectMessages(1, 0, ["owned by none"]); - Meteor.apply("setUserId", [1], {wait: true}, afterFirstSetUserId); + Meteor.apply("setUserId", ["1"], {wait: true}, afterFirstSetUserId); })); var afterFirstSetUserId = expect(function() { @@ -358,7 +358,7 @@ if (Meteor.isClient) { "owned by one - a", "owned by one/two - a", "owned by one/two - b"]); - Meteor.apply("setUserId", [2], {wait: true}, afterSecondSetUserId); + Meteor.apply("setUserId", ["2"], {wait: true}, afterSecondSetUserId); }); var afterSecondSetUserId = expect(function() { @@ -367,7 +367,7 @@ if (Meteor.isClient) { "owned by one/two - b", "owned by two - a", "owned by two - b"]); - Meteor.apply("setUserId", [2], {wait: true}, afterThirdSetUserId); + Meteor.apply("setUserId", ["2"], {wait: true}, afterThirdSetUserId); }); var afterThirdSetUserId = expect(function() { @@ -383,11 +383,11 @@ if (Meteor.isClient) { }, function(test, expect) { var key = Random.id(); Meteor.subscribe("recordUserIdOnStop", key); - Meteor.apply("setUserId", [100], {wait: true}, expect(function () {})); - Meteor.apply("setUserId", [101], {wait: true}, expect(function () {})); + Meteor.apply("setUserId", ["100"], {wait: true}, expect(function () {})); + Meteor.apply("setUserId", ["101"], {wait: true}, expect(function () {})); Meteor.call("userIdWhenStopped", key, expect(function (err, result) { test.isFalse(err); - test.equal(result, 100); + test.equal(result, "100"); })); } ]); @@ -406,6 +406,7 @@ if (Meteor.isServer) { }; Meteor.methods({ "livedata/setup" : function (id) { + check(id, String); if (Meteor.isServer) { pubHandles[id] = {}; Meteor.publish("pub1"+id, function () { @@ -420,6 +421,7 @@ Meteor.methods({ } }, "livedata/pub1go" : function (id) { + check(id, String); if (Meteor.isServer) { pubHandles[id].pub1.added("MultiPubCollection" + id, "foo", {a: "aa"}); @@ -428,6 +430,7 @@ Meteor.methods({ return 0; }, "livedata/pub2go" : function (id) { + check(id, String); if (Meteor.isServer) { pubHandles[id].pub2.added("MultiPubCollection" + id , "foo", {b: "bb"}); return 2; @@ -602,6 +605,7 @@ if (Meteor.isClient) { if (Meteor.isServer) { Meteor.methods({ "s2s": function (arg) { + check(arg, String); return "s2s " + arg; } }); diff --git a/packages/livedata/package.js b/packages/livedata/package.js index 542a2a36dc6..4bc45aece7d 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -7,7 +7,7 @@ Npm.depends({sockjs: "0.3.4", websocket: "1.0.7"}); Package.on_use(function (api) { - api.use(['random', 'ejson', 'json', 'underscore', 'deps', 'logging'], + api.use(['random', 'ejson', 'json', 'underscore', 'deps', 'logging', 'check'], ['client', 'server']); // Transport diff --git a/packages/madewith/madewith.js b/packages/madewith/madewith.js index fd5c80d89c4..37055061c2b 100644 --- a/packages/madewith/madewith.js +++ b/packages/madewith/madewith.js @@ -12,6 +12,7 @@ var apps = new Meteor.Collection('madewith_apps', {manager: server}); server.methods({ vote: function (hostname) { + // This is a stub, so it doesn't need to call check. apps.update({name: hostname}, {$inc: {vote_count: 1}}); } }); diff --git a/packages/mongo-livedata/allow_tests.js b/packages/mongo-livedata/allow_tests.js index b0911e8488c..4529b453522 100644 --- a/packages/mongo-livedata/allow_tests.js +++ b/packages/mongo-livedata/allow_tests.js @@ -10,6 +10,8 @@ if (Meteor.isServer) { // it and fail due to the collection not yet existing. So we are very hacky // and use a publish. Meteor.publish("allowTests", function (nonce, idGeneration) { + check(nonce, String); + check(idGeneration, String); var cursors = []; var needToConfigure = undefined; diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 976a49f5331..4bd0721e0b0 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -500,6 +500,8 @@ Meteor.Collection.prototype._defineMutationMethods = function() { _.each(['insert', 'update', 'remove'], function (method) { m[self._prefix + method] = function (/* ... */) { + // All the methods do their own validation, instead of using check(). + check(arguments, [Match.Any]); try { if (this.isSimulation) { diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index 6e79b899d15..3ccc8d72ab9 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -5,6 +5,12 @@ var TRANSFORMS = {}; if (Meteor.isServer) { Meteor.methods({ createInsecureCollection: function (name, options) { + check(name, String); + check(options, Match.Optional({ + transformName: Match.Optional(String), + idGeneration: Match.Optional(String) + })); + if (options && options.transformName) { options.transform = TRANSFORMS[options.transformName]; } diff --git a/packages/srp/package.js b/packages/srp/package.js index 1e2982ffb96..2c18d5a3ea4 100644 --- a/packages/srp/package.js +++ b/packages/srp/package.js @@ -4,7 +4,7 @@ Package.describe({ }); Package.on_use(function (api) { - api.use('random', ['client', 'server']); + api.use(['random', 'check'], ['client', 'server']); api.add_files(['biginteger.js', 'sha256.js', 'srp.js'], ['client', 'server']); }); diff --git a/packages/srp/srp.js b/packages/srp/srp.js index 8513d1a8f9b..5fbd0cd1ae0 100644 --- a/packages/srp/srp.js +++ b/packages/srp/srp.js @@ -32,6 +32,12 @@ Meteor._srp.generateVerifier = function (password, options) { }; }; +// For use with check(). +Meteor._srp.matchVerifier = { + identity: String, + salt: String, + verifier: String +}; /** diff --git a/packages/test-in-console/reporter.js b/packages/test-in-console/reporter.js index 93268c33c7a..668abc4a965 100644 --- a/packages/test-in-console/reporter.js +++ b/packages/test-in-console/reporter.js @@ -10,6 +10,8 @@ if (Meteor.settings && Meteor.methods({ report: function (reports) { + // XXX Could do a more precise validation here; reports are complex! + check(reports, [Object]); if (url) { Meteor.http.post(url, { data: reports diff --git a/packages/tinytest/tinytest_server.js b/packages/tinytest/tinytest_server.js index be8419aa177..035a2996107 100644 --- a/packages/tinytest/tinytest_server.js +++ b/packages/tinytest/tinytest_server.js @@ -3,6 +3,7 @@ var handlesForRun = {}; var reportsForRun = {}; Meteor.publish(Meteor._ServerTestResultsSubscription, function (runId) { + check(runId, String); var self = this; if (!_.has(handlesForRun, runId)) handlesForRun[runId] = [self]; @@ -22,6 +23,8 @@ Meteor.publish(Meteor._ServerTestResultsSubscription, function (runId) { Meteor.methods({ 'tinytest/run': function (runId, pathPrefix) { + check(runId, String); + check(pathPrefix, Match.Optional([String])); this.unblock(); // XXX using private API === lame @@ -56,6 +59,7 @@ Meteor.methods({ future.wait(); }, 'tinytest/clearResults': function (runId) { + check(runId, String); _.each(handlesForRun[runId], function (handle) { // XXX this doesn't actually notify the client that it has been // unsubscribed. From 6776c354b80e25ff5445dc627d826ecf42bdfd87 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 17 Apr 2013 11:54:54 -0700 Subject: [PATCH 015/102] start of docs for check --- docs/client/api.html | 43 +++++++++++++++++++++++++++++++++++++++++++ docs/client/api.js | 43 +++++++++++++++++++++++++++++++++++++++++++ docs/client/docs.js | 6 ++++++ 3 files changed, 92 insertions(+) diff --git a/docs/client/api.html b/docs/client/api.html index 921c182596d..0fa422de619 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2273,6 +2273,49 @@

Template instances

{{/api_box_inline}} +

Match

+ +Meteor methods and publish functions take arbitrary [EJSON](#ejson) types as +arguments, but most arguments are expected to be of a particular type. Meteor's +`check` package is a lightweight library for checking that arguments and other +values are of the expected type. + +XXX EXAMPLE HERE + +{{> api_box check}} + +If the match fails, `check` throws a `Match.Error` describing how it failed. If +this error gets sent over the wire to the client, it will appear only as +`Meteor.Error(400, "Match Failed")`; the failure details will be written to the +server logs but not revealed to the client. + +{{> api_box match_test}} + +{{#api_box_inline matchpatterns}} + +The following patterns can be used as pattern arguments to `check` and `Match.test`: + + +
+{{#dtdd "String, Number, Boolean, undefined, null"}} +Matches a primitive of the given type. +{{/dtdd}} + +{{#dtdd "Match.Any"}} +Matches any value. +{{/dtdd}} + +{{#dtdd "A constructor function"}} +Matches any element that is an instance of that type. For example, `Date`. +{{/dtdd}} + +{{#dtdd "[pattern]"}} +A one-element array matches an array of elements, each of which match the +{{/dtdd}} + +
+ +{{/api_box_inline}}

Timers

diff --git a/docs/client/api.js b/docs/client/api.js index 2a0b0b19445..cc0b3266f2d 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1357,6 +1357,49 @@ Template.api.accounts_emailTemplates = { +Template.api.check = { + id: "check", + name: "check(value, pattern)", + locus: "Anywhere", + descr: ["Checks that a value matches a [pattern](#matchpatterns). If the value does not match the pattern, throws a `Match.Error`."], + args: [ + { + name: "value", + type: "Any", + descr: "The value to check" + }, + { + name: "pattern", + type: "Match pattern", + descr: "The [pattern](#matchpatterns) to match `value` against" + } + ] +}; + +Template.api.match_test = { + id: "match_test", + name: "Match.test(value, pattern)", + locus: "Anywhere", + descr: ["Returns true if the value matches the [pattern](#matchpatterns)."], + args: [ + { + name: "value", + type: "Any", + descr: "The value to check" + }, + { + name: "pattern", + type: "Match pattern", + descr: "The [pattern](#matchpatterns) to match `value` against" + } + ] +}; + +Template.api.matchpatterns = { + id: "matchpatterns", + name: "Match patterns" +}; + Template.api.setTimeout = { id: "meteor_settimeout", name: "Meteor.setTimeout(func, delay)", diff --git a/docs/client/docs.js b/docs/client/docs.js index 352262e2126..2c13526aa32 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -242,6 +242,12 @@ var toc = [ {name: "Reactivity isolation", style: "noncode", id: "isolate"} ], + "Match", [ + "check", + "Match.test", + {name: "Match patterns", style: "noncode"} + ], + "Timers", [ "Meteor.setTimeout", "Meteor.setInterval", From f72a7223c516a9fb24d29918d66fe3ac6c785c2f Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 17 Apr 2013 17:45:38 -0700 Subject: [PATCH 016/102] More check docs --- docs/client/api.html | 75 +++++++++++++++++-- docs/client/packages.html | 1 + .../packages/audit-argument-checks.html | 18 +++++ packages/check/match.js | 2 + 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 docs/client/packages/audit-argument-checks.html diff --git a/docs/client/api.html b/docs/client/api.html index 0fa422de619..e88f73e322c 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2278,9 +2278,27 @@

Match

Meteor methods and publish functions take arbitrary [EJSON](#ejson) types as arguments, but most arguments are expected to be of a particular type. Meteor's `check` package is a lightweight library for checking that arguments and other -values are of the expected type. +values are of the expected type. For example: -XXX EXAMPLE HERE + Meteor.publish("chats-in-room", function (roomId) { + // We wouldn't want a user to be able to pass an arbitrary + // selector object like {$ne: null} here. + check(roomId, String); + return Chats.find({room: roomId}); + }); + Meteor.methods({m: function (x, y) { + // x must be an object, and can have keys other than + // data and timestamps. + check(x, Match.ObjectIncluding({ + // Required binary buffer. + data: Match.Where(EJSON.isBinary), + // Optional, but if present must be an array of dates. + timestamps: Match.Optional([Date]) + })); + // If y is provided, it must be an array of objects with + // exactly these two keys. + check(y, Match.Optional([{a: String, b: Boolean}])); + }}); {{> api_box check}} @@ -2297,22 +2315,63 @@

Match

+{{#dtdd "Match.Any"}} +Matches any value. +{{/dtdd}} + {{#dtdd "String, Number, Boolean, undefined, null"}} Matches a primitive of the given type. {{/dtdd}} -{{#dtdd "Match.Any"}} -Matches any value. +{{#dtdd "[pattern]"}} +A one-element array matches an array of elements, each of which match +*pattern*. For example, `[Number]` matches a (possibly empty) array of numbers; +`[Match.Any]` matches any array. +{{/dtdd}} + +{{#dtdd "{key1: pattern1, key2: pattern2, ...}"}} +Matches an Object with the given keys, with values matching the given patterns. +If any *pattern* is a `Match.Optional`, that key does not need to exist +in the object. The value may not contain any keys not listed in the pattern. +The value must be a plain Object with no special prototype. {{/dtdd}} -{{#dtdd "A constructor function"}} -Matches any element that is an instance of that type. For example, `Date`. +{{#dtdd "Match.ObjectIncluding({key1: pattern1, key2: pattern2, ...})"}} +Matches an Object with the given keys; the value may also have other keys +with arbitrary values. {{/dtdd}} -{{#dtdd "[pattern]"}} -A one-element array matches an array of elements, each of which match the +{{#dtdd "Object"}} +Matches any plain Object with any keys; equivalent to +`Match.ObjectIncluding({})`. {{/dtdd}} +{{#dtdd "Match.Optional(pattern)"}} +Matches either `undefined` or something that matches *pattern*. +{{/dtdd}} + +{{#dtdd "Match.OneOf(pattern1, pattern2, ...)"}} +Matches any value that matches at least one of the provided patterns. +{{/dtdd}} + +{{#dtdd "Any constructor function (eg, Date)"}} +Matches any element that is an instance of that type. +{{/dtdd}} + +{{#dtdd "Match.Where(condition)"}} +Calls the function *condition* with the value as the argument. If *condition* +returns true, this matches. If *condition* throws a `Match.Error` or returns +false, this fails. If *condition* throws any other error, that error is thrown +from the call to `check` or `Match.test`. Examples: + + check(buffer, Match.Where(EJSON.isBinary)); + + NonEmptyString = Match.Where(function (x) { + validate(x, String); + return x.length > 0; + } + check(arg, NonEmptyString); +{{/dtdd}}
{{/api_box_inline}} diff --git a/docs/client/packages.html b/docs/client/packages.html index 0d2d9e892ec..eb16c738243 100644 --- a/docs/client/packages.html +++ b/docs/client/packages.html @@ -19,6 +19,7 @@

Packages

{{> pkg_accounts_ui}} {{> pkg_appcache}} {{> pkg_amplify}} +{{> pkg_audit_argument_checks}} {{> pkg_backbone}} {{> pkg_bootstrap}} {{> pkg_coffeescript}} diff --git a/docs/client/packages/audit-argument-checks.html b/docs/client/packages/audit-argument-checks.html new file mode 100644 index 00000000000..6f598a2f1e2 --- /dev/null +++ b/docs/client/packages/audit-argument-checks.html @@ -0,0 +1,18 @@ + diff --git a/packages/check/match.js b/packages/check/match.js index c2af5afef55..c690e81f8df 100644 --- a/packages/check/match.js +++ b/packages/check/match.js @@ -75,6 +75,8 @@ var Optional = function (pattern) { }; var OneOf = function (choices) { + if (_.isEmpty(choices)) + throw new Error("Must provide at least one choice to Match.OneOf"); this.choices = choices; }; From e9e558cd984cdc1025947d68b9d1ebd05c3e28bf Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 17 Apr 2013 17:46:53 -0700 Subject: [PATCH 017/102] toc, also reorder appcache/amplify --- docs/client/docs.js | 3 ++- docs/client/packages.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/client/docs.js b/docs/client/docs.js index 2c13526aa32..7ef70bca809 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -319,8 +319,9 @@ var toc = [ "Packages", [ [ "accounts-ui", - "appcache", "amplify", + "appcache", + "audit-argument-checks", "backbone", "bootstrap", "coffeescript", diff --git a/docs/client/packages.html b/docs/client/packages.html index eb16c738243..0ee701bbe8f 100644 --- a/docs/client/packages.html +++ b/docs/client/packages.html @@ -17,8 +17,8 @@

Packages

$ meteor remove {{> pkg_accounts_ui}} -{{> pkg_appcache}} {{> pkg_amplify}} +{{> pkg_appcache}} {{> pkg_audit_argument_checks}} {{> pkg_backbone}} {{> pkg_bootstrap}} From 296d4f5ccfff9c7c7f9edcabd6b86b3dcbd07058 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 17 Apr 2013 17:59:45 -0700 Subject: [PATCH 018/102] docs ready for review --- docs/client/concepts.html | 24 +++++++++++++++++++ .../packages/audit-argument-checks.html | 6 ++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 4376a85854c..0747ae41110 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -262,6 +262,30 @@

Authentication and user accounts

releases will include support for other databases. {{/note}} + +

Input validation

+ +Meteor allows your methods and publish functions to take arguments of any +[JSON](http://json.org/) type. (In fact, Meteor's wire protocol supports +[EJSON](#ejson), an extension of JSON which also supports other common types +like dates and binary buffers.) JavaScript's dynamic typing means you don't need +to declare precise types of every variable in your app, but it's usually helpful +to ensure that the arguments that clients are passing to your methods and +publish functions are of the type that you expect. + +Meteor provides a [lightweight library](#match) for checking that arguments and +other values are the type you expect them to be. Simply start your functions +with statements like `check(username, String)` or +`check(office, {building: String, room: Number})`. The `check` call will +throw an error if its argument is of an unexpected type. + +If you like using `check` to validate your input, Meteor also provides an easy +way to make sure that all of your methods and publish functions validate all of +their arguments. Just run +meteor add [audit-argument-checks](#auditargumentchecks) and any +method or publish function which skips `check`ing any of its arguments will fail +with an exception. + {{/better_markdown}} diff --git a/docs/client/packages/audit-argument-checks.html b/docs/client/packages/audit-argument-checks.html index 6f598a2f1e2..d2b311d9272 100644 --- a/docs/client/packages/audit-argument-checks.html +++ b/docs/client/packages/audit-argument-checks.html @@ -6,9 +6,9 @@ This package causes Meteor to require that all arguments passed to methods and publish functions are [`check`ed](#check). Any method that does not pass each one of its arguments to `check` will throw an error, which will be logged on the -server and which will appear to the client as a `500 Internal server -error`. This is a simple way to help ensure that your app has complete check -coverage. +server and which will appear to the client as a +`500 Internal server error`. This is a simple way to help ensure that your +app has complete check coverage. Methods and publish functions that do not need to validate their arguments can simply run `check(arguments, [Match.Any])` to satisfy the From c12f3851d111f60ea018aa50fb881e20df13a5ca Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Thu, 18 Apr 2013 19:30:44 -0700 Subject: [PATCH 019/102] Add audit-argument-checks to parties. This way when we do QA, we will exercise the package. --- examples/parties/.meteor/packages | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/parties/.meteor/packages b/examples/parties/.meteor/packages index fae85f5ca59..b2c4da929f7 100644 --- a/examples/parties/.meteor/packages +++ b/examples/parties/.meteor/packages @@ -11,3 +11,4 @@ bootstrap email accounts-facebook accounts-twitter +audit-argument-checks From 45dea05b21a1960fb5e2be8d20530dabdaa1aded Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Thu, 18 Apr 2013 20:38:16 -0700 Subject: [PATCH 020/102] Minor wordsmithing (Meteor provides the package whether or not you like to use it =) --- docs/client/concepts.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 0747ae41110..cd26c5ef26d 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -279,9 +279,8 @@

Input validation

`check(office, {building: String, room: Number})`. The `check` call will throw an error if its argument is of an unexpected type. -If you like using `check` to validate your input, Meteor also provides an easy -way to make sure that all of your methods and publish functions validate all of -their arguments. Just run +Meteor also provides an easy way to make sure that all of your methods +and publish functions validate all of their arguments. Just run meteor add [audit-argument-checks](#auditargumentchecks) and any method or publish function which skips `check`ing any of its arguments will fail with an exception. From 9f4f8b29582a05238429e02f68d4285bb4fe46e3 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Thu, 18 Apr 2013 21:22:14 -0700 Subject: [PATCH 021/102] simplify example. also a missed validate -> check. --- docs/client/api.html | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index e88f73e322c..e866683b0da 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2281,23 +2281,21 @@

Match

values are of the expected type. For example: Meteor.publish("chats-in-room", function (roomId) { - // We wouldn't want a user to be able to pass an arbitrary - // selector object like {$ne: null} here. + // Make sure roomId is a string, not an arbitrary mongo selector object. check(roomId, String); return Chats.find({room: roomId}); }); - Meteor.methods({m: function (x, y) { - // x must be an object, and can have keys other than - // data and timestamps. - check(x, Match.ObjectIncluding({ - // Required binary buffer. - data: Match.Where(EJSON.isBinary), - // Optional, but if present must be an array of dates. - timestamps: Match.Optional([Date]) - })); - // If y is provided, it must be an array of objects with - // exactly these two keys. - check(y, Match.Optional([{a: String, b: Boolean}])); + + Meteor.methods({addChat: function (roomId, message) { + check(roomId, String); + check(message, { + text: String, + timestamp: Date, + // Optional, but if present must be an array of strings. + tags: Match.Optional([String]) + }); + + // ... do something with the message ... }}); {{> api_box check}} @@ -2367,7 +2365,7 @@

Match

check(buffer, Match.Where(EJSON.isBinary)); NonEmptyString = Match.Where(function (x) { - validate(x, String); + check(x, String); return x.length > 0; } check(arg, NonEmptyString); From b6282eadd9f49e058f42ff95cb8f118035dcb624 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 17 Apr 2013 18:56:47 -0700 Subject: [PATCH 022/102] Publish user's login service fields in case autopublish is on. Publish all but secret fields to the current user, and basic identification information for other users. --- packages/accounts-base/accounts_server.js | 44 +++++++++++++++---- packages/accounts-facebook/facebook_server.js | 5 +++ packages/accounts-github/github_server.js | 5 +++ packages/accounts-google/google_server.js | 5 +++ packages/accounts-meetup/meetup_server.js | 5 +++ packages/accounts-twitter/twitter_server.js | 12 +++++ packages/accounts-weibo/weibo_server.js | 5 +++ 7 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 6795423ad8f..85b4589a7aa 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -280,22 +280,50 @@ Accounts.updateOrCreateUserFromExternalService = function( // Publish the current user's record to the client. Meteor.publish(null, function() { - if (this.userId) + if (this.userId) { return Meteor.users.find( {_id: this.userId}, {fields: {profile: 1, username: 1, emails: 1}}); - else { + } else { return null; } -}, {is_auto: true}); +}, /*suppress autopublish warning*/{is_auto: true}); + +// If autopublish is on, publish these user fields. These are added +// to by the various accounts packages (eg accounts-google). We can't +// implement this by running multiple publish functions since DDP only +// merges only across top-level fields, not subfields (such as +// 'services.facebook.accessToken') +Accounts._autopublishFields = { + loggedInUser: ['profile', 'username', 'emails'], + allUsers: ['profile', 'username'] +}; -// If autopublish is on, also publish everyone else's user record. Meteor.default_server.onAutopublish(function () { - var handler = function () { - return Meteor.users.find( - {}, {fields: {profile: 1, username: 1}}); + // ['profile', 'username'] -> {profile: 1, username: 1} + var toFieldSelector = function(fields) { + return _.object(_.map(fields, function(field) { + return [field, 1]; + })); }; - Meteor.default_server.publish(null, handler, {is_auto: true}); + + Meteor.default_server.publish(null, function () { + return Meteor.users.find( + {_id: this.userId}, + {fields: toFieldSelector(Accounts._autopublishFields.loggedInUser)}); + }, /*suppress autopublish warning*/{is_auto: true}); + + // XXX this publish is neither dedup-able nor is it optimized by our + // special treatment of queries on a specific _id. Therefore this + // will have O(n^2) run-time performance every time a user document + // is changed (eg someone logging in). If this is a problem, we can + // instead write a manual publish function which filters out fields + // based on 'this.userId'. + Meteor.default_server.publish(null, function () { + return Meteor.users.find( + {_id: {$ne: this.userId}}, + {fields: toFieldSelector(Accounts._autopublishFields.allUsers)}); + }, /*suppress autopublish warning*/{is_auto: true}); }); // Publish all login service configuration fields other than secret. diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 38ff1cae8a9..9fc0823043c 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -1,5 +1,10 @@ var querystring = Npm.require('querystring'); +// with autopublish on: publish all fields to the logged in user; +// only the user's facebook id to others +Accounts._autopublishFields.loggedInUser.push('services.facebook'); +Accounts._autopublishFields.allUsers.push('services.facebook.id'); + Accounts.oauth.registerService('facebook', 2, function(query) { var response = getTokenResponse(query); diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index 36f9ef4817e..8b91d45d190 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -1,3 +1,8 @@ +// with autopublish on: publish all fields to the logged in user; +// only the user's github username to others +Accounts._autopublishFields.loggedInUser.push('services.github'); +Accounts._autopublishFields.allUsers.push('services.github.username'); + Accounts.oauth.registerService('github', 2, function(query) { var accessToken = getAccessToken(query); diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index d7cc7a17641..a80511eeaee 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -1,3 +1,8 @@ +// with autopublish on: publish all fields to the logged in user; +// only the user's public picture to others +Accounts._autopublishFields.loggedInUser.push('services.google'); +Accounts._autopublishFields.allUsers.push('services.google.picture'); + Accounts.oauth.registerService('google', 2, function(query) { var response = getTokens(query); diff --git a/packages/accounts-meetup/meetup_server.js b/packages/accounts-meetup/meetup_server.js index bd1ff64d090..604d4f7a2a5 100644 --- a/packages/accounts-meetup/meetup_server.js +++ b/packages/accounts-meetup/meetup_server.js @@ -1,3 +1,8 @@ +// with autopublish on: publish all fields to the logged in user; +// only the user's meetup user id +Accounts._autopublishFields.loggedInUser.push('services.meetup'); +Accounts._autopublishFields.allUsers.push('services.meetup.id'); + Accounts.oauth.registerService('meetup', 2, function(query) { var accessToken = getAccessToken(query); diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index ff7aa929545..9d95b2077bf 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,3 +1,15 @@ +// with autopublish on: publish all fields other than access token and +// secret to the user; only the user's twitter screenName and profile +// images to others +Accounts._autopublishFields.loggedInUser.push('services.twitter.id'); +Accounts._autopublishFields.loggedInUser.push('services.twitter.screenName'); +Accounts._autopublishFields.loggedInUser.push('services.twitter.lang'); +Accounts._autopublishFields.loggedInUser.push('services.twitter.profile_image_url'); +Accounts._autopublishFields.loggedInUser.push('services.twitter.profile_image_url_https'); +Accounts._autopublishFields.allUsers.push('services.twitter.screenName'); +Accounts._autopublishFields.allUsers.push('services.twitter.profile_image_url'); +Accounts._autopublishFields.allUsers.push('services.twitter.profile_image_url_https'); + Accounts.oauth.registerService('twitter', 1, function(oauthBinding) { var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json').data; diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 9e75cf0333e..c7c8e30511d 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -1,3 +1,8 @@ +// with autopublish on: publish all fields to the logged in user; +// only the user's weibo screen name to others +Accounts._autopublishFields.loggedInUser.push('services.weibo'); +Accounts._autopublishFields.allUsers.push('services.weibo.screenName'); + Accounts.oauth.registerService('weibo', 2, function(query) { var response = getTokenResponse(query); From e144b4e246816a246ca805b041edeea42fe8ab89 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 17 Apr 2013 19:15:11 -0700 Subject: [PATCH 023/102] With autopublish, publish others' google user ids --- packages/accounts-google/google_server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index a80511eeaee..59ab7cbc3d4 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -1,6 +1,7 @@ // with autopublish on: publish all fields to the logged in user; -// only the user's public picture to others +// only the user's google id and public picture to others Accounts._autopublishFields.loggedInUser.push('services.google'); +Accounts._autopublishFields.allUsers.push('services.google.id'); Accounts._autopublishFields.allUsers.push('services.google.picture'); Accounts.oauth.registerService('google', 2, function(query) { From 54916ce535071a70551f2b79bebcef2859b36a26 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 17 Apr 2013 19:23:39 -0700 Subject: [PATCH 024/102] Docs for publishing more fields on user documents --- docs/client/api.html | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index e866683b0da..feb4b508310 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -1439,13 +1439,12 @@

Accounts

{fields: {'other': 1, 'things': 1}}); }); -If the `autopublish` package is installed, the `username` and `profile` fields -for all users are published to all clients. To publish specific fields from all -users: - - Meteor.publish("allUserData", function () { - return Meteor.users.find({}, {fields: {'nested.things': 1}}); - }); +If the `autopublish` package is installed, many subfields of +`services` are also published. If the `autopublish` package is +installed, many subfields of `services` are also published for the +current user. Some subfields of `services` also published for +other login services, such as their Facebook ID (under +`services.facebook.id`). Users are by default allowed to specify their own `profile` field with [`Accounts.createUser`](#accounts_createuser) and modify it with From a72134fd80ac802d04d05d8fd36fb9d3cd1f225b Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 18 Apr 2013 10:42:16 -0700 Subject: [PATCH 025/102] Better security, comments, docs and APIs for publishing more user document fields --- docs/client/api.html | 14 +++--- packages/accounts-base/accounts_server.js | 45 ++++++++++++++----- packages/accounts-facebook/facebook_server.js | 15 +++++-- packages/accounts-github/github_server.js | 11 +++-- packages/accounts-google/google_server.js | 25 ++++++++--- packages/accounts-meetup/meetup_server.js | 12 +++-- packages/accounts-twitter/twitter_server.js | 26 +++++------ packages/accounts-weibo/weibo_server.js | 10 +++-- 8 files changed, 105 insertions(+), 53 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index feb4b508310..4300236b42b 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -1439,12 +1439,14 @@

Accounts

{fields: {'other': 1, 'things': 1}}); }); -If the `autopublish` package is installed, many subfields of -`services` are also published. If the `autopublish` package is -installed, many subfields of `services` are also published for the -current user. Some subfields of `services` also published for -other login services, such as their Facebook ID (under -`services.facebook.id`). +If the autopublish package is installed, information about all users +on the system is published to all clients. This includes `username`, +`profile`, and any fields in `services` that are meant to be public +(eg `services.facebook.id`, +`services.twitter.screenName`). Additionally, when using autopublish +more information is published for the currently logged in user, +including access tokens. This allows making API calls directly from +the client for services that allow this. Users are by default allowed to specify their own `profile` field with [`Accounts.createUser`](#accounts_createuser) and modify it with diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 85b4589a7aa..c0d710ac8f0 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -289,14 +289,27 @@ Meteor.publish(null, function() { } }, /*suppress autopublish warning*/{is_auto: true}); -// If autopublish is on, publish these user fields. These are added -// to by the various accounts packages (eg accounts-google). We can't -// implement this by running multiple publish functions since DDP only -// merges only across top-level fields, not subfields (such as -// 'services.facebook.accessToken') +// If autopublish is on, publish these user fields. Login service +// packages (eg accounts-google) add to these by calling +// Accounts.addAutopublishFields Notably, this isn't implemented with +// multiple publishes since DDP only merges only across top-level +// fields, not subfields (such as 'services.facebook.accessToken') Accounts._autopublishFields = { loggedInUser: ['profile', 'username', 'emails'], - allUsers: ['profile', 'username'] + otherUsers: ['profile', 'username'] +}; + +// Add to the list of fields or subfields to be automatically +// published if autopublish is on +// +// @param opts {Object} with: +// - forLoggedInUser {Array} Array of fields published to the logged-in user +// - forOtherUsers {Array} Array of fields published to users that aren't logged in +Accounts.addAutopublishFields = function(opts) { + Accounts._autopublishFields.loggedInUser.push.apply( + Accounts._autopublishFields.loggedInUser, opts.forLoggedInUser); + Accounts._autopublishFields.otherUsers.push.apply( + Accounts._autopublishFields.otherUsers, opts.forOtherUsers); }; Meteor.default_server.onAutopublish(function () { @@ -308,9 +321,13 @@ Meteor.default_server.onAutopublish(function () { }; Meteor.default_server.publish(null, function () { - return Meteor.users.find( - {_id: this.userId}, - {fields: toFieldSelector(Accounts._autopublishFields.loggedInUser)}); + if (this.userId) { + return Meteor.users.find( + {_id: this.userId}, + {fields: toFieldSelector(Accounts._autopublishFields.loggedInUser)}); + } else { + return null; + } }, /*suppress autopublish warning*/{is_auto: true}); // XXX this publish is neither dedup-able nor is it optimized by our @@ -320,9 +337,15 @@ Meteor.default_server.onAutopublish(function () { // instead write a manual publish function which filters out fields // based on 'this.userId'. Meteor.default_server.publish(null, function () { + var selector; + if (this.userId) + selector = {_id: {$ne: this.userId}}; + else + selector = {}; + return Meteor.users.find( - {_id: {$ne: this.userId}}, - {fields: toFieldSelector(Accounts._autopublishFields.allUsers)}); + selector, + {fields: toFieldSelector(Accounts._autopublishFields.otherUsers)}); }, /*suppress autopublish warning*/{is_auto: true}); }); diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 9fc0823043c..a510887b878 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -1,9 +1,16 @@ var querystring = Npm.require('querystring'); -// with autopublish on: publish all fields to the logged in user; -// only the user's facebook id to others -Accounts._autopublishFields.loggedInUser.push('services.facebook'); -Accounts._autopublishFields.allUsers.push('services.facebook.id'); +Accounts.addAutopublishFields({ + // publish all fields including access token, which can legitimately + // be used from the client (if transmitted over ssl or on + // localhost). https://developers.facebook.com/docs/concepts/login/access-tokens-and-types/, + // "Sharing of Access Tokens" + forLoggedInUser: ['services.facebook'], + forOtherUsers: [ + // https://www.facebook.com/help/167709519956542 + 'services.facebook.id', 'services.facebook.username', 'services.facebook.gender' + ] +}); Accounts.oauth.registerService('facebook', 2, function(query) { diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index 8b91d45d190..1d712c2c896 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -1,7 +1,10 @@ -// with autopublish on: publish all fields to the logged in user; -// only the user's github username to others -Accounts._autopublishFields.loggedInUser.push('services.github'); -Accounts._autopublishFields.allUsers.push('services.github.username'); +Accounts.addAutopublishFields({ + // not sure whether the github api can be used from the browser, + // thus not sure if we should be sending access tokens; but we do it + // for all other oauth2 providers, and it may come in handy. + forLoggedInUser: ['services.github'], + forOtherUsers: ['services.github.username'] +}); Accounts.oauth.registerService('github', 2, function(query) { diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 59ab7cbc3d4..1c5f3c273c9 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -1,8 +1,22 @@ -// with autopublish on: publish all fields to the logged in user; -// only the user's google id and public picture to others -Accounts._autopublishFields.loggedInUser.push('services.google'); -Accounts._autopublishFields.allUsers.push('services.google.id'); -Accounts._autopublishFields.allUsers.push('services.google.picture'); +// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall +var whitelisted = ['id', 'email', 'verified_email', 'name', 'given_name', + 'family_name', 'picture', 'locale', 'timezone', 'gender']; + +Accounts.addAutopublishFields({ + forLoggedInUser: _.map( + // publish access token since it can be used from the client (if + // transmitted over ssl or on + // localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent + // refresh token probably shouldn't be sent down. + whitelisted.concat(['accessToken', 'expiresAt']), // don't publish refresh token + function (subfield) { return 'services.google.' + subfield; }), + + forOtherUsers: _.map( + // even with autopublish, no legitimate web app should be + // publishing all users' emails + _.without(whitelisted, 'email', 'verified_email'), + function (subfield) { return 'services.google.' + subfield; }) +}); Accounts.oauth.registerService('google', 2, function(query) { @@ -16,7 +30,6 @@ Accounts.oauth.registerService('google', 2, function(query) { }; // include all fields from google - // https://developers.google.com/accounts/docs/OAuth2Login#userinfocall var whitelisted = ['id', 'email', 'verified_email', 'name', 'given_name', 'family_name', 'picture', 'locale', 'timezone', 'gender']; diff --git a/packages/accounts-meetup/meetup_server.js b/packages/accounts-meetup/meetup_server.js index 604d4f7a2a5..56afbbb643f 100644 --- a/packages/accounts-meetup/meetup_server.js +++ b/packages/accounts-meetup/meetup_server.js @@ -1,7 +1,11 @@ -// with autopublish on: publish all fields to the logged in user; -// only the user's meetup user id -Accounts._autopublishFields.loggedInUser.push('services.meetup'); -Accounts._autopublishFields.allUsers.push('services.meetup.id'); +Accounts.addAutopublishFields({ + // publish all fields including access token, which can legitimately + // be used from the client (if transmitted over ssl or on + // localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit + forLoggedInUser: ['services.meetup'], + forOtherUsers: ['services.meetup.id'] +}); + Accounts.oauth.registerService('meetup', 2, function(query) { diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 9d95b2077bf..0915fd3b3e2 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,14 +1,15 @@ -// with autopublish on: publish all fields other than access token and -// secret to the user; only the user's twitter screenName and profile -// images to others -Accounts._autopublishFields.loggedInUser.push('services.twitter.id'); -Accounts._autopublishFields.loggedInUser.push('services.twitter.screenName'); -Accounts._autopublishFields.loggedInUser.push('services.twitter.lang'); -Accounts._autopublishFields.loggedInUser.push('services.twitter.profile_image_url'); -Accounts._autopublishFields.loggedInUser.push('services.twitter.profile_image_url_https'); -Accounts._autopublishFields.allUsers.push('services.twitter.screenName'); -Accounts._autopublishFields.allUsers.push('services.twitter.profile_image_url'); -Accounts._autopublishFields.allUsers.push('services.twitter.profile_image_url_https'); +// https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials +var whitelisted = ['profile_image_url', 'profile_image_url_https', 'lang']; + +var autopublishedFields = _.map( + // don't send access token. https://dev.twitter.com/discussions/5025 + whitelisted.concat(['id', 'screenName']), + function (subfield) { return 'services.twitter.' + subfield; }); + +Accounts.addAutopublishFields({ + forLoggedInUser: autopublishedFields, + forOtherUsers: autopublishedFields +}); Accounts.oauth.registerService('twitter', 1, function(oauthBinding) { var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json').data; @@ -21,9 +22,6 @@ Accounts.oauth.registerService('twitter', 1, function(oauthBinding) { }; // include helpful fields from twitter - // https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials - var whitelisted = ['profile_image_url', 'profile_image_url_https', 'lang']; - var fields = _.pick(identity, whitelisted); _.extend(serviceData, fields); diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index c7c8e30511d..51d8215b0a0 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -1,7 +1,9 @@ -// with autopublish on: publish all fields to the logged in user; -// only the user's weibo screen name to others -Accounts._autopublishFields.loggedInUser.push('services.weibo'); -Accounts._autopublishFields.allUsers.push('services.weibo.screenName'); +Accounts.addAutopublishFields({ + // publish all fields including access token, which can legitimately + // be used from the client (if transmitted over ssl or on localhost) + forLoggedInUser: ['services.weibo'], + forOtherUsers: ['services.weibo.screenName'] +}); Accounts.oauth.registerService('weibo', 2, function(query) { From a5af1d6b8c422f791d1cc1b3307f0ceb93555f18 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 19 Apr 2013 11:37:53 -0700 Subject: [PATCH 026/102] remove duplicate code --- packages/accounts-google/google_server.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 1c5f3c273c9..bb8d4825498 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -29,10 +29,6 @@ Accounts.oauth.registerService('google', 2, function(query) { expiresAt: (+new Date) + (1000 * response.expiresIn) }; - // include all fields from google - var whitelisted = ['id', 'email', 'verified_email', 'name', 'given_name', - 'family_name', 'picture', 'locale', 'timezone', 'gender']; - var fields = _.pick(identity, whitelisted); _.extend(serviceData, fields); From c9bacb897e6c15c256656999268c0dfcc038e01b Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 19 Apr 2013 11:42:19 -0700 Subject: [PATCH 027/102] Update History.md --- History.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/History.md b/History.md index 3432bc4bde2..b7c6eb4cbd6 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,8 @@ ## vNEXT +* With `autopublish` on, publish many useful fields on `Meteor.users`. + ## v0.6.2 * Better error reporting: From 03085e8842e38f50e70bdf569b9059dcb4d29264 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 19 Apr 2013 14:05:17 -0700 Subject: [PATCH 028/102] New HTTP sync API --- docs/client/api.html | 18 +++++++----------- packages/http/httpcall_server.js | 17 ++++++++++------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 4300236b42b..68d3d4cfd41 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2826,12 +2826,13 @@

Meteor.http

or `data` option is used to specify a body, in which case the parameters will be appended to the URL instead. -The callback receives two arguments, `error` and `result`. The `error` -argument will contain an Error if the request fails in any way, -including a network error, time-out, or an HTTP status code in the 400 -or 500 range. The result object is always -defined. When run in synchronous mode, the `result` is returned from the -function, and the `error` value is a stored as a property in `result`. +The callback receives two arguments, `error` and `result`. The +`error` argument will contain an Error if the request fails in any +way, including a network error, time-out, or an HTTP status code in +the 400 or 500 range. In case of a 4xx/5xx HTTP status code, the +`response` property on `error` matches the contents the result object +would have had otherwise. When run in synchronous mode, either +`result` is returned from the function, or `error` is thrown. Contents of the result object: @@ -2853,11 +2854,6 @@

Meteor.http

Object
A dictionary of HTTP headers from the response.
-
error - Error
-
Error object if the request failed. Matches the error callback parameter.
- - Example server method: diff --git a/packages/http/httpcall_server.js b/packages/http/httpcall_server.js index e3f24157edc..e7554b0daa0 100644 --- a/packages/http/httpcall_server.js +++ b/packages/http/httpcall_server.js @@ -65,7 +65,10 @@ Meteor.http.call = function(method, url, options, callback) { // Sync mode fut = new Future; callback = function(error, result) { - fut.ret(result); + if (error) + fut.throw(error); + else + fut.ret(result); }; } else { // Async mode @@ -75,13 +78,13 @@ Meteor.http.call = function(method, url, options, callback) { }); } - // wrap callback to always return a result object, and always - // have an 'error' property in result + // wrap callback to add a 'response' property on an error, in case + // we have both (http 4xx/5xx error, which has a response payload) callback = (function(callback) { - return function(error, result) { - result = result || {}; - result.error = error; - callback(error, result); + return function(error, response) { + if (error && response) + error.response = response; + callback(error, response); }; })(callback); From 6e924979be81ca2993775f5f4ce972204dcb90b6 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 22 Apr 2013 11:15:28 -0700 Subject: [PATCH 029/102] More http sync - improve docs - add tests for sync case - modify http tests so that they now pass - Still TODO: Update call sites (oauth) --- docs/client/api.html | 6 +- packages/http/httpcall_client.js | 12 ++-- packages/http/httpcall_tests.js | 111 ++++++++++++++++++++++--------- 3 files changed, 88 insertions(+), 41 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 68d3d4cfd41..f6354bdac77 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2830,9 +2830,9 @@

Meteor.http

`error` argument will contain an Error if the request fails in any way, including a network error, time-out, or an HTTP status code in the 400 or 500 range. In case of a 4xx/5xx HTTP status code, the -`response` property on `error` matches the contents the result object -would have had otherwise. When run in synchronous mode, either -`result` is returned from the function, or `error` is thrown. +`response` property on `error` matches the contents of the result +object. When run in synchronous mode, either `result` is returned +from the function, or `error` is thrown. Contents of the result object: diff --git a/packages/http/httpcall_client.js b/packages/http/httpcall_client.js index 6471696c3c3..3fab89b3ea1 100644 --- a/packages/http/httpcall_client.js +++ b/packages/http/httpcall_client.js @@ -57,13 +57,13 @@ Meteor.http.call = function(method, url, options, callback) { ////////// Callback wrapping ////////// - // wrap callback to always return a result object, and always - // have an 'error' property in result + // wrap callback to add a 'response' property on an error, in case + // we have both (http 4xx/5xx error, which has a response payload) callback = (function(callback) { - return function(error, result) { - result = result || {}; - result.error = error; - callback(error, result); + return function(error, response) { + if (error && response) + error.response = response; + callback(error, response); }; })(callback); diff --git a/packages/http/httpcall_tests.js b/packages/http/httpcall_tests.js index 472a05e34d4..b9d72202d23 100644 --- a/packages/http/httpcall_tests.js +++ b/packages/http/httpcall_tests.js @@ -36,8 +36,12 @@ testAsyncMulti("httpcall - basic", [ if (Meteor.isServer) { // test sync version - var result = Meteor.http.call("GET", url_prefix()+url, options); - callback(result.error, result); + try { + var result = Meteor.http.call("GET", url_prefix()+url, options); + callback(undefined, result); + } catch (e) { + callback(e, e.response); + } } }; @@ -71,23 +75,42 @@ testAsyncMulti("httpcall - errors", [ function(test, expect) { // Accessing unknown server (should fail to make any connection) - Meteor.http.call("GET", "http://asfd.asfd/", expect( - function(error, result) { - test.isTrue(error); - test.isTrue(result); - test.equal(error, result.error); - })); + var unknownServerCallback = function(error, result) { + test.isTrue(error); + test.isFalse(result); + test.isFalse(error.response); + }; + Meteor.http.call("GET", "http://asfd.asfd/", expect(unknownServerCallback)); + + if (Meteor.isServer) { + // test sync version + try { + var unknownServerResult = Meteor.http.call("GET", "http://asfd.asfd/"); + unknownServerCallback(undefined, unknownServerResult); + } catch (e) { + unknownServerCallback(e, e.response); + } + } // Server serves 500 - Meteor.http.call("GET", url_prefix()+"/fail", expect( - function(error, result) { - test.isTrue(error); - test.isTrue(result); - test.equal(error, result.error); - - test.equal(result.statusCode, 500); - })); - + error500Callback = function(error, result) { + test.isTrue(error); + test.isTrue(result); + test.isTrue(error.response); + test.equal(result, error.response); + test.equal(error.response.statusCode, 500); + }; + Meteor.http.call("GET", url_prefix()+"/fail", expect(error500Callback)); + + if (Meteor.isServer) { + // test sync version + try { + var error500Result = Meteor.http.call("GET", url_prefix()+"/fail"); + error500Callback(undefined, error500Result); + } catch (e) { + error500Callback(e, e.response); + } + } } ]); @@ -95,27 +118,51 @@ testAsyncMulti("httpcall - timeout", [ function(test, expect) { // Should time out + var timeoutCallback = function(error, result) { + test.isTrue(error); + test.isFalse(result); + test.isFalse(error.response); + }; + var timeoutUrl = url_prefix()+"/slow-"+Random.id(); Meteor.http.call( - "GET", url_prefix()+"/slow-"+Random.id(), + "GET", timeoutUrl, { timeout: 500 }, - expect(function(error, result) { - test.isTrue(error); - test.equal(error, result.error); - })); + expect(timeoutCallback)); + + if (Meteor.isServer) { + // test sync version + try { + var timeoutResult = Meteor.http.call("GET", timeoutUrl, { timeout: 500 }); + timeoutCallback(undefined, timeoutResult); + } catch (e) { + timeoutCallback(e, e.response); + } + } // Should not time out + var noTimeoutCallback = function(error, result) { + test.isFalse(error); + test.isTrue(result); + test.equal(result.statusCode, 200); + var data = result.data; + test.equal(data.url.substring(0, 4), "/foo"); + test.equal(data.method, "GET"); + }; + var noTimeoutUrl = url_prefix()+"/foo-"+Random.id(); Meteor.http.call( - "GET", url_prefix()+"/foo-"+Random.id(), + "GET", noTimeoutUrl, { timeout: 2000 }, - expect(function(error, result) { - test.isFalse(error); - test.isTrue(result); - test.equal(result.statusCode, 200); - var data = result.data; - test.equal(data.url.substring(0, 4), "/foo"); - test.equal(data.method, "GET"); - - })); + expect(noTimeoutCallback)); + + if (Meteor.isServer) { + // test sync version + try { + var noTimeoutResult = Meteor.http.call("GET", noTimeoutUrl, { timeout: 2000 }); + noTimeoutCallback(undefined, noTimeoutResult); + } catch (e) { + noTimeoutCallback(e, e.response); + } + } } ]); From a66816635120bbb407c9ed11129485f74d8643ec Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 22 Apr 2013 12:30:50 -0700 Subject: [PATCH 030/102] More concise server logging of unexpected errors. 'exception.stack' already contains 'exception.toString()' at the top. The original code printed the exception message twice. --- packages/livedata/livedata_server.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index ea5dde0cdab..e3331d48ef1 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -1353,8 +1353,7 @@ var wrapInternalException = function (exception, context) { // tests can set the 'expected' flag on an exception so it won't go to the // server log if (!exception.expected) - Meteor._debug("Exception " + context, exception.toString(), - exception.stack); + Meteor._debug("Exception " + context, exception.stack); return new Meteor.Error(500, "Internal server error"); }; From f6028b1578dee5698b0ab3b801bdafd3701ac5f2 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 22 Apr 2013 12:33:37 -0700 Subject: [PATCH 031/102] OAuth login services use the new http sync api --- packages/accounts-facebook/facebook_server.js | 96 +++++++++---------- packages/accounts-github/github_server.js | 49 +++++----- packages/accounts-google/google_server.js | 50 +++++----- packages/accounts-meetup/meetup_server.js | 46 +++++---- .../accounts-oauth1-helper/oauth1_binding.js | 20 ++-- packages/accounts-weibo/weibo_server.js | 45 +++++---- 6 files changed, 154 insertions(+), 152 deletions(-) diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index a510887b878..3ca049453d3 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -45,63 +45,63 @@ var getTokenResponse = function (query) { if (!config) throw new Accounts.ConfigError("Service not configured"); - // Request an access token - var result = Meteor.http.get( - "https://graph.facebook.com/oauth/access_token", { - params: { - client_id: config.appId, - redirect_uri: Meteor.absoluteUrl("_oauth/facebook?close"), - client_secret: config.secret, - code: query.code - } - }); - - var response = result.content; - - if (result.error) { - throw new Error("Failed to complete OAuth handshake with Facebook. " + - "HTTP Error " + result.statusCode + ": " + response); - } - - // Errors come back as JSON but success looks like a query encoded - // in a url - var error_response; + var responseContent; try { - // Just try to parse so that we know if we failed or not, - // while storing the parsed results - error_response = JSON.parse(response); - } catch (e) { - error_response = null; + // Request an access token + responseContent = Meteor.http.get( + "https://graph.facebook.com/oauth/access_token", { + params: { + client_id: config.appId, + redirect_uri: Meteor.absoluteUrl("_oauth/facebook?close"), + client_secret: config.secret, + code: query.code + } + }).content; + } catch (err) { + throw new Error("Failed to complete OAuth handshake with Facebook. " + + err + (err.response ? ": " + err.response.content : "")); } - if (error_response) { - throw new Error("Failed to complete OAuth handshake with Facebook. " + response); - } else { - // Success! Extract the facebook access token and expiration - // time from the response - var parsedResponse = querystring.parse(response); - var fbAccessToken = parsedResponse.access_token; - var fbExpires = parsedResponse.expires; + // If 'responseContent' parses as JSON, it is an error. + (function () { + // Errors come back as JSON but success looks like a query encoded + // in a url + var errorResponse; + try { + // Just try to parse so that we know if we failed or not, + // while storing the parsed results + errorResponse = JSON.parse(responseContent); + } catch (e) { + errorResponse = null; + } - if (!fbAccessToken) { - throw new Error("Failed to complete OAuth handshake with facebook " + - "-- can't find access token in HTTP response. " + response); + if (errorResponse) { + throw new Error("Failed to complete OAuth handshake with Facebook. " + responseContent); } - return { - accessToken: fbAccessToken, - expiresIn: fbExpires - }; + })(); + + // Success! Extract the facebook access token and expiration + // time from the response + var parsedResponse = querystring.parse(responseContent); + var fbAccessToken = parsedResponse.access_token; + var fbExpires = parsedResponse.expires; + + if (!fbAccessToken) { + throw new Error("Failed to complete OAuth handshake with facebook " + + "-- can't find access token in HTTP response. " + responseContent); } + return { + accessToken: fbAccessToken, + expiresIn: fbExpires + }; }; var getIdentity = function (accessToken) { - var result = Meteor.http.get("https://graph.facebook.com/me", { - params: {access_token: accessToken}}); - - if (result.error) { + try { + return Meteor.http.get("https://graph.facebook.com/me", { + params: {access_token: accessToken}}).data; + } catch (err) { throw new Error("Failed to fetch identity from Facebook. " + - "HTTP Error " + result.statusCode + ": " + result.content); - } else { - return result.data; + err + (err.response ? ": " + err.response.content : "")); } }; diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index 1d712c2c896..d76f3deff49 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -27,36 +27,37 @@ var getAccessToken = function (query) { if (!config) throw new Accounts.ConfigError("Service not configured"); - var result = Meteor.http.post( - "https://github.com/login/oauth/access_token", { - headers: {Accept: 'application/json'}, - params: { - code: query.code, - client_id: config.clientId, - client_secret: config.secret, - redirect_uri: Meteor.absoluteUrl("_oauth/github?close"), - state: query.state - } - }); - - if (result.error) { // if the http response was an error + var response; + try { + response = Meteor.http.post( + "https://github.com/login/oauth/access_token", { + headers: {Accept: 'application/json'}, + params: { + code: query.code, + client_id: config.clientId, + client_secret: config.secret, + redirect_uri: Meteor.absoluteUrl("_oauth/github?close"), + state: query.state + } + }); + } catch (err) { throw new Error("Failed to complete OAuth handshake with GitHub. " + - "HTTP Error " + result.statusCode + ": " + result.content); - } else if (result.data.error) { // if the http response was a json object with an error attribute - throw new Error("Failed to complete OAuth handshake with GitHub. " + result.data.error); + err + (err.response ? ": " + err.response.content : "")); + } + if (response.data.error) { // if the http response was a json object with an error attribute + throw new Error("Failed to complete OAuth handshake with GitHub. " + response.data.error); } else { - return result.data.access_token; + return response.data.access_token; } }; var getIdentity = function (accessToken) { - var result = Meteor.http.get( - "https://api.github.com/user", - {params: {access_token: accessToken}}); - if (result.error) { + try { + return Meteor.http.get( + "https://api.github.com/user", + {params: {access_token: accessToken}}).data; + } catch (err) { throw new Error("Failed to fetch identity from GitHub. " + - "HTTP Error " + result.statusCode + ": " + result.content); - } else { - return result.data; + err + (err.response ? ": " + err.response.content : "")); } }; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index bb8d4825498..9ccb3b1c962 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -53,39 +53,39 @@ var getTokens = function (query) { if (!config) throw new Accounts.ConfigError("Service not configured"); - var result = Meteor.http.post( - "https://accounts.google.com/o/oauth2/token", {params: { - code: query.code, - client_id: config.clientId, - client_secret: config.secret, - redirect_uri: Meteor.absoluteUrl("_oauth/google?close"), - grant_type: 'authorization_code' - }}); - - - if (result.error) { // if the http response was an error + var response; + try { + response = Meteor.http.post( + "https://accounts.google.com/o/oauth2/token", {params: { + code: query.code, + client_id: config.clientId, + client_secret: config.secret, + redirect_uri: Meteor.absoluteUrl("_oauth/google?close"), + grant_type: 'authorization_code' + }}); + } catch (err) { throw new Error("Failed to complete OAuth handshake with Google. " + - "HTTP Error " + result.statusCode + ": " + result.content); - } else if (result.data.error) { // if the http response was a json object with an error attribute - throw new Error("Failed to complete OAuth handshake with Google. " + result.data.error); + err + (err.response ? ": " + err.response.content : "")); + } + + if (response.data.error) { // if the http response was a json object with an error attribute + throw new Error("Failed to complete OAuth handshake with Google. " + response.data.error); } else { return { - accessToken: result.data.access_token, - refreshToken: result.data.refresh_token, - expiresIn: result.data.expires_in + accessToken: response.data.access_token, + refreshToken: response.data.refresh_token, + expiresIn: response.data.expires_in }; } }; var getIdentity = function (accessToken) { - var result = Meteor.http.get( - "https://www.googleapis.com/oauth2/v1/userinfo", - {params: {access_token: accessToken}}); - - if (result.error) { + try { + return Meteor.http.get( + "https://www.googleapis.com/oauth2/v1/userinfo", + {params: {access_token: accessToken}}).data; + } catch (err) { throw new Error("Failed to fetch identity from Google. " + - "HTTP Error " + result.statusCode + ": " + result.content); - } else { - return result.data; + err + (err.response ? ": " + err.response.content : "")); } }; diff --git a/packages/accounts-meetup/meetup_server.js b/packages/accounts-meetup/meetup_server.js index 56afbbb643f..cab41746bbb 100644 --- a/packages/accounts-meetup/meetup_server.js +++ b/packages/accounts-meetup/meetup_server.js @@ -26,33 +26,37 @@ var getAccessToken = function (query) { if (!config) throw new Accounts.ConfigError("Service not configured"); - var result = Meteor.http.post( - "https://secure.meetup.com/oauth2/access", {headers: {Accept: 'application/json'}, params: { - code: query.code, - client_id: config.clientId, - client_secret: config.secret, - grant_type: 'authorization_code', - redirect_uri: Meteor.absoluteUrl("_oauth/meetup?close"), - state: query.state - }}); - if (result.error) { // if the http response was an error + var response; + try { + response = Meteor.http.post( + "https://secure.meetup.com/oauth2/access", {headers: {Accept: 'application/json'}, params: { + code: query.code, + client_id: config.clientId, + client_secret: config.secret, + grant_type: 'authorization_code', + redirect_uri: Meteor.absoluteUrl("_oauth/meetup?close"), + state: query.state + }}); + } catch (err) { throw new Error("Failed to complete OAuth handshake with Meetup. " + - "HTTP Error " + result.statusCode + ": " + result.content); - } else if (result.data.error) { // if the http response was a json object with an error attribute - throw new Error("Failed to complete OAuth handshake with Meetup. " + result.data.error); + err + (err.response ? ": " + err.response.content : "")); + } + + if (response.data.error) { // if the http response was a json object with an error attribute + throw new Error("Failed to complete OAuth handshake with Meetup. " + response.data.error); } else { - return result.data.access_token; + return response.data.access_token; } }; var getIdentity = function (accessToken) { - var result = Meteor.http.get( - "https://secure.meetup.com/2/members", - {params: {member_id: 'self', access_token: accessToken}}); - if (result.error) { + try { + var response = Meteor.http.get( + "https://secure.meetup.com/2/members", + {params: {member_id: 'self', access_token: accessToken}}); + return response.data.results && response.data.results[0]; + } catch (err) { throw new Error("Failed to fetch identity from Meetup. " + - "HTTP Error " + result.statusCode + ": " + result.content); - } else { - return result.data.results && result.data.results[0]; + err + (err.response ? ": " + err.response.content : "")); } }; diff --git a/packages/accounts-oauth1-helper/oauth1_binding.js b/packages/accounts-oauth1-helper/oauth1_binding.js index 56d4a7d0131..416e0ef9f4b 100644 --- a/packages/accounts-oauth1-helper/oauth1_binding.js +++ b/packages/accounts-oauth1-helper/oauth1_binding.js @@ -116,19 +116,17 @@ OAuth1Binding.prototype._call = function(method, url, headers, params) { var authString = self._getAuthHeaderString(headers); // Make signed request - var response = Meteor.http.call(method, url, { - params: params, - headers: { - Authorization: authString - } - }); - - if (response.error) { + try { + return Meteor.http.call(method, url, { + params: params, + headers: { + Authorization: authString + } + }); + } catch (err) { throw new Error("Failed to send OAuth1 http request to " + url + ". " + - "HTTP Error " + response.statusCode + ": " + response.content); + err + (err.response ? ": " + err.response.content : "")); } - - return response; }; OAuth1Binding.prototype._encodeHeader = function(header) { diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 51d8215b0a0..f8e9a1021b5 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -40,40 +40,39 @@ var getTokenResponse = function (query) { if (!config) throw new Accounts.ConfigError("Service not configured"); - var result = Meteor.http.post( - "https://api.weibo.com/oauth2/access_token", {params: { - code: query.code, - client_id: config.clientId, - client_secret: config.secret, - redirect_uri: Meteor.absoluteUrl("_oauth/weibo?close", {replaceLocalhost: true}), - grant_type: 'authorization_code' - }}); - - if (result.error) { // if the http response was an error + var response; + try { + response = Meteor.http.post( + "https://api.weibo.com/oauth2/access_token", {params: { + code: query.code, + client_id: config.clientId, + client_secret: config.secret, + redirect_uri: Meteor.absoluteUrl("_oauth/weibo?close", {replaceLocalhost: true}), + grant_type: 'authorization_code' + }}); + } catch (err) { throw new Error("Failed to complete OAuth handshake with Weibo. " + - "HTTP Error " + result.statusCode + ": " + result.content); + err + (err.response ? ": " + err.response.content : "")); } // result.headers["content-type"] is 'text/plain;charset=UTF-8', so // the http package doesn't automatically populate result.data - result.data = JSON.parse(result.content); + response.data = JSON.parse(response.content); - if (result.data.error) { // if the http response was a json object with an error attribute - throw new Error("Failed to complete OAuth handshake with Weibo. " + result.data.error); + if (response.data.error) { // if the http response was a json object with an error attribute + throw new Error("Failed to complete OAuth handshake with Weibo. " + response.data.error); } else { - return result.data; + return response.data; } }; var getIdentity = function (accessToken, userId) { - var result = Meteor.http.get( - "https://api.weibo.com/2/users/show.json", - {params: {access_token: accessToken, uid: userId}}); - - if (result.error) { + try { + return Meteor.http.get( + "https://api.weibo.com/2/users/show.json", + {params: {access_token: accessToken, uid: userId}}).data; + } catch (err) { throw new Error("Failed to fetch identity from Weibo. " + - "HTTP Error " + result.statusCode + ": " + result.content); - } else { - return result.data; + err + (err.response ? ": " + err.response.content : "")); } }; From b8b8f1b6f4fa75f159d3e930203c4941af8fc88c Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 22 Apr 2013 15:31:14 -0700 Subject: [PATCH 032/102] Simplify error handling in accounts-facebook --- packages/accounts-facebook/facebook_server.js | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 3ca049453d3..0f32e081ce4 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -37,6 +37,16 @@ Accounts.oauth.registerService('facebook', 2, function(query) { }; }); +// checks whether a string parses as JSON +var isJSON = function (str) { + try { + JSON.parse(str); + return true; + } catch (e) { + return false; + } +}; + // returns an object containing: // - accessToken // - expiresIn: lifetime of token in seconds @@ -63,22 +73,10 @@ var getTokenResponse = function (query) { } // If 'responseContent' parses as JSON, it is an error. - (function () { - // Errors come back as JSON but success looks like a query encoded - // in a url - var errorResponse; - try { - // Just try to parse so that we know if we failed or not, - // while storing the parsed results - errorResponse = JSON.parse(responseContent); - } catch (e) { - errorResponse = null; - } - - if (errorResponse) { - throw new Error("Failed to complete OAuth handshake with Facebook. " + responseContent); - } - })(); + // XXX which facebook error causes this behvaior? + if (isJSON(responseContent)) { + throw new Error("Failed to complete OAuth handshake with Facebook. " + responseContent); + } // Success! Extract the facebook access token and expiration // time from the response From 11550d154c4c68eeff4a0b82f9fda2c51c015bc6 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 22 Apr 2013 15:42:45 -0700 Subject: [PATCH 033/102] Update History.md --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index b7c6eb4cbd6..6cdebb37e9f 100644 --- a/History.md +++ b/History.md @@ -3,6 +3,9 @@ * With `autopublish` on, publish many useful fields on `Meteor.users`. +* When using the `http` package on the server synchronously, errors + are thrown rather than passed in `result.error` + ## v0.6.2 * Better error reporting: From fe57ab10c78e00923ab2ad055b7568eb819ea3eb Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 22 Apr 2013 16:04:20 -0700 Subject: [PATCH 034/102] Simplify server http code by using future.resolver() --- packages/http/httpcall_server.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/http/httpcall_server.js b/packages/http/httpcall_server.js index e7554b0daa0..a2de5247859 100644 --- a/packages/http/httpcall_server.js +++ b/packages/http/httpcall_server.js @@ -64,12 +64,7 @@ Meteor.http.call = function(method, url, options, callback) { if (! callback) { // Sync mode fut = new Future; - callback = function(error, result) { - if (error) - fut.throw(error); - else - fut.ret(result); - }; + callback = fut.resolver(); // throw errors, return results } else { // Async mode // re-enter user code in a Fiber From fc81e29f3e4384396dae0e309448a122b35802ff Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Tue, 23 Apr 2013 13:25:24 -0700 Subject: [PATCH 035/102] Update the readme that gets generated within the .npm folder of packages --- tools/meteor_npm.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/meteor_npm.js b/tools/meteor_npm.js index 3284db4dc47..5e30957d173 100644 --- a/tools/meteor_npm.js +++ b/tools/meteor_npm.js @@ -201,9 +201,13 @@ _.extend(exports, { fs.writeFileSync( path.join(newPackageNpmDir, 'README'), // XXX copy? - "This directory and its contents are automatically generated when you change this\n" - + "package's npm dependencies. Commit this directory to source control so that\n" - + "others run the same versions of sub-dependencies.\n" + "This directory and the contained npm-shrinkwrap.json file are automatically\n" + + "generated when you change this package's npm dependencies. Commit this\n" + + "directory and the npm-shrinkwrap.json file to source control so that\n" + + "others run the same versions of sub-dependencies.\n\n" + + "Note the .gitignore in this directory is configured to ignore the\n" + + "node_modules sub-directory that meteor automatically creates.\n" + ); }, From b4d96d57957d0f990fd5e9752f0428235a2c102c Mon Sep 17 00:00:00 2001 From: "P. Mark Anderson" Date: Wed, 24 Apr 2013 09:44:38 -0700 Subject: [PATCH 036/102] Added user agent to account-github's getIdentity() function. This is untested. See https://github.com/meteor/meteor/issues/981 --- packages/accounts-github/github_server.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index 36f9ef4817e..6eb69874054 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -43,8 +43,10 @@ var getAccessToken = function (query) { var getIdentity = function (accessToken) { var result = Meteor.http.get( - "https://api.github.com/user", - {params: {access_token: accessToken}}); + "https://api.github.com/user", { + headers: {"User-Agent": "Meteor/1.0"}, + params: {access_token: accessToken} + }); if (result.error) { throw new Error("Failed to fetch identity from GitHub. " + "HTTP Error " + result.statusCode + ": " + result.content); From e43c94ee2e993c40721956c481d6fcc3485c3093 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 24 Apr 2013 09:55:59 -0700 Subject: [PATCH 037/102] accounts-github: Generate user agent string from release version --- packages/accounts-github/github_server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index 6eb69874054..aa0a23bfc6d 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -42,9 +42,13 @@ var getAccessToken = function (query) { }; var getIdentity = function (accessToken) { + var userAgent = "Meteor"; + if (Meteor.release) + userAgent += "/" + Meteor.release; + var result = Meteor.http.get( "https://api.github.com/user", { - headers: {"User-Agent": "Meteor/1.0"}, + headers: {"User-Agent": userAgent}, // http://developer.github.com/v3/#user-agent-required params: {access_token: accessToken} }); if (result.error) { From bd421582b8a7322bee21ca78a204673da26d5ccb Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 24 Apr 2013 10:01:23 -0700 Subject: [PATCH 038/102] Update History.md --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 6cdebb37e9f..9f6bc351f99 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,9 @@ ## vNEXT +* When authenticating with GitHub, include a user agent string. This + unbreaks "Sign in with GitHub" + * With `autopublish` on, publish many useful fields on `Meteor.users`. * When using the `http` package on the server synchronously, errors From 356301f7d01251e8c1f1629242292b96a5fb35f2 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 24 Apr 2013 10:04:41 -0700 Subject: [PATCH 039/102] Thank pmark --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 9f6bc351f99..a6f2aaa92fb 100644 --- a/History.md +++ b/History.md @@ -9,6 +9,9 @@ * When using the `http` package on the server synchronously, errors are thrown rather than passed in `result.error` +Patches contributed by GitHub user pmark. + + ## v0.6.2 * Better error reporting: From 1a7c2d81fd7a909db5b4c1d1a00ff6c8675bfcfc Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 24 Apr 2013 11:50:50 -0700 Subject: [PATCH 040/102] Another round of cleanup on .npm/README. --- tools/meteor_npm.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tools/meteor_npm.js b/tools/meteor_npm.js index 5e30957d173..cfcd1c57e5a 100644 --- a/tools/meteor_npm.js +++ b/tools/meteor_npm.js @@ -200,14 +200,13 @@ _.extend(exports, { _createReadme: function(newPackageNpmDir) { fs.writeFileSync( path.join(newPackageNpmDir, 'README'), - // XXX copy? - "This directory and the contained npm-shrinkwrap.json file are automatically\n" - + "generated when you change this package's npm dependencies. Commit this\n" - + "directory and the npm-shrinkwrap.json file to source control so that\n" - + "others run the same versions of sub-dependencies.\n\n" - + "Note the .gitignore in this directory is configured to ignore the\n" - + "node_modules sub-directory that meteor automatically creates.\n" - + "This directory and the files immediately inside it are automatically generated\n" + + "when you change this package's NPM dependencies. Commit the files in this\n" + + "directory (npm-shrinkwrap.json, .gitignore, and this README) to source control\n" + + "so that others run the same versions of sub-dependencies.\n" + + "\n" + + "You should NOT check in the node_modules directory that Meteor automatically\n" + + "creates; if you are using git, the .gitignore file tells git to ignore it.\n" ); }, From b8886b3935a1cb5079389a2a6216c4dbfa1bffa6 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 9 Apr 2013 15:18:00 -0700 Subject: [PATCH 041/102] Improve CLI testing - Test fake installed version of CLI - Test springboarding against a fake release --- scripts/cli-test.sh | 19 +++++++++++++++---- scripts/run-tools-tests.sh | 26 ++++++++++++++++++-------- scripts/tools-springboard-test.sh | 12 ++++++++++++ 3 files changed, 45 insertions(+), 12 deletions(-) create mode 100755 scripts/tools-springboard-test.sh diff --git a/scripts/cli-test.sh b/scripts/cli-test.sh index 42f9fe6daf4..0531a35544c 100755 --- a/scripts/cli-test.sh +++ b/scripts/cli-test.sh @@ -3,11 +3,23 @@ # NOTE: by default this tests the working copy, not the installed # meteor. To test the installed meteor, pass in --global. To test a # version of meteor installed in a specific directory, set the -# METEOR_WAREHOUSE_DIR environment variable. +# METEOR_TOOLS_TREE_DIR and METEOR_WAREHOUSE_DIR environment variable. + +set -e -x cd `dirname $0`/.. -METEOR="$(pwd)/meteor" +echo "FOO $METEOR_TOOLS_TREE_DIR" + +# METEOR_TOOLS_TREE_DIR is set in run-tools-tests.sh in order to test +# running an installed version of Meteor (though notably without +# testing springboarding, which is separately tested by +# tools-springboard-test.sh) +if [ -z "$METEOR_TOOLS_TREE_DIR" ]; then + METEOR="`pwd`/meteor" +else + METEOR="$METEOR_TOOLS_TREE_DIR/bin/meteor" +fi if [ -z "$NODE" ]; then NODE="$(pwd)/scripts/node.sh" @@ -42,13 +54,12 @@ OUTPUT="$TEST_TMPDIR/output" trap 'echo "[...]"; tail -25 $OUTPUT; echo FAILED ; rm -rf "$TEST_TMPDIR" >/dev/null 2>&1' EXIT cd "$TEST_TMPDIR" -set -e -x ## Begin actual tests if [ -n "$INSTALLED_METEOR" ]; then - if [ -n "$TEST_RELEASE" ]; then + if [ -n "$TEST_RELEASE" ]; then $METEOR --version | grep $TEST_RELEASE >> $OUTPUT else $METEOR --version >> $OUTPUT diff --git a/scripts/run-tools-tests.sh b/scripts/run-tools-tests.sh index 4899eff4136..d6e002d5008 100755 --- a/scripts/run-tools-tests.sh +++ b/scripts/run-tools-tests.sh @@ -12,27 +12,37 @@ make_temp_dir() { mktemp -d -t $1.XXXXXX } -## Test the Meteor CLI from an installed tools (tests loading packages -## into the warehouse). Notably +### +### Test the Meteor CLI from an installed tools (tests loading +### packages into the warehouse). +### TEST_TMPDIR=$(make_temp_dir meteor-installed-cli-tests) -TOOLS_DIR="$TEST_TMPDIR/tools-tree" -TARGET_DIR="$TOOLS_DIR" admin/build-tools-tree.sh +export METEOR_TOOLS_TREE_DIR="$TEST_TMPDIR/tools-tree" # used in cli-test.sh and tools-springboard-test.sh +TARGET_DIR="$METEOR_TOOLS_TREE_DIR" admin/build-tools-tree.sh # Create a warehouse. export METEOR_WAREHOUSE_DIR=$(make_temp_dir meteor-installed-cli-tests-warehouse) # Download a bootstrap tarball into it. (launch-meteor recreates the directory.) rmdir "$METEOR_WAREHOUSE_DIR" admin/launch-meteor --version # downloads the bootstrap tarball -export METEOR_DIR="$TOOLS_DIR/bin" + +# Test springboarding specifically +./tools-springboard-test.sh +# CLI tests (without springboarding, but with a warehouse) ./cli-test.sh + +unset METEOR_TOOLS_TREE_DIR unset METEOR_WAREHOUSE_DIR -unset METEOR_DIR -## Bundler unit tests +### +### Bundler unit tests +### ./bundler-test.sh -## Test the Meteor CLI from a checkout. We do this last because it is least likely to fail. +### +### Test the Meteor CLI from a checkout. We do this last because it is least likely to fail. +### ./cli-test.sh diff --git a/scripts/tools-springboard-test.sh b/scripts/tools-springboard-test.sh new file mode 100755 index 00000000000..8fa83ce5f26 --- /dev/null +++ b/scripts/tools-springboard-test.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e -x + +if [ -z "$METEOR_TOOLS_TREE_DIR" ]; then + echo "\$METEOR_TOOLS_TREE_DIR must be set" + exit 1 +fi + +METEOR="$METEOR_TOOLS_TREE_DIR/bin/meteor" + +$METEOR --release release-used-to-test-springboarding | grep "THIS IS A FAKE RELEASE ONLY USED TO TEST ENGINE SPRINGBOARDING" From 1ef5cfceb14fe174605556bec3629a7c392c4d43 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 24 Apr 2013 15:27:10 -0700 Subject: [PATCH 042/102] Glasser code review --- scripts/cli-test.sh | 2 -- scripts/tools-springboard-test.sh | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/cli-test.sh b/scripts/cli-test.sh index 0531a35544c..daa306ae1bc 100755 --- a/scripts/cli-test.sh +++ b/scripts/cli-test.sh @@ -9,8 +9,6 @@ set -e -x cd `dirname $0`/.. -echo "FOO $METEOR_TOOLS_TREE_DIR" - # METEOR_TOOLS_TREE_DIR is set in run-tools-tests.sh in order to test # running an installed version of Meteor (though notably without # testing springboarding, which is separately tested by diff --git a/scripts/tools-springboard-test.sh b/scripts/tools-springboard-test.sh index 8fa83ce5f26..3f833b0ff74 100755 --- a/scripts/tools-springboard-test.sh +++ b/scripts/tools-springboard-test.sh @@ -9,4 +9,6 @@ fi METEOR="$METEOR_TOOLS_TREE_DIR/bin/meteor" +# This release was built from the 'release-for-testing-springboarding' +# tag in GitHub. All it does is print this string and exit. $METEOR --release release-used-to-test-springboarding | grep "THIS IS A FAKE RELEASE ONLY USED TO TEST ENGINE SPRINGBOARDING" From c6c24dffb58e16657017aa936ca6f87de8de5e83 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 24 Apr 2013 15:34:58 -0700 Subject: [PATCH 043/102] Correct comment --- scripts/tools-springboard-test.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/tools-springboard-test.sh b/scripts/tools-springboard-test.sh index 3f833b0ff74..6f8d26c9985 100755 --- a/scripts/tools-springboard-test.sh +++ b/scripts/tools-springboard-test.sh @@ -9,6 +9,7 @@ fi METEOR="$METEOR_TOOLS_TREE_DIR/bin/meteor" -# This release was built from the 'release-for-testing-springboarding' -# tag in GitHub. All it does is print this string and exit. -$METEOR --release release-used-to-test-springboarding | grep "THIS IS A FAKE RELEASE ONLY USED TO TEST ENGINE SPRINGBOARDING" +# This release was built from the +# 'release/release-used-to-test-springboarding' tag in GitHub. All it +# does is print this string and exit. +$METEOR --release release/used-to-test-springboarding | grep "THIS IS A FAKE RELEASE ONLY USED TO TEST ENGINE SPRINGBOARDING" From fb61e884dbf4fa3edde6895385fac58339bf78b8 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 23 Apr 2013 15:58:40 -0700 Subject: [PATCH 044/102] Don't create new var scope for js files in client/compatibility/ --- docs/client/concepts.html | 6 ++++++ packages/meteor/package.js | 5 +++-- tools/bundler.js | 25 +++++++++++++++++++------ tools/packages.js | 8 +++++++- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index cd26c5ef26d..49c02bcfed5 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -43,6 +43,12 @@

Structuring your application

You're free to use a single JavaScript file for your entire application, or create a nested tree of separate files, or anything in between. +Some JavaScript libraries only work when placed in the +`client/compatibility` subdirectory. Files in this directory are +executed without being wrapped in a new variable scope. This means +that each top-level `var` defines a global variable. In addition, +these files are executed before other client-side JavaScript files. + Files outside the `client`, `server` and `tests` subdirectories are loaded on both the client and the server! That's the place for model definitions and other functions. Meteor provides the variables [`isClient`](#meteor_isclient) and diff --git a/packages/meteor/package.js b/packages/meteor/package.js index dafe07c0901..01946c1b9db 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -6,12 +6,13 @@ Package.describe({ }); Package.register_extension( - "js", function (bundle, source_path, serve_path, where) { + "js", function (bundle, source_path, serve_path, where, opt) { bundle.add_resource({ type: "js", path: serve_path, source_file: source_path, - where: where + where: where, + compatibility: opt.compatibility }); } ); diff --git a/tools/bundler.js b/tools/bundler.js index 5dbab04c249..c0f51ab0555 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -115,7 +115,7 @@ var PackageBundlingInfo = function (pkg, bundle) { }); }, - add_files: function (paths, where) { + add_files: function (paths, where, opt) { if (!(paths instanceof Array)) paths = paths ? [paths] : []; if (!(where instanceof Array)) @@ -123,7 +123,7 @@ var PackageBundlingInfo = function (pkg, bundle) { _.each(where, function (w) { _.each(paths, function (rel_path) { - self.add_file(rel_path, w); + self.add_file(rel_path, w, opt); }); }); }, @@ -188,8 +188,11 @@ _.extend(PackageBundlingInfo.prototype, { return candidates[0]; }, - add_file: function (rel_path, where) { + // opt {Object} + // - compatibility {Boolean} In case this is a JS file, don't wrap in a closure. + add_file: function (rel_path, where, opt) { var self = this; + opt = opt || {}; if (self.files[where][rel_path]) return; @@ -210,7 +213,8 @@ _.extend(PackageBundlingInfo.prototype, { handler(self.bundle.api, sourcePath, path.join(self.pkg.serve_root, rel_path), - where); + where, + opt); } else { // If we don't have an extension handler, serve this file // as a static resource. @@ -302,6 +306,10 @@ var Bundle = function () { * * data: the data to send. overrides source_file if present. you * must still set path (except for "head" and "body".) + * + * compatibility: (only for js files) when set, don't wrap code in + * a closure. used for client-side javascript libraries that use + * the `function foo()` or `var foo =` syntax to define globals. */ add_resource: function (options) { var source_file = options.source_file || options.path; @@ -337,11 +345,16 @@ var Bundle = function () { // scope (eg, file-level vars are file-scoped). On the server, this // is done in server/server.js to inject the Npm symbol. // + // Some client-side Javascript libraries define globals with `var foo =` or + // `function bar()` which only work if loaded directly from a script tag. If + // `options.compatibility` is set, don't wrap in a closure to enable using + // such libraries. + // // The ".call(this)" allows you to do a top-level "this.foo = " // to define global variables when using "use strict" // (http://es5.github.io/#x15.3.4.4); this is the only way to do // it in CoffeeScript. - if (w === "client") { + if (w === "client" && !options.compatibility) { wrapped = Buffer.concat([ new Buffer("(function(){ "), data, @@ -673,7 +686,7 @@ _.extend(Bundle.prototype, { contents = self.files.client[file]; delete self.files.client[file]; self.files.client_cacheable[file] = contents; - url = file + '?' + sha1(contents) + url = file + '?' + sha1(contents); } else throw new Error('unable to find file: ' + file); diff --git a/tools/packages.js b/tools/packages.js index 8e98150976c..4930b52f0fa 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -238,7 +238,13 @@ _.extend(Package.prototype, { api.use(project.get_packages(app_dir)); // -- Source files -- - api.add_files(sources_except(api, "server"), "client"); + var inCompatibilityMode = function (filename) { + return filename.indexOf(path.sep + 'client' + path.sep + 'compatibility' + path.sep) !== -1; + }; + var clientFiles = sources_except(api, "server"); + api.add_files(_.filter(clientFiles, inCompatibilityMode), "client", {compatibility: true}); + api.add_files(_.reject(clientFiles, inCompatibilityMode), "client"); + api.add_files(sources_except(api, "client"), "server"); }); From 47ba95dfc4a0848c77d1a562d7c1f404e3de8657 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 25 Apr 2013 10:58:27 -0700 Subject: [PATCH 045/102] wrapping --- tools/bundler.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/bundler.js b/tools/bundler.js index c0f51ab0555..c9a7029260e 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -345,10 +345,11 @@ var Bundle = function () { // scope (eg, file-level vars are file-scoped). On the server, this // is done in server/server.js to inject the Npm symbol. // - // Some client-side Javascript libraries define globals with `var foo =` or - // `function bar()` which only work if loaded directly from a script tag. If - // `options.compatibility` is set, don't wrap in a closure to enable using - // such libraries. + // Some client-side Javascript libraries define globals + // with `var foo =` or `function bar()` which only work if + // loaded directly from a script tag. If + // `options.compatibility` is set, don't wrap in a closure + // to enable using such libraries. // // The ".call(this)" allows you to do a top-level "this.foo = " // to define global variables when using "use strict" From 4a32bb81f3d59195f9e52ba31fa4f97d307c164c Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 25 Apr 2013 13:47:03 -0700 Subject: [PATCH 046/102] options.compatibility -> options.raw --- packages/meteor/package.js | 2 +- tools/bundler.js | 8 ++++---- tools/packages.js | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/meteor/package.js b/packages/meteor/package.js index 01946c1b9db..805adff618f 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -12,7 +12,7 @@ Package.register_extension( path: serve_path, source_file: source_path, where: where, - compatibility: opt.compatibility + raw: opt.raw }); } ); diff --git a/tools/bundler.js b/tools/bundler.js index c9a7029260e..ae2de8d43be 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -189,7 +189,7 @@ _.extend(PackageBundlingInfo.prototype, { }, // opt {Object} - // - compatibility {Boolean} In case this is a JS file, don't wrap in a closure. + // - raw {Boolean} In case this is a JS file, don't wrap in a closure. add_file: function (rel_path, where, opt) { var self = this; opt = opt || {}; @@ -307,7 +307,7 @@ var Bundle = function () { * data: the data to send. overrides source_file if present. you * must still set path (except for "head" and "body".) * - * compatibility: (only for js files) when set, don't wrap code in + * raw: (only for js files) when set, don't wrap code in * a closure. used for client-side javascript libraries that use * the `function foo()` or `var foo =` syntax to define globals. */ @@ -348,14 +348,14 @@ var Bundle = function () { // Some client-side Javascript libraries define globals // with `var foo =` or `function bar()` which only work if // loaded directly from a script tag. If - // `options.compatibility` is set, don't wrap in a closure + // `options.raw` is set, don't wrap in a closure // to enable using such libraries. // // The ".call(this)" allows you to do a top-level "this.foo = " // to define global variables when using "use strict" // (http://es5.github.io/#x15.3.4.4); this is the only way to do // it in CoffeeScript. - if (w === "client" && !options.compatibility) { + if (w === "client" && !options.raw) { wrapped = Buffer.concat([ new Buffer("(function(){ "), data, diff --git a/tools/packages.js b/tools/packages.js index 4930b52f0fa..8678652c882 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -238,12 +238,12 @@ _.extend(Package.prototype, { api.use(project.get_packages(app_dir)); // -- Source files -- - var inCompatibilityMode = function (filename) { + var shouldLoadRaw = function (filename) { return filename.indexOf(path.sep + 'client' + path.sep + 'compatibility' + path.sep) !== -1; }; var clientFiles = sources_except(api, "server"); - api.add_files(_.filter(clientFiles, inCompatibilityMode), "client", {compatibility: true}); - api.add_files(_.reject(clientFiles, inCompatibilityMode), "client"); + api.add_files(_.filter(clientFiles, shouldLoadRaw), "client", {raw: true}); + api.add_files(_.reject(clientFiles, shouldLoadRaw), "client"); api.add_files(sources_except(api, "client"), "server"); }); From c9e9c73da70d606716216216fea6220c12aa465e Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 25 Apr 2013 13:47:55 -0700 Subject: [PATCH 047/102] Update History.md --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 5a26e1f5a19..2efcaa25499 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,9 @@ ## vNEXT +* Files in the 'client/compatibility/' subdirectory of a Meteor app do + not get wrapped in a new variable scope. + * With `autopublish` on, publish many useful fields on `Meteor.users`. * When using the `http` package on the server synchronously, errors From eecc97715feecdaae054d85d1d40ec32f1b1bb1b Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 25 Apr 2013 14:15:42 -0700 Subject: [PATCH 048/102] Don't get confused (on the server) if transform returns something without _id. Fixes #974. --- History.md | 2 + packages/mongo-livedata/mongo_driver.js | 12 ++++-- .../mongo-livedata/mongo_livedata_tests.js | 41 +++++++++++++++---- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/History.md b/History.md index 2efcaa25499..104a73f182a 100644 --- a/History.md +++ b/History.md @@ -9,6 +9,8 @@ * When using the `http` package on the server synchronously, errors are thrown rather than passed in `result.error` +* Cursor transform functions on the server no longer are required to return + objects with correct `_id` fields. #974 ## v0.6.2.1 diff --git a/packages/mongo-livedata/mongo_driver.js b/packages/mongo-livedata/mongo_driver.js index 3209418b7f6..22556dbee0c 100644 --- a/packages/mongo-livedata/mongo_driver.js +++ b/packages/mongo-livedata/mongo_driver.js @@ -466,11 +466,17 @@ _.extend(SynchronousCursor.prototype, { var doc = self._synchronousNextObject().wait(); if (!doc || !doc._id) return null; doc = replaceTypes(doc, replaceMongoAtomWithMeteor); - if (self._transform) - doc = self._transform(doc); + + // Did Mongo give us duplicate documents in the same cursor? If so, ignore + // this one. (Do this before the transform, since transform might return + // some unrelated value.) var strId = Meteor.idStringify(doc._id); if (self._visitedIds[strId]) continue; self._visitedIds[strId] = true; + + if (self._transform) + doc = self._transform(doc); + return doc; } }, @@ -797,7 +803,7 @@ _.extend(LiveResultsSet.prototype, { self._synchronousCursor.rewind(); } else { self._synchronousCursor = self._mongoHandle._createSynchronousCursor( - self._cursorDescription, false); + self._cursorDescription, false /* !useTransform */); } var newResults = self._synchronousCursor.getRawObjects(self._ordered); var oldResults = self._results; diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index 3ccc8d72ab9..ea9c7d3031b 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -616,6 +616,7 @@ testAsyncMulti('mongo-livedata - document with a date, ' + idGeneration, [ testAsyncMulti('mongo-livedata - document goes through a transform, ' + idGeneration, [ function (test, expect) { + var self = this; var seconds = function (doc) { doc.seconds = function () {return doc.d.getSeconds();}; return doc; @@ -632,30 +633,54 @@ testAsyncMulti('mongo-livedata - document goes through a transform, ' + idGenera Meteor.subscribe('c-' + collectionName); } - var coll = new Meteor.Collection(collectionName, collectionOptions); + self.coll = new Meteor.Collection(collectionName, collectionOptions); + var obs; var expectAdd = expect(function (doc) { test.equal(doc.seconds(), 50); }); var expectRemove = expect (function (doc) { test.equal(doc.seconds(), 50); + obs.stop(); }); - coll.insert({d: new Date(1356152390004)}, expect(function (err, id) { + self.coll.insert({d: new Date(1356152390004)}, expect(function (err, id) { test.isFalse(err); test.isTrue(id); - var cursor = coll.find(); - cursor.observe({ + var cursor = self.coll.find(); + obs = cursor.observe({ added: expectAdd, removed: expectRemove }); test.equal(cursor.count(), 1); test.equal(cursor.fetch()[0].seconds(), 50); - test.equal(coll.findOne().seconds(), 50); - test.equal(coll.findOne({}, {transform: null}).seconds, undefined); - test.equal(coll.findOne({}, { + test.equal(self.coll.findOne().seconds(), 50); + test.equal(self.coll.findOne({}, {transform: null}).seconds, undefined); + test.equal(self.coll.findOne({}, { transform: function (doc) {return {seconds: doc.d.getSeconds()};} }).seconds, 50); - coll.remove(id); + self.coll.remove(id); + })); + }, + function (test, expect) { + var self = this; + self.coll.insert({d: new Date(1356152390004)}, expect(function (err, id) { + test.isFalse(err); + test.isTrue(id); + self.id1 = id; + })); + self.coll.insert({d: new Date(1356152391004)}, expect(function (err, id) { + test.isFalse(err); + test.isTrue(id); + self.id2 = id; })); + }, + function (test, expect) { + var self = this; + // Test that a transform that returns something other than a document with + // an _id (eg, a number) works. Regression test for #974. + test.equal(self.coll.find({}, { + transform: function (doc) { return doc.d.getSeconds(); }, + sort: {d: 1} + }).fetch(), [50, 51]); } ]); From c83fcbb5caa33eca2425b90b4642e4efa6df62d2 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Sun, 21 Apr 2013 13:40:50 -0400 Subject: [PATCH 049/102] Bump CoffeeScript version to 1.6.2. --- packages/coffeescript/.npm/npm-shrinkwrap.json | 6 +++--- packages/coffeescript/package.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/coffeescript/.npm/npm-shrinkwrap.json b/packages/coffeescript/.npm/npm-shrinkwrap.json index c4c39214dc9..8463f1d69a2 100644 --- a/packages/coffeescript/.npm/npm-shrinkwrap.json +++ b/packages/coffeescript/.npm/npm-shrinkwrap.json @@ -1,9 +1,9 @@ { "dependencies": { "coffee-script": { - "version": "1.5.0", - "from": "coffee-script@1.5.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.5.0.tgz" + "version": "1.6.2", + "from": "coffee-script@1.6.2", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.2.tgz" } } } diff --git a/packages/coffeescript/package.js b/packages/coffeescript/package.js index f7f36194db6..e8884d59597 100644 --- a/packages/coffeescript/package.js +++ b/packages/coffeescript/package.js @@ -2,7 +2,7 @@ Package.describe({ summary: "Javascript dialect with fewer braces and semicolons" }); -Npm.depends({"coffee-script": "1.5.0"}); +Npm.depends({"coffee-script": "1.6.2"}); var coffeescript_handler = function(bundle, source_path, serve_path, where) { var fs = Npm.require('fs'); From 8b047447fc3e0b54b9d713253d5a0d7db248e550 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 25 Apr 2013 14:24:13 -0700 Subject: [PATCH 050/102] Update HISTORY and notices. --- History.md | 4 ++++ scripts/admin/notices.json | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/History.md b/History.md index 104a73f182a..770447fe19d 100644 --- a/History.md +++ b/History.md @@ -12,6 +12,10 @@ * Cursor transform functions on the server no longer are required to return objects with correct `_id` fields. #974 +* Upgrade CoffeeScript from 1.5.0 to 1.6.2. #972 + +Patch contributed by GitHub user awwx. + ## v0.6.2.1 * When authenticating with GitHub, include a user agent string. This diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index fd536ac6265..df2cc410ab6 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -19,5 +19,12 @@ }, { "release": "0.6.2.1" + }, + { + "release": "NEXT", + "packageNotices": { + "coffeescript": ["CoffeeScript has been updated to 1.6.2 from 1.5.0. See", + "http://coffeescript.org/#changelog"] + } } ] From b9693f4e1e11f8536a36a4c09e00a4049a79041d Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 25 Apr 2013 14:39:45 -0700 Subject: [PATCH 051/102] Update .npm for newish code (eg 1a7c2d8). --- packages/coffeescript/.npm/README | 10 +++++++--- packages/email/.npm/README | 10 +++++++--- packages/email/.npm/npm-shrinkwrap.json | 9 ++++++--- packages/less/.npm/README | 10 +++++++--- packages/less/.npm/npm-shrinkwrap.json | 5 +++-- packages/livedata/.npm/README | 10 +++++++--- packages/livedata/.npm/npm-shrinkwrap.json | 9 ++++++--- packages/mongo-livedata/.npm/README | 10 +++++++--- packages/mongo-livedata/.npm/npm-shrinkwrap.json | 3 ++- packages/stylus/.npm/README | 10 +++++++--- packages/stylus/.npm/npm-shrinkwrap.json | 9 +++++---- 11 files changed, 64 insertions(+), 31 deletions(-) diff --git a/packages/coffeescript/.npm/README b/packages/coffeescript/.npm/README index c3466f6699b..3d492553a43 100644 --- a/packages/coffeescript/.npm/README +++ b/packages/coffeescript/.npm/README @@ -1,3 +1,7 @@ -This directory and its contents are automatically generated when you change this -package's npm dependencies. Commit this directory to source control so that -others run the same versions of sub-dependencies. +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/email/.npm/README b/packages/email/.npm/README index c3466f6699b..3d492553a43 100644 --- a/packages/email/.npm/README +++ b/packages/email/.npm/README @@ -1,3 +1,7 @@ -This directory and its contents are automatically generated when you change this -package's npm dependencies. Commit this directory to source control so that -others run the same versions of sub-dependencies. +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/email/.npm/npm-shrinkwrap.json b/packages/email/.npm/npm-shrinkwrap.json index 33f087579e2..c4f33506ebc 100644 --- a/packages/email/.npm/npm-shrinkwrap.json +++ b/packages/email/.npm/npm-shrinkwrap.json @@ -7,7 +7,8 @@ "dependencies": { "mimelib-noiconv": { "version": "0.1.9", - "from": "mimelib-noiconv@0.1.9" + "from": "mimelib-noiconv@*", + "resolved": "https://registry.npmjs.org/mimelib-noiconv/-/mimelib-noiconv-0.1.9.tgz" } } }, @@ -18,7 +19,8 @@ "dependencies": { "rai": { "version": "0.1.7", - "from": "rai@0.1.7" + "from": "rai@*", + "resolved": "https://registry.npmjs.org/rai/-/rai-0.1.7.tgz" }, "xoauth2": { "version": "0.1.6", @@ -29,7 +31,8 @@ }, "stream-buffers": { "version": "0.2.3", - "from": "stream-buffers@0.2.3" + "from": "stream-buffers@0.2.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-0.2.3.tgz" } } } diff --git a/packages/less/.npm/README b/packages/less/.npm/README index c3466f6699b..3d492553a43 100644 --- a/packages/less/.npm/README +++ b/packages/less/.npm/README @@ -1,3 +1,7 @@ -This directory and its contents are automatically generated when you change this -package's npm dependencies. Commit this directory to source control so that -others run the same versions of sub-dependencies. +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/less/.npm/npm-shrinkwrap.json b/packages/less/.npm/npm-shrinkwrap.json index ec9d9905d89..a2cb8489045 100644 --- a/packages/less/.npm/npm-shrinkwrap.json +++ b/packages/less/.npm/npm-shrinkwrap.json @@ -2,12 +2,13 @@ "dependencies": { "less": { "version": "1.3.3", - "from": "https://registry.npmjs.org/less/-/less-1.3.3.tgz", + "from": "less@1.3.3", "resolved": "https://registry.npmjs.org/less/-/less-1.3.3.tgz", "dependencies": { "ycssmin": { "version": "1.0.1", - "from": "ycssmin@1.0.1" + "from": "ycssmin@>=1.0.1", + "resolved": "https://registry.npmjs.org/ycssmin/-/ycssmin-1.0.1.tgz" } } } diff --git a/packages/livedata/.npm/README b/packages/livedata/.npm/README index c3466f6699b..3d492553a43 100644 --- a/packages/livedata/.npm/README +++ b/packages/livedata/.npm/README @@ -1,3 +1,7 @@ -This directory and its contents are automatically generated when you change this -package's npm dependencies. Commit this directory to source control so that -others run the same versions of sub-dependencies. +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/livedata/.npm/npm-shrinkwrap.json b/packages/livedata/.npm/npm-shrinkwrap.json index 3d7836988b4..d0804a5f3e8 100644 --- a/packages/livedata/.npm/npm-shrinkwrap.json +++ b/packages/livedata/.npm/npm-shrinkwrap.json @@ -3,20 +3,23 @@ "sockjs": { "version": "0.3.4", "from": "sockjs@0.3.4", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.4.tgz", "dependencies": { "node-uuid": { "version": "1.3.3", - "from": "node-uuid@1.3.3" + "from": "node-uuid@1.3.3", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.3.3.tgz" }, "faye-websocket": { "version": "0.4.0", - "from": "faye-websocket@0.4.0" + "from": "faye-websocket@0.4.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.4.0.tgz" } } }, "websocket": { "version": "1.0.7", - "from": "https://registry.npmjs.org/websocket/-/websocket-1.0.7.tgz", + "from": "websocket@1.0.7", "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.7.tgz" } } diff --git a/packages/mongo-livedata/.npm/README b/packages/mongo-livedata/.npm/README index c3466f6699b..3d492553a43 100644 --- a/packages/mongo-livedata/.npm/README +++ b/packages/mongo-livedata/.npm/README @@ -1,3 +1,7 @@ -This directory and its contents are automatically generated when you change this -package's npm dependencies. Commit this directory to source control so that -others run the same versions of sub-dependencies. +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/mongo-livedata/.npm/npm-shrinkwrap.json b/packages/mongo-livedata/.npm/npm-shrinkwrap.json index 09ef5e1eb16..cb8b20df8aa 100644 --- a/packages/mongo-livedata/.npm/npm-shrinkwrap.json +++ b/packages/mongo-livedata/.npm/npm-shrinkwrap.json @@ -7,7 +7,8 @@ "dependencies": { "bson": { "version": "0.1.8", - "from": "bson@0.1.8" + "from": "bson@0.1.8", + "resolved": "https://registry.npmjs.org/bson/-/bson-0.1.8.tgz" } } } diff --git a/packages/stylus/.npm/README b/packages/stylus/.npm/README index c3466f6699b..3d492553a43 100644 --- a/packages/stylus/.npm/README +++ b/packages/stylus/.npm/README @@ -1,3 +1,7 @@ -This directory and its contents are automatically generated when you change this -package's npm dependencies. Commit this directory to source control so that -others run the same versions of sub-dependencies. +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/stylus/.npm/npm-shrinkwrap.json b/packages/stylus/.npm/npm-shrinkwrap.json index a38e4bb1bc5..0d114c6d209 100644 --- a/packages/stylus/.npm/npm-shrinkwrap.json +++ b/packages/stylus/.npm/npm-shrinkwrap.json @@ -7,21 +7,22 @@ }, "stylus": { "version": "0.30.1", - "from": "https://registry.npmjs.org/stylus/-/stylus-0.30.1.tgz", + "from": "stylus@0.30.1", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.30.1.tgz", "dependencies": { "cssom": { "version": "0.2.5", - "from": "cssom@0.2.5" + "from": "cssom@0.2.x", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.2.5.tgz" }, "mkdirp": { "version": "0.3.5", - "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "from": "mkdirp@0.3.x", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" }, "debug": { "version": "0.7.2", - "from": "debug@0.7.2", + "from": "debug@*", "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.2.tgz" } } From a15d1ecdee1fd951fdee3d628fd1322938d69b4b Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Tue, 23 Apr 2013 14:27:44 -0400 Subject: [PATCH 052/102] Unpolyfill localstorage. Fixes #634. The unit test now passes in Firefox when dom.storage.enabled is set to false (if we care :) Tested in Chrome, Firefox, and IE 6 and 7. --- packages/accounts-base/localstorage_token.js | 12 ++-- packages/accounts-base/package.js | 2 +- .../localstorage_polyfill.js | 57 ----------------- .../localstorage_polyfill_tests.js | 9 --- packages/localstorage-polyfill/package.js | 16 ----- packages/localstorage/localstorage.js | 64 +++++++++++++++++++ packages/localstorage/localstorage_tests.js | 8 +++ packages/localstorage/package.js | 16 +++++ 8 files changed, 95 insertions(+), 89 deletions(-) delete mode 100644 packages/localstorage-polyfill/localstorage_polyfill.js delete mode 100644 packages/localstorage-polyfill/localstorage_polyfill_tests.js delete mode 100644 packages/localstorage-polyfill/package.js create mode 100644 packages/localstorage/localstorage.js create mode 100644 packages/localstorage/localstorage_tests.js create mode 100644 packages/localstorage/package.js diff --git a/packages/accounts-base/localstorage_token.js b/packages/accounts-base/localstorage_token.js index d827b9b35cd..34ac30998e4 100644 --- a/packages/accounts-base/localstorage_token.js +++ b/packages/accounts-base/localstorage_token.js @@ -36,8 +36,8 @@ Accounts._isolateLoginTokenForTest = function () { }; Accounts._storeLoginToken = function(userId, token) { - localStorage.setItem(userIdKey, userId); - localStorage.setItem(loginTokenKey, token); + Meteor._localStorage.setItem(userIdKey, userId); + Meteor._localStorage.setItem(loginTokenKey, token); // to ensure that the localstorage poller doesn't end up trying to // connect a second time @@ -45,8 +45,8 @@ Accounts._storeLoginToken = function(userId, token) { }; Accounts._unstoreLoginToken = function() { - localStorage.removeItem(userIdKey); - localStorage.removeItem(loginTokenKey); + Meteor._localStorage.removeItem(userIdKey); + Meteor._localStorage.removeItem(loginTokenKey); // to ensure that the localstorage poller doesn't end up trying to // connect a second time @@ -54,11 +54,11 @@ Accounts._unstoreLoginToken = function() { }; Accounts._storedLoginToken = function() { - return localStorage.getItem(loginTokenKey); + return Meteor._localStorage.getItem(loginTokenKey); }; Accounts._storedUserId = function() { - return localStorage.getItem(userIdKey); + return Meteor._localStorage.getItem(userIdKey); }; diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index e0c200293d8..ce80b187d58 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -4,7 +4,7 @@ Package.describe({ Package.on_use(function (api) { api.use('underscore', 'server'); - api.use('localstorage-polyfill', 'client'); + api.use('localstorage', 'client'); api.use('accounts-urls', 'client'); // need this because of the Meteor.users collection but in the future diff --git a/packages/localstorage-polyfill/localstorage_polyfill.js b/packages/localstorage-polyfill/localstorage_polyfill.js deleted file mode 100644 index de970679af5..00000000000 --- a/packages/localstorage-polyfill/localstorage_polyfill.js +++ /dev/null @@ -1,57 +0,0 @@ -if (!window.localStorage) { - window.localStorage = (function () { - // XXX eliminate dependency on jQuery, detect browsers ourselves - if ($.browser.msie) { // If we are on IE, which support userData - var userdata = document.createElement('span'); // could be anything - userdata.style.behavior = 'url("#default#userData")'; - userdata.id = 'localstorage-polyfill-helper'; - userdata.style.display = 'none'; - document.getElementsByTagName("head")[0].appendChild(userdata); - - var userdataKey = 'localStorage'; - userdata.load(userdataKey); - - return { - setItem: function (key, val) { - userdata.setAttribute(key, val); - userdata.save(userdataKey); - }, - - removeItem: function (key) { - userdata.removeAttribute(key); - userdata.save(userdataKey); - }, - - getItem: function (key) { - userdata.load(userdataKey); - return userdata.getAttribute(key); - } - }; - } else { - Meteor._debug( - "You are running a browser with no localStorage or userData " - + "support. Logging in from one tab will not cause another " - + "tab to be logged in."); - - // XXX This doesn't actually work in Firefox with dom.storage.enabled = - // false: the assignment to window.localStorage is ignored. If we care at - // all about this use case, we should probably define Meteor.localStorage - // instead of doing a polyfill. (This causes this package's test to fail - // in that situation.) - - return { - _data: {}, - - setItem: function (key, val) { - this._data[key] = val; - }, - removeItem: function (key) { - delete this._data[key]; - }, - getItem: function (key) { - return this._data[key]; - } - }; - }; - })(); -} diff --git a/packages/localstorage-polyfill/localstorage_polyfill_tests.js b/packages/localstorage-polyfill/localstorage_polyfill_tests.js deleted file mode 100644 index af603d54b92..00000000000 --- a/packages/localstorage-polyfill/localstorage_polyfill_tests.js +++ /dev/null @@ -1,9 +0,0 @@ -Tinytest.add("localStorage polyfill", function (test) { - // Doesn't actually test preservation across reloads since that is hard. - // userData should do that for us so it's unlikely this wouldn't work. - localStorage.setItem("key", "value"); - test.equal(localStorage.getItem("key"), "value"); - localStorage.removeItem("key"); - test.equal(localStorage.getItem("key"), null); -}); - diff --git a/packages/localstorage-polyfill/package.js b/packages/localstorage-polyfill/package.js deleted file mode 100644 index f6e269b2e2f..00000000000 --- a/packages/localstorage-polyfill/package.js +++ /dev/null @@ -1,16 +0,0 @@ -Package.describe({ - summary: "Simulates the localStorage API on IE 6,7 using userData", -}); - -Package.on_use(function (api) { - api.use('jquery', 'client'); // XXX only used for browser detection. remove. - - api.add_files('localstorage_polyfill.js', 'client'); -}); - -Package.on_test(function (api) { - api.use('localstorage-polyfill', 'client'); - api.use('tinytest'); - - api.add_files('localstorage_polyfill_tests.js', 'client'); -}); diff --git a/packages/localstorage/localstorage.js b/packages/localstorage/localstorage.js new file mode 100644 index 00000000000..1cf6e307319 --- /dev/null +++ b/packages/localstorage/localstorage.js @@ -0,0 +1,64 @@ +if (window.localStorage) { + Meteor._localStorage = { + getItem: function (key) { + return window.localStorage.getItem(key); + }, + setItem: function (key, value) { + window.localStorage.setItem(key, value); + }, + removeItem: function (key) { + window.localStorage.removeItem(key); + } + }; +} +// XXX eliminate dependency on jQuery, detect browsers ourselves +else if ($.browser.msie) { // If we are on IE, which support userData + var userdata = document.createElement('span'); // could be anything + userdata.style.behavior = 'url("#default#userData")'; + userdata.id = 'localstorage-helper'; + userdata.style.display = 'none'; + document.getElementsByTagName("head")[0].appendChild(userdata); + + var userdataKey = 'localStorage'; + userdata.load(userdataKey); + + Meteor._localStorage = { + setItem: function (key, val) { + userdata.setAttribute(key, val); + userdata.save(userdataKey); + }, + + removeItem: function (key) { + userdata.removeAttribute(key); + userdata.save(userdataKey); + }, + + getItem: function (key) { + userdata.load(userdataKey); + return userdata.getAttribute(key); + } + }; +} else { + Meteor._debug( + "You are running a browser with no localStorage or userData " + + "support. Logging in from one tab will not cause another " + + "tab to be logged in."); + + Meteor._localStorage = { + _data: {}, + + setItem: function (key, val) { + this._data[key] = val; + }, + removeItem: function (key) { + delete this._data[key]; + }, + getItem: function (key) { + var value = this._data[key]; + if (value === undefined) + return null; + else + return value; + } + }; +} diff --git a/packages/localstorage/localstorage_tests.js b/packages/localstorage/localstorage_tests.js new file mode 100644 index 00000000000..3e480328cd1 --- /dev/null +++ b/packages/localstorage/localstorage_tests.js @@ -0,0 +1,8 @@ +Tinytest.add("localStorage", function (test) { + // Doesn't actually test preservation across reloads since that is hard. + // userData should do that for us so it's unlikely this wouldn't work. + Meteor._localStorage.setItem("key", "value"); + test.equal(Meteor._localStorage.getItem("key"), "value"); + Meteor._localStorage.removeItem("key"); + test.equal(Meteor._localStorage.getItem("key"), null); +}); diff --git a/packages/localstorage/package.js b/packages/localstorage/package.js new file mode 100644 index 00000000000..d434fab2ab9 --- /dev/null +++ b/packages/localstorage/package.js @@ -0,0 +1,16 @@ +Package.describe({ + summary: "Simulates local storage on IE 6,7 using userData", +}); + +Package.on_use(function (api) { + api.use('jquery', 'client'); // XXX only used for browser detection. remove. + + api.add_files('localstorage.js', 'client'); +}); + +Package.on_test(function (api) { + api.use('localstorage', 'client'); + api.use('tinytest'); + + api.add_files('localstorage_tests.js', 'client'); +}); From 209dbf2fc7e3c1b3d9c5910f3340d605d8f22f04 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 25 Apr 2013 16:48:26 -0700 Subject: [PATCH 053/102] localstorage package should be internal --- packages/localstorage/package.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/localstorage/package.js b/packages/localstorage/package.js index d434fab2ab9..c6570323cb0 100644 --- a/packages/localstorage/package.js +++ b/packages/localstorage/package.js @@ -1,5 +1,6 @@ Package.describe({ summary: "Simulates local storage on IE 6,7 using userData", + internal: true }); Package.on_use(function (api) { From 8781477e645a9f96b000d7b639f92afc0a811a23 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 25 Apr 2013 16:48:34 -0700 Subject: [PATCH 054/102] Update notices and HISTORY. --- History.md | 8 +++++++- scripts/admin/notices.json | 6 +++++- tools/meteor.js | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index 770447fe19d..b022deda8c5 100644 --- a/History.md +++ b/History.md @@ -14,7 +14,13 @@ * Upgrade CoffeeScript from 1.5.0 to 1.6.2. #972 -Patch contributed by GitHub user awwx. +* The `localstorage-polyfill` smart package has been replaced by a + `localstorage` package, which defines a `Meteor._localStorage` API instead of + trying to replace the DOM `window.localStorage` facility. (Now, apps can use + the existence of `window.localStorage` to detect if the full localStorage API + is supported.) + +Patches contributed by GitHub user awwx. ## v0.6.2.1 diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index df2cc410ab6..8f389ce64b8 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -24,7 +24,11 @@ "release": "NEXT", "packageNotices": { "coffeescript": ["CoffeeScript has been updated to 1.6.2 from 1.5.0. See", - "http://coffeescript.org/#changelog"] + "http://coffeescript.org/#changelog"], + "localstorage-polyfill": [ + "The localstorage-polyfill package has been replaced by the localstorage", + "package, which creates an object at Meteor._localStorage instead of", + "pretending to be window.localStorage."] } } ] diff --git a/tools/meteor.js b/tools/meteor.js index adb878950e9..93f9ef7adf2 100644 --- a/tools/meteor.js +++ b/tools/meteor.js @@ -457,6 +457,8 @@ Fiber(function () { path.basename(context.appDir), context.releaseVersion); // Print any notices relevant to this upgrade. + // XXX This doesn't include package-specific notices for packages that + // are included transitively (eg, packages used by app packages). var packages = project.get_packages(context.appDir); warehouse.printNotices(appRelease, context.releaseVersion, packages); } From f4808b5a688cb6cebd4e1ccefab7768b6d81d38c Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Tue, 16 Apr 2013 17:25:40 -0400 Subject: [PATCH 055/102] Add Chromium to the browsers supported by appcache --- packages/appcache/appcache-server.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index f01ee6659d9..dac9171dde6 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -4,9 +4,24 @@ var crypto = Npm.require('crypto'); var fs = Npm.require('fs'); var path = Npm.require('path'); -var knownBrowsers = ['android', 'chrome', 'firefox', 'ie', 'mobileSafari', 'safari']; - -var browsersEnabledByDefault = ['android', 'chrome', 'ie', 'mobileSafari', 'safari']; +var knownBrowsers = [ + 'android', + 'chrome', + 'chromium', + 'firefox', + 'ie', + 'mobileSafari', + 'safari' +]; + +var browsersEnabledByDefault = [ + 'android', + 'chrome', + 'chromium', + 'ie', + 'mobileSafari', + 'safari' +]; var enabledBrowsers = {}; _.each(browsersEnabledByDefault, function (browser) { From 3e7d38ee9ad0b0d127614c330d6b226616196e37 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Wed, 17 Apr 2013 08:50:44 -0400 Subject: [PATCH 056/102] also add Chromium to the list of browsers in the docs --- docs/client/packages/appcache.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client/packages/appcache.html b/docs/client/packages/appcache.html index 35e94c89025..fd7591618f7 100644 --- a/docs/client/packages/appcache.html +++ b/docs/client/packages/appcache.html @@ -44,7 +44,7 @@ }); The supported browsers that can be enabled or disabled are `android`, -`chrome`, `firefox`, `ie`, `mobileSafari` and `safari`. +`chrome`, `chromium`, `firefox`, `ie`, `mobileSafari` and `safari`. Browsers limit the amount of data they will put in the application cache, which can vary due to factors such as how much disk space is From 734a82fa5842674d0d112322c006e688cdcb8ff7 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 25 Apr 2013 16:51:20 -0700 Subject: [PATCH 057/102] HISTORY update. --- History.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index b022deda8c5..c7ea3d58083 100644 --- a/History.md +++ b/History.md @@ -18,7 +18,9 @@ `localstorage` package, which defines a `Meteor._localStorage` API instead of trying to replace the DOM `window.localStorage` facility. (Now, apps can use the existence of `window.localStorage` to detect if the full localStorage API - is supported.) + is supported.) #979 + +* Support `appcache` on Chromium. #958 Patches contributed by GitHub user awwx. From e6ff44477b74f9fd7d0d335a0065f488ab173848 Mon Sep 17 00:00:00 2001 From: Christine Spang Date: Wed, 17 Apr 2013 17:28:11 -0400 Subject: [PATCH 058/102] Add support for arbitrary headers to email package. Ordering is not preserved since we're using a dictionary, but header ordering doesn't matter anyway. --- packages/email/email.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/email/email.js b/packages/email/email.js index d092305e68d..985bdd4794c 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -85,12 +85,12 @@ var smtpSend = function (mc) { * @param options.subject {String} RFC5322 "Subject:" line * @param options.text {String} RFC5322 mail body (plain text) * @param options.html {String} RFC5322 mail body (HTML) + * @param options.headers {Object} custom RFC5322 headers (dictionary) */ Email.send = function (options) { var mc = new MailComposer(); // setup message data - // XXX support arbitrary headers // XXX support attachments (once we have a client/server-compatible binary // Buffer class) mc.setMessageOption({ @@ -104,6 +104,10 @@ Email.send = function (options) { html: options.html }); + if (options.headers !== undefined) + for (var header in options.headers) + mc.addHeader(header, options.headers[header]); + maybeMakePool(); if (smtpPool) { From 0b74f9b8fa0d6f679932de9114cacbaeba9de9c8 Mon Sep 17 00:00:00 2001 From: Christine Spang Date: Wed, 17 Apr 2013 22:19:07 -0400 Subject: [PATCH 059/102] Make email tests include a custom header. I'd like to test that adding multiple headers is working fine too, but the current tests are already order-dependent. Fixing this properly would probably involve the tests depending on something like mailparser. --- packages/email/email_tests.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/email/email_tests.js b/packages/email/email_tests.js index 8327ce8aa8d..b6fcf3026fa 100644 --- a/packages/email/email_tests.js +++ b/packages/email/email_tests.js @@ -14,7 +14,8 @@ Tinytest.add("email - dev mode smoke test", function (test) { to: "bar@example.com", cc: ["friends@example.com", "enemies@example.com"], subject: "This is the subject", - text: "This is the body\nof the message\nFrom us." + text: "This is the body\nof the message\nFrom us.", + headers: {'X-Meteor-Test': 'a custom header'} }); // Note that we use the local "stream" here rather than Email._output_stream // in case a concurrent test run mutates Email._output_stream too. @@ -22,6 +23,7 @@ Tinytest.add("email - dev mode smoke test", function (test) { test.equal(stream.getContentsAsString("utf8"), "====== BEGIN MAIL #0 ======\n" + "MIME-Version: 1.0\r\n" + + "X-Meteor-Test: a custom header\r\n" + "From: foo@example.com\r\n" + "To: bar@example.com\r\n" + "Cc: friends@example.com, enemies@example.com\r\n" + From 17d81e64bc94da404d00cba2cc5291a3f9a40496 Mon Sep 17 00:00:00 2001 From: Christine Spang Date: Wed, 17 Apr 2013 23:14:23 -0400 Subject: [PATCH 060/102] Update documentation for Email.send. --- docs/client/api.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/client/api.js b/docs/client/api.js index cc0b3266f2d..7f6bea72c25 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1804,6 +1804,10 @@ Template.api.email_send = { {name: "html", type: "String", descr: rfc('mail body (HTML)') + }, + {name: "headers", + type: "Object", + descr: rfc('custom headers (dictionary)') } ] }; From bd0b88caed7b8e03da4948deb97a935d963f6e94 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 25 Apr 2013 17:06:06 -0700 Subject: [PATCH 061/102] Use underscore (to avoid prototype issues). --- packages/email/email.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/email/email.js b/packages/email/email.js index 985bdd4794c..497ba517010 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -104,9 +104,9 @@ Email.send = function (options) { html: options.html }); - if (options.headers !== undefined) - for (var header in options.headers) - mc.addHeader(header, options.headers[header]); + _.each(options.headers, function (value, name) { + mc.addHeader(name, value); + }); maybeMakePool(); From ed4a962655b6b87433bb87995807335fc993db39 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 25 Apr 2013 17:07:16 -0700 Subject: [PATCH 062/102] update HISTORY --- History.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index c7ea3d58083..bcb2f238c38 100644 --- a/History.md +++ b/History.md @@ -14,6 +14,8 @@ * Upgrade CoffeeScript from 1.5.0 to 1.6.2. #972 +* `Email.send` has a new `headers` option to set arbitrary headers. #963 + * The `localstorage-polyfill` smart package has been replaced by a `localstorage` package, which defines a `Meteor._localStorage` API instead of trying to replace the DOM `window.localStorage` facility. (Now, apps can use @@ -22,7 +24,7 @@ * Support `appcache` on Chromium. #958 -Patches contributed by GitHub user awwx. +Patches contributed by GitHub users awwx and spang. ## v0.6.2.1 From c69ab5d350dae66672558056ebeab8dec8c37554 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 25 Apr 2013 12:48:02 -0700 Subject: [PATCH 063/102] Include response in HTTP 4xx/5xx error messages --- packages/accounts-facebook/facebook_server.js | 8 ++++---- packages/accounts-github/github_server.js | 8 ++++---- packages/accounts-google/google_server.js | 8 ++++---- packages/accounts-meetup/meetup_server.js | 10 +++++----- packages/accounts-oauth1-helper/oauth1_binding.js | 4 ++-- packages/accounts-weibo/weibo_server.js | 8 ++++---- packages/http/httpcall_client.js | 4 ++-- packages/http/httpcall_common.js | 12 ++++++++++++ packages/http/httpcall_server.js | 4 ++-- packages/http/httpcall_tests.js | 10 ++++++++++ packages/http/test_responder.js | 3 ++- 11 files changed, 51 insertions(+), 28 deletions(-) diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 0f32e081ce4..8a78b9970c7 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -68,8 +68,8 @@ var getTokenResponse = function (query) { } }).content; } catch (err) { - throw new Error("Failed to complete OAuth handshake with Facebook. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error completing OAuth handshake with Facebook:"); + throw err; } // If 'responseContent' parses as JSON, it is an error. @@ -99,7 +99,7 @@ var getIdentity = function (accessToken) { return Meteor.http.get("https://graph.facebook.com/me", { params: {access_token: accessToken}}).data; } catch (err) { - throw new Error("Failed to fetch identity from Facebook. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error fetching identity from Facebook:"); + throw err; } }; diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index 84ab5bab8b1..b4eae1a5a63 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -49,8 +49,8 @@ var getAccessToken = function (query) { } }); } catch (err) { - throw new Error("Failed to complete OAuth handshake with GitHub. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error completing OAuth handshake with Github:"); + throw err; } if (response.data.error) { // if the http response was a json object with an error attribute throw new Error("Failed to complete OAuth handshake with GitHub. " + response.data.error); @@ -67,7 +67,7 @@ var getIdentity = function (accessToken) { params: {access_token: accessToken} }).data; } catch (err) { - throw new Error("Failed to fetch identity from GitHub. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error fetching identity from GitHub:"); + throw err; } }; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 9ccb3b1c962..3535dfe184e 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -64,8 +64,8 @@ var getTokens = function (query) { grant_type: 'authorization_code' }}); } catch (err) { - throw new Error("Failed to complete OAuth handshake with Google. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error completing OAuth handshake with Google:"); + throw err; } if (response.data.error) { // if the http response was a json object with an error attribute @@ -85,7 +85,7 @@ var getIdentity = function (accessToken) { "https://www.googleapis.com/oauth2/v1/userinfo", {params: {access_token: accessToken}}).data; } catch (err) { - throw new Error("Failed to fetch identity from Google. " + - err + (err.response ? ": " + err.response.content : "")); + console.log("Error fetching identity from Google: "); + throw err; } }; diff --git a/packages/accounts-meetup/meetup_server.js b/packages/accounts-meetup/meetup_server.js index cab41746bbb..ce387122ccc 100644 --- a/packages/accounts-meetup/meetup_server.js +++ b/packages/accounts-meetup/meetup_server.js @@ -38,12 +38,12 @@ var getAccessToken = function (query) { state: query.state }}); } catch (err) { - throw new Error("Failed to complete OAuth handshake with Meetup. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error completing OAuth handshake with Meetup:"); + throw err; } if (response.data.error) { // if the http response was a json object with an error attribute - throw new Error("Failed to complete OAuth handshake with Meetup. " + response.data.error); + throw new Error("Error completeing OAuth handshake with Meetup. " + response.data.error); } else { return response.data.access_token; } @@ -56,7 +56,7 @@ var getIdentity = function (accessToken) { {params: {member_id: 'self', access_token: accessToken}}); return response.data.results && response.data.results[0]; } catch (err) { - throw new Error("Failed to fetch identity from Meetup. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error fetching identity from Meetup:"); + throw err; } }; diff --git a/packages/accounts-oauth1-helper/oauth1_binding.js b/packages/accounts-oauth1-helper/oauth1_binding.js index 416e0ef9f4b..2657a56e1a7 100644 --- a/packages/accounts-oauth1-helper/oauth1_binding.js +++ b/packages/accounts-oauth1-helper/oauth1_binding.js @@ -124,8 +124,8 @@ OAuth1Binding.prototype._call = function(method, url, headers, params) { } }); } catch (err) { - throw new Error("Failed to send OAuth1 http request to " + url + ". " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error sending OAuth1 request to " + url + ":"); + throw err; } }; diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index f8e9a1021b5..8d3d631e2fc 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -51,8 +51,8 @@ var getTokenResponse = function (query) { grant_type: 'authorization_code' }}); } catch (err) { - throw new Error("Failed to complete OAuth handshake with Weibo. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error completing OAuth handshake with Weibo:"); + throw err; } // result.headers["content-type"] is 'text/plain;charset=UTF-8', so @@ -72,7 +72,7 @@ var getIdentity = function (accessToken, userId) { "https://api.weibo.com/2/users/show.json", {params: {access_token: accessToken, uid: userId}}).data; } catch (err) { - throw new Error("Failed to fetch identity from Weibo. " + - err + (err.response ? ": " + err.response.content : "")); + console.error("Error fetching identity from Weibo:"); + throw err; } }; diff --git a/packages/http/httpcall_client.js b/packages/http/httpcall_client.js index 3fab89b3ea1..96c1e8c0bb4 100644 --- a/packages/http/httpcall_client.js +++ b/packages/http/httpcall_client.js @@ -149,8 +149,8 @@ Meteor.http.call = function(method, url, options, callback) { Meteor.http._populateData(response); var error = null; - if (xhr.status >= 400) - error = new Error("failed"); + if (response.statusCode >= 400) + error = Meteor.http._makeErrorByStatus(response.statusCode, response.content); callback(error, response); } diff --git a/packages/http/httpcall_common.js b/packages/http/httpcall_common.js index b9974596d25..472caae0d7f 100644 --- a/packages/http/httpcall_common.js +++ b/packages/http/httpcall_common.js @@ -1,6 +1,18 @@ Meteor.http = Meteor.http || {}; +Meteor.http._makeErrorByStatus = function(statusCode, content) { + var truncate = function(str, length) { + return str.length > length ? str.slice(0, length) + '...' : str; + }; + + var message = "failed [" + statusCode + "]"; + if (content) + message += " " + truncate(content, 140); + + return new Error(message); +}; + Meteor.http._encodeParams = function(params) { var buf = []; _.each(params, function(value, key) { diff --git a/packages/http/httpcall_server.js b/packages/http/httpcall_server.js index a2de5247859..298a95b2c9f 100644 --- a/packages/http/httpcall_server.js +++ b/packages/http/httpcall_server.js @@ -112,8 +112,8 @@ Meteor.http.call = function(method, url, options, callback) { Meteor.http._populateData(response); - if (res.statusCode >= 400) - error = new Error("Failed [" + res.statusCode + "]"); + if (response.statusCode >= 400) + error = Meteor.http._makeErrorByStatus(response.statusCode, response.content); } callback(error, response); diff --git a/packages/http/httpcall_tests.js b/packages/http/httpcall_tests.js index b9d72202d23..53beffbe623 100644 --- a/packages/http/httpcall_tests.js +++ b/packages/http/httpcall_tests.js @@ -95,10 +95,20 @@ testAsyncMulti("httpcall - errors", [ // Server serves 500 error500Callback = function(error, result) { test.isTrue(error); + test.isTrue(error.message.indexOf("500") !== -1); // message has statusCode + test.isTrue(error.message.indexOf( + error.response.content.substring(0, 10)) !== -1); // message has part of content + test.isTrue(result); test.isTrue(error.response); test.equal(result, error.response); test.equal(error.response.statusCode, 500); + + // in test_responder.js we make a very long response body, to make sure + // that we truncate messages. first of all, make sure we didn't make that + // message too short, so that we can be sure we're verifying that we truncate. + test.isTrue(error.response.content.length > 160); + test.isTrue(error.message.length < 160); // make sure we truncate. }; Meteor.http.call("GET", url_prefix()+"/fail", expect(error500Callback)); diff --git a/packages/http/test_responder.js b/packages/http/test_responder.js index d2f5ec6b901..f6edb3d0cb1 100644 --- a/packages/http/test_responder.js +++ b/packages/http/test_responder.js @@ -10,7 +10,8 @@ var respond = function(req, res) { return; } else if (req.url === "/fail") { res.statusCode = 500; - res.end("SOME SORT OF SERVER ERROR"); + res.end("SOME SORT OF SERVER ERROR. " + + "MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. MAKE THIS VERY LONG TO MAKE SURE WE TRUNCATE. "); return; } else if (req.url === "/redirect") { res.statusCode = 301; From 5d04576b914e3fea8a3f1c4dd336408c07844319 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 25 Apr 2013 14:04:26 -0700 Subject: [PATCH 064/102] HTTP error messages don't have newlines --- packages/http/httpcall_common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http/httpcall_common.js b/packages/http/httpcall_common.js index 472caae0d7f..a11a927d6fb 100644 --- a/packages/http/httpcall_common.js +++ b/packages/http/httpcall_common.js @@ -8,7 +8,7 @@ Meteor.http._makeErrorByStatus = function(statusCode, content) { var message = "failed [" + statusCode + "]"; if (content) - message += " " + truncate(content, 140); + message += " " + truncate(content.replace(/\n/g, " "), 140); return new Error(message); }; From be8dc471139662ea18a771d36017fa8c50252881 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 30 Apr 2013 11:42:46 -0700 Subject: [PATCH 065/102] Better error rethrowing --- packages/accounts-facebook/facebook_server.js | 6 ++---- packages/accounts-github/github_server.js | 6 ++---- packages/accounts-google/google_server.js | 6 ++---- packages/accounts-meetup/meetup_server.js | 8 +++----- packages/accounts-oauth1-helper/oauth1_binding.js | 3 +-- packages/accounts-weibo/weibo_server.js | 6 ++---- packages/http/httpcall_common.js | 4 +++- packages/http/httpcall_tests.js | 4 ++-- 8 files changed, 17 insertions(+), 26 deletions(-) diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 8a78b9970c7..7b3092ceafe 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -68,8 +68,7 @@ var getTokenResponse = function (query) { } }).content; } catch (err) { - console.error("Error completing OAuth handshake with Facebook:"); - throw err; + throw new Error("Failed to complete OAuth handshake with Facebook. " + err.message); } // If 'responseContent' parses as JSON, it is an error. @@ -99,7 +98,6 @@ var getIdentity = function (accessToken) { return Meteor.http.get("https://graph.facebook.com/me", { params: {access_token: accessToken}}).data; } catch (err) { - console.error("Error fetching identity from Facebook:"); - throw err; + throw new Error("Failed to fetch identity from Facebook. " + err.message); } }; diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index b4eae1a5a63..0eabf024973 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -49,8 +49,7 @@ var getAccessToken = function (query) { } }); } catch (err) { - console.error("Error completing OAuth handshake with Github:"); - throw err; + throw new Error("Failed to complete OAuth handshake with Github. " + err.message); } if (response.data.error) { // if the http response was a json object with an error attribute throw new Error("Failed to complete OAuth handshake with GitHub. " + response.data.error); @@ -67,7 +66,6 @@ var getIdentity = function (accessToken) { params: {access_token: accessToken} }).data; } catch (err) { - console.error("Error fetching identity from GitHub:"); - throw err; + throw new Error("Failed to fetch identity from GitHub. " + err.message); } }; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 3535dfe184e..4d4925e47d0 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -64,8 +64,7 @@ var getTokens = function (query) { grant_type: 'authorization_code' }}); } catch (err) { - console.error("Error completing OAuth handshake with Google:"); - throw err; + throw new Error("Failed to complete OAuth handshake with Google. " + err.message); } if (response.data.error) { // if the http response was a json object with an error attribute @@ -85,7 +84,6 @@ var getIdentity = function (accessToken) { "https://www.googleapis.com/oauth2/v1/userinfo", {params: {access_token: accessToken}}).data; } catch (err) { - console.log("Error fetching identity from Google: "); - throw err; + throw new Error("Failed to fetch identity from Google. " + err.message); } }; diff --git a/packages/accounts-meetup/meetup_server.js b/packages/accounts-meetup/meetup_server.js index ce387122ccc..907d04725a7 100644 --- a/packages/accounts-meetup/meetup_server.js +++ b/packages/accounts-meetup/meetup_server.js @@ -38,12 +38,11 @@ var getAccessToken = function (query) { state: query.state }}); } catch (err) { - console.error("Error completing OAuth handshake with Meetup:"); - throw err; + throw new Error("Failed to complete OAuth handshake with Meetup. " + err.message); } if (response.data.error) { // if the http response was a json object with an error attribute - throw new Error("Error completeing OAuth handshake with Meetup. " + response.data.error); + throw new Error("Failed to complete OAuth handshake with Meetup. " + response.data.error); } else { return response.data.access_token; } @@ -56,7 +55,6 @@ var getIdentity = function (accessToken) { {params: {member_id: 'self', access_token: accessToken}}); return response.data.results && response.data.results[0]; } catch (err) { - console.error("Error fetching identity from Meetup:"); - throw err; + throw new Error("Failed to fetch identity from Meetup: " + err.message); } }; diff --git a/packages/accounts-oauth1-helper/oauth1_binding.js b/packages/accounts-oauth1-helper/oauth1_binding.js index 2657a56e1a7..59c0f2b211b 100644 --- a/packages/accounts-oauth1-helper/oauth1_binding.js +++ b/packages/accounts-oauth1-helper/oauth1_binding.js @@ -124,8 +124,7 @@ OAuth1Binding.prototype._call = function(method, url, headers, params) { } }); } catch (err) { - console.error("Error sending OAuth1 request to " + url + ":"); - throw err; + throw new Error("Failed to send OAuth1 request to " + url + ". " + err.message); } }; diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 8d3d631e2fc..5ef6a39d42d 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -51,8 +51,7 @@ var getTokenResponse = function (query) { grant_type: 'authorization_code' }}); } catch (err) { - console.error("Error completing OAuth handshake with Weibo:"); - throw err; + throw new Error("Failed to complete OAuth handshake with Weibo. " + err.message); } // result.headers["content-type"] is 'text/plain;charset=UTF-8', so @@ -72,7 +71,6 @@ var getIdentity = function (accessToken, userId) { "https://api.weibo.com/2/users/show.json", {params: {access_token: accessToken, uid: userId}}).data; } catch (err) { - console.error("Error fetching identity from Weibo:"); - throw err; + throw new Error("Failed to fetch identity from Weibo. " + err.message); } }; diff --git a/packages/http/httpcall_common.js b/packages/http/httpcall_common.js index a11a927d6fb..11bb1cfc757 100644 --- a/packages/http/httpcall_common.js +++ b/packages/http/httpcall_common.js @@ -2,13 +2,15 @@ Meteor.http = Meteor.http || {}; Meteor.http._makeErrorByStatus = function(statusCode, content) { + var MAX_LENGTH = 160; // if you change this, also change the appropriate test + var truncate = function(str, length) { return str.length > length ? str.slice(0, length) + '...' : str; }; var message = "failed [" + statusCode + "]"; if (content) - message += " " + truncate(content.replace(/\n/g, " "), 140); + message += " " + truncate(content.replace(/\n/g, " "), MAX_LENGTH); return new Error(message); }; diff --git a/packages/http/httpcall_tests.js b/packages/http/httpcall_tests.js index 53beffbe623..7ad2ec02831 100644 --- a/packages/http/httpcall_tests.js +++ b/packages/http/httpcall_tests.js @@ -107,8 +107,8 @@ testAsyncMulti("httpcall - errors", [ // in test_responder.js we make a very long response body, to make sure // that we truncate messages. first of all, make sure we didn't make that // message too short, so that we can be sure we're verifying that we truncate. - test.isTrue(error.response.content.length > 160); - test.isTrue(error.message.length < 160); // make sure we truncate. + test.isTrue(error.response.content.length > 180); + test.isTrue(error.message.length < 180); // make sure we truncate. }; Meteor.http.call("GET", url_prefix()+"/fail", expect(error500Callback)); From 91c273fa417a30a1012451e3fb2bbd50270a60d5 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Thu, 2 May 2013 16:38:44 -0700 Subject: [PATCH 066/102] Fix server synch queue drain to work like client one --- packages/meteor/fiber_helpers.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/meteor/fiber_helpers.js b/packages/meteor/fiber_helpers.js index e1b0778c316..9d0ce4180db 100644 --- a/packages/meteor/fiber_helpers.js +++ b/packages/meteor/fiber_helpers.js @@ -52,6 +52,11 @@ Meteor._SynchronousQueue = function () { // that task. We use this to throw an error rather than deadlocking if the // user calls runTask from within a task on the same fiber. self._currentTaskFiber = undefined; + // This is true if we're currently draining. While we're draining, a further + // drain is a noop, to prevent infinite loops. "drain" is a heuristic type + // operation, that has a meaning like unto "what a naive person would expect + // when modifying a table from an observe" + self._draining = false; }; _.extend(Meteor._SynchronousQueue.prototype, { @@ -91,11 +96,15 @@ _.extend(Meteor._SynchronousQueue.prototype, { drain: function () { var self = this; + if (self._draining) + return; if (!self.safeToRunTask()) return; + self._draining = true; while (!_.isEmpty(self._taskHandles)) { self.flush(); } + self._draining = false; }, _scheduleRun: function () { From cc57b443d4214ff876fbf26c2d3fd5816cc76de8 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 3 May 2013 17:10:33 -0700 Subject: [PATCH 067/102] Bump mongo version to 2.4 --- meteor | 2 +- scripts/generate-dev-bundle.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor b/meteor index df3adf39f1a..513e1dcd19e 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.3.0 +BUNDLE_VERSION=0.3.1 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/scripts/generate-dev-bundle.sh b/scripts/generate-dev-bundle.sh index 7c1e7fb6d6c..55f9abca6e6 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -145,7 +145,7 @@ cd ../.. # click 'changelog' under the current version, then 'release notes' in # the upper right. cd "$DIR" -MONGO_VERSION="2.2.1" +MONGO_VERSION="2.4.3" MONGO_NAME="mongodb-${MONGO_OS}-${ARCH}-${MONGO_VERSION}" MONGO_URL="http://fastdl.mongodb.org/${MONGO_OS}/${MONGO_NAME}.tgz" curl "$MONGO_URL" | tar -xz From 310defcefc4765998bf7638871c09d532ff9a4a4 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 3 May 2013 17:13:19 -0700 Subject: [PATCH 068/102] Bump dev bundle number higher to avoid versions that were taken but not merged. --- meteor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor b/meteor index 513e1dcd19e..20d4053a4d5 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.3.1 +BUNDLE_VERSION=0.3.3 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. From 9e0e7e1138776b0452f2b2ad946dcdc6a6b0045c Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 3 May 2013 17:35:38 -0700 Subject: [PATCH 069/102] Fix typo in springboard test script that caused test to fail --- scripts/tools-springboard-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tools-springboard-test.sh b/scripts/tools-springboard-test.sh index 6f8d26c9985..9d0c196b772 100755 --- a/scripts/tools-springboard-test.sh +++ b/scripts/tools-springboard-test.sh @@ -12,4 +12,4 @@ METEOR="$METEOR_TOOLS_TREE_DIR/bin/meteor" # This release was built from the # 'release/release-used-to-test-springboarding' tag in GitHub. All it # does is print this string and exit. -$METEOR --release release/used-to-test-springboarding | grep "THIS IS A FAKE RELEASE ONLY USED TO TEST ENGINE SPRINGBOARDING" +$METEOR --release release-used-to-test-springboarding | grep "THIS IS A FAKE RELEASE ONLY USED TO TEST ENGINE SPRINGBOARDING" From 028062bef4ba263c792315fdc96b02f15502f67a Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 6 May 2013 12:12:46 -0700 Subject: [PATCH 070/102] Minor improvements to accounts-facebook --- packages/accounts-facebook/facebook_configure.html | 3 +++ packages/accounts-oauth-helper/oauth_client.js | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/accounts-facebook/facebook_configure.html b/packages/accounts-facebook/facebook_configure.html index aa0344a8c97..223b8adf2b3 100644 --- a/packages/accounts-facebook/facebook_configure.html +++ b/packages/accounts-facebook/facebook_configure.html @@ -9,6 +9,9 @@
  • Create New App (Only a name is required.)
  • +
  • + Set "Sandbox Mode" to "Disabled" +
  • Under "Select how your app integrates with Facebook", expand "Website with Facebook Login".
  • diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index c8b7082622f..42944a23568 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -8,9 +8,7 @@ // @param dimensions {optional Object(width, height)} The dimensions of // the popup. If not passed defaults to something sane Accounts.oauth.initiateLogin = function(state, url, callback, dimensions) { - // XXX these dimensions worked well for facebook and google, but - // it's sort of weird to have these here. Maybe an optional - // argument instead? + // default dimensions that worked well for facebook and google var popup = openCenteredPopup( url, (dimensions && dimensions.width) || 650, From 98c5efd8f16b92bf0855fbf43016a7475ee33943 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 6 May 2013 12:13:02 -0700 Subject: [PATCH 071/102] Prerequisites for python-ddp-client --- examples/unfinished/python-ddp-client/README | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/unfinished/python-ddp-client/README diff --git a/examples/unfinished/python-ddp-client/README b/examples/unfinished/python-ddp-client/README new file mode 100644 index 00000000000..44071bac3b6 --- /dev/null +++ b/examples/unfinished/python-ddp-client/README @@ -0,0 +1 @@ +sudo easy_install ws4py From 3952be8183864360621d8c31d340a7fc733ee99d Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 6 May 2013 15:37:17 -0700 Subject: [PATCH 072/102] Fix EJSON base64 decoding when 'A' is 3rd or 4th char in a block. Fixes #1001. --- History.md | 2 ++ packages/ejson/base64.js | 4 ++-- packages/ejson/base64_test.js | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index bcb2f238c38..8ef12e7cf9c 100644 --- a/History.md +++ b/History.md @@ -22,6 +22,8 @@ the existence of `window.localStorage` to detect if the full localStorage API is supported.) #979 +* Fix EJSON base64 decoding bug. #1001 + * Support `appcache` on Chromium. #958 Patches contributed by GitHub users awwx and spang. diff --git a/packages/ejson/base64.js b/packages/ejson/base64.js index b34d626e47f..1d4bcdef7b3 100644 --- a/packages/ejson/base64.js +++ b/packages/ejson/base64.js @@ -106,14 +106,14 @@ EJSON._base64Decode = function (str) { two = (v & 0x0F) << 4; break; case 2: - if (v > 0) { + if (v >= 0) { two = two | (v >> 2); arr[j++] = two; three = (v & 0x03) << 6; } break; case 3: - if (v > 0) { + if (v >= 0) { arr[j++] = three | v; } break; diff --git a/packages/ejson/base64_test.js b/packages/ejson/base64_test.js index 75b22e48cda..09401de0fbd 100644 --- a/packages/ejson/base64_test.js +++ b/packages/ejson/base64_test.js @@ -42,3 +42,18 @@ Tinytest.add("base64 - wikipedia examples", function (test) { test.equal(arrayToAscii(EJSON._base64Decode(t.res)), t.txt); }); }); + +Tinytest.add("base64 - non-text examples", function (test) { + var tests = [ + {array: [0, 0, 0], b64: "AAAA"}, + {array: [0, 0, 1], b64: "AAAB"} + ]; + _.each(tests, function(t) { + test.equal(EJSON._base64Encode(t.array), t.b64); + var expectedAsBinary = EJSON.newBinary(t.array.length); + _.each(t.array, function (val, i) { + expectedAsBinary[i] = val; + }); + test.equal(EJSON._base64Decode(t.b64), expectedAsBinary); + }); +}); From c39e44e6402ed410cd370eba62ca2a214098881b Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 6 May 2013 15:50:42 -0700 Subject: [PATCH 073/102] Reorder test output to have name on the right and drop fixed width. Now long test names don't wrap. --- packages/test-in-browser/driver.css | 6 +++--- packages/test-in-browser/driver.html | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/test-in-browser/driver.css b/packages/test-in-browser/driver.css index c1a79401166..2913c17355a 100644 --- a/packages/test-in-browser/driver.css +++ b/packages/test-in-browser/driver.css @@ -33,7 +33,6 @@ body { .test_table { font-family: Arial, sans-serif; - width: 500px; font-size: 16px; } @@ -50,6 +49,7 @@ body { } .test_table .testname { + margin-left: 200px; line-height: 24px; vertical-align: middle; text-decoration: underline; @@ -84,7 +84,7 @@ body { position: absolute; height: 100%; width: 100px; - right: 80px; + left: 0px; top: 0; text-align: center; line-height: 24px; @@ -96,7 +96,7 @@ body { height: 100%; width: 75px; margin-right: 5px; - right: 0; + left: 100px; top: 0; text-align: right; line-height: 24px; diff --git a/packages/test-in-browser/driver.html b/packages/test-in-browser/driver.html index cc8766936d7..7d1c8e20f6c 100644 --- a/packages/test-in-browser/driver.html +++ b/packages/test-in-browser/driver.html @@ -100,16 +100,16 @@