From 8bf2e0edae455d1b9b6729c4cad713a36db17ef5 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 13 May 2013 18:16:14 -0700 Subject: [PATCH 01/60] More gitignore files. --- packages/audit-argument-checks/.gitignore | 1 + packages/coffeescript/.gitignore | 1 + packages/less/.gitignore | 1 + packages/localstorage/.gitignore | 1 + packages/stylus/.gitignore | 1 + 5 files changed, 5 insertions(+) create mode 100644 packages/audit-argument-checks/.gitignore create mode 100644 packages/coffeescript/.gitignore create mode 100644 packages/less/.gitignore create mode 100644 packages/localstorage/.gitignore create mode 100644 packages/stylus/.gitignore diff --git a/packages/audit-argument-checks/.gitignore b/packages/audit-argument-checks/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/audit-argument-checks/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/coffeescript/.gitignore b/packages/coffeescript/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/coffeescript/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/less/.gitignore b/packages/less/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/less/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/localstorage/.gitignore b/packages/localstorage/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/localstorage/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/stylus/.gitignore b/packages/stylus/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/stylus/.gitignore @@ -0,0 +1 @@ +.build* From bc2be8bb58a40bb99322f2bc5545a5e4338b23d6 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Sun, 12 May 2013 01:31:39 -0700 Subject: [PATCH 02/60] Our version of underscore doesn't have 'partial' anymore since we downgraded. Missed one instance of this because I missed arrays in the EJSON support in mongo testing. --- packages/mongo-livedata/mongo_driver.js | 2 +- packages/mongo-livedata/mongo_livedata_tests.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/mongo-livedata/mongo_driver.js b/packages/mongo-livedata/mongo_driver.js index 22556dbee0..1922570c4e 100644 --- a/packages/mongo-livedata/mongo_driver.js +++ b/packages/mongo-livedata/mongo_driver.js @@ -15,7 +15,7 @@ var Future = Npm.require(path.join('fibers', 'future')); var replaceNames = function (filter, thing) { if (typeof thing === "object") { if (_.isArray(thing)) { - return _.map(thing, _.partial(replaceNames, filter)); + return _.map(thing, _.bind(replaceNames, null, filter)); } var ret = {}; _.each(thing, function (value, key) { diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index 84efed85f1..948a7962bd 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -27,22 +27,24 @@ Meteor._FailureTestCollection = new Meteor.Collection("___meteor_failure_test_collection"); // For test "document with a custom type" -var Dog = function (name, color) { +var Dog = function (name, color, actions) { var self = this; self.color = color; self.name = name; + self.actions = actions || [{name: "wag"}, {name: "swim"}]; }; _.extend(Dog.prototype, { getName: function () { return this.name;}, getColor: function () { return this.name;}, equals: function (other) { return other.name === this.name && - other.color === this.color; }, - toJSONValue: function () { return {color: this.color, name: this.name};}, + other.color === this.color && + EJSON.equals(other.actions, this.actions);}, + toJSONValue: function () { return {color: this.color, name: this.name, actions: this.actions};}, typeName: function () { return "dog"; }, clone: function () { return new Dog(this.name, this.color); }, speak: function () { return "woof"; } }); -EJSON.addType("dog", function (o) { return new Dog(o.name, o.color);}); +EJSON.addType("dog", function (o) { return new Dog(o.name, o.color, o.actions);}); // Parameterize tests. From 773a86c80c80ab3bb83b51b926ce3b2498d390d5 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 13 May 2013 21:41:38 -0700 Subject: [PATCH 03/60] Support 'tap' event. --- docs/client/api.html | 6 ++++++ packages/universal-events/events-w3c.js | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/client/api.html b/docs/client/api.html index f6354bdac7..7fcdbde1fc 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2213,6 +2213,12 @@ The user presses a keyboard key. `keypress` is most useful for catching typing in text fields, while `keydown` and `keyup` can be used for arrow keys or modifier keys. {{/dtdd}} + +{{#dtdd "tap"}} Tap on an element. On touch-enabled +devices, this is a replacement to `click` that fires immediately. +These events are synthesized from `touchmove` and `touchend`. +{{/dtdd}} + Other DOM events are available as well, but for the events above, diff --git a/packages/universal-events/events-w3c.js b/packages/universal-events/events-w3c.js index acdaa040d7..466e6c85ae 100644 --- a/packages/universal-events/events-w3c.js +++ b/packages/universal-events/events-w3c.js @@ -85,6 +85,11 @@ _.extend(UniversalEventListener._impl.w3c.prototype, { ret.push('mouseout'); } + if (type === 'tap') { + ret.push('touchmove'); + ret.push('touchend'); + } + return ret; }, @@ -194,12 +199,22 @@ _.extend(UniversalEventListener._impl.w3c.prototype, { (event.currentTarget !== event.relatedTarget && ! DomUtils.elementContains( event.currentTarget, event.relatedTarget)))) { - if (event.type === 'mouseover'){ + if (event.type === 'mouseover') { sendUIEvent('mouseenter', event.currentTarget, false); } else if (event.type === 'mouseout') { sendUIEvent('mouseleave', event.currentTarget, false); } } + + if (event.type === 'touchmove') { + event.currentTarget._notTapping = true; + } + if (event.type === 'touchend') { + if (!event.currentTarget._notTapping) { + sendUIEvent('tap', event.currentTarget, true); + } + delete event.currentTarget._notTapping; + } } }); From d36de8253dee61b6e1a504a97233f5834621ab9d Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 13 May 2013 21:41:38 -0700 Subject: [PATCH 04/60] Support 'tap' event. --- docs/client/api.html | 6 ++++++ packages/universal-events/events-w3c.js | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/client/api.html b/docs/client/api.html index f6354bdac7..7fcdbde1fc 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2213,6 +2213,12 @@ The user presses a keyboard key. `keypress` is most useful for catching typing in text fields, while `keydown` and `keyup` can be used for arrow keys or modifier keys. {{/dtdd}} + +{{#dtdd "tap"}} Tap on an element. On touch-enabled +devices, this is a replacement to `click` that fires immediately. +These events are synthesized from `touchmove` and `touchend`. +{{/dtdd}} + Other DOM events are available as well, but for the events above, diff --git a/packages/universal-events/events-w3c.js b/packages/universal-events/events-w3c.js index acdaa040d7..466e6c85ae 100644 --- a/packages/universal-events/events-w3c.js +++ b/packages/universal-events/events-w3c.js @@ -85,6 +85,11 @@ _.extend(UniversalEventListener._impl.w3c.prototype, { ret.push('mouseout'); } + if (type === 'tap') { + ret.push('touchmove'); + ret.push('touchend'); + } + return ret; }, @@ -194,12 +199,22 @@ _.extend(UniversalEventListener._impl.w3c.prototype, { (event.currentTarget !== event.relatedTarget && ! DomUtils.elementContains( event.currentTarget, event.relatedTarget)))) { - if (event.type === 'mouseover'){ + if (event.type === 'mouseover') { sendUIEvent('mouseenter', event.currentTarget, false); } else if (event.type === 'mouseout') { sendUIEvent('mouseleave', event.currentTarget, false); } } + + if (event.type === 'touchmove') { + event.currentTarget._notTapping = true; + } + if (event.type === 'touchend') { + if (!event.currentTarget._notTapping) { + sendUIEvent('tap', event.currentTarget, true); + } + delete event.currentTarget._notTapping; + } } }); From 6063ca90ef9c24fbbaf1a48ecf5a85c3d36e65f6 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 14 May 2013 10:48:33 -0700 Subject: [PATCH 05/60] Oops, didn't mean to add this file --- packages/localstorage-polyfill/.gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/localstorage-polyfill/.gitignore diff --git a/packages/localstorage-polyfill/.gitignore b/packages/localstorage-polyfill/.gitignore deleted file mode 100644 index 677a6fc263..0000000000 --- a/packages/localstorage-polyfill/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.build* From 4c2e1ef1b6ab7232208d604c51dc8eca52e85084 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 14 May 2013 11:16:37 -0700 Subject: [PATCH 06/60] Fix Opera test failures by reverting unnecessary part of 583508e. Opera seems to have some consistent but difficult to diagnose bugs related to using _.map in this context. (As in, minifying the test was difficult because there seemed to be some odd action at a distance, but a given test failure was fully reproducible.) This appears to work while still preserving the feature added in 583508e. Also fix a missing var (which does not appear to be the original problem). --- packages/ejson/ejson.js | 8 +++++--- packages/ejson/ejson_test.js | 21 --------------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 02c7a8f603..68e5218e82 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -296,14 +296,16 @@ EJSON.clone = function (v) { return new Date(v.getTime()); if (EJSON.isBinary(v)) { ret = EJSON.newBinary(v.length); - for (i = 0; i < v.length; i++) { + for (var i = 0; i < v.length; i++) { ret[i] = v[i]; } return ret; } - // Clone arrays (and turn 'arguments' into an array). if (_.isArray(v) || _.isArguments(v)) { - return _.map(v, EJSON.clone); + ret = v.slice(0); + for (i = 0; i < v.length; i++) + ret[i] = EJSON.clone(ret[i]); + return ret; } // 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 bd3e5d6310..4f1e6b6ac6 100644 --- a/packages/ejson/ejson_test.js +++ b/packages/ejson/ejson_test.js @@ -51,24 +51,3 @@ 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 1fc2dabf6d0e837f847e7b32ca284348b1238ed5 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 14 May 2013 11:43:44 -0700 Subject: [PATCH 07/60] Oops, didn't mean to revert test! --- packages/ejson/ejson_test.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/ejson/ejson_test.js b/packages/ejson/ejson_test.js index 4f1e6b6ac6..bd3e5d6310 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 ded617f0c8eca7c7319f00a54cebe0d35fb0b99b Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 14 May 2013 11:16:37 -0700 Subject: [PATCH 08/60] Fix Opera test failures by reverting unnecessary part of 583508e. Opera seems to have some consistent but difficult to diagnose bugs related to using _.map in this context. (As in, minifying the test was difficult because there seemed to be some odd action at a distance, but a given test failure was fully reproducible.) This appears to work while still preserving the feature added in 583508e. Also fix a missing var (which does not appear to be the original problem). --- packages/ejson/ejson.js | 8 +++++--- packages/ejson/ejson_test.js | 21 --------------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 02c7a8f603..68e5218e82 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -296,14 +296,16 @@ EJSON.clone = function (v) { return new Date(v.getTime()); if (EJSON.isBinary(v)) { ret = EJSON.newBinary(v.length); - for (i = 0; i < v.length; i++) { + for (var i = 0; i < v.length; i++) { ret[i] = v[i]; } return ret; } - // Clone arrays (and turn 'arguments' into an array). if (_.isArray(v) || _.isArguments(v)) { - return _.map(v, EJSON.clone); + ret = v.slice(0); + for (i = 0; i < v.length; i++) + ret[i] = EJSON.clone(ret[i]); + return ret; } // 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 bd3e5d6310..4f1e6b6ac6 100644 --- a/packages/ejson/ejson_test.js +++ b/packages/ejson/ejson_test.js @@ -51,24 +51,3 @@ 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 3090e86c251690433b099e650052da646952d11e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 14 May 2013 11:43:44 -0700 Subject: [PATCH 09/60] Oops, didn't mean to revert test! --- packages/ejson/ejson_test.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/ejson/ejson_test.js b/packages/ejson/ejson_test.js index 4f1e6b6ac6..bd3e5d6310 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 1a3aa2531226c5465b28f032ed23c52af580f070 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 14 May 2013 12:32:47 -0700 Subject: [PATCH 10/60] Oops, you can't use slice on arguments. This time tests actually pass. --- packages/ejson/ejson.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 68e5218e82..9e031b7b97 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -302,9 +302,11 @@ EJSON.clone = function (v) { return ret; } if (_.isArray(v) || _.isArguments(v)) { - ret = v.slice(0); + // For some reason, _.map doesn't work in this context on Opera (weird test + // failures). + ret = []; for (i = 0; i < v.length; i++) - ret[i] = EJSON.clone(ret[i]); + ret[i] = EJSON.clone(v[i]); return ret; } // handle general user-defined typed Objects if they have a clone method From afe5867f6870c127d336e0debbd21c466300ca29 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 14 May 2013 12:32:47 -0700 Subject: [PATCH 11/60] Oops, you can't use slice on arguments. This time tests actually pass. --- packages/ejson/ejson.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 68e5218e82..9e031b7b97 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -302,9 +302,11 @@ EJSON.clone = function (v) { return ret; } if (_.isArray(v) || _.isArguments(v)) { - ret = v.slice(0); + // For some reason, _.map doesn't work in this context on Opera (weird test + // failures). + ret = []; for (i = 0; i < v.length; i++) - ret[i] = EJSON.clone(ret[i]); + ret[i] = EJSON.clone(v[i]); return ret; } // handle general user-defined typed Objects if they have a clone method From a14b278a29ab009c97c36bd34b2897429569ed9a Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 14 May 2013 14:14:07 -0700 Subject: [PATCH 12/60] Unbreak IE9 OAuth login in some weird edge case --- packages/accounts-oauth-helper/oauth_client.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index 42944a2356..9e66d1d456 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -15,10 +15,20 @@ Accounts.oauth.initiateLogin = function(state, url, callback, dimensions) { (dimensions && dimensions.height) || 331); var checkPopupOpen = setInterval(function() { - // Fix for #328 - added a second test criteria (popup.closed === undefined) - // to humour this Android quirk: - // http://code.google.com/p/android/issues/detail?id=21061 - if (popup.closed || popup.closed === undefined) { + try { + // Fix for #328 - added a second test criteria (popup.closed === undefined) + // to humour this Android quirk: + // http://code.google.com/p/android/issues/detail?id=21061 + var popupClosed = popup.closed || popup.closed === undefined; + } catch (e) { + // For some unknown reason, IE9 (and others?) sometimes (when + // the popup closes too quickly?) throws "SCRIPT16386: No such + // interface supported" when trying to read 'popup.closed'. Try + // again in 100ms. + return; + } + + if (popupClosed) { clearInterval(checkPopupOpen); tryLoginAfterPopupClosed(state, callback); } From 43548bc8c41d197e5ff1bdd78d802f8cf4b7169d Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 14 May 2013 22:58:17 -0700 Subject: [PATCH 13/60] History tweaks, notices, banner. --- History.md | 11 +++++++++-- scripts/admin/banner.txt | 7 ++++--- scripts/admin/notices.json | 9 ++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/History.md b/History.md index 4baa50e2d7..fe9e111e08 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,9 @@ ## vNEXT + +## v0.6.3 + * Add new `check` package for ensuring that a value matches a required type and structure. This is used to validate untrusted input from the client. See http://docs.meteor.com/#match for details. @@ -10,12 +13,15 @@ * With `autopublish` on, publish many useful fields on `Meteor.users`. -* Files in the 'client/compatibility/' subdirectory of a Meteor app do +* Files in the `client/compatibility/` subdirectory of a Meteor app do not get wrapped in a new variable scope. This is useful for third-party libraries which expect `var` statements at the outermost level to be global. -* When using the `http` package on the server synchronously, errors +* Add synthetic `tap` event for use on touch enabled devices. This is a + replacement for `click` that fires immediately. + +* When using the `http` package synchronously on the server, errors are thrown rather than passed in `result.error` * The `manager` option to the `Meteor.Collection` constructor is now called @@ -50,6 +56,7 @@ Patches contributed by GitHub users awwx, jagill, spang, and timhaines. + ## v0.6.2.1 * When authenticating with GitHub, include a user agent string. This diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index f9b37dc2f7..ffb83249ad 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -1,5 +1,6 @@ -=> Meteor 0.6.2.1 released: Unbreak 'Sign in with GitHub' by sending a - user agent string on API requests. +=> Meteor 0.6.3 released: Websockets on by default, new `check` API to + validate untrusted input, `tap` events, and more. + See https://github.com/meteor/meteor/blob/devel/History.md for details. This is being downloaded in the background. Update your project - to Meteor 0.6.2.1 by running 'meteor update'. + to Meteor 0.6.3 by running 'meteor update'. diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index 8f389ce64b..5ff9ffba27 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -21,7 +21,11 @@ "release": "0.6.2.1" }, { - "release": "NEXT", + "release": "0.6.3", + "notices": [ + "When using the `http` package synchronously on the server, errors", + "are now thrown rather than passed in `result.error`.", + ], "packageNotices": { "coffeescript": ["CoffeeScript has been updated to 1.6.2 from 1.5.0. See", "http://coffeescript.org/#changelog"], @@ -30,5 +34,8 @@ "package, which creates an object at Meteor._localStorage instead of", "pretending to be window.localStorage."] } + }, + { + "release": "NEXT" } ] From 93af3ab238c782209fb836ef38dd61bb006691a2 Mon Sep 17 00:00:00 2001 From: Matt DeBergalis Date: Wed, 15 May 2013 11:14:59 -0700 Subject: [PATCH 14/60] tweaks --- scripts/admin/banner.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index ffb83249ad..d57435b6fd 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -1,6 +1,7 @@ -=> Meteor 0.6.3 released: Websockets on by default, new `check` API to - validate untrusted input, `tap` events, and more. - See https://github.com/meteor/meteor/blob/devel/History.md for details. +=> Meteor 0.6.3 released: Default to WebSockets in modern browsers, new + `check` API to validate untrusted input, synthetic `tap` events, + update to MongoDB 2.4, and more. See + https://github.com/meteor/meteor/blob/devel/History.md for details. This is being downloaded in the background. Update your project to Meteor 0.6.3 by running 'meteor update'. From b237ace7de9401a28080d9816fac8f65ddb00a51 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 15 May 2013 11:35:14 -0700 Subject: [PATCH 15/60] Fix trailing comma that breaks JSON parsing. --- scripts/admin/notices.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index 5ff9ffba27..ff76c53e37 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -24,7 +24,7 @@ "release": "0.6.3", "notices": [ "When using the `http` package synchronously on the server, errors", - "are now thrown rather than passed in `result.error`.", + "are now thrown rather than passed in `result.error`." ], "packageNotices": { "coffeescript": ["CoffeeScript has been updated to 1.6.2 from 1.5.0. See", From 9386af8ac7b9ce0b1532bd8a785cecf1577b4d1e Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 15 May 2013 11:40:54 -0700 Subject: [PATCH 16/60] Move http notices to package specific notice. --- scripts/admin/notices.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index ff76c53e37..1f7eada8dc 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -22,17 +22,16 @@ }, { "release": "0.6.3", - "notices": [ - "When using the `http` package synchronously on the server, errors", - "are now thrown rather than passed in `result.error`." - ], "packageNotices": { "coffeescript": ["CoffeeScript has been updated to 1.6.2 from 1.5.0. See", "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."] + "pretending to be window.localStorage."], + "http": [ + "When using the `http` package synchronously on the server, errors", + "are now thrown rather than passed in `result.error`."] } }, { From 2fb44613ce6ee818408ce4020a07ace4483f1e72 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 15 May 2013 11:52:09 -0700 Subject: [PATCH 17/60] Bump example version numbers. --- examples/leaderboard/.meteor/release | 2 +- examples/parties/.meteor/release | 2 +- examples/todos/.meteor/release | 2 +- examples/wordplay/.meteor/release | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/leaderboard/.meteor/release b/examples/leaderboard/.meteor/release index af7f6f3d0d..844f6a91ac 100644 --- a/examples/leaderboard/.meteor/release +++ b/examples/leaderboard/.meteor/release @@ -1 +1 @@ -0.6.2.1 +0.6.3 diff --git a/examples/parties/.meteor/release b/examples/parties/.meteor/release index af7f6f3d0d..844f6a91ac 100644 --- a/examples/parties/.meteor/release +++ b/examples/parties/.meteor/release @@ -1 +1 @@ -0.6.2.1 +0.6.3 diff --git a/examples/todos/.meteor/release b/examples/todos/.meteor/release index af7f6f3d0d..844f6a91ac 100644 --- a/examples/todos/.meteor/release +++ b/examples/todos/.meteor/release @@ -1 +1 @@ -0.6.2.1 +0.6.3 diff --git a/examples/wordplay/.meteor/release b/examples/wordplay/.meteor/release index af7f6f3d0d..844f6a91ac 100644 --- a/examples/wordplay/.meteor/release +++ b/examples/wordplay/.meteor/release @@ -1 +1 @@ -0.6.2.1 +0.6.3 From 546f4e5fb93699983aee11ef01510b29cf09b354 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 15 May 2013 11:55:31 -0700 Subject: [PATCH 18/60] bump docs version. --- docs/.meteor/release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.meteor/release b/docs/.meteor/release index af7f6f3d0d..844f6a91ac 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -0.6.2.1 +0.6.3 From d38034f35d90b2a922886b85a07fe9175bf51c06 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 15 May 2013 15:02:07 -0700 Subject: [PATCH 19/60] Fix client/compatibility/ in a non-nested directory --- tools/packages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/packages.js b/tools/packages.js index 8678652c88..3a25c1a81a 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -239,7 +239,7 @@ _.extend(Package.prototype, { // -- Source files -- var shouldLoadRaw = function (filename) { - return filename.indexOf(path.sep + 'client' + path.sep + 'compatibility' + path.sep) !== -1; + return filename.indexOf('client' + path.sep + 'compatibility' + path.sep) !== -1; }; var clientFiles = sources_except(api, "server"); api.add_files(_.filter(clientFiles, shouldLoadRaw), "client", {raw: true}); From 3f9e08cbbbbb751ec29c0eef3c94979336eb606b Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 15 May 2013 15:06:51 -0700 Subject: [PATCH 20/60] 0.6.3.1 --- scripts/admin/banner.txt | 8 +------- scripts/admin/notices.json | 3 +++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index d57435b6fd..0ee2d5c306 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -1,7 +1 @@ -=> Meteor 0.6.3 released: Default to WebSockets in modern browsers, new - `check` API to validate untrusted input, synthetic `tap` events, - update to MongoDB 2.4, and more. See - https://github.com/meteor/meteor/blob/devel/History.md for details. - - This is being downloaded in the background. Update your project - to Meteor 0.6.3 by running 'meteor update'. +=> Meteor 0.6.3.1: Small fix to 'client/compatibility' from 0.6.3 diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index 1f7eada8dc..5b726dfaad 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -34,6 +34,9 @@ "are now thrown rather than passed in `result.error`."] } }, + { + "release": "0.6.3.1" + }, { "release": "NEXT" } From 1b88047966cb63bdc816eb670d3d4d341537e548 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 15 May 2013 15:02:07 -0700 Subject: [PATCH 21/60] Fix client/compatibility/ in a non-nested directory --- tools/packages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/packages.js b/tools/packages.js index 8678652c88..3a25c1a81a 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -239,7 +239,7 @@ _.extend(Package.prototype, { // -- Source files -- var shouldLoadRaw = function (filename) { - return filename.indexOf(path.sep + 'client' + path.sep + 'compatibility' + path.sep) !== -1; + return filename.indexOf('client' + path.sep + 'compatibility' + path.sep) !== -1; }; var clientFiles = sources_except(api, "server"); api.add_files(_.filter(clientFiles, shouldLoadRaw), "client", {raw: true}); From f87f0bd52565031d3cf99a654e9adc4662a49eb9 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 15 May 2013 15:06:51 -0700 Subject: [PATCH 22/60] 0.6.3.1 --- scripts/admin/banner.txt | 8 +------- scripts/admin/notices.json | 3 +++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index d57435b6fd..0ee2d5c306 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -1,7 +1 @@ -=> Meteor 0.6.3 released: Default to WebSockets in modern browsers, new - `check` API to validate untrusted input, synthetic `tap` events, - update to MongoDB 2.4, and more. See - https://github.com/meteor/meteor/blob/devel/History.md for details. - - This is being downloaded in the background. Update your project - to Meteor 0.6.3 by running 'meteor update'. +=> Meteor 0.6.3.1: Small fix to 'client/compatibility' from 0.6.3 diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index 1f7eada8dc..5b726dfaad 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -34,6 +34,9 @@ "are now thrown rather than passed in `result.error`."] } }, + { + "release": "0.6.3.1" + }, { "release": "NEXT" } From 75ab6e61fab557285fadec66238988f230313168 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 15 May 2013 15:28:59 -0700 Subject: [PATCH 23/60] docs on 0.6.3.1 --- docs/.meteor/release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.meteor/release b/docs/.meteor/release index 844f6a91ac..a50a1dcf8c 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -0.6.3 +0.6.3.1 From b7754f8c288949edc03df5b45fa6c7aecd56156a Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Mon, 6 May 2013 15:37:01 -0700 Subject: [PATCH 24/60] Refactor Oauth packages and extract twitter package from accounts-twitter --- .../accounts-ui-viewer/accounts-ui-viewer.js | 6 +- packages/accounts-base/accounts_common.js | 21 +-- packages/accounts-base/accounts_server.js | 6 +- packages/accounts-base/package.js | 1 + packages/accounts-facebook/facebook_client.js | 4 +- packages/accounts-facebook/facebook_server.js | 4 +- packages/accounts-facebook/package.js | 2 +- packages/accounts-github/github_client.js | 4 +- packages/accounts-github/github_server.js | 4 +- packages/accounts-github/package.js | 2 +- packages/accounts-google/google_client.js | 4 +- packages/accounts-google/google_server.js | 4 +- packages/accounts-google/package.js | 2 +- packages/accounts-meetup/meetup_client.js | 4 +- packages/accounts-meetup/meetup_server.js | 4 +- packages/accounts-meetup/package.js | 2 +- packages/accounts-oauth/oauth_client.js | 18 +++ .../oauth_common.js | 0 packages/accounts-oauth/oauth_server.js | 51 +++++++ packages/accounts-oauth/oauth_tests.js | 2 + packages/accounts-oauth/package.js | 18 +++ .../accounts-oauth1-helper/oauth1_common.js | 1 - .../accounts-oauth1-helper/oauth1_tests.js | 140 ------------------ .../accounts-oauth2-helper/oauth2_common.js | 1 - .../accounts-oauth2-helper/oauth2_tests.js | 98 ------------ packages/accounts-twitter/package.js | 8 +- packages/accounts-twitter/twitter_client.js | 34 +---- packages/accounts-twitter/twitter_common.js | 7 - packages/accounts-twitter/twitter_server.js | 29 +--- .../login_buttons_single.js | 4 +- packages/accounts-weibo/package.js | 2 +- packages/accounts-weibo/weibo_client.js | 4 +- packages/accounts-weibo/weibo_server.js | 4 +- .../oauth_client.js | 24 +-- packages/oauth/oauth_common.js | 1 + .../oauth_server.js | 82 +++------- .../package.js | 5 +- .../oauth1_binding.js | 0 packages/oauth1/oauth1_common.js | 1 + .../oauth1_server.js | 28 ++-- packages/oauth1/oauth1_tests.js | 73 +++++++++ .../package.js | 7 +- packages/oauth2/oauth2_common.js | 1 + .../oauth2_server.js | 14 +- packages/oauth2/oauth2_tests.js | 36 +++++ .../package.js | 7 +- packages/service-configuration/package.js | 7 + .../service_configuration_common.js | 21 +++ packages/twitter/package.js | 18 +++ packages/twitter/twitter_client.js | 31 ++++ packages/twitter/twitter_common.js | 10 ++ .../twitter_configure.html | 0 .../twitter_configure.js | 0 packages/twitter/twitter_server.js | 26 ++++ 54 files changed, 420 insertions(+), 467 deletions(-) create mode 100644 packages/accounts-oauth/oauth_client.js rename packages/{accounts-oauth-helper => accounts-oauth}/oauth_common.js (100%) create mode 100644 packages/accounts-oauth/oauth_server.js create mode 100644 packages/accounts-oauth/oauth_tests.js create mode 100644 packages/accounts-oauth/package.js delete mode 100644 packages/accounts-oauth1-helper/oauth1_common.js delete mode 100644 packages/accounts-oauth1-helper/oauth1_tests.js delete mode 100644 packages/accounts-oauth2-helper/oauth2_common.js delete mode 100644 packages/accounts-oauth2-helper/oauth2_tests.js rename packages/{accounts-oauth-helper => oauth}/oauth_client.js (73%) create mode 100644 packages/oauth/oauth_common.js rename packages/{accounts-oauth-helper => oauth}/oauth_server.js (62%) rename packages/{accounts-oauth-helper => oauth}/package.js (70%) rename packages/{accounts-oauth1-helper => oauth1}/oauth1_binding.js (100%) create mode 100644 packages/oauth1/oauth1_common.js rename packages/{accounts-oauth1-helper => oauth1}/oauth1_server.js (65%) create mode 100644 packages/oauth1/oauth1_tests.js rename packages/{accounts-oauth1-helper => oauth1}/package.js (69%) create mode 100644 packages/oauth2/oauth2_common.js rename packages/{accounts-oauth2-helper => oauth2}/oauth2_server.js (52%) create mode 100644 packages/oauth2/oauth2_tests.js rename packages/{accounts-oauth2-helper => oauth2}/package.js (66%) create mode 100644 packages/service-configuration/package.js create mode 100644 packages/service-configuration/service_configuration_common.js create mode 100644 packages/twitter/package.js create mode 100644 packages/twitter/twitter_client.js create mode 100644 packages/twitter/twitter_common.js rename packages/{accounts-twitter => twitter}/twitter_configure.html (100%) rename packages/{accounts-twitter => twitter}/twitter_configure.js (100%) create mode 100644 packages/twitter/twitter_server.js diff --git a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js index eb1b306d39..cdd2bbefbf 100644 --- a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js +++ b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js @@ -162,12 +162,12 @@ if (Meteor.isClient) { 'click #controlpane button': function (event) { if (this.key === "fakeConfig") { var service = this.value; - if (! Accounts.loginServiceConfiguration.findOne({service: service})) - Accounts.loginServiceConfiguration.insert( + if (! ServiceConfiguration.configurations.findOne({service: service})) + ServiceConfiguration.configurations.insert( {service: service, fake: true}); } else if (this.key === "unconfig") { var service = this.value; - Accounts.loginServiceConfiguration.remove({service: service}); + ServiceConfiguration.configurations.remove({service: service}); } else if (this.key === "messages") { if (this.value === "error") { Accounts._loginButtonsSession.errorMessage('An error occurred! Gee golly gosh.'); diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index bf2f2328f2..62a624566b 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -48,24 +48,9 @@ Meteor.users = new Meteor.Collection("users", {_preventAutopublish: true}); // There is an allow call in accounts_server that restricts this // collection. - -// Table containing documents with configuration options for each -// login service -Accounts.loginServiceConfiguration = new Meteor.Collection( - "meteor_accounts_loginServiceConfiguration", {_preventAutopublish: true}); -// Leave this collection open in insecure mode. In theory, someone could -// hijack your oauth connect requests to a different endpoint or appId, -// but you did ask for 'insecure'. The advantage is that it is much -// easier to write a configuration wizard that works only in insecure -// mode. - - -// Thrown when trying to use a login service which is not configured -Accounts.ConfigError = function(description) { - this.message = description; -}; -Accounts.ConfigError.prototype = new Error(); -Accounts.ConfigError.prototype.name = 'Accounts.ConfigError'; +// loginServiceConfiguration and ConfigError are maintained for backwards compatibility +Accounts.loginServiceConfiguration = ServiceConfiguration.configurations; +Accounts.ConfigError = ServiceConfiguration.ConfigError; // Thrown when the user cancels the login process (eg, closes an oauth // popup, declines retina scan, etc) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index c0d710ac8f..696893f816 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -351,7 +351,7 @@ Meteor.default_server.onAutopublish(function () { // Publish all login service configuration fields other than secret. Meteor.publish("meteor.loginServiceConfiguration", function () { - return Accounts.loginServiceConfiguration.find({}, {fields: {secret: 0}}); + return ServiceConfiguration.configurations.find({}, {fields: {secret: 0}}); }, {is_auto: true}); // not techincally autopublish, but stops the warning. // Allow a one-time configuration for a login service. Modifications @@ -364,9 +364,9 @@ Meteor.methods({ // instead of ours). if (!Accounts[options.service]) throw new Meteor.Error(403, "Service unknown"); - if (Accounts.loginServiceConfiguration.findOne({service: options.service})) + if (ServiceConfiguration.configurations.findOne({service: options.service})) throw new Meteor.Error(403, "Service " + options.service + " already configured"); - Accounts.loginServiceConfiguration.insert(options); + ServiceConfiguration.configurations.insert(options); } }); diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index ce80b187d5..e533ba4601 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -6,6 +6,7 @@ Package.on_use(function (api) { api.use('underscore', 'server'); api.use('localstorage', 'client'); api.use('accounts-urls', 'client'); + api.use('service-configuration', ['client', 'server']); // need this because of the Meteor.users collection but in the future // we'd probably want to abstract this away diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index 614d7d3b56..8fa33c41a9 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -5,9 +5,9 @@ Meteor.loginWithFacebook = function (options, callback) { options = {}; } - var config = Accounts.loginServiceConfiguration.findOne({service: 'facebook'}); + var config = ServiceConfiguration.configurations.findOne({service: 'facebook'}); if (!config) { - callback && callback(new Accounts.ConfigError("Service not configured")); + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); return; } diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 7b3092ceaf..fc7235be31 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -51,9 +51,9 @@ var isJSON = function (str) { // - accessToken // - expiresIn: lifetime of token in seconds var getTokenResponse = function (query) { - var config = Accounts.loginServiceConfiguration.findOne({service: 'facebook'}); + var config = ServiceConfiguration.configurations.findOne({service: 'facebook'}); if (!config) - throw new Accounts.ConfigError("Service not configured"); + throw new ServiceConfiguration.ConfigError("Service not configured"); var responseContent; try { diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index a9fb73a8aa..035b553077 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -4,7 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('accounts-oauth2-helper', ['client', 'server']); + api.use('oauth2', ['client', 'server']); api.use('http', ['client', 'server']); api.use('templating', 'client'); diff --git a/packages/accounts-github/github_client.js b/packages/accounts-github/github_client.js index 5ef8365066..a29324caed 100644 --- a/packages/accounts-github/github_client.js +++ b/packages/accounts-github/github_client.js @@ -5,9 +5,9 @@ Meteor.loginWithGithub = function (options, callback) { options = {}; } - var config = Accounts.loginServiceConfiguration.findOne({service: 'github'}); + var config = ServiceConfiguration.configurations.findOne({service: 'github'}); if (!config) { - callback && callback(new Accounts.ConfigError("Service not configured")); + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); return; } var state = Random.id(); diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index 0eabf02497..02953e3ccc 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -28,9 +28,9 @@ if (Meteor.release) userAgent += "/" + Meteor.release; var getAccessToken = function (query) { - var config = Accounts.loginServiceConfiguration.findOne({service: 'github'}); + var config = ServiceConfiguration.configurations.findOne({service: 'github'}); if (!config) - throw new Accounts.ConfigError("Service not configured"); + throw new ServiceConfiguration.ConfigError("Service not configured"); var response; try { diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index e1fc2f4eb8..4eba7133be 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -4,7 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('accounts-oauth2-helper', ['client', 'server']); + api.use('oauth2', ['client', 'server']); api.use('http', ['client', 'server']); api.use('templating', 'client'); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 3697aabe6e..0471af568e 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -7,9 +7,9 @@ Meteor.loginWithGoogle = function (options, callback) { options = {}; } - var config = Accounts.loginServiceConfiguration.findOne({service: 'google'}); + var config = ServiceConfiguration.configurations.findOne({service: 'google'}); if (!config) { - callback && callback(new Accounts.ConfigError("Service not configured")); + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); return; } diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 4d4925e47d..463aa6b1df 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -49,9 +49,9 @@ Accounts.oauth.registerService('google', 2, function(query) { // - expiresIn: lifetime of token in seconds // - refreshToken, if this is the first authorization request var getTokens = function (query) { - var config = Accounts.loginServiceConfiguration.findOne({service: 'google'}); + var config = ServiceConfiguration.configurations.findOne({service: 'google'}); if (!config) - throw new Accounts.ConfigError("Service not configured"); + throw new ServiceConfiguration.ConfigError("Service not configured"); var response; try { diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index e8c799276f..099f0b1e18 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -4,7 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('accounts-oauth2-helper', ['client', 'server']); + api.use('oauth2', ['client', 'server']); api.use('http', ['client', 'server']); api.use('templating', 'client'); diff --git a/packages/accounts-meetup/meetup_client.js b/packages/accounts-meetup/meetup_client.js index f5b2056bef..ec4e30e7e1 100644 --- a/packages/accounts-meetup/meetup_client.js +++ b/packages/accounts-meetup/meetup_client.js @@ -5,9 +5,9 @@ Meteor.loginWithMeetup = function (options, callback) { options = {}; } - var config = Accounts.loginServiceConfiguration.findOne({service: 'meetup'}); + var config = ServiceConfiguration.configurations.findOne({service: 'meetup'}); if (!config) { - callback && callback(new Accounts.ConfigError("Service not configured")); + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); return; } var state = Random.id(); diff --git a/packages/accounts-meetup/meetup_server.js b/packages/accounts-meetup/meetup_server.js index 907d04725a..e0e5ac2a2d 100644 --- a/packages/accounts-meetup/meetup_server.js +++ b/packages/accounts-meetup/meetup_server.js @@ -22,9 +22,9 @@ Accounts.oauth.registerService('meetup', 2, function(query) { }); var getAccessToken = function (query) { - var config = Accounts.loginServiceConfiguration.findOne({service: 'meetup'}); + var config = ServiceConfiguration.configurations.findOne({service: 'meetup'}); if (!config) - throw new Accounts.ConfigError("Service not configured"); + throw new ServiceConfiguration.ConfigError("Service not configured"); var response; try { diff --git a/packages/accounts-meetup/package.js b/packages/accounts-meetup/package.js index 332253c2cc..6c047d23f5 100644 --- a/packages/accounts-meetup/package.js +++ b/packages/accounts-meetup/package.js @@ -4,7 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('accounts-oauth2-helper', ['client', 'server']); + api.use('oauth2', ['client', 'server']); api.use('http', ['client', 'server']); api.use('templating', 'client'); diff --git a/packages/accounts-oauth/oauth_client.js b/packages/accounts-oauth/oauth_client.js new file mode 100644 index 0000000000..ec5528ef2c --- /dev/null +++ b/packages/accounts-oauth/oauth_client.js @@ -0,0 +1,18 @@ +// Send an OAuth login method to the server. If the user authorized +// access in the popup this should log the user in, otherwise +// nothing should happen. +Accounts.oauth.tryLoginAfterPopupClosed = function(state, callback) { + Accounts.callLoginMethod({ + methodArguments: [{oauth: {state: state}}], + userCallback: callback && function (err) { + // Allow server to specify a specify subclass of errors. We should come + // up with a more generic way to do this! + if (err && err instanceof Meteor.Error && + err.error === Accounts.LoginCancelledError.numericError) { + callback(new Accounts.LoginCancelledError(err.details)); + } else { + callback(err); + } + }}); +}; + diff --git a/packages/accounts-oauth-helper/oauth_common.js b/packages/accounts-oauth/oauth_common.js similarity index 100% rename from packages/accounts-oauth-helper/oauth_common.js rename to packages/accounts-oauth/oauth_common.js diff --git a/packages/accounts-oauth/oauth_server.js b/packages/accounts-oauth/oauth_server.js new file mode 100644 index 0000000000..f1c306e65a --- /dev/null +++ b/packages/accounts-oauth/oauth_server.js @@ -0,0 +1,51 @@ +// Helper for registering OAuth based accounts packages. +// Adds an index to the user collection. +Accounts.oauth.registerService = function (name) { + // Accounts.updateOrCreateUserFromExternalService does a lookup by this id, + // so this should be a unique index. You might want to add indexes for other + // fields returned by your service (eg services.github.login) but you can do + // that in your app. + Meteor.users._ensureIndex('services.' + name + '.id', + {unique: 1, sparse: 1}); + +}; + +// For test cleanup only. (Mongo has a limit as to how many indexes it can have +// per collection.) +Accounts.oauth._unregisterService = function (name) { + var index = {}; + index['services.' + name + '.id'] = 1; + Meteor.users._dropIndex(index); +}; + + +// Listen to calls to `login` with an oauth option set. This is where +// users actually get logged in to meteor via oauth. +Accounts.registerLoginHandler(function (options) { + if (!options.oauth) + return undefined; // don't handle + + check(options.oauth, {state: String}); + + if (!_.has(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 + // popup somehow. + // + // we assume it was user canceled, and report it as such, using a + // Meteor.Error which the client can recognize. this will mask failures + // where things are misconfigured such that the server doesn't see the + // request but does close the window. This seems unlikely. + throw new Meteor.Error(Accounts.LoginCancelledError.numericError, + 'No matching login attempt found'); + } + var result = 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; + else + return Accounts.updateOrCreateUserFromExternalService(result.serviceName, result.serviceData, result.options); +}); + diff --git a/packages/accounts-oauth/oauth_tests.js b/packages/accounts-oauth/oauth_tests.js new file mode 100644 index 0000000000..8f1493d0b7 --- /dev/null +++ b/packages/accounts-oauth/oauth_tests.js @@ -0,0 +1,2 @@ +// XXX Add a test to ensure that successful logins call Accounts.updateOrCreateUserFromExternalService +// XXX Add a test to ensure that a missing or failed loginResult is handled correctly diff --git a/packages/accounts-oauth/package.js b/packages/accounts-oauth/package.js new file mode 100644 index 0000000000..2b3a0d75c6 --- /dev/null +++ b/packages/accounts-oauth/package.js @@ -0,0 +1,18 @@ +Package.describe({ + summary: "Common code for OAuth-based accounts services", + internal: true +}); + +Package.on_use(function (api) { + api.use('routepolicy', 'server'); + api.use('oauth', 'server'); + + api.add_files('oauth_common.js', ['client', 'server']); + api.add_files('oauth_client.js', 'client'); + api.add_files('oauth_server.js', 'server'); +}); + + +Package.on_test(function (api) { + api.add_files("oauth_tests.js", 'server'); +}); diff --git a/packages/accounts-oauth1-helper/oauth1_common.js b/packages/accounts-oauth1-helper/oauth1_common.js deleted file mode 100644 index d4ce446298..0000000000 --- a/packages/accounts-oauth1-helper/oauth1_common.js +++ /dev/null @@ -1 +0,0 @@ -Accounts.oauth1 = {}; \ No newline at end of file diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js deleted file mode 100644 index ebcd4e0609..0000000000 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ /dev/null @@ -1,140 +0,0 @@ - -Tinytest.add("oauth1 - loginResultForState is stored", function (test) { - var http = Npm.require('http'); - var twitterfooId = Random.id(); - var twitterfooName = 'nickname' + Random.id(); - var twitterfooAccessToken = Random.id(); - var twitterfooAccessTokenSecret = Random.id(); - var state = Random.id(); - var serviceName = Random.id(); - - OAuth1Binding.prototype.prepareRequestToken = function() {}; - OAuth1Binding.prototype.prepareAccessToken = function() { - this.accessToken = twitterfooAccessToken; - this.accessTokenSecret = twitterfooAccessTokenSecret; - }; - - Accounts.loginServiceConfiguration.insert({service: serviceName}); - Accounts[serviceName] = {}; - - try { - // register a fake login service - Accounts.oauth.registerService(serviceName, 1, function (query) { - return { - serviceData: { - id: twitterfooId, - screenName: twitterfooName, - accessToken: twitterfooAccessToken, - accessTokenSecret: twitterfooAccessTokenSecret - } - }; - }); - - // simulate logging in using twitterfoo - Accounts.oauth1._requestTokens[state] = twitterfooAccessToken; - - var req = { - method: "POST", - url: "/_oauth/" + serviceName + "?close", - query: { - state: state, - oauth_token: twitterfooAccessToken - } - }; - Accounts.oauth._middleware(req, new http.ServerResponse(req)); - - // verify that a user is created - var selector = {}; - selector["services." + serviceName + ".screenName"] = twitterfooName; - var user = Meteor.users.findOne(selector); - test.notEqual(user, undefined); - test.equal(user.services[serviceName].accessToken, - twitterfooAccessToken); - test.equal(user.services[serviceName].accessTokenSecret, - twitterfooAccessTokenSecret); - - // and that that user has a login token - test.equal(user.services.resume.loginTokens.length, 1); - var token = user.services.resume.loginTokens[0].token; - test.notEqual(token, undefined); - - // and that the login result for that user is prepared - test.equal( - Accounts.oauth._loginResultForState[state].id, user._id); - test.equal( - Accounts.oauth._loginResultForState[state].token, token); - } finally { - Accounts.oauth._unregisterService(serviceName); - } -}); - - -Tinytest.add("oauth1 - error in user creation", function (test) { - var http = Npm.require('http'); - var state = Random.id(); - var twitterfailId = Random.id(); - var twitterfailName = 'nickname' + Random.id(); - var twitterfailAccessToken = Random.id(); - var twitterfailAccessTokenSecret = Random.id(); - var serviceName = Random.id(); - - Accounts.loginServiceConfiguration.insert({service: serviceName}); - Accounts[serviceName] = {}; - - // Wire up access token so that verification passes - Accounts.oauth1._requestTokens[state] = twitterfailAccessToken; - - try { - // register a failing login service - Accounts.oauth.registerService(serviceName, 1, function (query) { - return { - serviceData: { - id: twitterfailId, - screenName: twitterfailName, - accessToken: twitterfailAccessToken, - accessTokenSecret: twitterfailAccessTokenSecret - }, - options: { - profile: {invalid: true} - } - }; - }); - - // a way to fail new users. duplicated from passwords_tests, but - // shouldn't hurt. - Accounts.validateNewUser(function (user) { - return !(user.profile && user.profile.invalid); - }); - - // simulate logging in with failure - Meteor._suppress_log(1); - var req = { - method: "POST", - url: "/_oauth/" + serviceName + "?close", - query: { - state: state, - oauth_token: twitterfailAccessToken - } - }; - - Accounts.oauth._middleware(req, new http.ServerResponse(req)); - - // verify that a user is not created - var selector = {}; - selector["services." + serviceName + ".screenName"] = twitterfailName; - var user = Meteor.users.findOne(selector); - test.equal(user, undefined); - - // verify an error is stored in login state - test.equal(Accounts.oauth._loginResultForState[state].error, 403); - - // verify error is handed back to login method. - test.throws(function () { - Meteor.apply('login', [{oauth: {version: 1, state: state}}]); - }); - } finally { - Accounts.oauth._unregisterService(serviceName); - } -}); - - diff --git a/packages/accounts-oauth2-helper/oauth2_common.js b/packages/accounts-oauth2-helper/oauth2_common.js deleted file mode 100644 index 0012a34cee..0000000000 --- a/packages/accounts-oauth2-helper/oauth2_common.js +++ /dev/null @@ -1 +0,0 @@ -Accounts.oauth2 = {}; \ No newline at end of file diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js deleted file mode 100644 index aae5b87cab..0000000000 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ /dev/null @@ -1,98 +0,0 @@ -Tinytest.add("oauth2 - loginResultForState is stored", function (test) { - var http = Npm.require('http'); - var foobookId = Random.id(); - var state = Random.id(); - var serviceName = Random.id(); - - Accounts.loginServiceConfiguration.insert({service: serviceName}); - Accounts[serviceName] = {}; - - try { - // register a fake login service - Accounts.oauth.registerService(serviceName, 2, function (query) { - return {serviceData: {id: foobookId}}; - }); - - // simulate logging in using foobook - var req = {method: "POST", - url: "/_oauth/" + serviceName + "?close", - query: {state: state}}; - Accounts.oauth._middleware(req, new http.ServerResponse(req)); - - // verify that a user is created - var selector = {}; - selector["services." + serviceName + ".id"] = foobookId; - var user = Meteor.users.findOne(selector); - test.notEqual(user, undefined); - test.equal(user.services[serviceName].id, foobookId); - - // and that that user has a login token - test.equal(user.services.resume.loginTokens.length, 1); - var token = user.services.resume.loginTokens[0].token; - test.notEqual(token, undefined); - - // and that the login result for that user is prepared - test.equal( - Accounts.oauth._loginResultForState[state].id, user._id); - test.equal( - Accounts.oauth._loginResultForState[state].token, token); - } finally { - Accounts.oauth._unregisterService(serviceName); - } -}); - - -Tinytest.add("oauth2 - error in user creation", function (test) { - var http = Npm.require('http'); - var state = Random.id(); - var failbookId = Random.id(); - var serviceName = Random.id(); - - Accounts.loginServiceConfiguration.insert({service: serviceName}); - Accounts[serviceName] = {}; - - try { - // register a failing login service - Accounts.oauth.registerService(serviceName, 2, function (query) { - return { - serviceData: { - id: failbookId - }, - options: { - profile: {invalid: true} - } - }; - }); - - // a way to fail new users. duplicated from passwords_tests, but - // shouldn't hurt. - Accounts.validateNewUser(function (user) { - return !(user.profile && user.profile.invalid); - }); - - // simulate logging in with failure - Meteor._suppress_log(1); - var req = {method: "POST", - url: "/_oauth/" + serviceName + "?close", - query: {state: state}}; - Accounts.oauth._middleware(req, new http.ServerResponse(req)); - - // verify that a user is not created - var selector = {}; - selector["services." + serviceName + ".id"] = failbookId; - var user = Meteor.users.findOne(selector); - test.equal(user, undefined); - - // verify an error is stored in login state - test.equal(Accounts.oauth._loginResultForState[state].error, 403); - - // verify error is handed back to login method. - test.throws(function () { - Meteor.apply('login', [{oauth: {version: 2, state: state}}]); - }); - } finally { - Accounts.oauth._unregisterService(serviceName); - } -}); - - diff --git a/packages/accounts-twitter/package.js b/packages/accounts-twitter/package.js index eeac247033..6ae4d92fc6 100644 --- a/packages/accounts-twitter/package.js +++ b/packages/accounts-twitter/package.js @@ -4,13 +4,13 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('accounts-oauth1-helper', ['client', 'server']); + api.use('accounts-oauth', ['client', 'server']); + api.use('twitter', ['client', 'server']); + api.use('http', ['client', 'server']); api.use('templating', 'client'); - api.add_files( - ['twitter_login_button.css', 'twitter_configure.html', 'twitter_configure.js'], - 'client'); + api.add_files('twitter_login_button.css', 'client'); api.add_files('twitter_common.js', ['client', 'server']); api.add_files('twitter_server.js', 'server'); diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 8c67c753d4..62ca4d09c1 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -1,31 +1,3 @@ -// XXX support options.requestPermissions as we do for Facebook, Google, Github -Meteor.loginWithTwitter = function (options, callback) { - // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - - var config = Accounts.loginServiceConfiguration.findOne({service: 'twitter'}); - if (!config) { - callback && callback(new Accounts.ConfigError("Service not configured")); - return; - } - - var state = Random.id(); - // We need to keep state across the next two 'steps' so we're adding - // a state parameter to the url and the callback url that we'll be returned - // to by oauth provider - - // url back to app, enters "step 2" as described in - // packages/accounts-oauth1-helper/oauth1_server.js - var callbackUrl = Meteor.absoluteUrl('_oauth/twitter?close&state=' + state); - - // url to app, enters "step 1" as described in - // packages/accounts-oauth1-helper/oauth1_server.js - var url = '/_oauth/twitter/?requestTokenAndRedirect=' - + encodeURIComponent(callbackUrl) - + '&state=' + state; - - Accounts.oauth.initiateLogin(state, url, callback); -}; +Meteor.loginWithTwitter = function(options, callback) { + Twitter.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); +}; \ No newline at end of file diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js index 3fdcd9d2bc..b1428d3bf4 100644 --- a/packages/accounts-twitter/twitter_common.js +++ b/packages/accounts-twitter/twitter_common.js @@ -1,10 +1,3 @@ if (!Accounts.twitter) { Accounts.twitter = {}; } - -Accounts.twitter._urls = { - requestToken: "https://api.twitter.com/oauth/request_token", - authorize: "https://api.twitter.com/oauth/authorize", - accessToken: "https://api.twitter.com/oauth/access_token", - authenticate: "https://api.twitter.com/oauth/authenticate" -}; diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 0915fd3b3e..fea172a744 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,36 +1,11 @@ -// https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials -var whitelisted = ['profile_image_url', 'profile_image_url_https', 'lang']; +Accounts.oauth.registerService('twitter'); var autopublishedFields = _.map( // don't send access token. https://dev.twitter.com/discussions/5025 - whitelisted.concat(['id', 'screenName']), + Twitter.whitelistedFields.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; - - var serviceData = { - id: identity.id_str, - screenName: identity.screen_name, - accessToken: oauthBinding.accessToken, - accessTokenSecret: oauthBinding.accessTokenSecret - }; - - // include helpful fields from twitter - var fields = _.pick(identity, whitelisted); - _.extend(serviceData, fields); - - return { - serviceData: serviceData, - options: { - profile: { - name: identity.name - } - } - }; -}); diff --git a/packages/accounts-ui-unstyled/login_buttons_single.js b/packages/accounts-ui-unstyled/login_buttons_single.js index 511dd61be9..b1f86b647f 100644 --- a/packages/accounts-ui-unstyled/login_buttons_single.js +++ b/packages/accounts-ui-unstyled/login_buttons_single.js @@ -10,7 +10,7 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({ loginButtonsSession.closeDropdown(); } else if (err instanceof Accounts.LoginCancelledError) { // do nothing - } else if (err instanceof Accounts.ConfigError) { + } else if (err instanceof ServiceConfiguration.ConfigError) { loginButtonsSession.configureService(serviceName); } else { loginButtonsSession.errorMessage(err.reason || "Unknown error"); @@ -30,7 +30,7 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({ }); Template._loginButtonsLoggedOutSingleLoginButton.configured = function () { - return !!Accounts.loginServiceConfiguration.findOne({service: this.name}); + return !!ServiceConfiguration.configurations.findOne({service: this.name}); }; Template._loginButtonsLoggedOutSingleLoginButton.capitalizedName = function () { diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index ddb17ea9f7..ce6a5dde31 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -4,7 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('accounts-oauth2-helper', ['client', 'server']); + api.use('oauth2', ['client', 'server']); api.use('http', ['client', 'server']); api.use('templating', 'client'); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index e76c94c0e6..44a8e02a2c 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -6,9 +6,9 @@ Meteor.loginWithWeibo = function (options, callback) { options = {}; } - var config = Accounts.loginServiceConfiguration.findOne({service: 'weibo'}); + var config = ServiceConfiguration.configurations.findOne({service: 'weibo'}); if (!config) { - callback && callback(new Accounts.ConfigError("Service not configured")); + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); return; } diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 5ef6a39d42..75f29785d7 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -36,9 +36,9 @@ Accounts.oauth.registerService('weibo', 2, function(query) { // - access_token // - expires_in: lifetime of this token in seconds (5 years(!) right now) var getTokenResponse = function (query) { - var config = Accounts.loginServiceConfiguration.findOne({service: 'weibo'}); + var config = ServiceConfiguration.configurations.findOne({service: 'weibo'}); if (!config) - throw new Accounts.ConfigError("Service not configured"); + throw new ServiceConfiguration.ConfigError("Service not configured"); var response; try { diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/oauth/oauth_client.js similarity index 73% rename from packages/accounts-oauth-helper/oauth_client.js rename to packages/oauth/oauth_client.js index 9e66d1d456..1b0de3ed40 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/oauth/oauth_client.js @@ -5,9 +5,11 @@ // @param callback {Function} Callback function to call on // completion. Takes one argument, null on success, or Error on // error. +// @param loginPopupClosedCallback the callback to call when the +// login screen is dismissed. Takes state and the callback // @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) { +Oauth.initiateLogin = function(state, url, callback, loginPopupClosedCallback, dimensions) { // default dimensions that worked well for facebook and google var popup = openCenteredPopup( url, @@ -30,29 +32,11 @@ Accounts.oauth.initiateLogin = function(state, url, callback, dimensions) { if (popupClosed) { clearInterval(checkPopupOpen); - tryLoginAfterPopupClosed(state, callback); + loginPopupClosedCallback(state, callback); } }, 100); }; -// Send an OAuth login method to the server. If the user authorized -// access in the popup this should log the user in, otherwise -// nothing should happen. -var tryLoginAfterPopupClosed = function(state, callback) { - Accounts.callLoginMethod({ - methodArguments: [{oauth: {state: state}}], - userCallback: callback && function (err) { - // Allow server to specify a specify subclass of errors. We should come - // up with a more generic way to do this! - if (err && err instanceof Meteor.Error && - err.error === Accounts.LoginCancelledError.numericError) { - callback(new Accounts.LoginCancelledError(err.details)); - } else { - callback(err); - } - }}); -}; - var openCenteredPopup = function(url, width, height) { var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft; diff --git a/packages/oauth/oauth_common.js b/packages/oauth/oauth_common.js new file mode 100644 index 0000000000..d33fd55ea4 --- /dev/null +++ b/packages/oauth/oauth_common.js @@ -0,0 +1 @@ +Oauth = {}; \ No newline at end of file diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/oauth/oauth_server.js similarity index 62% rename from packages/accounts-oauth-helper/oauth_server.js rename to packages/oauth/oauth_server.js index b6303ab45e..97de8ea5c3 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/oauth/oauth_server.js @@ -3,7 +3,8 @@ var Fiber = Npm.require('fibers'); Meteor._routePolicy.declare('/_oauth/', 'network'); -Accounts.oauth._services = {}; +Oauth._services = {}; + // Register a handler for an OAuth service. The handler will be called // when we get an incoming http request on /_oauth/{serviceName}. This @@ -12,6 +13,7 @@ Accounts.oauth._services = {}; // // @param name {String} e.g. "google", "facebook" // @param version {Number} OAuth version (1 or 2) +// @param urls For OAuth1 only, specify the service's urls // @param handleOauthRequest {Function(oauthBinding|query)} // - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider // - (For OAuth2 only) query {Object} parameters passed in query string @@ -19,70 +21,31 @@ Accounts.oauth._services = {}; // - {serviceData:, (optional options:)} where serviceData should end // up in the user's services[name] field // - `null` if the user declined to give permissions -Accounts.oauth.registerService = function (name, version, handleOauthRequest) { - if (Accounts.oauth._services[name]) +Oauth.registerService = function (name, version, urls, handleOauthRequest) { + if (Oauth._services[name]) throw new Error("Already registered the " + name + " OAuth service"); - // Accounts.updateOrCreateUserFromExternalService does a lookup by this id, - // so this should be a unique index. You might want to add indexes for other - // fields returned by your service (eg services.github.login) but you can do - // that in your app. - Meteor.users._ensureIndex('services.' + name + '.id', - {unique: 1, sparse: 1}); - - Accounts.oauth._services[name] = { + Oauth._services[name] = { serviceName: name, version: version, + urls: urls, handleOauthRequest: handleOauthRequest }; }; -// For test cleanup only. (Mongo has a limit as to how many indexes it can have -// per collection.) -Accounts.oauth._unregisterService = function (name) { - delete Accounts.oauth._services[name]; - var index = {}; - index['services.' + name + '.id'] = 1; - Meteor.users._dropIndex(index); +// For test cleanup only. +Oauth._unregisterService = function (name) { + delete Oauth._services[name]; }; + // When we get an incoming OAuth http request we complete the oauth // handshake, account and token setup before responding. The // results are stored in this map which is then read when the login // method is called. Maps state --> return value of `login` // // XXX we should periodically clear old entries -Accounts.oauth._loginResultForState = {}; - -// Listen to calls to `login` with an oauth option set. This is where -// users actually get logged in to meteor via oauth. -Accounts.registerLoginHandler(function (options) { - if (!options.oauth) - return undefined; // don't handle - - 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 - // popup somehow. - // - // we assume it was user canceled, and report it as such, using a - // Meteor.Error which the client can recognize. this will mask failures - // where things are misconfigured such that the server doesn't see the - // request but does close the window. This seems unlikely. - throw new Meteor.Error(Accounts.LoginCancelledError.numericError, - 'No matching login attempt found'); - } - 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; - else - return result; -}); +Oauth._loginResultForState = {}; // Listen to incoming OAuth http requests __meteor_bootstrap__.app @@ -92,11 +55,12 @@ __meteor_bootstrap__.app // calls and nothing else is wrapping this in a fiber // automatically Fiber(function () { - Accounts.oauth._middleware(req, res, next); + Oauth._middleware(req, res, next); }).run(); }); -Accounts.oauth._middleware = function (req, res, next) { + +Oauth._middleware = function (req, res, next) { // Make sure to catch any exceptions because otherwise we'd crash // the runner try { @@ -107,7 +71,7 @@ Accounts.oauth._middleware = function (req, res, next) { return; } - var service = Accounts.oauth._services[serviceName]; + var service = Oauth._services[serviceName]; // Skip everything if there's no service set by the oauth middleware if (!service) @@ -117,9 +81,9 @@ Accounts.oauth._middleware = function (req, res, next) { ensureConfigured(serviceName); if (service.version === 1) - Accounts.oauth1._handleRequest(service, req.query, res); + Oauth1._handleRequest(service, req.query, res); else if (service.version === 2) - Accounts.oauth2._handleRequest(service, req.query, res); + Oauth2._handleRequest(service, req.query, res); else throw new Error("Unexpected OAuth version " + service.version); } catch (err) { @@ -131,12 +95,12 @@ Accounts.oauth._middleware = function (req, res, next) { // we were passed. But then the developer wouldn't be able to // style the error or react to it in any way. if (req.query.state && err instanceof Error) - Accounts.oauth._loginResultForState[req.query.state] = err; + Oauth._loginResultForState[req.query.state] = err; // XXX the following is actually wrong. if someone wants to // redirect rather than close once we are done with the OAuth // flow, as supported by - // Accounts.oauth_renderOauthResults, this will still + // Oauth_renderOauthResults, this will still // close the popup instead. Once we fully support the redirect // flow (by supporting that in places such as // packages/facebook/facebook_client.js) we should revisit this. @@ -169,12 +133,12 @@ var oauthServiceName = function (req) { // Make sure we're configured var ensureConfigured = function(serviceName) { - if (!Accounts.loginServiceConfiguration.findOne({service: serviceName})) { - throw new Accounts.ConfigError("Service not configured"); + if (!ServiceConfiguration.configurations.findOne({service: serviceName})) { + throw new ServiceConfiguration.ConfigError("Service not configured"); }; }; -Accounts.oauth._renderOauthResults = function(res, query) { +Oauth._renderOauthResults = function(res, query) { // We support ?close and ?redirect=URL. Any other query should // just serve a blank page if ('close' in query) { // check with 'in' because we don't set a value diff --git a/packages/accounts-oauth-helper/package.js b/packages/oauth/package.js similarity index 70% rename from packages/accounts-oauth-helper/package.js rename to packages/oauth/package.js index 76a8a9b43a..82cc08f0ce 100644 --- a/packages/accounts-oauth-helper/package.js +++ b/packages/oauth/package.js @@ -1,13 +1,12 @@ Package.describe({ - summary: "Common code for OAuth-based login services", + summary: "Common code for OAuth-based services", internal: true }); Package.on_use(function (api) { - api.use('accounts-base', ['client', 'server']); api.use('routepolicy', 'server'); api.add_files('oauth_common.js', ['client', 'server']); api.add_files('oauth_client.js', 'client'); api.add_files('oauth_server.js', 'server'); -}); +}); \ No newline at end of file diff --git a/packages/accounts-oauth1-helper/oauth1_binding.js b/packages/oauth1/oauth1_binding.js similarity index 100% rename from packages/accounts-oauth1-helper/oauth1_binding.js rename to packages/oauth1/oauth1_binding.js diff --git a/packages/oauth1/oauth1_common.js b/packages/oauth1/oauth1_common.js new file mode 100644 index 0000000000..2d25e3b32b --- /dev/null +++ b/packages/oauth1/oauth1_common.js @@ -0,0 +1 @@ +Oauth1 = {}; \ No newline at end of file diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/oauth1/oauth1_server.js similarity index 65% rename from packages/accounts-oauth1-helper/oauth1_server.js rename to packages/oauth1/oauth1_server.js index 71d6ddecc6..afd1b2ef7e 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/oauth1/oauth1_server.js @@ -1,15 +1,15 @@ // A place to store request tokens pending verification -Accounts.oauth1._requestTokens = {}; +Oauth1._requestTokens = {}; // connect middleware -Accounts.oauth1._handleRequest = function (service, query, res) { +Oauth1._handleRequest = function (service, query, res) { - var config = Accounts.loginServiceConfiguration.findOne({service: service.serviceName}); + var config = ServiceConfiguration.configurations.findOne({service: service.serviceName}); if (!config) { - throw new Accounts.ConfigError("Service " + service.serviceName + " not configured"); + throw new ServiceConfiguration.ConfigError("Service " + service.serviceName + " not configured"); } - var urls = Accounts[service.serviceName]._urls; + var urls = service.urls; var oauthBinding = new OAuth1Binding( config.consumerKey, config.secret, urls); @@ -20,7 +20,7 @@ Accounts.oauth1._handleRequest = function (service, query, res) { oauthBinding.prepareRequestToken(query.requestTokenAndRedirect); // Keep track of request token so we can verify it on the next step - Accounts.oauth1._requestTokens[query.state] = oauthBinding.requestToken; + Oauth1._requestTokens[query.state] = oauthBinding.requestToken; // redirect to provider login, which will redirect back to "step 2" below var redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; @@ -33,8 +33,8 @@ Accounts.oauth1._handleRequest = function (service, query, res) { // token and access token secret and log in as user // Get the user's request token so we can verify it and clear it - var requestToken = Accounts.oauth1._requestTokens[query.state]; - delete Accounts.oauth1._requestTokens[query.state]; + var requestToken = Oauth1._requestTokens[query.state]; + delete Oauth1._requestTokens[query.state]; // Verify user authorized access and the oauth_token matches // the requestToken from previous step @@ -49,14 +49,16 @@ Accounts.oauth1._handleRequest = function (service, query, res) { // Run service-specific handler. var oauthResult = service.handleOauthRequest(oauthBinding); - // Get or create user doc and login token for reconnect. - Accounts.oauth._loginResultForState[query.state] = - Accounts.updateOrCreateUserFromExternalService( - service.serviceName, oauthResult.serviceData, oauthResult.options); + // Add the login result to the result map + Oauth._loginResultForState[query.state] = { + serviceName: service.serviceName, + serviceData: oauthResult.serviceData, + options: oauthResult.options + }; } } // Either close the window, redirect, or render nothing // if all else fails - Accounts.oauth._renderOauthResults(res, query); + Oauth._renderOauthResults(res, query); }; diff --git a/packages/oauth1/oauth1_tests.js b/packages/oauth1/oauth1_tests.js new file mode 100644 index 0000000000..da664fa392 --- /dev/null +++ b/packages/oauth1/oauth1_tests.js @@ -0,0 +1,73 @@ +Tinytest.add("oauth1 - loginResultForState is stored", function (test) { + var http = Npm.require('http'); + var twitterfooId = Random.id(); + var twitterfooName = 'nickname' + Random.id(); + var twitterfooAccessToken = Random.id(); + var twitterfooAccessTokenSecret = Random.id(); + var twitterOption1 = Random.id(); + var state = Random.id(); + var serviceName = Random.id(); + + var urls = { + requestToken: "https://example.com/oauth/request_token", + authorize: "https://example.com/oauth/authorize", + accessToken: "https://example.com/oauth/access_token", + authenticate: "https://example.com/oauth/authenticate" + }; + + OAuth1Binding.prototype.prepareRequestToken = function() {}; + OAuth1Binding.prototype.prepareAccessToken = function() { + this.accessToken = twitterfooAccessToken; + this.accessTokenSecret = twitterfooAccessTokenSecret; + }; + + ServiceConfiguration.configurations.insert({service: serviceName}); + + try { + // register a fake login service + Oauth.registerService(serviceName, 1, urls, function (query) { + return { + serviceData: { + id: twitterfooId, + screenName: twitterfooName, + accessToken: twitterfooAccessToken, + accessTokenSecret: twitterfooAccessTokenSecret + }, + options: { + option1: twitterOption1 + } + }; + }); + + // simulate logging in using twitterfoo + Oauth1._requestTokens[state] = twitterfooAccessToken; + + var req = { + method: "POST", + url: "/_oauth/" + serviceName + "?close", + query: { + state: state, + oauth_token: twitterfooAccessToken + } + }; + Oauth._middleware(req, new http.ServerResponse(req)); + + // Test that right data is placed on the loginResult map + test.equal( + Oauth._loginResultForState[state].serviceName, serviceName); + test.equal( + Oauth._loginResultForState[state].serviceData.id, twitterfooId); + test.equal( + Oauth._loginResultForState[state].serviceData.screenName, twitterfooName); + test.equal( + Oauth._loginResultForState[state].serviceData.accessToken, twitterfooAccessToken); + test.equal( + Oauth._loginResultForState[state].serviceData.accessTokenSecret, twitterfooAccessTokenSecret); + test.equal( + Oauth._loginResultForState[state].options.option1, twitterOption1); + + } finally { + Oauth._unregisterService(serviceName); + } +}); + diff --git a/packages/accounts-oauth1-helper/package.js b/packages/oauth1/package.js similarity index 69% rename from packages/accounts-oauth1-helper/package.js rename to packages/oauth1/package.js index 0e15ba5f4f..e741e2822c 100644 --- a/packages/accounts-oauth1-helper/package.js +++ b/packages/oauth1/package.js @@ -4,8 +4,8 @@ Package.describe({ }); Package.on_use(function (api) { - api.use('accounts-oauth-helper', 'client'); - api.use('accounts-base', ['client', 'server']); + api.use('service-configuration', ['client', 'server']); + api.use('oauth', 'client'); api.add_files('oauth1_binding.js', 'server'); api.add_files('oauth1_common.js', ['client', 'server']); @@ -13,6 +13,7 @@ Package.on_use(function (api) { }); Package.on_test(function (api) { - api.use('accounts-oauth1-helper', 'server'); + api.use('service-configuration', 'server'); + api.use('oauth1', 'server'); api.add_files("oauth1_tests.js", 'server'); }); diff --git a/packages/oauth2/oauth2_common.js b/packages/oauth2/oauth2_common.js new file mode 100644 index 0000000000..ef8a3f613b --- /dev/null +++ b/packages/oauth2/oauth2_common.js @@ -0,0 +1 @@ +Oauth2 = {}; \ No newline at end of file diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/oauth2/oauth2_server.js similarity index 52% rename from packages/accounts-oauth2-helper/oauth2_server.js rename to packages/oauth2/oauth2_server.js index b7e83d6c52..c36f7f55c7 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/oauth2/oauth2_server.js @@ -1,5 +1,5 @@ // connect middleware -Accounts.oauth2._handleRequest = function (service, query, res) { +Oauth2._handleRequest = function (service, query, res) { // check if user authorized access if (!query.error) { // Prepare the login results before returning. This way the @@ -8,13 +8,15 @@ Accounts.oauth2._handleRequest = function (service, query, res) { // Run service-specific handler. var oauthResult = service.handleOauthRequest(query); - // Get or create user doc and login token for reconnect. - Accounts.oauth._loginResultForState[query.state] = - Accounts.updateOrCreateUserFromExternalService( - service.serviceName, oauthResult.serviceData, oauthResult.options); + // Add the login result to the result map + Oauth._loginResultForState[query.state] = { + serviceName: service.serviceName, + serviceData: oauthResult.serviceData, + options: oauthResult.options + }; } // Either close the window, redirect, or render nothing // if all else fails - Accounts.oauth._renderOauthResults(res, query); + Oauth._renderOauthResults(res, query); }; diff --git a/packages/oauth2/oauth2_tests.js b/packages/oauth2/oauth2_tests.js new file mode 100644 index 0000000000..7dcf7e3906 --- /dev/null +++ b/packages/oauth2/oauth2_tests.js @@ -0,0 +1,36 @@ +Tinytest.add("oauth2 - loginResultForState is stored", function (test) { + var http = Npm.require('http'); + var foobookId = Random.id(); + var foobookOption1 = Random.id(); + var state = Random.id(); + var serviceName = Random.id(); + + ServiceConfiguration.configurations.insert({service: serviceName}); + + try { + // register a fake login service + Oauth.registerService(serviceName, 2, null, function (query) { + return { + serviceData: {id: foobookId}, + options: {option1: foobookOption1} + }; + }); + + // simulate logging in using foobook + var req = {method: "POST", + url: "/_oauth/" + serviceName + "?close", + query: {state: state}}; + Oauth._middleware(req, new http.ServerResponse(req)); + + // Test that the login result for that user is prepared + test.equal( + Oauth._loginResultForState[state].serviceName, serviceName); + test.equal( + Oauth._loginResultForState[state].serviceData.id, foobookId); + test.equal( + Oauth._loginResultForState[state].options.option1, foobookOption1); + + } finally { + Oauth._unregisterService(serviceName); + } +}); diff --git a/packages/accounts-oauth2-helper/package.js b/packages/oauth2/package.js similarity index 66% rename from packages/accounts-oauth2-helper/package.js rename to packages/oauth2/package.js index 8acf29c0be..4019d866ae 100644 --- a/packages/accounts-oauth2-helper/package.js +++ b/packages/oauth2/package.js @@ -4,14 +4,15 @@ Package.describe({ }); Package.on_use(function (api) { - api.use('accounts-oauth-helper', 'client'); - api.use('accounts-base', ['client', 'server']); + api.use('service-configuration', ['client', 'server']); + api.use('oauth', 'client'); api.add_files('oauth2_common.js', ['client', 'server']); api.add_files('oauth2_server.js', 'server'); }); Package.on_test(function (api) { - api.use('accounts-oauth2-helper', 'server'); + api.use('service-configuration', 'server'); + api.use('oauth2', 'server'); api.add_files("oauth2_tests.js", 'server'); }); diff --git a/packages/service-configuration/package.js b/packages/service-configuration/package.js new file mode 100644 index 0000000000..62b90e7504 --- /dev/null +++ b/packages/service-configuration/package.js @@ -0,0 +1,7 @@ +Package.describe({ + summary: "Manage the configuration for third-party services" +}); + +Package.on_use(function(api) { + api.add_files('service_configuration_common.js', ['client', 'server']); +}); \ No newline at end of file diff --git a/packages/service-configuration/service_configuration_common.js b/packages/service-configuration/service_configuration_common.js new file mode 100644 index 0000000000..f1d5dfc94c --- /dev/null +++ b/packages/service-configuration/service_configuration_common.js @@ -0,0 +1,21 @@ +if (typeof ServiceConfiguration === 'undefined') + ServiceConfiguration = {}; + + +// Table containing documents with configuration options for each +// login service +ServiceConfiguration.configurations = new Meteor.Collection( + "meteor_accounts_loginServiceConfiguration", {_preventAutopublish: true}); +// Leave this collection open in insecure mode. In theory, someone could +// hijack your oauth connect requests to a different endpoint or appId, +// but you did ask for 'insecure'. The advantage is that it is much +// easier to write a configuration wizard that works only in insecure +// mode. + + +// Thrown when trying to use a login service which is not configured +ServiceConfiguration.ConfigError = function(description) { + this.message = description; +}; +ServiceConfiguration.ConfigError.prototype = new Error(); +ServiceConfiguration.ConfigError.prototype.name = 'ServiceConfiguration.ConfigError'; \ No newline at end of file diff --git a/packages/twitter/package.js b/packages/twitter/package.js new file mode 100644 index 0000000000..179d396a2d --- /dev/null +++ b/packages/twitter/package.js @@ -0,0 +1,18 @@ +Package.describe({ + summary: "Login service for Twitter accounts" +}); + +Package.on_use(function(api) { + api.use('http', ['client', 'server']); + api.use('templating', 'client'); + api.use('service-configuration', ['client', 'server']); + api.use('oauth1', ['client', 'server']); + + api.add_files( + ['twitter_configure.html', 'twitter_configure.js'], + 'client'); + + api.add_files('twitter_common.js', ['client', 'server']); + api.add_files('twitter_server.js', 'server'); + api.add_files('twitter_client.js', 'client'); +}); \ No newline at end of file diff --git a/packages/twitter/twitter_client.js b/packages/twitter/twitter_client.js new file mode 100644 index 0000000000..190725de41 --- /dev/null +++ b/packages/twitter/twitter_client.js @@ -0,0 +1,31 @@ +// XXX support options.requestPermissions as we do for Facebook, Google, Github +Twitter.requestCredential = function (options, callback, loginPopupClosedCallback) { + // support both (options, callback) and (callback). + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + + var config = ServiceConfiguration.configurations.findOne({service: 'twitter'}); + if (!config) { + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + return; + } + + var state = Random.id(); + // We need to keep state across the next two 'steps' so we're adding + // a state parameter to the url and the callback url that we'll be returned + // to by oauth provider + + // url back to app, enters "step 2" as described in + // packages/accounts-oauth1-helper/oauth1_server.js + var callbackUrl = Meteor.absoluteUrl('_oauth/twitter?close&state=' + state); + + // url to app, enters "step 1" as described in + // packages/accounts-oauth1-helper/oauth1_server.js + var url = '/_oauth/twitter/?requestTokenAndRedirect=' + + encodeURIComponent(callbackUrl) + + '&state=' + state; + + Oauth.initiateLogin(state, url, callback, loginPopupClosedCallback); +}; diff --git a/packages/twitter/twitter_common.js b/packages/twitter/twitter_common.js new file mode 100644 index 0000000000..11fcb20788 --- /dev/null +++ b/packages/twitter/twitter_common.js @@ -0,0 +1,10 @@ +if (typeof Twitter === 'undefined') { + Twitter = {}; +} + +Twitter._urls = { + requestToken: "https://api.twitter.com/oauth/request_token", + authorize: "https://api.twitter.com/oauth/authorize", + accessToken: "https://api.twitter.com/oauth/access_token", + authenticate: "https://api.twitter.com/oauth/authenticate" +}; diff --git a/packages/accounts-twitter/twitter_configure.html b/packages/twitter/twitter_configure.html similarity index 100% rename from packages/accounts-twitter/twitter_configure.html rename to packages/twitter/twitter_configure.html diff --git a/packages/accounts-twitter/twitter_configure.js b/packages/twitter/twitter_configure.js similarity index 100% rename from packages/accounts-twitter/twitter_configure.js rename to packages/twitter/twitter_configure.js diff --git a/packages/twitter/twitter_server.js b/packages/twitter/twitter_server.js new file mode 100644 index 0000000000..783b917ba1 --- /dev/null +++ b/packages/twitter/twitter_server.js @@ -0,0 +1,26 @@ +// https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials +Twitter.whitelistedFields = ['profile_image_url', 'profile_image_url_https', 'lang']; + +Oauth.registerService('twitter', 1, Twitter._urls, function(oauthBinding) { + var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json').data; + + var serviceData = { + id: identity.id_str, + screenName: identity.screen_name, + accessToken: oauthBinding.accessToken, + accessTokenSecret: oauthBinding.accessTokenSecret + }; + + // include helpful fields from twitter + var fields = _.pick(identity, Twitter.whitelistedFields); + _.extend(serviceData, fields); + + return { + serviceData: serviceData, + options: { + profile: { + name: identity.name + } + } + }; +}); From 107a06fd8c504dad2eb2385b5d61b3a91e3573d3 Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Mon, 6 May 2013 16:10:18 -0700 Subject: [PATCH 25/60] Extract github package from accounts-github --- packages/accounts-github/github_client.js | 29 +------- packages/accounts-github/github_server.js | 66 +------------------ packages/accounts-github/package.js | 10 +-- packages/github/github_client.js | 27 ++++++++ packages/github/github_common.js | 3 + .../github_configure.html | 0 .../github_configure.js | 0 packages/github/github_server.js | 63 ++++++++++++++++++ packages/github/package.js | 17 +++++ 9 files changed, 118 insertions(+), 97 deletions(-) create mode 100644 packages/github/github_client.js create mode 100644 packages/github/github_common.js rename packages/{accounts-github => github}/github_configure.html (100%) rename packages/{accounts-github => github}/github_configure.js (100%) create mode 100644 packages/github/github_server.js create mode 100644 packages/github/package.js diff --git a/packages/accounts-github/github_client.js b/packages/accounts-github/github_client.js index a29324caed..cffd0ce805 100644 --- a/packages/accounts-github/github_client.js +++ b/packages/accounts-github/github_client.js @@ -1,26 +1,3 @@ -Meteor.loginWithGithub = function (options, callback) { - // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - - var config = ServiceConfiguration.configurations.findOne({service: 'github'}); - if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); - return; - } - var state = Random.id(); - - var scope = (options && options.requestPermissions) || []; - var flatScope = _.map(scope, encodeURIComponent).join('+'); - - var loginUrl = - 'https://github.com/login/oauth/authorize' + - '?client_id=' + config.clientId + - '&scope=' + flatScope + - '&redirect_uri=' + Meteor.absoluteUrl('_oauth/github?close') + - '&state=' + state; - - Accounts.oauth.initiateLogin(state, loginUrl, callback, {width: 900, height: 450}); -}; +Meteor.loginWithGithub = function(options, callback) { + Github.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); +}; \ No newline at end of file diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index 02953e3ccc..ca57098047 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -1,3 +1,5 @@ +Accounts.oauth.registerService('github'); + 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 @@ -5,67 +7,3 @@ Accounts.addAutopublishFields({ forLoggedInUser: ['services.github'], forOtherUsers: ['services.github.username'] }); - -Accounts.oauth.registerService('github', 2, function(query) { - - var accessToken = getAccessToken(query); - var identity = getIdentity(accessToken); - - return { - serviceData: { - id: identity.id, - accessToken: accessToken, - email: identity.email, - username: identity.login - }, - options: {profile: {name: identity.name}} - }; -}); - -// http://developer.github.com/v3/#user-agent-required -var userAgent = "Meteor"; -if (Meteor.release) - userAgent += "/" + Meteor.release; - -var getAccessToken = function (query) { - var config = ServiceConfiguration.configurations.findOne({service: 'github'}); - if (!config) - throw new ServiceConfiguration.ConfigError("Service not configured"); - - var response; - try { - response = Meteor.http.post( - "https://github.com/login/oauth/access_token", { - headers: { - Accept: 'application/json', - "User-Agent": userAgent - }, - 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. " + 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); - } else { - return response.data.access_token; - } -}; - -var getIdentity = function (accessToken) { - try { - return Meteor.http.get( - "https://api.github.com/user", { - headers: {"User-Agent": userAgent}, // http://developer.github.com/v3/#user-agent-required - params: {access_token: accessToken} - }).data; - } catch (err) { - throw new Error("Failed to fetch identity from GitHub. " + err.message); - } -}; diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index 4eba7133be..28c033a38d 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -1,16 +1,12 @@ Package.describe({ - summary: "Login service for Github accounts" + summary: "Accounts service for Github accounts" }); Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('oauth2', ['client', 'server']); - api.use('http', ['client', 'server']); - api.use('templating', 'client'); + api.use('github', ['client', 'server']); - api.add_files( - ['github_login_button.css', 'github_configure.html', 'github_configure.js'], - 'client'); + api.add_files('github_login_button.css', 'client'); api.add_files('github_common.js', ['client', 'server']); api.add_files('github_server.js', 'server'); diff --git a/packages/github/github_client.js b/packages/github/github_client.js new file mode 100644 index 0000000000..d0c389f1d7 --- /dev/null +++ b/packages/github/github_client.js @@ -0,0 +1,27 @@ +Github.requestCredential = function (options, callback, loginPopupClosedCallback) { + // support both (options, callback) and (callback). + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + + var config = ServiceConfiguration.configurations.findOne({service: 'github'}); + if (!config) { + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + return; + } + var state = Random.id(); + + var scope = (options && options.requestPermissions) || []; + var flatScope = _.map(scope, encodeURIComponent).join('+'); + + var loginUrl = + 'https://github.com/login/oauth/authorize' + + '?client_id=' + config.clientId + + '&scope=' + flatScope + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/github?close') + + '&state=' + state; + + Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback, + {width: 900, height: 450}); +}; diff --git a/packages/github/github_common.js b/packages/github/github_common.js new file mode 100644 index 0000000000..b01a5bec8a --- /dev/null +++ b/packages/github/github_common.js @@ -0,0 +1,3 @@ +if (typeof Github === 'undefined') { + Github = {}; +} diff --git a/packages/accounts-github/github_configure.html b/packages/github/github_configure.html similarity index 100% rename from packages/accounts-github/github_configure.html rename to packages/github/github_configure.html diff --git a/packages/accounts-github/github_configure.js b/packages/github/github_configure.js similarity index 100% rename from packages/accounts-github/github_configure.js rename to packages/github/github_configure.js diff --git a/packages/github/github_server.js b/packages/github/github_server.js new file mode 100644 index 0000000000..584344d064 --- /dev/null +++ b/packages/github/github_server.js @@ -0,0 +1,63 @@ +Oauth.registerService('github', 2, null, function(query) { + + var accessToken = getAccessToken(query); + var identity = getIdentity(accessToken); + + return { + serviceData: { + id: identity.id, + accessToken: accessToken, + email: identity.email, + username: identity.login + }, + options: {profile: {name: identity.name}} + }; +}); + +// http://developer.github.com/v3/#user-agent-required +var userAgent = "Meteor"; +if (Meteor.release) + userAgent += "/" + Meteor.release; + +var getAccessToken = function (query) { + var config = ServiceConfiguration.configurations.findOne({service: 'github'}); + if (!config) + throw new ServiceConfiguration.ConfigError("Service not configured"); + + var response; + try { + response = Meteor.http.post( + "https://github.com/login/oauth/access_token", { + headers: { + Accept: 'application/json', + "User-Agent": userAgent + }, + 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. " + 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); + } else { + return response.data.access_token; + } +}; + +var getIdentity = function (accessToken) { + try { + return Meteor.http.get( + "https://api.github.com/user", { + headers: {"User-Agent": userAgent}, // http://developer.github.com/v3/#user-agent-required + params: {access_token: accessToken} + }).data; + } catch (err) { + throw new Error("Failed to fetch identity from GitHub. " + err.message); + } +}; diff --git a/packages/github/package.js b/packages/github/package.js new file mode 100644 index 0000000000..18881e21b9 --- /dev/null +++ b/packages/github/package.js @@ -0,0 +1,17 @@ +Package.describe({ + summary: "Login service for Github accounts" +}); + +Package.on_use(function(api) { + api.use('oauth2', ['client', 'server']); + api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['github_configure.html', 'github_configure.js'], + 'client'); + + api.add_files('github_common.js', ['client', 'server']); + api.add_files('github_server.js', 'server'); + api.add_files('github_client.js', 'client'); +}); From b239f08ad7bba9fdfb87d6b7b75d158be06c2f6f Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Mon, 6 May 2013 16:28:58 -0700 Subject: [PATCH 26/60] Extract facebook package from accounts-facebook --- packages/accounts-facebook/facebook_client.js | 31 +----- packages/accounts-facebook/facebook_server.js | 104 +----------------- packages/accounts-facebook/package.js | 10 +- packages/facebook/facebook_client.js | 28 +++++ packages/facebook/facebook_common.js | 3 + .../facebook_configure.html | 0 .../facebook_configure.js | 0 packages/facebook/facebook_server.js | 92 ++++++++++++++++ packages/facebook/package.js | 17 +++ 9 files changed, 147 insertions(+), 138 deletions(-) create mode 100644 packages/facebook/facebook_client.js create mode 100644 packages/facebook/facebook_common.js rename packages/{accounts-facebook => facebook}/facebook_configure.html (100%) rename packages/{accounts-facebook => facebook}/facebook_configure.js (100%) create mode 100644 packages/facebook/facebook_server.js create mode 100644 packages/facebook/package.js diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index 8fa33c41a9..f69e45d914 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -1,28 +1,3 @@ -Meteor.loginWithFacebook = function (options, callback) { - // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - - var config = ServiceConfiguration.configurations.findOne({service: 'facebook'}); - if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); - return; - } - - var state = Random.id(); - var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); - var display = mobile ? 'touch' : 'popup'; - - var scope = "email"; - if (options && options.requestPermissions) - scope = options.requestPermissions.join(','); - - var loginUrl = - 'https://www.facebook.com/dialog/oauth?client_id=' + config.appId + - '&redirect_uri=' + Meteor.absoluteUrl('_oauth/facebook?close') + - '&display=' + display + '&scope=' + scope + '&state=' + state; - - Accounts.oauth.initiateLogin(state, loginUrl, callback); -}; +Meteor.loginWithFacebook = function(options, callback) { + Facebook.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); +}; \ No newline at end of file diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index fc7235be31..24f9ceefab 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -1,103 +1 @@ -var querystring = Npm.require('querystring'); - -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) { - - var response = getTokenResponse(query); - var accessToken = response.accessToken; - var identity = getIdentity(accessToken); - - var serviceData = { - accessToken: accessToken, - expiresAt: (+new Date) + (1000 * response.expiresIn) - }; - - // include all fields from facebook - // http://developers.facebook.com/docs/reference/login/public-profile-and-friend-list/ - var whitelisted = ['id', 'email', 'name', 'first_name', - 'last_name', 'link', 'username', 'gender', 'locale', 'age_range']; - - var fields = _.pick(identity, whitelisted); - _.extend(serviceData, fields); - - return { - serviceData: serviceData, - options: {profile: {name: identity.name}} - }; -}); - -// 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 -var getTokenResponse = function (query) { - var config = ServiceConfiguration.configurations.findOne({service: 'facebook'}); - if (!config) - throw new ServiceConfiguration.ConfigError("Service not configured"); - - var responseContent; - try { - // 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.message); - } - - // If 'responseContent' parses as JSON, it is an error. - // 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 - 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) { - 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. " + err.message); - } -}; +Accounts.oauth.registerService('facebook'); \ No newline at end of file diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index 035b553077..3b79215dc4 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -1,16 +1,12 @@ Package.describe({ - summary: "Login service for Facebook accounts" + summary: "Accounts service for Facebook accounts" }); Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('oauth2', ['client', 'server']); - api.use('http', ['client', 'server']); - api.use('templating', 'client'); + api.use('facebook', ['client', 'server']); - api.add_files( - ['facebook_login_button.css', 'facebook_configure.html', 'facebook_configure.js'], - 'client'); + api.add_files('facebook_login_button.css', 'client'); api.add_files('facebook_common.js', ['client', 'server']); api.add_files('facebook_server.js', 'server'); diff --git a/packages/facebook/facebook_client.js b/packages/facebook/facebook_client.js new file mode 100644 index 0000000000..100ace7ccb --- /dev/null +++ b/packages/facebook/facebook_client.js @@ -0,0 +1,28 @@ +Facebook.requestCredential = function (options, callback, loginPopupClosedCallback) { + // support both (options, callback) and (callback). + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + + var config = ServiceConfiguration.configurations.findOne({service: 'facebook'}); + if (!config) { + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + return; + } + + var state = Random.id(); + var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); + var display = mobile ? 'touch' : 'popup'; + + var scope = "email"; + if (options && options.requestPermissions) + scope = options.requestPermissions.join(','); + + var loginUrl = + 'https://www.facebook.com/dialog/oauth?client_id=' + config.appId + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/facebook?close') + + '&display=' + display + '&scope=' + scope + '&state=' + state; + + Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback); +}; diff --git a/packages/facebook/facebook_common.js b/packages/facebook/facebook_common.js new file mode 100644 index 0000000000..e3d7b26bcc --- /dev/null +++ b/packages/facebook/facebook_common.js @@ -0,0 +1,3 @@ +if (typeof Facebook === 'undefined') { + Facebook = {}; +} diff --git a/packages/accounts-facebook/facebook_configure.html b/packages/facebook/facebook_configure.html similarity index 100% rename from packages/accounts-facebook/facebook_configure.html rename to packages/facebook/facebook_configure.html diff --git a/packages/accounts-facebook/facebook_configure.js b/packages/facebook/facebook_configure.js similarity index 100% rename from packages/accounts-facebook/facebook_configure.js rename to packages/facebook/facebook_configure.js diff --git a/packages/facebook/facebook_server.js b/packages/facebook/facebook_server.js new file mode 100644 index 0000000000..fd4df3f04e --- /dev/null +++ b/packages/facebook/facebook_server.js @@ -0,0 +1,92 @@ +var querystring = Npm.require('querystring'); + + +Oauth.registerService('facebook', 2, null, function(query) { + + var response = getTokenResponse(query); + var accessToken = response.accessToken; + var identity = getIdentity(accessToken); + + var serviceData = { + accessToken: accessToken, + expiresAt: (+new Date) + (1000 * response.expiresIn) + }; + + // include all fields from facebook + // http://developers.facebook.com/docs/reference/login/public-profile-and-friend-list/ + var whitelisted = ['id', 'email', 'name', 'first_name', + 'last_name', 'link', 'username', 'gender', 'locale', 'age_range']; + + var fields = _.pick(identity, whitelisted); + _.extend(serviceData, fields); + + return { + serviceData: serviceData, + options: {profile: {name: identity.name}} + }; +}); + +// 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 +var getTokenResponse = function (query) { + var config = ServiceConfiguration.configurations.findOne({service: 'facebook'}); + if (!config) + throw new ServiceConfiguration.ConfigError("Service not configured"); + + var responseContent; + try { + // 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.message); + } + + // If 'responseContent' parses as JSON, it is an error. + // 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 + 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) { + 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. " + err.message); + } +}; diff --git a/packages/facebook/package.js b/packages/facebook/package.js new file mode 100644 index 0000000000..6294606cbc --- /dev/null +++ b/packages/facebook/package.js @@ -0,0 +1,17 @@ +Package.describe({ + summary: "Login service for Facebook accounts" +}); + +Package.on_use(function(api) { + api.use('oauth2', ['client', 'server']); + api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['facebook_configure.html', 'facebook_configure.js'], + 'client'); + + api.add_files('facebook_common.js', ['client', 'server']); + api.add_files('facebook_server.js', 'server'); + api.add_files('facebook_client.js', 'client'); +}); From 9b1b6da9ff020ffdf8e90e3f648848c0fa59bd72 Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Mon, 6 May 2013 16:44:58 -0700 Subject: [PATCH 27/60] Extract google package from accounts-google --- packages/accounts-facebook/facebook_server.js | 14 +++- packages/accounts-google/google_client.js | 42 +--------- packages/accounts-google/google_server.js | 78 +------------------ packages/accounts-google/package.js | 10 +-- packages/google/google_client.js | 39 ++++++++++ packages/google/google_common.js | 3 + .../google_configure.html | 0 .../google_configure.js | 0 packages/google/google_server.js | 74 ++++++++++++++++++ packages/google/package.js | 17 ++++ 10 files changed, 155 insertions(+), 122 deletions(-) create mode 100644 packages/google/google_client.js create mode 100644 packages/google/google_common.js rename packages/{accounts-google => google}/google_configure.html (100%) rename packages/{accounts-google => google}/google_configure.js (100%) create mode 100644 packages/google/google_server.js create mode 100644 packages/google/package.js diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 24f9ceefab..2699808eaa 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -1 +1,13 @@ -Accounts.oauth.registerService('facebook'); \ No newline at end of file +Accounts.oauth.registerService('facebook'); + +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' + ] +}); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 0471af568e..9190b154e2 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -1,39 +1,3 @@ -Meteor.loginWithGoogle = function (options, callback) { - // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } else if (!options) { - options = {}; - } - - var config = ServiceConfiguration.configurations.findOne({service: 'google'}); - if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); - return; - } - - var state = Random.id(); - - // always need this to get user id from google. - var requiredScope = ['https://www.googleapis.com/auth/userinfo.profile']; - var scope = ['https://www.googleapis.com/auth/userinfo.email']; - if (options.requestPermissions) - scope = options.requestPermissions; - scope = _.union(scope, requiredScope); - var flatScope = _.map(scope, encodeURIComponent).join('+'); - - // https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl - var accessType = options.requestOfflineToken ? 'offline' : 'online'; - - var loginUrl = - 'https://accounts.google.com/o/oauth2/auth' + - '?response_type=code' + - '&client_id=' + config.clientId + - '&scope=' + flatScope + - '&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') + - '&state=' + state + - '&access_type=' + accessType; - - Accounts.oauth.initiateLogin(state, loginUrl, callback); -}; +Meteor.loginWithGoogle = function(options, callback) { + Google.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); +}; \ No newline at end of file diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 463aa6b1df..5c8575fc18 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -1,6 +1,4 @@ -// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall -var whitelisted = ['id', 'email', 'verified_email', 'name', 'given_name', - 'family_name', 'picture', 'locale', 'timezone', 'gender']; +Accounts.oauth.registerService('google'); Accounts.addAutopublishFields({ forLoggedInUser: _.map( @@ -8,82 +6,12 @@ Accounts.addAutopublishFields({ // 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 + Google.whitelistedFields.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'), + _.without(Google.whitelistedFields, 'email', 'verified_email'), function (subfield) { return 'services.google.' + subfield; }) }); - -Accounts.oauth.registerService('google', 2, function(query) { - - var response = getTokens(query); - var accessToken = response.accessToken; - var identity = getIdentity(accessToken); - - var serviceData = { - accessToken: accessToken, - expiresAt: (+new Date) + (1000 * response.expiresIn) - }; - - var fields = _.pick(identity, whitelisted); - _.extend(serviceData, fields); - - // only set the token in serviceData if it's there. this ensures - // that we don't lose old ones (since we only get this on the first - // log in attempt) - if (response.refreshToken) - serviceData.refreshToken = response.refreshToken; - - return { - serviceData: serviceData, - options: {profile: {name: identity.name}} - }; -}); - -// returns an object containing: -// - accessToken -// - expiresIn: lifetime of token in seconds -// - refreshToken, if this is the first authorization request -var getTokens = function (query) { - var config = ServiceConfiguration.configurations.findOne({service: 'google'}); - if (!config) - throw new ServiceConfiguration.ConfigError("Service not configured"); - - 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. " + 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 Google. " + response.data.error); - } else { - return { - accessToken: response.data.access_token, - refreshToken: response.data.refresh_token, - expiresIn: response.data.expires_in - }; - } -}; - -var getIdentity = function (accessToken) { - 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. " + err.message); - } -}; diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index 099f0b1e18..3998e4caf5 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -1,16 +1,12 @@ Package.describe({ - summary: "Login service for Google accounts" + summary: "Accounts service for Google accounts" }); Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('oauth2', ['client', 'server']); - api.use('http', ['client', 'server']); - api.use('templating', 'client'); + api.use('google', ['client', 'server']); - api.add_files( - ['google_login_button.css', 'google_configure.html', 'google_configure.js'], - 'client'); + api.add_files('google_login_button.css', 'client'); api.add_files('google_common.js', ['client', 'server']); api.add_files('google_server.js', 'server'); diff --git a/packages/google/google_client.js b/packages/google/google_client.js new file mode 100644 index 0000000000..dcc1b63ad9 --- /dev/null +++ b/packages/google/google_client.js @@ -0,0 +1,39 @@ +Google.requestCredential = function (options, callback, loginPopupClosedCallback) { + // support both (options, callback) and (callback). + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } else if (!options) { + options = {}; + } + + var config = ServiceConfiguration.configurations.findOne({service: 'google'}); + if (!config) { + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + return; + } + + var state = Random.id(); + + // always need this to get user id from google. + var requiredScope = ['https://www.googleapis.com/auth/userinfo.profile']; + var scope = ['https://www.googleapis.com/auth/userinfo.email']; + if (options.requestPermissions) + scope = options.requestPermissions; + scope = _.union(scope, requiredScope); + var flatScope = _.map(scope, encodeURIComponent).join('+'); + + // https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl + var accessType = options.requestOfflineToken ? 'offline' : 'online'; + + var loginUrl = + 'https://accounts.google.com/o/oauth2/auth' + + '?response_type=code' + + '&client_id=' + config.clientId + + '&scope=' + flatScope + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') + + '&state=' + state + + '&access_type=' + accessType; + + Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback); +}; diff --git a/packages/google/google_common.js b/packages/google/google_common.js new file mode 100644 index 0000000000..167abf1779 --- /dev/null +++ b/packages/google/google_common.js @@ -0,0 +1,3 @@ +if (typeof Google === 'undefined') { + Google = {}; +} diff --git a/packages/accounts-google/google_configure.html b/packages/google/google_configure.html similarity index 100% rename from packages/accounts-google/google_configure.html rename to packages/google/google_configure.html diff --git a/packages/accounts-google/google_configure.js b/packages/google/google_configure.js similarity index 100% rename from packages/accounts-google/google_configure.js rename to packages/google/google_configure.js diff --git a/packages/google/google_server.js b/packages/google/google_server.js new file mode 100644 index 0000000000..afffde58a1 --- /dev/null +++ b/packages/google/google_server.js @@ -0,0 +1,74 @@ +// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall +Google.whitelistedFields = ['id', 'email', 'verified_email', 'name', 'given_name', + 'family_name', 'picture', 'locale', 'timezone', 'gender']; + + +Oauth.registerService('google', 2, null, function(query) { + + var response = getTokens(query); + var accessToken = response.accessToken; + var identity = getIdentity(accessToken); + + var serviceData = { + accessToken: accessToken, + expiresAt: (+new Date) + (1000 * response.expiresIn) + }; + + var fields = _.pick(identity, Google.whitelistedFields); + _.extend(serviceData, fields); + + // only set the token in serviceData if it's there. this ensures + // that we don't lose old ones (since we only get this on the first + // log in attempt) + if (response.refreshToken) + serviceData.refreshToken = response.refreshToken; + + return { + serviceData: serviceData, + options: {profile: {name: identity.name}} + }; +}); + +// returns an object containing: +// - accessToken +// - expiresIn: lifetime of token in seconds +// - refreshToken, if this is the first authorization request +var getTokens = function (query) { + var config = ServiceConfiguration.configurations.findOne({service: 'google'}); + if (!config) + throw new ServiceConfiguration.ConfigError("Service not configured"); + + 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. " + 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 Google. " + response.data.error); + } else { + return { + accessToken: response.data.access_token, + refreshToken: response.data.refresh_token, + expiresIn: response.data.expires_in + }; + } +}; + +var getIdentity = function (accessToken) { + 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. " + err.message); + } +}; diff --git a/packages/google/package.js b/packages/google/package.js new file mode 100644 index 0000000000..ffc58e067a --- /dev/null +++ b/packages/google/package.js @@ -0,0 +1,17 @@ +Package.describe({ + summary: "Login service for Google accounts" +}); + +Package.on_use(function(api) { + api.use('oauth2', ['client', 'server']); + api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['google_configure.html', 'google_configure.js'], + 'client'); + + api.add_files('google_common.js', ['client', 'server']); + api.add_files('google_server.js', 'server'); + api.add_files('google_client.js', 'client'); +}); From 8f2eb9b48195f5bc393f46b18ba2e7fb00725381 Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Mon, 6 May 2013 17:09:43 -0700 Subject: [PATCH 28/60] Extract meetup package from accounts-meetup --- packages/accounts-meetup/meetup_client.js | 36 ++----------- packages/accounts-meetup/meetup_server.js | 53 +------------------ packages/accounts-meetup/package.js | 10 ++-- packages/meetup/meetup_client.js | 33 ++++++++++++ packages/meetup/meetup_common.js | 3 ++ .../meetup_configure.html | 0 .../meetup_configure.js | 0 packages/meetup/meetup_server.js | 51 ++++++++++++++++++ packages/meetup/package.js | 17 ++++++ 9 files changed, 112 insertions(+), 91 deletions(-) create mode 100644 packages/meetup/meetup_client.js create mode 100644 packages/meetup/meetup_common.js rename packages/{accounts-meetup => meetup}/meetup_configure.html (100%) rename packages/{accounts-meetup => meetup}/meetup_configure.js (100%) create mode 100644 packages/meetup/meetup_server.js create mode 100644 packages/meetup/package.js diff --git a/packages/accounts-meetup/meetup_client.js b/packages/accounts-meetup/meetup_client.js index ec4e30e7e1..7652f7cbdb 100644 --- a/packages/accounts-meetup/meetup_client.js +++ b/packages/accounts-meetup/meetup_client.js @@ -1,33 +1,3 @@ -Meteor.loginWithMeetup = function (options, callback) { - // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - - var config = ServiceConfiguration.configurations.findOne({service: 'meetup'}); - if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); - return; - } - var state = Random.id(); - - var scope = (options && options.requestPermissions) || []; - var flatScope = _.map(scope, encodeURIComponent).join('+'); - - var loginUrl = - 'https://secure.meetup.com/oauth2/authorize' + - '?client_id=' + config.clientId + - '&response_type=code' + - '&scope=' + flatScope + - '&redirect_uri=' + Meteor.absoluteUrl('_oauth/meetup?close') + - '&state=' + state; - - // meetup box gets taller when permissions requested. - var height = 620; - if (_.without(scope, 'basic').length) - height += 130; - - Accounts.oauth.initiateLogin(state, loginUrl, callback, - {width: 900, height: height}); -}; +Meteor.loginWithMeetup = function(options, callback) { + Meetup.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); +}; \ No newline at end of file diff --git a/packages/accounts-meetup/meetup_server.js b/packages/accounts-meetup/meetup_server.js index e0e5ac2a2d..4b727ac031 100644 --- a/packages/accounts-meetup/meetup_server.js +++ b/packages/accounts-meetup/meetup_server.js @@ -1,3 +1,5 @@ +Accounts.oauth.registerService('meetup'); + Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately // be used from the client (if transmitted over ssl or on @@ -7,54 +9,3 @@ Accounts.addAutopublishFields({ }); -Accounts.oauth.registerService('meetup', 2, function(query) { - - var accessToken = getAccessToken(query); - var identity = getIdentity(accessToken); - - return { - serviceData: { - id: identity.id, - accessToken: accessToken - }, - options: {profile: {name: identity.name}} - }; -}); - -var getAccessToken = function (query) { - var config = ServiceConfiguration.configurations.findOne({service: 'meetup'}); - if (!config) - throw new ServiceConfiguration.ConfigError("Service not configured"); - - 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. " + 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 Meetup. " + response.data.error); - } else { - return response.data.access_token; - } -}; - -var getIdentity = function (accessToken) { - 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: " + err.message); - } -}; diff --git a/packages/accounts-meetup/package.js b/packages/accounts-meetup/package.js index 6c047d23f5..86b9f88834 100644 --- a/packages/accounts-meetup/package.js +++ b/packages/accounts-meetup/package.js @@ -1,16 +1,12 @@ Package.describe({ - summary: "Login service for Meetup accounts" + summary: "Accounts service for Meetup accounts" }); Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('oauth2', ['client', 'server']); - api.use('http', ['client', 'server']); - api.use('templating', 'client'); + api.use('meetup', ['client', 'server']); - api.add_files( - ['meetup_login_button.css', 'meetup_configure.html', 'meetup_configure.js'], - 'client'); + api.add_files('meetup_login_button.css', 'client'); api.add_files('meetup_common.js', ['client', 'server']); api.add_files('meetup_server.js', 'server'); diff --git a/packages/meetup/meetup_client.js b/packages/meetup/meetup_client.js new file mode 100644 index 0000000000..58c311fca7 --- /dev/null +++ b/packages/meetup/meetup_client.js @@ -0,0 +1,33 @@ +Meetup.requestCredential = function (options, callback, loginPopupClosedCallback) { + // support both (options, callback) and (callback). + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + + var config = ServiceConfiguration.configurations.findOne({service: 'meetup'}); + if (!config) { + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + return; + } + var state = Random.id(); + + var scope = (options && options.requestPermissions) || []; + var flatScope = _.map(scope, encodeURIComponent).join('+'); + + var loginUrl = + 'https://secure.meetup.com/oauth2/authorize' + + '?client_id=' + config.clientId + + '&response_type=code' + + '&scope=' + flatScope + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/meetup?close') + + '&state=' + state; + + // meetup box gets taller when permissions requested. + var height = 620; + if (_.without(scope, 'basic').length) + height += 130; + + Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback, + {width: 900, height: height}); +}; diff --git a/packages/meetup/meetup_common.js b/packages/meetup/meetup_common.js new file mode 100644 index 0000000000..15e2594fe1 --- /dev/null +++ b/packages/meetup/meetup_common.js @@ -0,0 +1,3 @@ +if (typeof Meetup === 'undefined') { + Meetup = {}; +} diff --git a/packages/accounts-meetup/meetup_configure.html b/packages/meetup/meetup_configure.html similarity index 100% rename from packages/accounts-meetup/meetup_configure.html rename to packages/meetup/meetup_configure.html diff --git a/packages/accounts-meetup/meetup_configure.js b/packages/meetup/meetup_configure.js similarity index 100% rename from packages/accounts-meetup/meetup_configure.js rename to packages/meetup/meetup_configure.js diff --git a/packages/meetup/meetup_server.js b/packages/meetup/meetup_server.js new file mode 100644 index 0000000000..ba2b9c08ab --- /dev/null +++ b/packages/meetup/meetup_server.js @@ -0,0 +1,51 @@ +Oauth.registerService('meetup', 2, null, function(query) { + + var accessToken = getAccessToken(query); + var identity = getIdentity(accessToken); + + return { + serviceData: { + id: identity.id, + accessToken: accessToken + }, + options: {profile: {name: identity.name}} + }; +}); + +var getAccessToken = function (query) { + var config = ServiceConfiguration.configurations.findOne({service: 'meetup'}); + if (!config) + throw new ServiceConfiguration.ConfigError("Service not configured"); + + 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. " + 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 Meetup. " + response.data.error); + } else { + return response.data.access_token; + } +}; + +var getIdentity = function (accessToken) { + 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: " + err.message); + } +}; diff --git a/packages/meetup/package.js b/packages/meetup/package.js new file mode 100644 index 0000000000..7136142ee1 --- /dev/null +++ b/packages/meetup/package.js @@ -0,0 +1,17 @@ +Package.describe({ + summary: "Login service for Meetup accounts" +}); + +Package.on_use(function(api) { + api.use('oauth2', ['client', 'server']); + api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['meetup_configure.html', 'meetup_configure.js'], + 'client'); + + api.add_files('meetup_common.js', ['client', 'server']); + api.add_files('meetup_server.js', 'server'); + api.add_files('meetup_client.js', 'client'); +}); From d57f7047f4388c67ed34d2aca13aabd54f8753e5 Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Mon, 6 May 2013 17:43:50 -0700 Subject: [PATCH 29/60] Extract weibo package from accounts-weibo --- packages/accounts-weibo/package.js | 10 +-- packages/accounts-weibo/weibo_client.js | 28 +------- packages/accounts-weibo/weibo_server.js | 71 +------------------ packages/weibo/package.js | 17 +++++ packages/weibo/weibo_client.js | 25 +++++++ packages/weibo/weibo_common.js | 3 + .../weibo_configure.html | 0 .../weibo_configure.js | 0 packages/weibo/weibo_server.js | 69 ++++++++++++++++++ 9 files changed, 122 insertions(+), 101 deletions(-) create mode 100644 packages/weibo/package.js create mode 100644 packages/weibo/weibo_client.js create mode 100644 packages/weibo/weibo_common.js rename packages/{accounts-weibo => weibo}/weibo_configure.html (100%) rename packages/{accounts-weibo => weibo}/weibo_configure.js (100%) create mode 100644 packages/weibo/weibo_server.js diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index ce6a5dde31..2b2a2c7c2c 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -1,16 +1,12 @@ Package.describe({ - summary: "Login service for Sina Weibo accounts" + summary: "Accounts service for Sina Weibo accounts" }); Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); - api.use('oauth2', ['client', 'server']); - api.use('http', ['client', 'server']); - api.use('templating', 'client'); + api.use('weibo', ['client', 'server']); - api.add_files( - ['weibo_login_button.css', 'weibo_configure.html', 'weibo_configure.js'], - 'client'); + api.add_files('weibo_login_button.css', 'client'); api.add_files('weibo_common.js', ['client', 'server']); api.add_files('weibo_server.js', 'server'); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 44a8e02a2c..d71d582490 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -1,25 +1,3 @@ -// XXX support options.requestPermissions as we do for Facebook, Google, Github -Meteor.loginWithWeibo = function (options, callback) { - // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - - var config = ServiceConfiguration.configurations.findOne({service: 'weibo'}); - if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); - return; - } - - var state = Random.id(); - // XXX need to support configuring access_type and scope - var loginUrl = - 'https://api.weibo.com/oauth2/authorize' + - '?response_type=code' + - '&client_id=' + config.clientId + - '&redirect_uri=' + Meteor.absoluteUrl('_oauth/weibo?close', {replaceLocalhost: true}) + - '&state=' + state; - - Accounts.oauth.initiateLogin(state, loginUrl, callback); -}; +Meteor.loginWithWeibo = function(options, callback) { + Weibo.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); +}; \ No newline at end of file diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 75f29785d7..18a3a12ae7 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -1,3 +1,5 @@ +Accounts.oauth.registerService('weibo'); + Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately // be used from the client (if transmitted over ssl or on localhost) @@ -5,72 +7,3 @@ Accounts.addAutopublishFields({ forOtherUsers: ['services.weibo.screenName'] }); -Accounts.oauth.registerService('weibo', 2, function(query) { - - var response = getTokenResponse(query); - var uid = parseInt(response.uid, 10); - - // different parts of weibo's api seem to expect numbers, or strings - // for uid. let's make sure they're both the same. - if (response.uid !== uid + "") - throw new Error("Expected 'uid' to parse to an integer: " + JSON.stringify(response)); - - var identity = getIdentity(response.access_token, uid); - - return { - serviceData: { - // We used to store this as a string, so keep it this way rather than - // add complexity to Account.updateOrCreateUserFromExternalService or - // force a database migration - id: uid + "", - accessToken: response.access_token, - screenName: identity.screen_name, - expiresAt: (+new Date) + (1000 * response.expires_in) - }, - options: {profile: {name: identity.screen_name}} - }; -}); - -// return an object containining: -// - uid -// - access_token -// - expires_in: lifetime of this token in seconds (5 years(!) right now) -var getTokenResponse = function (query) { - var config = ServiceConfiguration.configurations.findOne({service: 'weibo'}); - if (!config) - throw new ServiceConfiguration.ConfigError("Service not configured"); - - 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. " + err.message); - } - - // result.headers["content-type"] is 'text/plain;charset=UTF-8', so - // the http package doesn't automatically populate result.data - response.data = JSON.parse(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 Weibo. " + response.data.error); - } else { - return response.data; - } -}; - -var getIdentity = function (accessToken, userId) { - 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. " + err.message); - } -}; diff --git a/packages/weibo/package.js b/packages/weibo/package.js new file mode 100644 index 0000000000..ae2b709f1c --- /dev/null +++ b/packages/weibo/package.js @@ -0,0 +1,17 @@ +Package.describe({ + summary: "Login service for Weibo accounts" +}); + +Package.on_use(function(api) { + api.use('oauth2', ['client', 'server']); + api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['weibo_configure.html', 'weibo_configure.js'], + 'client'); + + api.add_files('weibo_common.js', ['client', 'server']); + api.add_files('weibo_server.js', 'server'); + api.add_files('weibo_client.js', 'client'); +}); diff --git a/packages/weibo/weibo_client.js b/packages/weibo/weibo_client.js new file mode 100644 index 0000000000..c183c167f6 --- /dev/null +++ b/packages/weibo/weibo_client.js @@ -0,0 +1,25 @@ +// XXX support options.requestPermissions as we do for Facebook, Google, Github +Weibo.requestCredential = function (options, callback, loginPopupClosedCallback) { + // support both (options, callback) and (callback). + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + + var config = ServiceConfiguration.configurations.findOne({service: 'weibo'}); + if (!config) { + callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + return; + } + + var state = Random.id(); + // XXX need to support configuring access_type and scope + var loginUrl = + 'https://api.weibo.com/oauth2/authorize' + + '?response_type=code' + + '&client_id=' + config.clientId + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/weibo?close', {replaceLocalhost: true}) + + '&state=' + state; + + Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback); +}; diff --git a/packages/weibo/weibo_common.js b/packages/weibo/weibo_common.js new file mode 100644 index 0000000000..7f26d9defe --- /dev/null +++ b/packages/weibo/weibo_common.js @@ -0,0 +1,3 @@ +if (typeof Weibo === 'undefined') { + Weibo = {}; +} diff --git a/packages/accounts-weibo/weibo_configure.html b/packages/weibo/weibo_configure.html similarity index 100% rename from packages/accounts-weibo/weibo_configure.html rename to packages/weibo/weibo_configure.html diff --git a/packages/accounts-weibo/weibo_configure.js b/packages/weibo/weibo_configure.js similarity index 100% rename from packages/accounts-weibo/weibo_configure.js rename to packages/weibo/weibo_configure.js diff --git a/packages/weibo/weibo_server.js b/packages/weibo/weibo_server.js new file mode 100644 index 0000000000..f0d9ff2df6 --- /dev/null +++ b/packages/weibo/weibo_server.js @@ -0,0 +1,69 @@ +Oauth.registerService('weibo', 2, null, function(query) { + + var response = getTokenResponse(query); + var uid = parseInt(response.uid, 10); + + // different parts of weibo's api seem to expect numbers, or strings + // for uid. let's make sure they're both the same. + if (response.uid !== uid + "") + throw new Error("Expected 'uid' to parse to an integer: " + JSON.stringify(response)); + + var identity = getIdentity(response.access_token, uid); + + return { + serviceData: { + // We used to store this as a string, so keep it this way rather than + // add complexity to Account.updateOrCreateUserFromExternalService or + // force a database migration + id: uid + "", + accessToken: response.access_token, + screenName: identity.screen_name, + expiresAt: (+new Date) + (1000 * response.expires_in) + }, + options: {profile: {name: identity.screen_name}} + }; +}); + +// return an object containining: +// - uid +// - access_token +// - expires_in: lifetime of this token in seconds (5 years(!) right now) +var getTokenResponse = function (query) { + var config = ServiceConfiguration.configurations.findOne({service: 'weibo'}); + if (!config) + throw new ServiceConfiguration.ConfigError("Service not configured"); + + 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. " + err.message); + } + + // result.headers["content-type"] is 'text/plain;charset=UTF-8', so + // the http package doesn't automatically populate result.data + response.data = JSON.parse(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 Weibo. " + response.data.error); + } else { + return response.data; + } +}; + +var getIdentity = function (accessToken, userId) { + 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. " + err.message); + } +}; From 00efa2fe51ac4b7e07d31b5b8840aef0fc635424 Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Tue, 7 May 2013 12:27:46 -0700 Subject: [PATCH 30/60] Update requestCredential method to take only 2 params - options and credentialRequestCompleteCallback --- packages/accounts-facebook/facebook_client.js | 3 ++- packages/accounts-facebook/package.js | 1 + packages/accounts-github/github_client.js | 3 ++- packages/accounts-github/package.js | 1 + packages/accounts-google/google_client.js | 3 ++- packages/accounts-google/package.js | 1 + packages/accounts-meetup/meetup_client.js | 3 ++- packages/accounts-meetup/package.js | 1 + packages/accounts-oauth/oauth_client.js | 13 ++++++++-- packages/accounts-oauth/oauth_server.js | 8 +++--- packages/accounts-twitter/twitter_client.js | 3 ++- packages/accounts-weibo/package.js | 1 + packages/accounts-weibo/weibo_client.js | 3 ++- packages/facebook/facebook_client.js | 19 +++++++++----- packages/github/github_client.js | 19 +++++++++----- packages/google/google_client.js | 19 +++++++++----- packages/meetup/meetup_client.js | 19 +++++++++----- packages/oauth/oauth_client.js | 13 +++++----- packages/oauth/oauth_server.js | 6 ++--- packages/oauth1/oauth1_server.js | 2 +- packages/oauth1/oauth1_tests.js | 20 +++++++------- packages/oauth2/oauth2_server.js | 2 +- packages/oauth2/oauth2_tests.js | 12 ++++----- packages/twitter/twitter_client.js | 26 +++++++++++-------- packages/weibo/weibo_client.js | 20 ++++++++------ 25 files changed, 134 insertions(+), 87 deletions(-) diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index f69e45d914..149bfa8a0e 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -1,3 +1,4 @@ Meteor.loginWithFacebook = function(options, callback) { - Facebook.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Facebook.requestCredential(options, credentialRequestCompleteCallback); }; \ No newline at end of file diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index 3b79215dc4..00bbdd22d7 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -4,6 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); + api.use('accounts-oauth', ['client', 'server']); api.use('facebook', ['client', 'server']); api.add_files('facebook_login_button.css', 'client'); diff --git a/packages/accounts-github/github_client.js b/packages/accounts-github/github_client.js index cffd0ce805..7649f2bebb 100644 --- a/packages/accounts-github/github_client.js +++ b/packages/accounts-github/github_client.js @@ -1,3 +1,4 @@ Meteor.loginWithGithub = function(options, callback) { - Github.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Github.requestCredential(options, credentialRequestCompleteCallback); }; \ No newline at end of file diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index 28c033a38d..f941273152 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -4,6 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); + api.use('accounts-oauth', ['client', 'server']); api.use('github', ['client', 'server']); api.add_files('github_login_button.css', 'client'); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 9190b154e2..96641ac3bd 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -1,3 +1,4 @@ Meteor.loginWithGoogle = function(options, callback) { - Google.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Google.requestCredential(options, credentialRequestCompleteCallback); }; \ No newline at end of file diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index 3998e4caf5..6ce0cfe15b 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -4,6 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); + api.use('accounts-oauth', ['client', 'server']); api.use('google', ['client', 'server']); api.add_files('google_login_button.css', 'client'); diff --git a/packages/accounts-meetup/meetup_client.js b/packages/accounts-meetup/meetup_client.js index 7652f7cbdb..6d0d674a2f 100644 --- a/packages/accounts-meetup/meetup_client.js +++ b/packages/accounts-meetup/meetup_client.js @@ -1,3 +1,4 @@ Meteor.loginWithMeetup = function(options, callback) { - Meetup.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Meetup.requestCredential(options, credentialRequestCompleteCallback); }; \ No newline at end of file diff --git a/packages/accounts-meetup/package.js b/packages/accounts-meetup/package.js index 86b9f88834..2c8eb330a1 100644 --- a/packages/accounts-meetup/package.js +++ b/packages/accounts-meetup/package.js @@ -4,6 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); + api.use('accounts-oauth', ['client', 'server']); api.use('meetup', ['client', 'server']); api.add_files('meetup_login_button.css', 'client'); diff --git a/packages/accounts-oauth/oauth_client.js b/packages/accounts-oauth/oauth_client.js index ec5528ef2c..5b189fdc05 100644 --- a/packages/accounts-oauth/oauth_client.js +++ b/packages/accounts-oauth/oauth_client.js @@ -1,9 +1,9 @@ // Send an OAuth login method to the server. If the user authorized // access in the popup this should log the user in, otherwise // nothing should happen. -Accounts.oauth.tryLoginAfterPopupClosed = function(state, callback) { +Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback) { Accounts.callLoginMethod({ - methodArguments: [{oauth: {state: state}}], + methodArguments: [{oauth: {credentialToken: credentialToken}}], userCallback: callback && function (err) { // Allow server to specify a specify subclass of errors. We should come // up with a more generic way to do this! @@ -16,3 +16,12 @@ Accounts.oauth.tryLoginAfterPopupClosed = function(state, callback) { }}); }; +Accounts.oauth.credentialRequestCompleteHandler = function(callback) { + return function (credentialTokenOrError) { + if(credentialTokenOrError && credentialTokenOrError instanceof Error) { + callback(credentialTokenOrError); + } else { + Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback); + } + }; +} diff --git a/packages/accounts-oauth/oauth_server.js b/packages/accounts-oauth/oauth_server.js index f1c306e65a..6c2c3fb4a5 100644 --- a/packages/accounts-oauth/oauth_server.js +++ b/packages/accounts-oauth/oauth_server.js @@ -25,10 +25,10 @@ Accounts.registerLoginHandler(function (options) { if (!options.oauth) return undefined; // don't handle - check(options.oauth, {state: String}); + check(options.oauth, {credentialToken: String}); - if (!_.has(Oauth._loginResultForState, options.oauth.state)) { - // OAuth state is not recognized, which could be either because the popup + if (!_.has(Oauth._loginResultForCredentialToken, options.oauth.credentialToken)) { + // OAuth credentialToken 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 // popup somehow. @@ -40,7 +40,7 @@ Accounts.registerLoginHandler(function (options) { throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'No matching login attempt found'); } - var result = Oauth._loginResultForState[options.oauth.state]; + var result = Oauth._loginResultForCredentialToken[options.oauth.credentialToken]; if (result instanceof Error) // We tried to login, but there was a fatal error. Report it back // to the user. diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 62ca4d09c1..887d4ad510 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -1,3 +1,4 @@ Meteor.loginWithTwitter = function(options, callback) { - Twitter.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Twitter.requestCredential(options, credentialRequestCompleteCallback); }; \ No newline at end of file diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index 2b2a2c7c2c..469a2589e5 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -4,6 +4,7 @@ Package.describe({ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); + api.use('accounts-oauth', ['client', 'server']); api.use('weibo', ['client', 'server']); api.add_files('weibo_login_button.css', 'client'); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index d71d582490..644c0176e6 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -1,3 +1,4 @@ Meteor.loginWithWeibo = function(options, callback) { - Weibo.requestCredential(options, callback, Accounts.oauth.tryLoginAfterPopupClosed); + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Weibo.requestCredential(options, credentialRequestCompleteCallback); }; \ No newline at end of file diff --git a/packages/facebook/facebook_client.js b/packages/facebook/facebook_client.js index 100ace7ccb..d9c2511cde 100644 --- a/packages/facebook/facebook_client.js +++ b/packages/facebook/facebook_client.js @@ -1,17 +1,22 @@ -Facebook.requestCredential = function (options, callback, loginPopupClosedCallback) { +// Request Facebook credentials for the user +// @param options {optional} +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on +// error. +Facebook.requestCredential = function (options, credentialRequestCompleteCallback) { // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; + if (!credentialRequestCompleteCallback && typeof options === 'function') { + credentialRequestCompleteCallback = options; options = {}; } var config = ServiceConfiguration.configurations.findOne({service: 'facebook'}); if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured")); return; } - var state = Random.id(); + var credentialToken = Random.id(); var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); var display = mobile ? 'touch' : 'popup'; @@ -22,7 +27,7 @@ Facebook.requestCredential = function (options, callback, loginPopupClosedCallba var loginUrl = 'https://www.facebook.com/dialog/oauth?client_id=' + config.appId + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/facebook?close') + - '&display=' + display + '&scope=' + scope + '&state=' + state; + '&display=' + display + '&scope=' + scope + '&state=' + credentialToken; - Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback); + Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback); }; diff --git a/packages/github/github_client.js b/packages/github/github_client.js index d0c389f1d7..8a74721272 100644 --- a/packages/github/github_client.js +++ b/packages/github/github_client.js @@ -1,16 +1,21 @@ -Github.requestCredential = function (options, callback, loginPopupClosedCallback) { +// Request Github credentials for the user +// @param options {optional} +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on +// error. +Github.requestCredential = function (options, credentialRequestCompleteCallback) { // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; + if (!credentialRequestCompleteCallback && typeof options === 'function') { + credentialRequestCompleteCallback = options; options = {}; } var config = ServiceConfiguration.configurations.findOne({service: 'github'}); if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured")); return; } - var state = Random.id(); + var credentialToken = Random.id(); var scope = (options && options.requestPermissions) || []; var flatScope = _.map(scope, encodeURIComponent).join('+'); @@ -20,8 +25,8 @@ Github.requestCredential = function (options, callback, loginPopupClosedCallback '?client_id=' + config.clientId + '&scope=' + flatScope + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/github?close') + - '&state=' + state; + '&state=' + credentialToken; - Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback, + Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback, {width: 900, height: 450}); }; diff --git a/packages/google/google_client.js b/packages/google/google_client.js index dcc1b63ad9..83b8c189cd 100644 --- a/packages/google/google_client.js +++ b/packages/google/google_client.js @@ -1,7 +1,12 @@ -Google.requestCredential = function (options, callback, loginPopupClosedCallback) { +// Request Google credentials for the user +// @param options {optional} +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on +// error. +Google.requestCredential = function (options, credentialRequestCompleteCallback) { // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; + if (!credentialRequestCompleteCallback && typeof options === 'function') { + credentialRequestCompleteCallback = options; options = {}; } else if (!options) { options = {}; @@ -9,11 +14,11 @@ Google.requestCredential = function (options, callback, loginPopupClosedCallback var config = ServiceConfiguration.configurations.findOne({service: 'google'}); if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured")); return; } - var state = Random.id(); + var credentialToken = Random.id(); // always need this to get user id from google. var requiredScope = ['https://www.googleapis.com/auth/userinfo.profile']; @@ -32,8 +37,8 @@ Google.requestCredential = function (options, callback, loginPopupClosedCallback '&client_id=' + config.clientId + '&scope=' + flatScope + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') + - '&state=' + state + + '&state=' + credentialToken + '&access_type=' + accessType; - Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback); + Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback); }; diff --git a/packages/meetup/meetup_client.js b/packages/meetup/meetup_client.js index 58c311fca7..1f8cbc4ee0 100644 --- a/packages/meetup/meetup_client.js +++ b/packages/meetup/meetup_client.js @@ -1,16 +1,21 @@ -Meetup.requestCredential = function (options, callback, loginPopupClosedCallback) { +// Request Meetup credentials for the user +// @param options {optional} +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on +// error. +Meetup.requestCredential = function (options, credentialRequestCompleteCallback) { // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; + if (!credentialRequestCompleteCallback && typeof options === 'function') { + credentialRequestCompleteCallback = options; options = {}; } var config = ServiceConfiguration.configurations.findOne({service: 'meetup'}); if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured")); return; } - var state = Random.id(); + var credentialToken = Random.id(); var scope = (options && options.requestPermissions) || []; var flatScope = _.map(scope, encodeURIComponent).join('+'); @@ -21,13 +26,13 @@ Meetup.requestCredential = function (options, callback, loginPopupClosedCallback '&response_type=code' + '&scope=' + flatScope + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/meetup?close') + - '&state=' + state; + '&state=' + credentialToken; // meetup box gets taller when permissions requested. var height = 620; if (_.without(scope, 'basic').length) height += 130; - Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback, + Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback, {width: 900, height: height}); }; diff --git a/packages/oauth/oauth_client.js b/packages/oauth/oauth_client.js index 1b0de3ed40..4d59040721 100644 --- a/packages/oauth/oauth_client.js +++ b/packages/oauth/oauth_client.js @@ -1,15 +1,13 @@ // Open a popup window pointing to a OAuth handshake page // -// @param state {String} The OAuth state generated by the client +// @param credentialToken {String} The OAuth credentialToken generated by the client // @param url {String} url to page -// @param callback {Function} Callback function to call on -// completion. Takes one argument, null on success, or Error on +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on // error. -// @param loginPopupClosedCallback the callback to call when the -// login screen is dismissed. Takes state and the callback // @param dimensions {optional Object(width, height)} The dimensions of // the popup. If not passed defaults to something sane -Oauth.initiateLogin = function(state, url, callback, loginPopupClosedCallback, dimensions) { +Oauth.initiateLogin = function(credentialToken, url, credentialRequestCompleteCallback, dimensions) { // default dimensions that worked well for facebook and google var popup = openCenteredPopup( url, @@ -32,11 +30,12 @@ Oauth.initiateLogin = function(state, url, callback, loginPopupClosedCallback, d if (popupClosed) { clearInterval(checkPopupOpen); - loginPopupClosedCallback(state, callback); + credentialRequestCompleteCallback(credentialToken); } }, 100); }; + var openCenteredPopup = function(url, width, height) { var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft; diff --git a/packages/oauth/oauth_server.js b/packages/oauth/oauth_server.js index 97de8ea5c3..7459cb9d3c 100644 --- a/packages/oauth/oauth_server.js +++ b/packages/oauth/oauth_server.js @@ -42,10 +42,10 @@ Oauth._unregisterService = function (name) { // When we get an incoming OAuth http request we complete the oauth // handshake, account and token setup before responding. The // results are stored in this map which is then read when the login -// method is called. Maps state --> return value of `login` +// method is called. Maps credentialToken --> return value of `login` // // XXX we should periodically clear old entries -Oauth._loginResultForState = {}; +Oauth._loginResultForCredentialToken = {}; // Listen to incoming OAuth http requests __meteor_bootstrap__.app @@ -95,7 +95,7 @@ Oauth._middleware = function (req, res, next) { // we were passed. But then the developer wouldn't be able to // style the error or react to it in any way. if (req.query.state && err instanceof Error) - Oauth._loginResultForState[req.query.state] = err; + Oauth._loginResultForCredentialToken[req.query.state] = err; // XXX the following is actually wrong. if someone wants to // redirect rather than close once we are done with the OAuth diff --git a/packages/oauth1/oauth1_server.js b/packages/oauth1/oauth1_server.js index afd1b2ef7e..6339573302 100644 --- a/packages/oauth1/oauth1_server.js +++ b/packages/oauth1/oauth1_server.js @@ -50,7 +50,7 @@ Oauth1._handleRequest = function (service, query, res) { var oauthResult = service.handleOauthRequest(oauthBinding); // Add the login result to the result map - Oauth._loginResultForState[query.state] = { + Oauth._loginResultForCredentialToken[query.state] = { serviceName: service.serviceName, serviceData: oauthResult.serviceData, options: oauthResult.options diff --git a/packages/oauth1/oauth1_tests.js b/packages/oauth1/oauth1_tests.js index da664fa392..51f286d5ad 100644 --- a/packages/oauth1/oauth1_tests.js +++ b/packages/oauth1/oauth1_tests.js @@ -1,11 +1,11 @@ -Tinytest.add("oauth1 - loginResultForState is stored", function (test) { +Tinytest.add("oauth1 - loginResultForCredentialToken is stored", function (test) { var http = Npm.require('http'); var twitterfooId = Random.id(); var twitterfooName = 'nickname' + Random.id(); var twitterfooAccessToken = Random.id(); var twitterfooAccessTokenSecret = Random.id(); var twitterOption1 = Random.id(); - var state = Random.id(); + var credentialToken = Random.id(); var serviceName = Random.id(); var urls = { @@ -40,13 +40,13 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { }); // simulate logging in using twitterfoo - Oauth1._requestTokens[state] = twitterfooAccessToken; + Oauth1._requestTokens[credentialToken] = twitterfooAccessToken; var req = { method: "POST", url: "/_oauth/" + serviceName + "?close", query: { - state: state, + state: credentialToken, oauth_token: twitterfooAccessToken } }; @@ -54,17 +54,17 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { // Test that right data is placed on the loginResult map test.equal( - Oauth._loginResultForState[state].serviceName, serviceName); + Oauth._loginResultForCredentialToken[credentialToken].serviceName, serviceName); test.equal( - Oauth._loginResultForState[state].serviceData.id, twitterfooId); + Oauth._loginResultForCredentialToken[credentialToken].serviceData.id, twitterfooId); test.equal( - Oauth._loginResultForState[state].serviceData.screenName, twitterfooName); + Oauth._loginResultForCredentialToken[credentialToken].serviceData.screenName, twitterfooName); test.equal( - Oauth._loginResultForState[state].serviceData.accessToken, twitterfooAccessToken); + Oauth._loginResultForCredentialToken[credentialToken].serviceData.accessToken, twitterfooAccessToken); test.equal( - Oauth._loginResultForState[state].serviceData.accessTokenSecret, twitterfooAccessTokenSecret); + Oauth._loginResultForCredentialToken[credentialToken].serviceData.accessTokenSecret, twitterfooAccessTokenSecret); test.equal( - Oauth._loginResultForState[state].options.option1, twitterOption1); + Oauth._loginResultForCredentialToken[credentialToken].options.option1, twitterOption1); } finally { Oauth._unregisterService(serviceName); diff --git a/packages/oauth2/oauth2_server.js b/packages/oauth2/oauth2_server.js index c36f7f55c7..e39f34228f 100644 --- a/packages/oauth2/oauth2_server.js +++ b/packages/oauth2/oauth2_server.js @@ -9,7 +9,7 @@ Oauth2._handleRequest = function (service, query, res) { var oauthResult = service.handleOauthRequest(query); // Add the login result to the result map - Oauth._loginResultForState[query.state] = { + Oauth._loginResultForCredentialToken[query.state] = { serviceName: service.serviceName, serviceData: oauthResult.serviceData, options: oauthResult.options diff --git a/packages/oauth2/oauth2_tests.js b/packages/oauth2/oauth2_tests.js index 7dcf7e3906..5bc23e3bb5 100644 --- a/packages/oauth2/oauth2_tests.js +++ b/packages/oauth2/oauth2_tests.js @@ -1,8 +1,8 @@ -Tinytest.add("oauth2 - loginResultForState is stored", function (test) { +Tinytest.add("oauth2 - loginResultForCredentialToken is stored", function (test) { var http = Npm.require('http'); var foobookId = Random.id(); var foobookOption1 = Random.id(); - var state = Random.id(); + var credentialToken = Random.id(); var serviceName = Random.id(); ServiceConfiguration.configurations.insert({service: serviceName}); @@ -19,16 +19,16 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { // simulate logging in using foobook var req = {method: "POST", url: "/_oauth/" + serviceName + "?close", - query: {state: state}}; + query: {state: credentialToken}}; Oauth._middleware(req, new http.ServerResponse(req)); // Test that the login result for that user is prepared test.equal( - Oauth._loginResultForState[state].serviceName, serviceName); + Oauth._loginResultForCredentialToken[credentialToken].serviceName, serviceName); test.equal( - Oauth._loginResultForState[state].serviceData.id, foobookId); + Oauth._loginResultForCredentialToken[credentialToken].serviceData.id, foobookId); test.equal( - Oauth._loginResultForState[state].options.option1, foobookOption1); + Oauth._loginResultForCredentialToken[credentialToken].options.option1, foobookOption1); } finally { Oauth._unregisterService(serviceName); diff --git a/packages/twitter/twitter_client.js b/packages/twitter/twitter_client.js index 190725de41..7a6ea679de 100644 --- a/packages/twitter/twitter_client.js +++ b/packages/twitter/twitter_client.js @@ -1,31 +1,35 @@ -// XXX support options.requestPermissions as we do for Facebook, Google, Github -Twitter.requestCredential = function (options, callback, loginPopupClosedCallback) { +// Request Twitter credentials for the user +// @param options {optional} XXX support options.requestPermissions +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on +// error. +Twitter.requestCredential = function (options, credentialRequestCompleteCallback) { // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; + if (!credentialRequestCompleteCallback && typeof options === 'function') { + credentialRequestCompleteCallback = options; options = {}; } var config = ServiceConfiguration.configurations.findOne({service: 'twitter'}); if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured")); return; } - var state = Random.id(); - // We need to keep state across the next two 'steps' so we're adding - // a state parameter to the url and the callback url that we'll be returned + var credentialToken = Random.id(); + // We need to keep credentialToken across the next two 'steps' so we're adding + // a credentialToken parameter to the url and the callback url that we'll be returned // to by oauth provider // url back to app, enters "step 2" as described in // packages/accounts-oauth1-helper/oauth1_server.js - var callbackUrl = Meteor.absoluteUrl('_oauth/twitter?close&state=' + state); + var callbackUrl = Meteor.absoluteUrl('_oauth/twitter?close&state=' + credentialToken); // url to app, enters "step 1" as described in // packages/accounts-oauth1-helper/oauth1_server.js var url = '/_oauth/twitter/?requestTokenAndRedirect=' + encodeURIComponent(callbackUrl) - + '&state=' + state; + + '&state=' + credentialToken; - Oauth.initiateLogin(state, url, callback, loginPopupClosedCallback); + Oauth.initiateLogin(credentialToken, url, credentialRequestCompleteCallback); }; diff --git a/packages/weibo/weibo_client.js b/packages/weibo/weibo_client.js index c183c167f6..f70eb831c6 100644 --- a/packages/weibo/weibo_client.js +++ b/packages/weibo/weibo_client.js @@ -1,25 +1,29 @@ -// XXX support options.requestPermissions as we do for Facebook, Google, Github -Weibo.requestCredential = function (options, callback, loginPopupClosedCallback) { +// Request Weibo credentials for the user +// @param options {optional} +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on +// error. +Weibo.requestCredential = function (options, credentialRequestCompleteCallback) { // support both (options, callback) and (callback). - if (!callback && typeof options === 'function') { - callback = options; + if (!credentialRequestCompleteCallback && typeof options === 'function') { + credentialRequestCompleteCallback = options; options = {}; } var config = ServiceConfiguration.configurations.findOne({service: 'weibo'}); if (!config) { - callback && callback(new ServiceConfiguration.ConfigError("Service not configured")); + credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured")); return; } - var state = Random.id(); + var credentialToken = Random.id(); // XXX need to support configuring access_type and scope var loginUrl = 'https://api.weibo.com/oauth2/authorize' + '?response_type=code' + '&client_id=' + config.clientId + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/weibo?close', {replaceLocalhost: true}) + - '&state=' + state; + '&state=' + credentialToken; - Oauth.initiateLogin(state, loginUrl, callback, loginPopupClosedCallback); + Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback); }; From a3f493bb79c97db1e8c8d88bba57b71eec9424ab Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Tue, 7 May 2013 14:42:15 -0700 Subject: [PATCH 31/60] Add retrieveCredential server methods --- packages/accounts-oauth/oauth_server.js | 4 ++-- packages/facebook/facebook_server.js | 4 ++++ packages/github/github_server.js | 4 ++++ packages/google/google_server.js | 4 ++++ packages/meetup/meetup_server.js | 5 +++++ packages/oauth/oauth_server.js | 11 +++++++++++ packages/twitter/twitter_server.js | 5 +++++ packages/weibo/weibo_server.js | 4 ++++ 8 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/accounts-oauth/oauth_server.js b/packages/accounts-oauth/oauth_server.js index 6c2c3fb4a5..775811289e 100644 --- a/packages/accounts-oauth/oauth_server.js +++ b/packages/accounts-oauth/oauth_server.js @@ -27,7 +27,7 @@ Accounts.registerLoginHandler(function (options) { check(options.oauth, {credentialToken: String}); - if (!_.has(Oauth._loginResultForCredentialToken, options.oauth.credentialToken)) { + if (!Oauth.hasCredential(options.oauth.credentialToken)) { // OAuth credentialToken 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 @@ -40,7 +40,7 @@ Accounts.registerLoginHandler(function (options) { throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'No matching login attempt found'); } - var result = Oauth._loginResultForCredentialToken[options.oauth.credentialToken]; + var result = Oauth.retrieveCredential(options.oauth.credentialToken); if (result instanceof Error) // We tried to login, but there was a fatal error. Report it back // to the user. diff --git a/packages/facebook/facebook_server.js b/packages/facebook/facebook_server.js index fd4df3f04e..59ee6cc729 100644 --- a/packages/facebook/facebook_server.js +++ b/packages/facebook/facebook_server.js @@ -90,3 +90,7 @@ var getIdentity = function (accessToken) { throw new Error("Failed to fetch identity from Facebook. " + err.message); } }; + +Facebook.retrieveCredential = function(credentialToken) { + return Oauth.retrieveCredential(credentialToken); +}; diff --git a/packages/github/github_server.js b/packages/github/github_server.js index 584344d064..00841cedf8 100644 --- a/packages/github/github_server.js +++ b/packages/github/github_server.js @@ -61,3 +61,7 @@ var getIdentity = function (accessToken) { throw new Error("Failed to fetch identity from GitHub. " + err.message); } }; + +Github.retrieveCredential = function(credentialToken) { + return Oauth.retrieveCredential(credentialToken); +}; diff --git a/packages/google/google_server.js b/packages/google/google_server.js index afffde58a1..f6f21da9b4 100644 --- a/packages/google/google_server.js +++ b/packages/google/google_server.js @@ -72,3 +72,7 @@ var getIdentity = function (accessToken) { throw new Error("Failed to fetch identity from Google. " + err.message); } }; + +Google.retrieveCredential = function(credentialToken) { + return Oauth.retrieveCredential(credentialToken); +}; \ No newline at end of file diff --git a/packages/meetup/meetup_server.js b/packages/meetup/meetup_server.js index ba2b9c08ab..aa5890d9c7 100644 --- a/packages/meetup/meetup_server.js +++ b/packages/meetup/meetup_server.js @@ -49,3 +49,8 @@ var getIdentity = function (accessToken) { throw new Error("Failed to fetch identity from Meetup: " + err.message); } }; + + +Meetup.retrieveCredential = function(credentialToken) { + return Oauth.retrieveCredential(credentialToken); +}; \ No newline at end of file diff --git a/packages/oauth/oauth_server.js b/packages/oauth/oauth_server.js index 7459cb9d3c..234fdd3cea 100644 --- a/packages/oauth/oauth_server.js +++ b/packages/oauth/oauth_server.js @@ -47,6 +47,16 @@ Oauth._unregisterService = function (name) { // XXX we should periodically clear old entries Oauth._loginResultForCredentialToken = {}; +Oauth.hasCredential = function(credentialToken) { + return _.has(Oauth._loginResultForCredentialToken, credentialToken); +} + +Oauth.retrieveCredential = function(credentialToken) { + result = Oauth._loginResultForCredentialToken[credentialToken]; + delete Oauth._loginResultForCredentialToken[credentialToken]; + return result; +} + // Listen to incoming OAuth http requests __meteor_bootstrap__.app .use(connect.query()) @@ -158,3 +168,4 @@ var closePopup = function(res) { ''; res.end(content, 'utf-8'); }; + diff --git a/packages/twitter/twitter_server.js b/packages/twitter/twitter_server.js index 783b917ba1..8d67554229 100644 --- a/packages/twitter/twitter_server.js +++ b/packages/twitter/twitter_server.js @@ -24,3 +24,8 @@ Oauth.registerService('twitter', 1, Twitter._urls, function(oauthBinding) { } }; }); + + +Twitter.retrieveCredential = function(credentialToken) { + return Oauth.retrieveCredential(credentialToken); +}; diff --git a/packages/weibo/weibo_server.js b/packages/weibo/weibo_server.js index f0d9ff2df6..4c6437899f 100644 --- a/packages/weibo/weibo_server.js +++ b/packages/weibo/weibo_server.js @@ -67,3 +67,7 @@ var getIdentity = function (accessToken, userId) { throw new Error("Failed to fetch identity from Weibo. " + err.message); } }; + +Weibo.retrieveCredential = function(credentialToken) { + return Oauth.retrieveCredential(credentialToken); +}; \ No newline at end of file From 44ac53f8199509183a1b3dc0fa4eb578f5e2e905 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 17 May 2013 18:25:26 -0700 Subject: [PATCH 32/60] Tweak package descriptions. --- packages/accounts-facebook/package.js | 2 +- packages/accounts-github/package.js | 2 +- packages/accounts-google/package.js | 2 +- packages/accounts-meetup/package.js | 2 +- packages/accounts-oauth/package.js | 2 +- packages/accounts-weibo/package.js | 2 +- packages/facebook/package.js | 5 ++++- packages/github/package.js | 5 ++++- packages/google/package.js | 5 ++++- packages/meetup/package.js | 5 ++++- packages/service-configuration/package.js | 5 +++-- packages/twitter/package.js | 7 +++++-- packages/weibo/package.js | 5 ++++- 13 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index 00bbdd22d7..afb98a391a 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Accounts service for Facebook accounts" + summary: "Login service for Facebook accounts" }); Package.on_use(function(api) { diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index f941273152..7b740bea5e 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Accounts service for Github accounts" + summary: "Login service for Github accounts" }); Package.on_use(function(api) { diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index 6ce0cfe15b..9329e9b992 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Accounts service for Google accounts" + summary: "Login service for Google accounts" }); Package.on_use(function(api) { diff --git a/packages/accounts-meetup/package.js b/packages/accounts-meetup/package.js index 2c8eb330a1..db4f222b39 100644 --- a/packages/accounts-meetup/package.js +++ b/packages/accounts-meetup/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Accounts service for Meetup accounts" + summary: "Login service for Meetup accounts" }); Package.on_use(function(api) { diff --git a/packages/accounts-oauth/package.js b/packages/accounts-oauth/package.js index 2b3a0d75c6..de748f249c 100644 --- a/packages/accounts-oauth/package.js +++ b/packages/accounts-oauth/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Common code for OAuth-based accounts services", + summary: "Common code for OAuth-based login services", internal: true }); diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index 469a2589e5..2d88b4294e 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Accounts service for Sina Weibo accounts" + summary: "Login service for Sina Weibo accounts" }); Package.on_use(function(api) { diff --git a/packages/facebook/package.js b/packages/facebook/package.js index 6294606cbc..f787120c99 100644 --- a/packages/facebook/package.js +++ b/packages/facebook/package.js @@ -1,5 +1,8 @@ Package.describe({ - summary: "Login service for Facebook accounts" + summary: "Facebook OAuth flow", + // internal for now. Should be external when it has a richer API to do + // actual API things with the service, not just handle the OAuth flow. + internal: true }); Package.on_use(function(api) { diff --git a/packages/github/package.js b/packages/github/package.js index 18881e21b9..e730a702ff 100644 --- a/packages/github/package.js +++ b/packages/github/package.js @@ -1,5 +1,8 @@ Package.describe({ - summary: "Login service for Github accounts" + summary: "Github OAuth flow", + // internal for now. Should be external when it has a richer API to do + // actual API things with the service, not just handle the OAuth flow. + internal: true }); Package.on_use(function(api) { diff --git a/packages/google/package.js b/packages/google/package.js index ffc58e067a..2070e6a8c1 100644 --- a/packages/google/package.js +++ b/packages/google/package.js @@ -1,5 +1,8 @@ Package.describe({ - summary: "Login service for Google accounts" + summary: "Google OAuth flow", + // internal for now. Should be external when it has a richer API to do + // actual API things with the service, not just handle the OAuth flow. + internal: true }); Package.on_use(function(api) { diff --git a/packages/meetup/package.js b/packages/meetup/package.js index 7136142ee1..c6733133fb 100644 --- a/packages/meetup/package.js +++ b/packages/meetup/package.js @@ -1,5 +1,8 @@ Package.describe({ - summary: "Login service for Meetup accounts" + summary: "Meetup OAuth flow", + // internal for now. Should be external when it has a richer API to do + // actual API things with the service, not just handle the OAuth flow. + internal: true }); Package.on_use(function(api) { diff --git a/packages/service-configuration/package.js b/packages/service-configuration/package.js index 62b90e7504..cf7e246a46 100644 --- a/packages/service-configuration/package.js +++ b/packages/service-configuration/package.js @@ -1,7 +1,8 @@ Package.describe({ - summary: "Manage the configuration for third-party services" + summary: "Manage the configuration for third-party services", + internal: true }); Package.on_use(function(api) { api.add_files('service_configuration_common.js', ['client', 'server']); -}); \ No newline at end of file +}); diff --git a/packages/twitter/package.js b/packages/twitter/package.js index 179d396a2d..36b2e302aa 100644 --- a/packages/twitter/package.js +++ b/packages/twitter/package.js @@ -1,5 +1,8 @@ Package.describe({ - summary: "Login service for Twitter accounts" + summary: "Twitter OAuth flow", + // internal for now. Should be external when it has a richer API to do + // actual API things with the service, not just handle the OAuth flow. + internal: true }); Package.on_use(function(api) { @@ -15,4 +18,4 @@ Package.on_use(function(api) { api.add_files('twitter_common.js', ['client', 'server']); api.add_files('twitter_server.js', 'server'); api.add_files('twitter_client.js', 'client'); -}); \ No newline at end of file +}); diff --git a/packages/weibo/package.js b/packages/weibo/package.js index ae2b709f1c..59be95331e 100644 --- a/packages/weibo/package.js +++ b/packages/weibo/package.js @@ -1,5 +1,8 @@ Package.describe({ - summary: "Login service for Weibo accounts" + summary: "Weibo OAuth flow", + // internal for now. Should be external when it has a richer API to do + // actual API things with the service, not just handle the OAuth flow. + internal: true }); Package.on_use(function(api) { From 3123d78adf6653137b4fb74a28be1ae0be942f8c Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 17 May 2013 21:29:12 -0700 Subject: [PATCH 33/60] History.md --- History.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/History.md b/History.md index fe9e111e08..5f76d84dd1 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,13 @@ ## vNEXT +* Separate OAuth flow logic from Accounts into separate packages. The + `facebook`, `github`, `google`, `meetup`, `twitter`, and `weibo` + packages can be used to perform an OAuth exchange without creating an + account and logging in. #1024 + +Patch contributed by GitHub user timhaines. + ## v0.6.3 From 9822ace0466aad4ab3cbf4e22315b8179c2a550c Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 20 May 2013 21:47:59 -0700 Subject: [PATCH 34/60] Update node mongodb driver. New version reconnects to mongo better when there is a new primary. Passes package and tools tests plus basic app sanity testing, but I have not done and deep or specific testing. --- packages/mongo-livedata/.npm/npm-shrinkwrap.json | 5 ++++- packages/mongo-livedata/package.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/mongo-livedata/.npm/npm-shrinkwrap.json b/packages/mongo-livedata/.npm/npm-shrinkwrap.json index 81fdc99dda..9f96baa024 100644 --- a/packages/mongo-livedata/.npm/npm-shrinkwrap.json +++ b/packages/mongo-livedata/.npm/npm-shrinkwrap.json @@ -1,10 +1,13 @@ { "dependencies": { "mongodb": { - "version": "1.2.13", + "version": "1.3.5", "dependencies": { "bson": { "version": "0.1.8" + }, + "kerberos": { + "version": "0.0.2" } } } diff --git a/packages/mongo-livedata/package.js b/packages/mongo-livedata/package.js index 8b0b21ac37..ab6d60fc99 100644 --- a/packages/mongo-livedata/package.js +++ b/packages/mongo-livedata/package.js @@ -12,7 +12,7 @@ Package.describe({ internal: true }); -Npm.depends({mongodb: "1.2.13"}); +Npm.depends({mongodb: "1.3.5"}); Package.on_use(function (api) { api.use(['random', 'ejson', 'json', 'underscore', 'minimongo', 'logging', 'livedata'], From 24e38c75c67da56f03b1eb8770d99920a6a139b4 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Tue, 21 May 2013 13:30:35 -0700 Subject: [PATCH 35/60] Decode the platform string that is part of the URL used when browserstack goes to deployed tests --- packages/test-in-console/driver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-in-console/driver.js b/packages/test-in-console/driver.js index 53329b789e..1aaa6f5f23 100644 --- a/packages/test-in-console/driver.js +++ b/packages/test-in-console/driver.js @@ -23,7 +23,7 @@ var resultSet = {}; var toReport = []; var hrefPath = document.location.href.split("/"); -var platform = hrefPath.length && hrefPath[hrefPath.length - 1]; +var platform = decodeURIComponent(hrefPath.length && hrefPath[hrefPath.length - 1]); if (!platform) platform = "local"; var doReport = Meteor && From 9557a8b0ab6feaef6e80992b012a234b96829ccd Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 21 May 2013 20:02:33 -0700 Subject: [PATCH 36/60] Upgrade node-mongo-native to the latest version, released today. --- packages/mongo-livedata/.npm/npm-shrinkwrap.json | 2 +- packages/mongo-livedata/package.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mongo-livedata/.npm/npm-shrinkwrap.json b/packages/mongo-livedata/.npm/npm-shrinkwrap.json index 9f96baa024..9a825e9347 100644 --- a/packages/mongo-livedata/.npm/npm-shrinkwrap.json +++ b/packages/mongo-livedata/.npm/npm-shrinkwrap.json @@ -1,7 +1,7 @@ { "dependencies": { "mongodb": { - "version": "1.3.5", + "version": "1.3.6", "dependencies": { "bson": { "version": "0.1.8" diff --git a/packages/mongo-livedata/package.js b/packages/mongo-livedata/package.js index ab6d60fc99..a0733a63ea 100644 --- a/packages/mongo-livedata/package.js +++ b/packages/mongo-livedata/package.js @@ -12,7 +12,7 @@ Package.describe({ internal: true }); -Npm.depends({mongodb: "1.3.5"}); +Npm.depends({mongodb: "1.3.6"}); Package.on_use(function (api) { api.use(['random', 'ejson', 'json', 'underscore', 'minimongo', 'logging', 'livedata'], From 46b75bcd180c97f1ccf12f92b8d2d30cafb1c38e Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Sat, 11 May 2013 06:36:30 -0400 Subject: [PATCH 37/60] explicitly don't match boxed versions of primitives --- packages/check/match.js | 3 +-- packages/check/match_test.js | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/check/match.js b/packages/check/match.js index c690e81f8d..2783352679 100644 --- a/packages/check/match.js +++ b/packages/check/match.js @@ -103,8 +103,7 @@ var checkSubtree = function (value, pattern) { return; // Basic atomic types. - // XXX do we have to worry about if value is boxed (eg String)? will that - // happen? + // Do not match boxed objects (e.g. String, Boolean) for (var i = 0; i < typeofChecks.length; ++i) { if (pattern === typeofChecks[i][0]) { if (typeof value === typeofChecks[i][1]) diff --git a/packages/check/match_test.js b/packages/check/match_test.js index e22ff9405c..1253167883 100644 --- a/packages/check/match_test.js +++ b/packages/check/match_test.js @@ -75,6 +75,9 @@ Tinytest.add("check - check", function (test) { }); }); fails(true, Match.OneOf(String, Number, undefined, null, [Boolean])); + fails(new String("foo"), String); + fails(new Boolean(true), Boolean); + fails(new Number(123), Number); matches([1, 2, 3], [Number]); matches([], [Number]); From 1ad813951bbaee348022dba1e59093cd6028203b Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Sat, 11 May 2013 08:37:48 -0400 Subject: [PATCH 38/60] Allow new Random instances to be constructed with specified seed. For repeatable unit test failures with "random" data it's useful to be able to create deterministic random number sequences. Introduce `Random.create(seed...)` which returns a object with the `Random` API (`id()`, `choice()`, etc.) initialized with the passed seed(s). --- packages/random/package.js | 5 ++ packages/random/random.js | 84 ++++++++++++++++++++------------- packages/random/random_tests.js | 15 ++++++ 3 files changed, 71 insertions(+), 33 deletions(-) create mode 100644 packages/random/random_tests.js diff --git a/packages/random/package.js b/packages/random/package.js index 18f30588a1..96f9f43070 100644 --- a/packages/random/package.js +++ b/packages/random/package.js @@ -7,3 +7,8 @@ Package.on_use(function (api, where) { where = where || ['client', 'server']; api.add_files('random.js', where); }); + +Package.on_test(function(api) { + api.use('random'); + api.add_files('random_tests.js', ['client', 'server']); +}); diff --git a/packages/random/random.js b/packages/random/random.js index ace3331a00..5faddb9d4c 100644 --- a/packages/random/random.js +++ b/packages/random/random.js @@ -1,8 +1,6 @@ -Random = {}; - // see http://baagoe.org/en/wiki/Better_random_numbers_for_javascript // for a full discussion and Alea implementation. -Random._Alea = function () { +var Alea = function () { function Mash() { var n = 0xefc8249d; @@ -75,6 +73,53 @@ Random._Alea = function () { } (Array.prototype.slice.call(arguments))); }; +var UNMISTAKABLE_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz"; + +var create = function (/* arguments */) { + + var random = Alea.apply(null, arguments); + + var self = {}; + + var bind = function (fn) { + return _.bind(fn, self); + }; + + return _.extend(self, { + _Alea: Alea, + + create: create, + + fraction: random, + + choice: bind(function (arrayOrString) { + var index = Math.floor(this.fraction() * arrayOrString.length); + if (typeof arrayOrString === "string") + return arrayOrString.substr(index, 1); + else + return arrayOrString[index]; + }), + + id: bind(function() { + var digits = []; + // Length of 17 preserves around 96 bits of entropy, which is the + // amount of state in our PRNG + for (var i = 0; i < 17; i++) { + digits[i] = this.choice(UNMISTAKABLE_CHARS); + } + return digits.join(""); + }), + + hexString: bind(function (digits) { + var hexDigits = []; + for (var i = 0; i < digits; ++i) { + hexDigits.push(this.choice("0123456789abcdef")); + } + return hexDigits.join(''); + }) + }); +}; + // instantiate RNG. Heuristically collect entropy from various sources // client sources @@ -104,33 +149,6 @@ var pid = (typeof process !== 'undefined' && process.pid) || 1; // XXX On the server, use the crypto module (OpenSSL) instead of this PRNG. // (Make Random.fraction be generated from Random.hexString instead of the // other way around, and generate Random.hexString from crypto.randomBytes.) -Random.fraction = new Random._Alea([ - new Date(), height, width, agent, pid, Math.random()]); - -Random.choice = function (arrayOrString) { - var index = Math.floor(Random.fraction() * arrayOrString.length); - if (typeof arrayOrString === "string") - return arrayOrString.substr(index, 1); - else - return arrayOrString[index]; -}; - -var UNMISTAKABLE_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz"; -Random.id = function() { - var digits = []; - // Length of 17 preserves around 96 bits of entropy, which is the - // amount of state in our PRNG - for (var i = 0; i < 17; i++) { - digits[i] = Random.choice(UNMISTAKABLE_CHARS); - } - return digits.join(""); -}; - -var HEX_DIGITS = "0123456789abcdef"; -Random.hexString = function (digits) { - var hexDigits = []; - for (var i = 0; i < digits; ++i) { - hexDigits.push(Random.choice("0123456789abcdef")); - } - return hexDigits.join(''); -}; +Random = create([ + new Date(), height, width, agent, pid, Math.random() +]); diff --git a/packages/random/random_tests.js b/packages/random/random_tests.js new file mode 100644 index 0000000000..52e5b852e5 --- /dev/null +++ b/packages/random/random_tests.js @@ -0,0 +1,15 @@ +Tinytest.add('random', function (test) { + // Deterministic with a specified seed, which should generate the + // same sequence in all environments. + // + // For repeatable unit test failures using deterministic random + // number sequences it's fine if a new Meteor release changes the + // algorithm being used and it starts generating a different + // sequence for a seed, as long as the sequence is consistent for + // a particular release. + var random = Random.create(0); + test.equal(random.id(), "cp9hWvhg8GSvuZ9os"); + test.equal(random.id(), "3f3k6Xo7rrHCifQhR"); + test.equal(random.id(), "shxDnjWWmnKPEoLhM"); + test.equal(random.id(), "6QTjB8C5SEqhmz4ni"); +}); From 1a77d78f0521e55f448c11b0f73ff95b881e5e53 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 22 May 2013 21:42:02 -0700 Subject: [PATCH 39/60] note in History --- History.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 5f76d84dd1..eb2800b26c 100644 --- a/History.md +++ b/History.md @@ -6,7 +6,11 @@ packages can be used to perform an OAuth exchange without creating an account and logging in. #1024 -Patch contributed by GitHub user timhaines. +* Allow new `Random` instances to be constructed with specified seed. This + can be used to create repeatable test cases for code that picks random + values. #1033 + +Patches contributed by GitHub users awwx and timhaines. ## v0.6.3 From d53799d7a5d46eeeb3e5d5b287c549e3c53457b5 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Sat, 11 May 2013 15:03:45 -0400 Subject: [PATCH 40/60] return true/false from EJSON.isBinary --- packages/ejson/ejson.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 9e031b7b97..57bb569bfc 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -211,8 +211,8 @@ EJSON.parse = function (item) { }; EJSON.isBinary = function (obj) { - return (typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) || - (obj && obj.$Uint8ArrayPolyfill); + return !!((typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) || + (obj && obj.$Uint8ArrayPolyfill)); }; EJSON.equals = function (a, b, options) { From 4a99d1b3ee1bcdd57125d82939d32c6143ecd0d1 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Tue, 7 May 2013 14:44:53 -0400 Subject: [PATCH 41/60] Support Meteor.defer in inactive iOS tabs. In iOS Safari, `setTimeout` and `setInterval` events are not delivered to inactive tabs (unless and until they become active again). This means that using `setTimeout(fn, 0)` to run `fn` in the next event loop can in fact delay `fn` indefinitely. This implementation uses the native `setImmediate` (when available) or `postMessage` (all other modern browsers); falling back to `setTimeout` if the first two aren't available. The `qa` subdirectory includes a manual test to check that `defer` is working in inactive tabs. (Sadly the test can't run automatically because scripts aren't allowed to open child windows except in response to user events). Factors out some common code in `timers.js`. --- LICENSE.txt | 6 + packages/meteor/package.js | 3 + .../defer-in-inactive-tab/.meteor/.gitignore | 1 + .../qa/defer-in-inactive-tab/.meteor/packages | 5 + .../meteor/qa/defer-in-inactive-tab/README.md | 13 ++ .../meteor/qa/defer-in-inactive-tab/test.html | 52 +++++++ .../meteor/qa/defer-in-inactive-tab/test.js | 57 +++++++ packages/meteor/setimmediate.js | 141 ++++++++++++++++++ packages/meteor/timers.js | 50 +++---- packages/meteor/timers_tests.js | 21 +++ 10 files changed, 319 insertions(+), 30 deletions(-) create mode 100644 packages/meteor/qa/defer-in-inactive-tab/.meteor/.gitignore create mode 100644 packages/meteor/qa/defer-in-inactive-tab/.meteor/packages create mode 100644 packages/meteor/qa/defer-in-inactive-tab/README.md create mode 100644 packages/meteor/qa/defer-in-inactive-tab/test.html create mode 100644 packages/meteor/qa/defer-in-inactive-tab/test.js create mode 100644 packages/meteor/setimmediate.js create mode 100644 packages/meteor/timers_tests.js diff --git a/LICENSE.txt b/LICENSE.txt index 2a6fbc3e05..1c328a550a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -346,6 +346,12 @@ node-kexec: https://github.com/jprichardson/node-kexec Copyright (c) 2011-2012 JP Richardson +---------- +setImmediate: https://github.com/NobleJS/setImmediate +---------- + +Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola + ============== Apache License diff --git a/packages/meteor/package.js b/packages/meteor/package.js index 805adff618..0e4124c3f9 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -34,6 +34,7 @@ Package.on_use(function (api, where) { api.add_files('client_environment.js', 'client'); api.add_files('server_environment.js', 'server'); api.add_files('helpers.js', ['client', 'server']); + api.add_files('setimmediate.js', ['client', 'server']); api.add_files('timers.js', ['client', 'server']); api.add_files('errors.js', ['client', 'server']); api.add_files('fiber_helpers.js', 'server'); @@ -63,4 +64,6 @@ Package.on_test(function (api) { api.add_files('fiber_helpers_test.js', ['server']); api.add_files('url_tests.js', ['client', 'server']); + + api.add_files('timers_tests.js', ['client', 'server']); }); diff --git a/packages/meteor/qa/defer-in-inactive-tab/.meteor/.gitignore b/packages/meteor/qa/defer-in-inactive-tab/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/packages/meteor/qa/defer-in-inactive-tab/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/packages/meteor/qa/defer-in-inactive-tab/.meteor/packages b/packages/meteor/qa/defer-in-inactive-tab/.meteor/packages new file mode 100644 index 0000000000..1a791704ad --- /dev/null +++ b/packages/meteor/qa/defer-in-inactive-tab/.meteor/packages @@ -0,0 +1,5 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + diff --git a/packages/meteor/qa/defer-in-inactive-tab/README.md b/packages/meteor/qa/defer-in-inactive-tab/README.md new file mode 100644 index 0000000000..b039b0778b --- /dev/null +++ b/packages/meteor/qa/defer-in-inactive-tab/README.md @@ -0,0 +1,13 @@ +# Defer in Inactive Tab + +Tests that `Meteor.defer` works in an inactive tab in iOS Safari. + +(`setTimeout` and `setInterval` events aren't delivered to inactive +tabs in iOS Safari until they become active again). + +Sadly we have to run the test manually because scripts aren't allowed +to open windows themselves except in response to user events. + +This test will not run on Chrome for iOS because the storage event is +not implemented in that browser. Also doesn't attempt to run on +versions of IE that don't support `window.addEventListener`. diff --git a/packages/meteor/qa/defer-in-inactive-tab/test.html b/packages/meteor/qa/defer-in-inactive-tab/test.html new file mode 100644 index 0000000000..ac8815279e --- /dev/null +++ b/packages/meteor/qa/defer-in-inactive-tab/test.html @@ -0,0 +1,52 @@ + + defer in inactive tab + + + + + {{> route}} + + + + + + + diff --git a/packages/meteor/qa/defer-in-inactive-tab/test.js b/packages/meteor/qa/defer-in-inactive-tab/test.js new file mode 100644 index 0000000000..33844c2933 --- /dev/null +++ b/packages/meteor/qa/defer-in-inactive-tab/test.js @@ -0,0 +1,57 @@ +if (Meteor.isClient) { + + var isParent = (window.location.pathname === '/'); + var isChild = ! isParent; + + Template.route.isParent = function () { + return isParent; + }; + + Template.parent.testStatus = function () { + return Session.get('testStatus'); + }; + + Template.parent.events({ + 'click #openTab': function () { + window.open('/child'); + }, + + 'click #runTest': function () { + if (localStorage.getItem('ping') === '!' || + localStorage.getItem('pong') === '!') { + Session.set('testStatus', 'Test already run. Close the second tab (if open), refresh this page, and run again.'); + } + else { + localStorage.setItem('ping', '!'); + } + } + }); + + if (isParent) { + Session.set('testStatus', ''); + + Meteor.startup(function () { + localStorage.setItem('ping', null); + localStorage.setItem('pong', null); + }); + window.addEventListener('storage', function (event) { + if (event.key === 'pong' && event.newValue === '!') { + Session.set('testStatus', 'test successful'); + } + }); + } + + if (isChild) { + window.addEventListener('storage', function (event) { + if (event.key === 'ping' && event.newValue === '!') { + // If we used setTimeout here in iOS Safari it wouldn't + // work (unless we switched tabs) because setTimeout and + // setInterval events don't fire in inactive tabs. + Meteor.defer(function () { + localStorage.setItem('pong', '!'); + }); + } + }); + } + +} diff --git a/packages/meteor/setimmediate.js b/packages/meteor/setimmediate.js new file mode 100644 index 0000000000..8cf75fbdfc --- /dev/null +++ b/packages/meteor/setimmediate.js @@ -0,0 +1,141 @@ +// Chooses one of three setImmediate implementations: +// +// * Native setImmediate (IE 10, Node 0.9+) +// +// * postMessage (many browsers) +// +// * setTimeout (fallback) +// +// The postMessage implementation is based on +// https://github.com/NobleJS/setImmediate/tree/1.0.1 +// +// Don't use `nextTick` for Node since it runs its callbacks before +// I/O, which is stricter than we're looking for. +// +// Not installed as a polyfill, as our public API is `Meteor.defer`. +// Since we're not trying to be a polyfill, we have some +// simplifications: +// +// If one invocation of a setImmediate callback pauses itself by a +// call to alert/prompt/showModelDialog, the NobleJS polyfill +// implementation ensured that no setImmedate callback would run until +// the first invocation completed. While correct per the spec, what it +// would mean for us in practice is that any reactive updates relying +// on Meteor.defer would be hung in the main window until the modal +// dialog was dismissed. Thus we only ensure that a setImmediate +// function is called in a later event loop. +// +// We don't need to support using a string to be eval'ed for the +// callback, arguments to the function, or clearImmediate. + +"use strict"; + +var global = this; + + +// IE 10, Node >= 9.1 + +function useSetImmediate() { + if (! global.setImmediate) + return null; + else { + var setImmediate = function (fn) { + global.setImmediate(fn); + }; + setImmediate.implementation = 'setImmediate'; + return setImmediate; + } +} + + +// Android 2.3.6, Chrome 26, Firefox 20, IE 8-9, iOS 5.1.1 Safari + +function usePostMessage() { + // The test against `importScripts` prevents this implementation + // from being installed inside a web worker, where + // `global.postMessage` means something completely different and + // can't be used for this purpose. + + if (!global.postMessage || global.importScripts) { + return null; + } + + // Avoid synchronous post message implementations. + + var postMessageIsAsynchronous = true; + var oldOnMessage = global.onmessage; + global.onmessage = function () { + postMessageIsAsynchronous = false; + }; + global.postMessage("", "*"); + global.onmessage = oldOnMessage; + + if (! postMessageIsAsynchronous) + return null; + + var funcIndex = 0; + var funcs = {}; + + // Installs an event handler on `global` for the `message` event: see + // * https://developer.mozilla.org/en/DOM/window.postMessage + // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages + + // XXX use Random.id() here? + var MESSAGE_PREFIX = "Meteor._setImmediate." + Math.random() + '.'; + + function isStringAndStartsWith(string, putativeStart) { + return (typeof string === "string" && + string.substring(0, putativeStart.length) === putativeStart); + } + + function onGlobalMessage(event) { + // This will catch all incoming messages (even from other + // windows!), so we need to try reasonably hard to avoid letting + // anyone else trick us into firing off. We test the origin is + // still this window, and that a (randomly generated) + // unpredictable identifying prefix is present. + if (event.source === global && + isStringAndStartsWith(event.data, MESSAGE_PREFIX)) { + var index = event.data.substring(MESSAGE_PREFIX.length); + try { + if (funcs[index]) + funcs[index](); + } + finally { + delete funcs[index]; + } + } + } + + if (global.addEventListener) { + global.addEventListener("message", onGlobalMessage, false); + } else { + global.attachEvent("onmessage", onGlobalMessage); + } + + var setImmediate = function (fn) { + // Make `global` post a message to itself with the handle and + // identifying prefix, thus asynchronously invoking our + // onGlobalMessage listener above. + ++funcIndex; + funcs[funcIndex] = fn; + global.postMessage(MESSAGE_PREFIX + funcIndex, "*"); + }; + setImmediate.implementation = 'postMessage'; + return setImmediate; +} + + +function useTimeout() { + var setImmediate = function (fn) { + global.setTimeout(fn, 0); + }; + setImmediate.implementation = 'setTimeout'; + return setImmediate; +} + + +Meteor._setImmediate = + useSetImmediate() || + usePostMessage() || + useTimeout(); diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index ac70858e23..14651120ca 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -1,36 +1,31 @@ +var withCurrentInvocation = function (f) { + if (Meteor._CurrentInvocation) { + if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation) + throw new Error("Can't set timers inside simulations"); + return function () { Meteor._CurrentInvocation.withValue(null, f); }; + } + else + return f; +}; + +var bindAndCatch = function (context, f) { + return Meteor.bindEnvironment(withCurrentInvocation(f), function (e) { + // XXX report nicely (or, should we catch it at all?) + Meteor._debug("Exception from " + context + ":", e); + }); +}; + _.extend(Meteor, { // Meteor.setTimeout and Meteor.setInterval callbacks scheduled // inside a server method are not part of the method invocation and // should clear out the CurrentInvocation environment variable. setTimeout: function (f, duration) { - if (Meteor._CurrentInvocation) { - if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation) - throw new Error("Can't set timers inside simulations"); - - var f_with_ci = f; - f = function () { Meteor._CurrentInvocation.withValue(null, f_with_ci); }; - } - - return setTimeout(Meteor.bindEnvironment(f, function (e) { - // XXX report nicely (or, should we catch it at all?) - Meteor._debug("Exception from setTimeout callback:", e.stack); - }), duration); + return setTimeout(bindAndCatch("setTimeout callback", f), duration); }, setInterval: function (f, duration) { - if (Meteor._CurrentInvocation) { - if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation) - throw new Error("Can't set timers inside simulations"); - - var f_with_ci = f; - f = function () { Meteor._CurrentInvocation.withValue(null, f_with_ci); }; - } - - return setInterval(Meteor.bindEnvironment(f, function (e) { - // XXX report nicely (or, should we catch it at all?) - Meteor._debug("Exception from setInterval callback:", e); - }), duration); + return setInterval(bindAndCatch("setInterval callback", f), duration); }, clearInterval: function(x) { @@ -41,16 +36,11 @@ _.extend(Meteor, { return clearTimeout(x); }, - // won't be necessary once we clobber the global setTimeout - // // XXX consider making this guarantee ordering of defer'd callbacks, like // Deps.afterFlush or Node's nextTick (in practice). Then tests can do: // callSomethingThatDefersSomeWork(); // Meteor.defer(expect(somethingThatValidatesThatTheWorkHappened)); defer: function (f) { - // Older Firefox will pass an argument to the setTimeout callback - // function, indicating the "actual lateness." It's non-standard, - // so for defer, standardize on not having it. - Meteor.setTimeout(function () {f();}, 0); + Meteor._setImmediate(bindAndCatch("defer callback", f)); } }); diff --git a/packages/meteor/timers_tests.js b/packages/meteor/timers_tests.js new file mode 100644 index 0000000000..246f7e7b39 --- /dev/null +++ b/packages/meteor/timers_tests.js @@ -0,0 +1,21 @@ +Tinytest.addAsync('timers - defer', function (test, onComplete) { + var x = 'a'; + Meteor.defer(function () { + test.equal(x, 'b'); + onComplete(); + }); + x = 'b'; +}); + +Tinytest.addAsync('timers - nested defer', function (test, onComplete) { + var x = 'a'; + Meteor.defer(function () { + test.equal(x, 'b'); + Meteor.defer(function () { + test.equal(x, 'c'); + onComplete(); + }); + x = 'c'; + }); + x = 'b'; +}); From bb4afdff5ba6daf1e6c7b6d0a21d7471f421cbb6 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 22 May 2013 21:58:43 -0700 Subject: [PATCH 42/60] move app out of package --- .../other}/defer-in-inactive-tab/.meteor/.gitignore | 0 .../qa => examples/other}/defer-in-inactive-tab/.meteor/packages | 0 .../meteor/qa => examples/other}/defer-in-inactive-tab/README.md | 0 .../meteor/qa => examples/other}/defer-in-inactive-tab/test.html | 0 .../meteor/qa => examples/other}/defer-in-inactive-tab/test.js | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename {packages/meteor/qa => examples/other}/defer-in-inactive-tab/.meteor/.gitignore (100%) rename {packages/meteor/qa => examples/other}/defer-in-inactive-tab/.meteor/packages (100%) rename {packages/meteor/qa => examples/other}/defer-in-inactive-tab/README.md (100%) rename {packages/meteor/qa => examples/other}/defer-in-inactive-tab/test.html (100%) rename {packages/meteor/qa => examples/other}/defer-in-inactive-tab/test.js (100%) diff --git a/packages/meteor/qa/defer-in-inactive-tab/.meteor/.gitignore b/examples/other/defer-in-inactive-tab/.meteor/.gitignore similarity index 100% rename from packages/meteor/qa/defer-in-inactive-tab/.meteor/.gitignore rename to examples/other/defer-in-inactive-tab/.meteor/.gitignore diff --git a/packages/meteor/qa/defer-in-inactive-tab/.meteor/packages b/examples/other/defer-in-inactive-tab/.meteor/packages similarity index 100% rename from packages/meteor/qa/defer-in-inactive-tab/.meteor/packages rename to examples/other/defer-in-inactive-tab/.meteor/packages diff --git a/packages/meteor/qa/defer-in-inactive-tab/README.md b/examples/other/defer-in-inactive-tab/README.md similarity index 100% rename from packages/meteor/qa/defer-in-inactive-tab/README.md rename to examples/other/defer-in-inactive-tab/README.md diff --git a/packages/meteor/qa/defer-in-inactive-tab/test.html b/examples/other/defer-in-inactive-tab/test.html similarity index 100% rename from packages/meteor/qa/defer-in-inactive-tab/test.html rename to examples/other/defer-in-inactive-tab/test.html diff --git a/packages/meteor/qa/defer-in-inactive-tab/test.js b/examples/other/defer-in-inactive-tab/test.js similarity index 100% rename from packages/meteor/qa/defer-in-inactive-tab/test.js rename to examples/other/defer-in-inactive-tab/test.js From 65636b5996be5f26c33ab35cc674c0e83d80b630 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 22 May 2013 22:02:13 -0700 Subject: [PATCH 43/60] History. --- History.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/History.md b/History.md index eb2800b26c..2249b5927e 100644 --- a/History.md +++ b/History.md @@ -6,6 +6,8 @@ packages can be used to perform an OAuth exchange without creating an account and logging in. #1024 +* Make `Meteor.defer` work in an inactive tab in iOS. #1023 + * Allow new `Random` instances to be constructed with specified seed. This can be used to create repeatable test cases for code that picks random values. #1033 From 65a8832649cd4e49446f8c1b3492d38a142c135a Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Tue, 14 May 2013 15:32:32 -0600 Subject: [PATCH 44/60] Add comment to past package --- packages/past/past.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/past/past.js b/packages/past/past.js index 054cf33c37..70cea1f9d2 100644 --- a/packages/past/past.js +++ b/packages/past/past.js @@ -1,3 +1,8 @@ +// This file is used to set up aliases and methods to preserve backwards on some +// deprecated methods. Care should be taken when adding aliases and methods +// that the target will not be undefined, as the past package is loaded early. +// In some cases, it may be best to define the alias in the package it refers to. + // Old under_score version of camelCase public API names. Meteor.is_client = Meteor.isClient; Meteor.is_server = Meteor.isServer; From 7aebc95f50868b570a4dc9d891c6d4c2106e1197 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 22 May 2013 22:42:00 -0700 Subject: [PATCH 45/60] Add missing word. --- packages/past/past.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/past/past.js b/packages/past/past.js index 70cea1f9d2..7bdffe3263 100644 --- a/packages/past/past.js +++ b/packages/past/past.js @@ -1,7 +1,8 @@ -// This file is used to set up aliases and methods to preserve backwards on some -// deprecated methods. Care should be taken when adding aliases and methods -// that the target will not be undefined, as the past package is loaded early. -// In some cases, it may be best to define the alias in the package it refers to. +// This file is used to set up aliases and methods to preserve backwards +// compatibility on some deprecated methods. Care should be taken when +// adding aliases and methods that the target will not be undefined, as +// the past package is loaded early. In some cases, it may be best to +// define the alias in the package it refers to. // Old under_score version of camelCase public API names. Meteor.is_client = Meteor.isClient; From cac2368d201eca584f2b5c54afb10b3b30e3cbe9 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 16 May 2013 10:36:47 -0400 Subject: [PATCH 46/60] Fix CoffeeScript error reporting. Fixes #1050. With the upgrade to CoffeeScript 1.6.2 the source file name and line number of a parse error is no longer present in the `message` field of the exception. --- packages/coffeescript/package.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/coffeescript/package.js b/packages/coffeescript/package.js index e8884d5959..a8c6479fbf 100644 --- a/packages/coffeescript/package.js +++ b/packages/coffeescript/package.js @@ -15,7 +15,11 @@ var coffeescript_handler = function(bundle, source_path, serve_path, where) { try { contents = coffee.compile(contents.toString('utf8'), options); } catch (e) { - return bundle.error(e.message); + return bundle.error( + source_path + ':' + + (e.location ? (e.location.first_line + ': ') : ' ') + + e.message + ); } contents = new Buffer(contents); From 28ea851c2aad0095fb16230c66973339783a567c Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 22 May 2013 22:45:06 -0700 Subject: [PATCH 47/60] History --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 2249b5927e..5dba98985c 100644 --- a/History.md +++ b/History.md @@ -12,6 +12,9 @@ can be used to create repeatable test cases for code that picks random values. #1033 +* Fix CoffeeScript error reporting to include source file and line + number again. #1052 + Patches contributed by GitHub users awwx and timhaines. From a59f9aa945b702807b8f200a8a8506e32f586160 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 22 May 2013 22:54:37 -0700 Subject: [PATCH 48/60] Remove fixed width constraint. Fixes #1043. --- packages/test-in-browser/driver.css | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/test-in-browser/driver.css b/packages/test-in-browser/driver.css index 2913c17355..897ab9dbe7 100644 --- a/packages/test-in-browser/driver.css +++ b/packages/test-in-browser/driver.css @@ -71,7 +71,6 @@ body { border-left: 2px solid #def; padding: 4px; position: relative; - width: 600px; } .test_table .test .testrow { position: relative; overflow: hidden; /*hasLayout*/ } From 4890e48d60c791ee47ba6dfa8b8f21d145c5130f Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 23 May 2013 13:55:12 -0400 Subject: [PATCH 49/60] Fix name of extracted method in meteor/timers.js When I refactored meteor/timers.js I extracted a method I called "withCurrentInvocation", but what the code is actually doing is ensuring that timer callbacks run *without* the current method invocation (if any). Rename to "withoutInvocation". --- packages/meteor/timers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index 14651120ca..698d2ab507 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -1,4 +1,4 @@ -var withCurrentInvocation = function (f) { +var withoutInvocation = function (f) { if (Meteor._CurrentInvocation) { if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation) throw new Error("Can't set timers inside simulations"); @@ -9,7 +9,7 @@ var withCurrentInvocation = function (f) { }; var bindAndCatch = function (context, f) { - return Meteor.bindEnvironment(withCurrentInvocation(f), function (e) { + return Meteor.bindEnvironment(withoutInvocation(f), function (e) { // XXX report nicely (or, should we catch it at all?) Meteor._debug("Exception from " + context + ":", e); }); From 67f5efeea28ba5128db6cdbb1d58b4b45ef4cc8a Mon Sep 17 00:00:00 2001 From: Sean McCann Date: Sun, 19 May 2013 13:10:36 -0400 Subject: [PATCH 50/60] Format properties and method names as code --- History.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/History.md b/History.md index 5dba98985c..744cc50ea0 100644 --- a/History.md +++ b/History.md @@ -166,8 +166,8 @@ Patches contributed by GitHub users andreas-karlsson and awwx. * `{{#with}}` helper now only includes its block if its argument is not falsey, and runs an `{{else}}` block if provided if the argument is falsey. #770, #866 -* Twitter login now stores profile_image_url and profile_image_url_https - attributes in the user.services.twitter namespace. #788 +* Twitter login now stores `profile_image_url` and `profile_image_url_https` + attributes in the `user.services.twitter` namespace. #788 * Allow packages to register file extensions with dots in the filename. @@ -235,7 +235,7 @@ mquandalle, Primigenus, raix, reustle, and timhaines. * Publish functions may now return an array of cursors to publish. Currently, the cursors must all be from different collections. #716 -* User documents have id's when onCreateUser and validateNewUser hooks run. +* User documents have id's when `onCreateUser` and `validateNewUser` hooks run. * Encode and store custom EJSON types in MongoDB. From 7e9f36a583635e913e350d13e9a4e645fc6689bc Mon Sep 17 00:00:00 2001 From: Stuart Johnston Date: Fri, 24 May 2013 21:49:10 +0100 Subject: [PATCH 51/60] Translate RegEx in lower levels of $and/$or/$nor selectors Recurses over $and/$or/$nor selectors to translate RegEx into {$regex, $options}. Resolves #1089 --- packages/mongo-livedata/collection.js | 9 +++- .../mongo-livedata/mongo_livedata_tests.js | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index d67ea71158..1019e97811 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -238,8 +238,6 @@ Meteor.Collection._rewriteSelector = function (selector) { var ret = {}; _.each(selector, function (value, key) { if (value instanceof RegExp) { - // XXX should also do this translation at lower levels (eg if the outer - // level is $and/$or/$nor, or if there's an $elemMatch) ret[key] = {$regex: value.source}; var regexOptions = ''; // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options @@ -251,6 +249,13 @@ Meteor.Collection._rewriteSelector = function (selector) { if (regexOptions) ret[key].$options = regexOptions; } + else if (_.contains(['$or','$and','$nor'], key)) { + // Translate lower levels of $and/$or/$nor + ret[key] = [] + _.each(value, function (v, k) { + ret[key].push(Meteor.Collection._rewriteSelector(v)) + }) + } else ret[key] = value; }); diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index 948a7962bd..f32674e76b 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -823,6 +823,47 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) { {x: {$regex: '^o+B'}}); test.equal(Meteor.Collection._rewriteSelector('foo'), {_id: 'foo'}); + + test.equal( + Meteor.Collection._rewriteSelector( + {'$or': [ + {x: /^o/}, + {y: /^p/}, + {z: 'q'} + ]} + ), + {'$or': [ + {x: {$regex: '^o'}}, + {y: {$regex: '^p'}}, + {z: 'q'} + ]} + ) + + test.equal( + Meteor.Collection._rewriteSelector( + {'$or': [ + {'$and': [ + {x: /^a/i}, + {y: /^b/} + ]}, + {'$nor': [ + {s: /^c/}, + {t: /^d/i} + ]} + ]} + ), + {'$or': [ + {'$and': [ + {x: {$regex: '^a', $options: 'i'}}, + {y: {$regex: '^b'}} + ]}, + {'$nor': [ + {s: {$regex: '^c'}}, + {t: {$regex: '^d', $options: 'i'}} + ]} + ]} + ) + var oid = new Meteor.Collection.ObjectID(); test.equal(Meteor.Collection._rewriteSelector(oid), {_id: oid}); From a90b3218546572f5417af97db3afd3e672732746 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 24 May 2013 15:34:15 -0700 Subject: [PATCH 52/60] Clean up style, update HISTORY. --- History.md | 4 +++- packages/mongo-livedata/collection.js | 7 +++---- packages/mongo-livedata/mongo_livedata_tests.js | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/History.md b/History.md index 744cc50ea0..88d76f9109 100644 --- a/History.md +++ b/History.md @@ -15,7 +15,9 @@ * Fix CoffeeScript error reporting to include source file and line number again. #1052 -Patches contributed by GitHub users awwx and timhaines. +* Fix Mongo queries which nested JavaScript RegExp objects inside `$or`. #1089 + +Patches contributed by GitHub users awwx, johnston, and timhaines. ## v0.6.3 diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 1019e97811..54f2083812 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -251,10 +251,9 @@ Meteor.Collection._rewriteSelector = function (selector) { } else if (_.contains(['$or','$and','$nor'], key)) { // Translate lower levels of $and/$or/$nor - ret[key] = [] - _.each(value, function (v, k) { - ret[key].push(Meteor.Collection._rewriteSelector(v)) - }) + ret[key] = _.map(value, function (v) { + return Meteor.Collection._rewriteSelector(v); + }); } else ret[key] = value; diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index f32674e76b..72a29559d3 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -831,13 +831,13 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) { {y: /^p/}, {z: 'q'} ]} - ), + ), {'$or': [ {x: {$regex: '^o'}}, {y: {$regex: '^p'}}, {z: 'q'} ]} - ) + ); test.equal( Meteor.Collection._rewriteSelector( @@ -851,7 +851,7 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) { {t: /^d/i} ]} ]} - ), + ), {'$or': [ {'$and': [ {x: {$regex: '^a', $options: 'i'}}, @@ -862,7 +862,7 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) { {t: {$regex: '^d', $options: 'i'}} ]} ]} - ) + ); var oid = new Meteor.Collection.ObjectID(); test.equal(Meteor.Collection._rewriteSelector(oid), From 357ec8e8b8070b2a2a4612cb626776e7f14e8058 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 24 May 2013 19:01:28 -0700 Subject: [PATCH 53/60] Add more info to error message. #1064. --- tools/mongo_exit_codes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/mongo_exit_codes.js b/tools/mongo_exit_codes.js index 4e5dfe2e79..97421fb660 100644 --- a/tools/mongo_exit_codes.js +++ b/tools/mongo_exit_codes.js @@ -58,7 +58,9 @@ exports.Codes = { 100 : { code: 100, symbol: "EXIT_UNCAUGHT", longText: "MongoDB had an unspecified uncaught exception.\n" + - "Check to make sure that MongoDB is able to write to its database directory." + "This can be caused by MongoDB being unable to write to a local database.\n" + + "Check that you have permissions to write to .meteor/local. MongoDB does\n" + + "not support filesystems like NFS that do not allow file locking." } }; From 15e190f073ba19af94b17f668a3e3e63459ceb58 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 5 Apr 2013 21:33:20 -0700 Subject: [PATCH 54/60] Upgrade to Underscore 1.4.4. --- packages/underscore/underscore.js | 176 +++++++++++++++++------------- scripts/generate-dev-bundle.sh | 2 +- 2 files changed, 102 insertions(+), 76 deletions(-) diff --git a/packages/underscore/underscore.js b/packages/underscore/underscore.js index 1ebe2671b9..a12f0d96cf 100644 --- a/packages/underscore/underscore.js +++ b/packages/underscore/underscore.js @@ -1,6 +1,6 @@ -// Underscore.js 1.4.2 +// Underscore.js 1.4.4 // http://underscorejs.org -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. // Underscore may be freely distributed under the MIT license. (function() { @@ -24,7 +24,6 @@ var push = ArrayProto.push, slice = ArrayProto.slice, concat = ArrayProto.concat, - unshift = ArrayProto.unshift, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; @@ -61,11 +60,11 @@ } exports._ = _; } else { - root['_'] = _; + root._ = _; } // Current version. - _.VERSION = '1.4.2'; + _.VERSION = '1.4.4'; // Collection Functions // -------------------- @@ -102,6 +101,8 @@ return results; }; + var reduceError = 'Reduce of empty array with no initial value'; + // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { @@ -119,7 +120,7 @@ memo = iterator.call(context, memo, value, index, list); } }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + if (!initial) throw new TypeError(reduceError); return memo; }; @@ -130,7 +131,7 @@ if (obj == null) obj = []; if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { if (context) iterator = _.bind(iterator, context); - return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); } var length = obj.length; if (length !== +length) { @@ -146,7 +147,7 @@ memo = iterator.call(context, memo, obj[index], index, list); } }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + if (!initial) throw new TypeError(reduceError); return memo; }; @@ -177,12 +178,9 @@ // Return all the elements for which a truth test fails. _.reject = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); }; // Determine whether all of the elements match a truth test. @@ -216,20 +214,19 @@ // Determine if the array or object contains a given value (using `===`). // Aliased as `include`. _.contains = _.include = function(obj, target) { - var found = false; - if (obj == null) return found; + if (obj == null) return false; if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { + return any(obj, function(value) { return value === target; }); - return found; }; // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); return _.map(obj, function(value) { - return (_.isFunction(method) ? method : value[method]).apply(value, args); + return (isFunc ? method : value[method]).apply(value, args); }); }; @@ -239,10 +236,10 @@ }; // Convenience version of a common use case of `filter`: selecting only objects - // with specific `key:value` pairs. - _.where = function(obj, attrs) { - if (_.isEmpty(attrs)) return []; - return _.filter(obj, function(value) { + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? null : []; + return _[first ? 'find' : 'filter'](obj, function(value) { for (var key in attrs) { if (attrs[key] !== value[key]) return false; } @@ -250,6 +247,12 @@ }); }; + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + // Return the maximum element or (element-based computation). // Can't optimize arrays of integers longer than 65,535 elements. // See: https://bugs.webkit.org/show_bug.cgi?id=80797 @@ -258,7 +261,7 @@ return Math.max.apply(Math, obj); } if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -Infinity}; + var result = {computed : -Infinity, value: -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed >= result.computed && (result = {value : value, computed : computed}); @@ -272,7 +275,7 @@ return Math.min.apply(Math, obj); } if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : Infinity}; + var result = {computed : Infinity, value: Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed < result.computed && (result = {value : value, computed : computed}); @@ -321,7 +324,7 @@ // An internal function used for aggregate "group by" operations. var group = function(obj, value, context, behavior) { var result = {}; - var iterator = lookupIterator(value); + var iterator = lookupIterator(value || _.identity); each(obj, function(value, index) { var key = iterator.call(context, value, index, obj); behavior(result, key, value); @@ -341,7 +344,7 @@ // either a string attribute to count by, or a function that returns the // criterion. _.countBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { + return group(obj, value, context, function(result, key) { if (!_.has(result, key)) result[key] = 0; result[key]++; }); @@ -363,12 +366,14 @@ // Safely convert anything iterable into a real, live array. _.toArray = function(obj) { if (!obj) return []; - if (obj.length === +obj.length) return slice.call(obj); + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); return _.values(obj); }; // Return the number of elements in an object. _.size = function(obj) { + if (obj == null) return 0; return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; }; @@ -379,6 +384,7 @@ // values in the array. Aliased as `head` and `take`. The **guard** check // allows it to work with `_.map`. _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; }; @@ -393,6 +399,7 @@ // Get the last element of an array. Passing **n** will return the last N // values in the array. The **guard** check allows it to work with `_.map`. _.last = function(array, n, guard) { + if (array == null) return void 0; if ((n != null) && !guard) { return slice.call(array, Math.max(array.length - n, 0)); } else { @@ -410,7 +417,7 @@ // Trim out all falsy values from an array. _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); + return _.filter(array, _.identity); }; // Internal implementation of a recursive `flatten` function. @@ -439,6 +446,11 @@ // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } var initial = iterator ? _.map(array, iterator, context) : array; var results = []; var seen = []; @@ -491,6 +503,7 @@ // pairs, or two parallel arrays of the same length -- one of keys, and one of // the corresponding values. _.object = function(list, values) { + if (list == null) return {}; var result = {}; for (var i = 0, l = list.length; i < l; i++) { if (values) { @@ -561,25 +574,23 @@ // Function (ahem) Functions // ------------------ - // Reusable constructor function for prototype setting. - var ctor = function(){}; - // Create a function bound to a given object (assigning `this`, and arguments, - // optionally). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - if (!_.isFunction(func)) throw new TypeError; - args = slice.call(arguments, 2); - return bound = function() { - if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); - ctor.prototype = func.prototype; - var self = new ctor; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) return result; - return self; + var args = slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(slice.call(arguments))); + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); }; }; @@ -587,7 +598,7 @@ // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); + if (funcs.length === 0) funcs = _.functions(obj); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; @@ -618,25 +629,26 @@ // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. _.throttle = function(func, wait) { - var context, args, timeout, throttling, more, result; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + var context, args, timeout, result; + var previous = 0; + var later = function() { + previous = new Date; + timeout = null; + result = func.apply(context, args); + }; return function() { - context = this; args = arguments; - var later = function() { + var now = new Date; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); timeout = null; - if (more) { - result = func.apply(context, args); - } - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - throttling = true; + previous = now; result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); } - whenDone(); return result; }; }; @@ -754,8 +766,10 @@ // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } } }); return obj; @@ -784,8 +798,10 @@ // Fill in a given object with default properties. _.defaults = function(obj) { each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; + if (source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } } }); return obj; @@ -950,7 +966,7 @@ // Is a given object a finite number? _.isFinite = function(obj) { - return _.isNumber(obj) && isFinite(obj); + return isFinite(obj) && !isNaN(parseFloat(obj)); }; // Is the given value `NaN`? (NaN is the only number which does not equal itself). @@ -996,7 +1012,9 @@ // Run a function **n** times. _.times = function(n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); + var accum = Array(n); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; }; // Return a random integer between min and max (inclusive). @@ -1005,7 +1023,7 @@ max = min; min = 0; } - return min + (0 | Math.random() * (max - min + 1)); + return min + Math.floor(Math.random() * (max - min + 1)); }; // List of HTML entities for escaping. @@ -1061,7 +1079,7 @@ // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { - var id = idCounter++; + var id = ++idCounter + ''; return prefix ? prefix + id : id; }; @@ -1096,6 +1114,7 @@ // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. _.template = function(text, data, settings) { + var render; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. @@ -1111,11 +1130,18 @@ text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset) .replace(escaper, function(match) { return '\\' + escapes[match]; }); - source += - escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : - interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : - evaluate ? "';\n" + evaluate + "\n__p+='" : ''; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } index = offset + match.length; + return match; }); source += "';\n"; @@ -1127,7 +1153,7 @@ source + "return __p;\n"; try { - var render = new Function(settings.variable || 'obj', '_', source); + render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; diff --git a/scripts/generate-dev-bundle.sh b/scripts/generate-dev-bundle.sh index 55f9abca6e..40642edc61 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -104,7 +104,7 @@ npm install useragent@2.0.1 npm install request@2.12.0 npm install keypress@0.1.0 npm install http-proxy@0.8.5 -npm install underscore@1.4.2 # 1.4.4 is a performance regression +npm install underscore@1.4.4 npm install fstream@0.1.21 npm install tar@0.1.14 npm install kexec@0.1.1 From 8e630c6b8de31a6720821ea9101193b0d1412b72 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 5 Apr 2013 21:36:29 -0700 Subject: [PATCH 55/60] Improve test driver performance. Upgrading Underscore to 1.4.4 fixes a bug where _.throttle would sometimes take double its timeout to deliver calls. So with this bugfix, our test suite performs worse because reactivity happens more often. Reduce the throttling to one update of the big table per second instead of twice a second. (Also, there's no need to defer and call flush; the changed call does that!) --- packages/test-in-browser/driver.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/test-in-browser/driver.js b/packages/test-in-browser/driver.js index 5654b44664..41471e0d7e 100644 --- a/packages/test-in-browser/driver.js +++ b/packages/test-in-browser/driver.js @@ -397,7 +397,7 @@ var reportResults = function(results) { countDeps.changed(); } - _.defer(_throttled_update); + _throttled_update(); }; // forget all of the events for a particular test @@ -417,5 +417,4 @@ var forgetEvents = function (results) { var _throttled_update = _.throttle(function() { resultsDeps.changed(); - Deps.flush(); -}, 500); +}, 1000); From 34c42d76b5e0a749616328bdeb5d4d13b11cd932 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 24 May 2013 16:53:01 -0700 Subject: [PATCH 56/60] Upgrade Connect to 2.x. Set $NODE_ENV appropriately (to 'development' in 'meteor run' and 'production' otherwise) so that connect doesn't send stack traces over the network in production. --- packages/force-ssl/force_ssl_server.js | 12 ++++----- packages/http/httpcall_tests.js | 2 +- packages/livedata/stream_server.js | 14 +++++----- scripts/generate-dev-bundle.sh | 6 +---- tools/run.js | 3 +++ tools/server/server.js | 36 +++++++++++++++++--------- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/packages/force-ssl/force_ssl_server.js b/packages/force-ssl/force_ssl_server.js index 4c17daac21..5a2a725876 100644 --- a/packages/force-ssl/force_ssl_server.js +++ b/packages/force-ssl/force_ssl_server.js @@ -4,10 +4,10 @@ // an approach similar to overshadowListeners in // https://github.com/sockjs/sockjs-node/blob/cf820c55af6a9953e16558555a31decea554f70e/src/utils.coffee -var app = __meteor_bootstrap__.app; -var oldAppListeners = app.listeners('request').slice(0); -app.removeAllListeners('request'); -app.addListener('request', function (req, res) { +var httpServer = __meteor_bootstrap__.httpServer; +var oldHttpServerListeners = httpServer.listeners('request').slice(0); +httpServer.removeAllListeners('request'); +httpServer.addListener('request', function (req, res) { // allow connections if they have been handled w/ ssl already // (either by us or by a proxy) OR the connection is entirely over @@ -56,8 +56,8 @@ app.addListener('request', function (req, res) { // connection is OK. Proceed normally. var args = arguments; - _.each(oldAppListeners, function(oldListener) { - oldListener.apply(app, args); + _.each(oldHttpServerListeners, function(oldListener) { + oldListener.apply(httpServer, args); }); }); diff --git a/packages/http/httpcall_tests.js b/packages/http/httpcall_tests.js index 7ad2ec0283..252eae25a4 100644 --- a/packages/http/httpcall_tests.js +++ b/packages/http/httpcall_tests.js @@ -2,7 +2,7 @@ var _XHR_URL_PREFIX = "/http_test_responder"; var url_prefix = function () { if (Meteor.isServer && _XHR_URL_PREFIX.indexOf("http") !== 0) { - var address = __meteor_bootstrap__.app.address(); + var address = __meteor_bootstrap__.httpServer.address(); _XHR_URL_PREFIX = "http://127.0.0.1:" + address.port + _XHR_URL_PREFIX; } return _XHR_URL_PREFIX; diff --git a/packages/livedata/stream_server.js b/packages/livedata/stream_server.js index e3125198c4..6f7b3788b4 100644 --- a/packages/livedata/stream_server.js +++ b/packages/livedata/stream_server.js @@ -29,7 +29,7 @@ Meteor._DdpStreamServer = function () { // converts to Unix sockets) but for now, raise the delay. disconnect_delay: 60 * 1000, jsessionid: false}); - self.server.installHandlers(__meteor_bootstrap__.app); + self.server.installHandlers(__meteor_bootstrap__.httpServer); // Support the /websocket endpoint self._redirectWebsocketEndpoint(); @@ -83,9 +83,9 @@ _.extend(Meteor._DdpStreamServer.prototype, { // an approach similar to overshadowListeners in // https://github.com/sockjs/sockjs-node/blob/cf820c55af6a9953e16558555a31decea554f70e/src/utils.coffee _.each(['request', 'upgrade'], function(event) { - var app = __meteor_bootstrap__.app; - var oldAppListeners = app.listeners(event).slice(0); - app.removeAllListeners(event); + var httpServer = __meteor_bootstrap__.httpServer; + var oldHttpServerListeners = httpServer.listeners(event).slice(0); + httpServer.removeAllListeners(event); // request and upgrade have different arguments passed but // we only care about the first one which is always request @@ -97,11 +97,11 @@ _.extend(Meteor._DdpStreamServer.prototype, { request.url === '/websocket/') request.url = '/sockjs/websocket'; - _.each(oldAppListeners, function(oldListener) { - oldListener.apply(app, args); + _.each(oldHttpServerListeners, function(oldListener) { + oldListener.apply(httpServer, args); }); }; - app.addListener(event, newListener); + httpServer.addListener(event, newListener); }); } }); diff --git a/scripts/generate-dev-bundle.sh b/scripts/generate-dev-bundle.sh index 40642edc61..724a9d2b18 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -95,7 +95,7 @@ which npm # you update version numbers. cd "$DIR/lib/node_modules" -npm install connect@1.9.2 # not 2.x yet. sockjs doesn't work w/ new connect +npm install connect@2.7.10 npm install optimist@0.3.5 npm install semver@1.1.0 npm install handlebars@1.0.7 @@ -110,10 +110,6 @@ npm install tar@0.1.14 npm install kexec@0.1.1 npm install shell-quote@0.0.1 -# allow clientMaxAge to be set to 0: -# https://github.com/tomgco/gzippo/pull/49 -npm install https://github.com/meteor/gzippo/tarball/1e4b955439 - # uglify-js has a bug which drops 'undefined' in arrays: # https://github.com/mishoo/UglifyJS2/pull/97 npm install https://github.com/meteor/UglifyJS2/tarball/aa5abd14d3 diff --git a/tools/run.js b/tools/run.js index a1de6994d8..dba2946bc4 100644 --- a/tools/run.js +++ b/tools/run.js @@ -240,6 +240,9 @@ var start_server = function (options) { env.METEOR_SETTINGS = settings; } + // Display errors from (eg) the NPM connect module over the network. + env.NODE_ENV = 'development'; + var nodeOptions = _.clone(options.nodeOptions); nodeOptions.push(path.join(options.bundlePath, 'main.js')); nodeOptions.push('--keepalive'); diff --git a/tools/server/server.js b/tools/server/server.js index 9bd710f918..211c1d32c8 100644 --- a/tools/server/server.js +++ b/tools/server/server.js @@ -3,11 +3,20 @@ var Fiber = require("fibers"); var fs = require("fs"); +var http = require("http"); var path = require("path"); var url = require("url"); +// connect (and some other NPM modules) use $NODE_ENV to make some decisions; +// eg, if $NODE_ENV is not production, they send stack traces on error. connect +// considers 'development' to be the default mode, but that's less safe than +// assuming 'production' to be the default. If you really want development mode, +// set it in your wrapper script (eg, run.js). We need to run this very early, +// since connect makes this decision when it is require'd. +if (!process.env.NODE_ENV) + process.env.NODE_ENV = 'production'; + var connect = require('connect'); -var gzippo = require('gzippo'); var argv = require('optimist').argv; var useragent = require('useragent'); @@ -166,18 +175,19 @@ var run = function () { throw new Error("MONGO_URL must be set in environment"); // webserver - var app = connect.createServer(); + var app = connect(); + + // Auto-compress any json, javascript, or text. + app.use(connect.compress()); + var static_cacheable_path = path.join(bundle_dir, 'static_cacheable'); if (fs.existsSync(static_cacheable_path)) // cacheable files are files that should never change. Typically // named by their hash (eg meteor bundled js and css files). // cache them ~forever (1yr) - // - // 'root' option is to work around an issue in connect/gzippo. - // See https://github.com/meteor/meteor/pull/852 - app.use(gzippo.staticGzip(static_cacheable_path, - {clientMaxAge: 1000 * 60 * 60 * 24 * 365, - root: '/'})); + app.use(connect.static(static_cacheable_path, + {maxAge: 1000 * 60 * 60 * 24 * 365})); + // cache non-cacheable file anyway. This isn't really correct, as // users can change the files and changes won't propogate // immediately. However, if we don't cache them, browsers will @@ -186,9 +196,10 @@ var run = function () { // bust caches. That way we can both get good caching behavior and // allow users to change assets without delay. // https://github.com/meteor/meteor/issues/773 - app.use(gzippo.staticGzip(path.join(bundle_dir, 'static'), - {clientMaxAge: 1000 * 60 * 60 * 24, - root: '/'})); + app.use(connect.static(path.join(bundle_dir, 'static'), + {maxAge: 1000 * 60 * 60 * 24})); + + var httpServer = http.createServer(app); // read bundle config file var info_raw = @@ -200,6 +211,7 @@ var run = function () { __meteor_bootstrap__ = { startup_hooks: [], app: app, + httpServer: httpServer, // metadata about this bundle bundle: bundle, // function that takes a connect `req` object and returns a summary @@ -317,7 +329,7 @@ var run = function () { _.each(__meteor_bootstrap__.startup_hooks, function (x) { x(); }); // only start listening after all the startup code has run. - app.listen(port, function() { + httpServer.listen(port, function() { if (argv.keepalive) console.log("LISTENING"); // must match run.js }); From 043b6530d7d52f1dd91deeca8c20760fba2c856e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 28 May 2013 12:23:42 -0700 Subject: [PATCH 57/60] Bump dev bundle version for connect and underscore upgrades. (0.3.4 was used on the linker branch.) --- meteor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor b/meteor index 20d4053a4d..16aa2a0641 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.3.3 +BUNDLE_VERSION=0.3.5 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. From 7d24fbd4c0b8149d1e71e10ed86661d1de589f1d Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 28 May 2013 12:26:11 -0700 Subject: [PATCH 58/60] Update HISTORY for upgrades. --- History.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/History.md b/History.md index 88d76f9109..4b2dbdca8d 100644 --- a/History.md +++ b/History.md @@ -17,6 +17,10 @@ * Fix Mongo queries which nested JavaScript RegExp objects inside `$or`. #1089 +* Upgrade Underscore from 1.4.2 to 1.4.4. + +* Upgrade Connect from 1.9.2 to 2.7.10. + Patches contributed by GitHub users awwx, johnston, and timhaines. From e43f873f6b39e4d64318a7c503539da42aaac642 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 28 May 2013 13:26:15 -0700 Subject: [PATCH 59/60] Update shrinkwrap file because mongodb module got re-pushed. mongodb 1.3.6 was originally published depending on bson 0.1.8 and was re-published depending on 0.1.9 (oops). --- packages/mongo-livedata/.npm/npm-shrinkwrap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mongo-livedata/.npm/npm-shrinkwrap.json b/packages/mongo-livedata/.npm/npm-shrinkwrap.json index 9a825e9347..f4a1429a74 100644 --- a/packages/mongo-livedata/.npm/npm-shrinkwrap.json +++ b/packages/mongo-livedata/.npm/npm-shrinkwrap.json @@ -4,7 +4,7 @@ "version": "1.3.6", "dependencies": { "bson": { - "version": "0.1.8" + "version": "0.1.9" }, "kerberos": { "version": "0.0.2" From 75149b75f3cbab3000aabb3d6e7c6caf652aea01 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 28 May 2013 14:15:31 -0700 Subject: [PATCH 60/60] Upgrade node-http-proxy to 0.10.1. Intentionally not choosing 0.10.2, which has a websocket proxying Node 0.10 semi-fix which I found to sometimes corrupt data (on Node 0.10). See my analysis on https://github.com/nodejitsu/node-http-proxy/pull/402 I do not know whether or not the PR corrupts data on 0.8, but it isn't necessary there, so I'm going to hold off on taking that change until the promised future complete rewrite of http-proxy to use the new 0.10 streams API. --- History.md | 4 +++- meteor | 2 +- scripts/generate-dev-bundle.sh | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/History.md b/History.md index 4b2dbdca8d..3de732487e 100644 --- a/History.md +++ b/History.md @@ -17,7 +17,9 @@ * Fix Mongo queries which nested JavaScript RegExp objects inside `$or`. #1089 -* Upgrade Underscore from 1.4.2 to 1.4.4. +* Upgrade Underscore from 1.4.2 to 1.4.4. #776 + +* Upgrade http-proxy from 0.8.5 to 0.10.1. #513 * Upgrade Connect from 1.9.2 to 2.7.10. diff --git a/meteor b/meteor index 16aa2a0641..eefa2949df 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.3.5 +BUNDLE_VERSION=0.3.6 # 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 724a9d2b18..ecd274ad7e 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -103,7 +103,7 @@ npm install clean-css@0.8.3 npm install useragent@2.0.1 npm install request@2.12.0 npm install keypress@0.1.0 -npm install http-proxy@0.8.5 +npm install http-proxy@0.10.1 # not 0.10.2, which contains a sketchy websocket change npm install underscore@1.4.4 npm install fstream@0.1.21 npm install tar@0.1.14