Merge branch 'devel' into shark

Conflicts:
	packages/templating/deftemplate.js
	packages/templating/package.js
	packages/templating/plugin/html_scanner.js
	packages/test-in-browser/driver.js
	tools/packages.js
This commit is contained in:
David Glasser
2013-08-01 17:17:18 -07:00
376 changed files with 4448 additions and 7244 deletions

View File

@@ -1,6 +1,8 @@
## vNEXT
* Fix Mongo selectors of the form: {$regex: /foo/}.
* Calling `findOne()` on the server no longer loads the full query result
into memory.
@@ -18,6 +20,18 @@
* Delete login tokens from server when user logs out.
* Renames (may require doc updates):
- `Meteor.default_connection` - `Meteor.connection`
- `Meteor.default_server` - `Meteor.server`
- `Meteor.connect` - `DDP.connect`
- `Meteor.http` - `HTTP`
## v0.6.4.1
* Update mongodb driver to use version 0.2.1 of the bson module.
## v0.6.4
* Separate OAuth flow logic from Accounts into separate packages. The

View File

@@ -3,6 +3,7 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
standard-app-packages
jquery
underscore
showdown

View File

@@ -1 +1 @@
0.6.4
0.6.5-rc2

View File

@@ -12,6 +12,15 @@ on the client, just on the server, or *Anywhere*.
{{> api_box isClient}}
{{> api_box isServer}}
{{#note}}
`Meteor.isServer` can be used to limit where code runs, but it does not
prevent code from being sent to the client. Any sensitive code that you
don't want served to the client, such as code containing passwords or
authentication mechanisms, should be kept in the `server` directory.
{{/note}}
{{> api_box startup}}
On a server, the function will run as soon as the server process is
@@ -413,8 +422,8 @@ the server. The return value is an object with the following fields:
values are `connected` (the connection is up and
running), `connecting` (disconnected and trying to open a
new connection), `failed` (permanently failed to connect; e.g., the client
and server support different versions of DDP) and `waiting` (failed
to connect and waiting to try to reconnect).
and server support different versions of DDP), `waiting` (failed
to connect and waiting to try to reconnect) and `offline` (user has disconnected the connection).
{{/dtdd}}
{{#dtdd name="retryCount" type="Number"}}
@@ -441,11 +450,24 @@ to get realtime updates.
{{> api_box reconnect}}
{{> api_box disconnect}}
Call this method to temporarily disconnect from the server and stop all
live data updates. While the client is disconnected it will not receive
updates to collections, method calls will be queued until the
connection is reestablished, and hot code push will be disabled.
Call [Meteor.reconnect](#meteor_reconnect) to reestablish the connection
and resume data transfer.
This can be used to save battery on mobile devices when real time
updates are not required.
{{> api_box connect}}
To call methods on another Meteor application or subscribe to its data
sets, call `Meteor.connect` with the URL of the application.
`Meteor.connect` returns an object which provides:
sets, call `DDP.connect` with the URL of the application.
`DDP.connect` returns an object which provides:
* `subscribe` -
Subscribe to a record set. See
@@ -463,6 +485,8 @@ sets, call `Meteor.connect` with the URL of the application.
[Meteor.status](#meteor_status).
* `reconnect` -
See [Meteor.reconnect](#meteor_reconnect).
* `disconnect` -
See [Meteor.disconnect](#meteor_disconnect).
* `onReconnect` - Set this to a function to be called as the first step of
reconnecting. This function can call methods which will be executed before
any other outstanding methods. For example, this can be used to re-establish
@@ -473,11 +497,6 @@ When you call `Meteor.subscribe`, `Meteor.status`, `Meteor.call`, and
`Meteor.apply`, you are using a connection back to that default
server.
{{#warning}}
In this release, `Meteor.connect` can only be called on the client.
Servers can not yet connect to other servers.
{{/warning}}
<h2 id="collections"><span>Collections</span></h2>
Meteor stores data in *collections*. To get started, declare a
@@ -2795,9 +2814,9 @@ For example, the `toJSONValue` method for
return this.toHexString();
};
<h2 id="meteor_http"><span>Meteor.http</span></h2>
<h2 id="HTTP"><span>HTTP</span></h2>
`Meteor.http` provides an HTTP API on the client and server. To use
`HTTP` provides an HTTP request API on the client and server. To use
these functions, add the HTTP package to your project with `$ meteor add
http`.
@@ -2868,8 +2887,8 @@ Example server method:
Meteor.methods({checkTwitter: function (userId) {
check(userId, String);
this.unblock();
var result = Meteor.http.call("GET", "http://api.twitter.com/xyz",
{params: {user: userId}});
var result = HTTP.call("GET", "http://api.twitter.com/xyz",
{params: {user: userId}});
if (result.statusCode === 200)
return true
return false;
@@ -2877,13 +2896,13 @@ Example server method:
Example asynchronous HTTP call:
Meteor.http.call("POST", "http://api.twitter.com/xyz",
{data: {some: "json", stuff: 1}},
function (error, result) {
if (result.statusCode === 200) {
Session.set("twizzled", true);
}
});
HTTP.call("POST", "http://api.twitter.com/xyz",
{data: {some: "json", stuff: 1}},
function (error, result) {
if (result.statusCode === 200) {
Session.set("twizzled", true);
}
});
{{> api_box http_get}}

View File

@@ -447,9 +447,17 @@ Template.api.reconnect = {
"This method does nothing if the client is already connected."]
};
Template.api.disconnect = {
id: "meteor_disconnect",
name: "Meteor.disconnect()",
locus: "Client",
descr: [
"Disconnect the client from the server."]
};
Template.api.connect = {
id: "meteor_connect",
name: "Meteor.connect(url)",
name: "DDP.connect(url)",
locus: "Client",
descr: ["Connect to the server of a different Meteor application to subscribe to its document sets and invoke its remote methods."],
args: [
@@ -1066,6 +1074,11 @@ Template.api.loginWithExternalService = {
name: "requestOfflineToken",
type: "Boolean",
descr: "If true, asks the user for permission to act on their behalf when offline. This stores an additional offline token in the `services` field of the user document. Currently only supported with Google."
},
{
name: "forceApprovalPrompt",
type: "Boolean",
descr: "If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google."
}
]
};
@@ -1572,7 +1585,7 @@ Template.api.equals = {
Template.api.httpcall = {
id: "meteor_http_call",
name: "Meteor.http.call(method, url [, options] [, asyncCallback])",
name: "HTTP.call(method, url [, options] [, asyncCallback])",
locus: "Anywhere",
descr: ["Perform an outbound HTTP request."],
args: [
@@ -1619,30 +1632,30 @@ Template.api.httpcall = {
Template.api.http_get = {
id: "meteor_http_get",
name: "Meteor.http.get(url, [options], [asyncCallback])",
name: "HTTP.get(url, [options], [asyncCallback])",
locus: "Anywhere",
descr: ["Send an HTTP GET request. Equivalent to `Meteor.http.call(\"GET\", ...)`."]
descr: ["Send an HTTP GET request. Equivalent to `HTTP.call(\"GET\", ...)`."]
};
Template.api.http_post = {
id: "meteor_http_post",
name: "Meteor.http.post(url, [options], [asyncCallback])",
name: "HTTP.post(url, [options], [asyncCallback])",
locus: "Anywhere",
descr: ["Send an HTTP POST request. Equivalent to `Meteor.http.call(\"POST\", ...)`."]
descr: ["Send an HTTP POST request. Equivalent to `HTTP.call(\"POST\", ...)`."]
};
Template.api.http_put = {
id: "meteor_http_put",
name: "Meteor.http.put(url, [options], [asyncCallback])",
name: "HTTP.put(url, [options], [asyncCallback])",
locus: "Anywhere",
descr: ["Send an HTTP PUT request. Equivalent to `Meteor.http.call(\"PUT\", ...)`."]
descr: ["Send an HTTP PUT request. Equivalent to `HTTP.call(\"PUT\", ...)`."]
};
Template.api.http_del = {
id: "meteor_http_del",
name: "Meteor.http.del(url, [options], [asyncCallback])",
name: "HTTP.del(url, [options], [asyncCallback])",
locus: "Anywhere",
descr: ["Send an HTTP DELETE request. Equivalent to `Meteor.http.call(\"DELETE\", ...)`. (Named `del` to avoid conflict with JavaScript's `delete`.)"]
descr: ["Send an HTTP DELETE request. Equivalent to `HTTP.call(\"DELETE\", ...)`. (Named `del` to avoid conflict with JavaScript's `delete`.)"]
};

View File

@@ -684,12 +684,13 @@ To get started, run
$ meteor bundle myapp.tgz
This command will generate a fully-contained Node.js application in
the form of a tarball. To run this application, you need to provide
Node.js 0.8 and a MongoDB server. You can then run the application by
invoking node, specifying the HTTP port for the application to listen
on, and the MongoDB endpoint. If you don't already have a MongoDB
server, we can recommend our friends at [MongoHQ](http://mongohq.com).
This command will generate a fully-contained Node.js application in the form of
a tarball. To run this application, you need to provide Node.js 0.8 and a
MongoDB server. (The current release of Meteor has been tested with Node
0.8.24.) You can then run the application by invoking node, specifying the HTTP
port for the application to listen on, and the MongoDB endpoint. If you don't
already have a MongoDB server, we can recommend our friends at
[MongoHQ](http://mongohq.com).
$ PORT=3000 MONGO_URL=mongodb://localhost:27017/myapp node bundle/main.js
@@ -704,7 +705,7 @@ have `npm` available, and run the following:
$ cd bundle/server/node_modules
$ rm -r fibers
$ npm install fibers@1.0.0
$ npm install fibers@1.0.1
{{/warning}}
{{/better_markdown}}

View File

@@ -139,7 +139,8 @@ var toc = [
{name: "Server connections", id: "connections"}, [
"Meteor.status",
"Meteor.reconnect",
"Meteor.connect"
"Meteor.disconnect",
"DDP.connect"
],
{name: "Collections", id: "collections"}, [
@@ -305,12 +306,12 @@ var toc = [
],
"Meteor.http", [
"Meteor.http.call",
{name: "Meteor.http.get", id: "meteor_http_get"},
{name: "Meteor.http.post", id: "meteor_http_post"},
{name: "Meteor.http.put", id: "meteor_http_put"},
{name: "Meteor.http.del", id: "meteor_http_del"}
"HTTP", [
"HTTP.call",
{name: "HTTP.get", id: "meteor_http_get"},
{name: "HTTP.post", id: "meteor_http_post"},
{name: "HTTP.put", id: "meteor_http_put"},
{name: "HTTP.del", id: "meteor_http_del"}
],
"Email", [
"Email.send"

View File

@@ -3,6 +3,7 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
standard-app-packages
autopublish
insecure
preserve-inputs

View File

@@ -1 +1 @@
0.6.4
0.6.4.1

View File

@@ -3,3 +3,4 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
standard-app-packages

View File

@@ -5,3 +5,4 @@
preserve-inputs
accounts-google
standard-app-packages

View File

@@ -6,3 +6,4 @@
insecure
preserve-inputs
random
standard-app-packages

View File

@@ -4,3 +4,4 @@
# but you can also edit it by hand.
autopublish
standard-app-packages

View File

@@ -3,6 +3,7 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
standard-app-packages
preserve-inputs
accounts-ui
accounts-password

View File

@@ -1 +1 @@
0.6.4
0.6.4.1

View File

@@ -3,6 +3,7 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
standard-app-packages
underscore
backbone
spiderable

View File

@@ -1 +1 @@
0.6.4
0.6.4.1

View File

@@ -13,3 +13,4 @@ accounts-github
accounts-password
underscore
accounts-facebook
standard-app-packages

View File

@@ -6,3 +6,4 @@
underscore
jquery
jquery-layout
standard-app-packages

View File

@@ -7,3 +7,4 @@ insecure
preserve-inputs
bootstrap
random
standard-app-packages

View File

@@ -5,3 +5,4 @@
less
coffeescript
standard-app-packages

View File

@@ -4,3 +4,4 @@
# but you can also edit it by hand.
autopublish
standard-app-packages

View File

@@ -4,3 +4,4 @@
# but you can also edit it by hand.
autopublish
standard-app-packages

View File

@@ -1,6 +1,6 @@
if (Meteor.is_client) {
if (Meteor.isClient) {
Template.page.nodespec = function (fn) {
var parts = [fn()];
var replaceParts = function(regex, replacementFunc) {
@@ -64,4 +64,4 @@ if (Meteor.is_client) {
return new Handlebars.SafeString('<div class="spacer">&nbsp;</div>');
};
}
}

View File

@@ -4,3 +4,4 @@
# but you can also edit it by hand.
autopublish
standard-app-packages

View File

@@ -1,4 +1,4 @@
Leaderboard = Meteor.connect("http://leader2.meteor.com/sockjs");
Leaderboard = DDP.connect("http://leader2.meteor.com/sockjs");
// XXX I'd rather this be Leaderboard.Players.. can this API be easier?
Players = new Meteor.Collection("players", {manager: Leaderboard});

View File

@@ -6,3 +6,4 @@
autopublish
preserve-inputs
jsparse
standard-app-packages

View File

@@ -1,6 +1,6 @@
if (Meteor.is_client) {
if (Meteor.isClient) {
Meteor.startup(function () {
if (! Session.get("input"))
Session.set("input", "var x = 3");

View File

@@ -3,6 +3,7 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
standard-app-packages
autopublish
insecure
preserve-inputs

View File

@@ -4,4 +4,5 @@
# but you can also edit it by hand.
jquery
backbone
backbone
standard-app-packages

View File

@@ -6,3 +6,4 @@
jquery
jquery-layout
jquery-history
standard-app-packages

View File

@@ -3,6 +3,7 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
standard-app-packages
insecure
jquery
preserve-inputs

View File

@@ -1 +1 @@
0.6.4
0.6.4.1

2
meteor
View File

@@ -1,6 +1,6 @@
#!/bin/bash
BUNDLE_VERSION=0.3.11
BUNDLE_VERSION=0.3.13
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.

View File

@@ -4,7 +4,7 @@
// This is reactive.
Meteor.userId = function () {
return Meteor.default_connection.userId();
return Meteor.connection.userId();
};
var loggingIn = false;
@@ -45,7 +45,7 @@ Meteor.user = function () {
// - Updating the Meteor.loggingIn() reactive data source
// - Calling the method in 'wait' mode
// - On success, saving the resume token to localStorage
// - On success, calling Meteor.default_connection.setUserId()
// - On success, calling Meteor.connection.setUserId()
// - Setting up an onReconnect handler which logs in with
// the resume token
//
@@ -57,6 +57,7 @@ Meteor.user = function () {
// its error will be passed to the callback).
// - userCallback: Will be called with no arguments once the user is fully
// logged in, or with the error on error.
//
Accounts.callLoginMethod = function (options) {
options = _.extend({
methodName: 'login',
@@ -78,19 +79,19 @@ Accounts.callLoginMethod = function (options) {
// getting the results of subscription rerun, we WILL NOT re-send this
// method (because we never re-send methods whose results we've received)
// but we WILL call loggedInAndDataReadyCallback at "reconnect quiesce"
// time. This will lead to _makeClientLoggedIn(result.id) even though we
// time. This will lead to makeClientLoggedIn(result.id) even though we
// haven't actually sent a login method!
//
// But by making sure that we send this "resume" login in that case (and
// calling _makeClientLoggedOut if it fails), we'll end up with an accurate
// calling makeClientLoggedOut if it fails), we'll end up with an accurate
// client-side userId. (It's important that livedata_connection guarantees
// that the "reconnect quiesce"-time call to loggedInAndDataReadyCallback
// will occur before the callback from the resume login call.)
var onResultReceived = function (err, result) {
if (err || !result || !result.token) {
Meteor.default_connection.onReconnect = null;
Meteor.connection.onReconnect = null;
} else {
Meteor.default_connection.onReconnect = function() {
Meteor.connection.onReconnect = function() {
reconnected = true;
Accounts.callLoginMethod({
methodArguments: [{resume: result.token}],
@@ -100,7 +101,7 @@ Accounts.callLoginMethod = function (options) {
_suppressLoggingIn: true,
userCallback: function (error) {
if (error) {
Accounts._makeClientLoggedOut();
makeClientLoggedOut();
}
options.userCallback(error);
}});
@@ -138,7 +139,7 @@ Accounts.callLoginMethod = function (options) {
}
// Make the client logged in. (The user data should already be loaded!)
Accounts._makeClientLoggedIn(result.id, result.token);
makeClientLoggedIn(result.id, result.token);
options.userCallback();
};
@@ -151,15 +152,15 @@ Accounts.callLoginMethod = function (options) {
loggedInAndDataReadyCallback);
};
Accounts._makeClientLoggedOut = function() {
Accounts._unstoreLoginToken();
Meteor.default_connection.setUserId(null);
Meteor.default_connection.onReconnect = null;
makeClientLoggedOut = function() {
unstoreLoginToken();
Meteor.connection.setUserId(null);
Meteor.connection.onReconnect = null;
};
Accounts._makeClientLoggedIn = function(userId, token) {
Accounts._storeLoginToken(userId, token);
Meteor.default_connection.setUserId(userId);
makeClientLoggedIn = function(userId, token) {
storeLoginToken(userId, token);
Meteor.connection.setUserId(userId);
};
Meteor.logout = function (callback) {
@@ -167,7 +168,7 @@ Meteor.logout = function (callback) {
if (error) {
callback && callback(error);
} else {
Accounts._makeClientLoggedOut();
makeClientLoggedOut();
callback && callback();
}
});
@@ -182,6 +183,7 @@ var loginServicesHandle = Meteor.subscribe("meteor.loginServiceConfiguration");
// A reactive function returning whether the loginServiceConfiguration
// subscription is ready. Used by accounts-ui to hide the login button
// until we have all the configuration loaded
//
Accounts.loginServicesConfigured = function () {
return loginServicesHandle.ready();
};

View File

@@ -1,10 +1,8 @@
// @export Accounts
if (typeof Accounts === 'undefined')
Accounts = {};
Accounts = {};
if (!Accounts._options) {
Accounts._options = {};
}
// Currently this is read directly by packages like accounts-password
// and accounts-ui-unstyled.
Accounts._options = {};
// Set up config for the accounts system. Call this on both the client
// and the server.
@@ -21,6 +19,7 @@ if (!Accounts._options) {
// client signups.
// - forbidClientAccountCreation {Boolean}
// Do not allow clients to create accounts directly.
//
Accounts.config = function(options) {
// validate option keys
var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation"];
@@ -45,6 +44,7 @@ Accounts.config = function(options) {
// Users table. Don't use the normal autopublish, since we want to hide
// some fields. Code to autopublish this is in accounts_server.js.
// XXX Allow users to configure this collection name.
//
Meteor.users = new Meteor.Collection("users", {_preventAutopublish: true});
// There is an allow call in accounts_server that restricts this
// collection.

View File

@@ -12,7 +12,7 @@ Meteor.userId = function () {
// user expects. The way to make this work in a publish is to do
// Meteor.find(this.userId()).observe and recompute when the user
// record changes.
var currentInvocation = Meteor._CurrentInvocation.get();
var currentInvocation = DDP._CurrentInvocation.get();
if (!currentInvocation)
throw new Error("Meteor.userId can only be invoked in method calls. Use this.userId in publish functions.");
return currentInvocation.userId;
@@ -36,20 +36,21 @@ Meteor.user = function () {
// - `undefined`, meaning don't handle;
// - {id: userId, token: *}, if the user logged in successfully.
// - throw an error, if the user failed to log in.
//
Accounts.registerLoginHandler = function(handler) {
Accounts._loginHandlers.push(handler);
loginHandlers.push(handler);
};
// list of all registered handlers.
Accounts._loginHandlers = [];
loginHandlers = [];
// Try all of the registered login handlers until one of them doesn'
// return `undefined`, meaning it handled this call to `login`. Return
// that return value, which ought to be a {id/token} pair.
var tryAllLoginHandlers = function (options) {
for (var i = 0; i < Accounts._loginHandlers.length; ++i) {
var handler = Accounts._loginHandlers[i];
for (var i = 0; i < loginHandlers.length; ++i) {
var handler = loginHandlers[i];
var result = handler(options);
if (result !== undefined)
return result;
@@ -80,7 +81,7 @@ Meteor.methods({
logout: function() {
if (this._sessionData.loginToken && this.userId)
Accounts._removeLoginToken(this.userId, this._sessionData.loginToken);
removeLoginToken(this.userId, this._sessionData.loginToken);
this.setUserId(null);
}
});
@@ -109,11 +110,12 @@ Accounts.registerLoginHandler(function(options) {
});
// Semi-public. Used by other login methods to generate tokens.
//
Accounts._generateStampedLoginToken = function () {
return {token: Random.id(), when: +(new Date)};
};
Accounts._removeLoginToken = function (userId, loginToken) {
removeLoginToken = function (userId, loginToken) {
Meteor.users.update(userId, {
$pull: {
"services.resume.loginTokens": { "token": loginToken }
@@ -125,6 +127,7 @@ Accounts._removeLoginToken = function (userId, loginToken) {
///
/// CREATE USER HOOKS
///
var onCreateUserHook = null;
Accounts.onCreateUser = function (func) {
if (onCreateUserHook)
@@ -140,6 +143,8 @@ var defaultCreateUserHook = function (options, user) {
user.profile = options.profile;
return user;
};
// Called by accounts-password
Accounts.insertUserDoc = function (options, user) {
// - clone user document, to protect from modification
// - add createdAt timestamp
@@ -223,6 +228,7 @@ Accounts.validateNewUser = function (func) {
// (eg, profile)
// @returns {Object} Object with token and id keys, like the result
// of the "login" method.
//
Accounts.updateOrCreateUserFromExternalService = function(
serviceName, serviceData, options) {
options = _.clone(options || {});
@@ -306,60 +312,64 @@ Meteor.publish(null, function() {
// Accounts.addAutopublishFields Notably, this isn't implemented with
// multiple publishes since DDP only merges only across top-level
// fields, not subfields (such as 'services.facebook.accessToken')
Accounts._autopublishFields = {
var autopublishFields = {
loggedInUser: ['profile', 'username', 'emails'],
otherUsers: ['profile', 'username']
};
// Add to the list of fields or subfields to be automatically
// published if autopublish is on
// published if autopublish is on. Must be called from top-level
// code (ie, before Meteor.startup hooks run).
//
// @param opts {Object} with:
// - forLoggedInUser {Array} Array of fields published to the logged-in user
// - forOtherUsers {Array} Array of fields published to users that aren't logged in
Accounts.addAutopublishFields = function(opts) {
Accounts._autopublishFields.loggedInUser.push.apply(
Accounts._autopublishFields.loggedInUser, opts.forLoggedInUser);
Accounts._autopublishFields.otherUsers.push.apply(
Accounts._autopublishFields.otherUsers, opts.forOtherUsers);
autopublishFields.loggedInUser.push.apply(
autopublishFields.loggedInUser, opts.forLoggedInUser);
autopublishFields.otherUsers.push.apply(
autopublishFields.otherUsers, opts.forOtherUsers);
};
Meteor.default_server.onAutopublish(function () {
// ['profile', 'username'] -> {profile: 1, username: 1}
var toFieldSelector = function(fields) {
return _.object(_.map(fields, function(field) {
return [field, 1];
}));
};
if (Package.autopublish) {
// Use Meteor.startup to give other packages a chance to call
// addAutopublishFields.
Meteor.startup(function () {
// ['profile', 'username'] -> {profile: 1, username: 1}
var toFieldSelector = function(fields) {
return _.object(_.map(fields, function(field) {
return [field, 1];
}));
};
Meteor.server.publish(null, function () {
if (this.userId) {
return Meteor.users.find(
{_id: this.userId},
{fields: toFieldSelector(autopublishFields.loggedInUser)});
} else {
return null;
}
}, /*suppress autopublish warning*/{is_auto: true});
// XXX this publish is neither dedup-able nor is it optimized by our special
// treatment of queries on a specific _id. Therefore this will have O(n^2)
// run-time performance every time a user document is changed (eg someone
// logging in). If this is a problem, we can instead write a manual publish
// function which filters out fields based on 'this.userId'.
Meteor.server.publish(null, function () {
var selector;
if (this.userId)
selector = {_id: {$ne: this.userId}};
else
selector = {};
Meteor.default_server.publish(null, function () {
if (this.userId) {
return Meteor.users.find(
{_id: this.userId},
{fields: toFieldSelector(Accounts._autopublishFields.loggedInUser)});
} else {
return null;
}
}, /*suppress autopublish warning*/{is_auto: true});
// XXX this publish is neither dedup-able nor is it optimized by our
// special treatment of queries on a specific _id. Therefore this
// will have O(n^2) run-time performance every time a user document
// is changed (eg someone logging in). If this is a problem, we can
// instead write a manual publish function which filters out fields
// based on 'this.userId'.
Meteor.default_server.publish(null, function () {
var selector;
if (this.userId)
selector = {_id: {$ne: this.userId}};
else
selector = {};
return Meteor.users.find(
selector,
{fields: toFieldSelector(Accounts._autopublishFields.otherUsers)});
}, /*suppress autopublish warning*/{is_auto: true});
});
selector,
{fields: toFieldSelector(autopublishFields.otherUsers)});
}, /*suppress autopublish warning*/{is_auto: true});
});
}
// Publish all login service configuration fields other than secret.
Meteor.publish("meteor.loginServiceConfiguration", function () {
@@ -374,8 +384,13 @@ Meteor.methods({
// Don't let random users configure a service we haven't added yet (so
// that when we do later add it, it's set up with their configuration
// instead of ours).
if (!Accounts[options.service])
// XXX if service configuration is oauth-specific then this code should
// be in accounts-oauth; if it's not then the registry should be
// in this package
if (!(Accounts.oauth
&& _.contains(Accounts.oauth.serviceNames(), options.service))) {
throw new Meteor.Error(403, "Service unknown");
}
if (ServiceConfiguration.configurations.findOne({service: options.service}))
throw new Meteor.Error(403, "Service " + options.service + " already configured");
ServiceConfiguration.configurations.insert(options);

View File

@@ -3,6 +3,8 @@
// seconds to synchronize login state between multiple tabs in the same
// browser.
var lastLoginTokenWhenPolled;
// Login with a Meteor access token. This is the only public function
// here.
Meteor.loginWithToken = function (token, callback) {
@@ -14,8 +16,8 @@ Meteor.loginWithToken = function (token, callback) {
// Semi-internal API. Call this function to re-enable auto login after
// if it was disabled at startup.
Accounts._enableAutoLogin = function () {
Accounts._preventAutoLogin = false;
Accounts._pollStoredLoginToken();
autoLoginEnabled = true;
pollStoredLoginToken();
};
@@ -35,29 +37,32 @@ Accounts._isolateLoginTokenForTest = function () {
userIdKey = userIdKey + Random.id();
};
Accounts._storeLoginToken = function(userId, token) {
storeLoginToken = function(userId, token) {
Meteor._localStorage.setItem(userIdKey, userId);
Meteor._localStorage.setItem(loginTokenKey, token);
// to ensure that the localstorage poller doesn't end up trying to
// connect a second time
Accounts._lastLoginTokenWhenPolled = token;
lastLoginTokenWhenPolled = token;
};
Accounts._unstoreLoginToken = function() {
unstoreLoginToken = function() {
Meteor._localStorage.removeItem(userIdKey);
Meteor._localStorage.removeItem(loginTokenKey);
// to ensure that the localstorage poller doesn't end up trying to
// connect a second time
Accounts._lastLoginTokenWhenPolled = null;
lastLoginTokenWhenPolled = null;
};
Accounts._storedLoginToken = function() {
// This is private, but it is exported for now because it is used by a
// test in accounts-password.
//
var storedLoginToken = Accounts._storedLoginToken = function() {
return Meteor._localStorage.getItem(loginTokenKey);
};
Accounts._storedUserId = function() {
var storedUserId = function() {
return Meteor._localStorage.getItem(userIdKey);
};
@@ -66,19 +71,19 @@ Accounts._storedUserId = function() {
/// AUTO-LOGIN
///
if (!Accounts._preventAutoLogin) {
if (autoLoginEnabled) {
// Immediately try to log in via local storage, so that any DDP
// messages are sent after we have established our user account
var token = Accounts._storedLoginToken();
var token = storedLoginToken();
if (token) {
// On startup, optimistically present us as logged in while the
// request is in flight. This reduces page flicker on startup.
var userId = Accounts._storedUserId();
userId && Meteor.default_connection.setUserId(userId);
var userId = storedUserId();
userId && Meteor.connection.setUserId(userId);
Meteor.loginWithToken(token, function (err) {
if (err) {
Meteor._debug("Error logging in with token: " + err);
Accounts._makeClientLoggedOut();
makeClientLoggedOut();
}
});
}
@@ -86,21 +91,21 @@ if (!Accounts._preventAutoLogin) {
// Poll local storage every 3 seconds to login if someone logged in in
// another tab
Accounts._lastLoginTokenWhenPolled = token;
Accounts._pollStoredLoginToken = function() {
if (Accounts._preventAutoLogin)
lastLoginTokenWhenPolled = token;
var pollStoredLoginToken = function() {
if (! autoLoginEnabled)
return;
var currentLoginToken = Accounts._storedLoginToken();
var currentLoginToken = storedLoginToken();
// != instead of !== just to make sure undefined and null are treated the same
if (Accounts._lastLoginTokenWhenPolled != currentLoginToken) {
if (lastLoginTokenWhenPolled != currentLoginToken) {
if (currentLoginToken)
Meteor.loginWithToken(currentLoginToken); // XXX should we pass a callback here?
else
Meteor.logout();
}
Accounts._lastLoginTokenWhenPolled = currentLoginToken;
lastLoginTokenWhenPolled = currentLoginToken;
};
setInterval(Accounts._pollStoredLoginToken, 3000);
setInterval(pollStoredLoginToken, 3000);

View File

@@ -5,12 +5,14 @@ Package.describe({
Package.on_use(function (api) {
api.use('underscore', ['client', 'server']);
api.use('localstorage', 'client');
api.use('accounts-urls', ['client', 'server']);
api.use('deps', 'client');
api.use('check', 'server');
api.use('random', ['client', 'server']);
api.use('service-configuration', ['client', 'server']);
// needed for getting the currently logged-in user
api.use('livedata', ['client', 'server']);
// need this because of the Meteor.users collection but in the future
// we'd probably want to abstract this away
api.use('mongo-livedata', ['client', 'server']);
@@ -19,12 +21,21 @@ Package.on_use(function (api) {
// {{currentUser}}. If not, no biggie.
api.use('handlebars', 'client', {weak: true});
// Allow us to detect 'autopublish', and publish some Meteor.users fields if
// it's loaded.
api.use('autopublish', 'server', {weak: true});
api.export('Accounts');
api.add_files('accounts_common.js', ['client', 'server']);
api.add_files('accounts_server.js', 'server');
api.add_files('url_client.js', 'client');
api.add_files('url_server.js', 'server');
// accounts_client must be before localstorage_token, because
// localstorage_token attempts to call functions in accounts_client (eg
// Accounts.callLoginMethod) on startup.
// Accounts.callLoginMethod) on startup. And localstorage_token must be after
// url_client, which sets autoLoginEnabled.
api.add_files('accounts_client.js', 'client');
api.add_files('localstorage_token.js', 'client');
});

View File

@@ -1,6 +1,4 @@
// @export Accounts
if (typeof Accounts === 'undefined')
Accounts = {};
autoLoginEnabled = true;
// reads a reset password token from the url's hash fragment, if it's
// there. if so prevent automatically logging in since it could be
@@ -13,7 +11,7 @@ if (typeof Accounts === 'undefined')
var match;
match = window.location.hash.match(/^\#\/reset-password\/(.*)$/);
if (match) {
Accounts._preventAutoLogin = true;
autoLoginEnabled = false;
Accounts._resetPasswordToken = match[1];
window.location.hash = '';
}
@@ -30,7 +28,7 @@ if (match) {
// in line with the hash fragment approach)
match = window.location.hash.match(/^\#\/verify-email\/(.*)$/);
if (match) {
Accounts._preventAutoLogin = true;
autoLoginEnabled = false;
Accounts._verifyEmailToken = match[1];
window.location.hash = '';
}
@@ -40,7 +38,7 @@ if (match) {
// reset password links.
match = window.location.hash.match(/^\#\/enroll-account\/(.*)$/);
if (match) {
Accounts._preventAutoLogin = true;
autoLoginEnabled = false;
Accounts._enrollAccountToken = match[1];
window.location.hash = '';
}

View File

@@ -1,9 +1,6 @@
// @export Accounts
if (typeof Accounts === 'undefined')
Accounts = {};
// XXX These should probably not actually be public?
if (!Accounts.urls)
Accounts.urls = {};
Accounts.urls = {};
Accounts.urls.resetPassword = function (token) {
return Meteor.absoluteUrl('#/reset-password/' + token);

View File

@@ -0,0 +1,26 @@
Accounts.oauth.registerService('facebook');
if (Meteor.isClient) {
Meteor.loginWithFacebook = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Facebook.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
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'
]
});
}

View File

@@ -1,4 +0,0 @@
Meteor.loginWithFacebook = function(options, callback) {
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Facebook.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,3 +0,0 @@
if (!Accounts.facebook) {
Accounts.facebook = {};
}

View File

@@ -1,13 +0,0 @@
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'
]
});

View File

@@ -11,7 +11,5 @@ Package.on_use(function(api) {
api.add_files('facebook_login_button.css', 'client');
api.add_files('facebook_common.js', ['client', 'server']);
api.add_files('facebook_server.js', 'server');
api.add_files('facebook_client.js', 'client');
api.add_files("facebook.js");
});

View File

@@ -0,0 +1,22 @@
Accounts.oauth.registerService('github');
if (Meteor.isClient) {
Meteor.loginWithGithub = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Github.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
// not sure whether the github api can be used from the browser,
// thus not sure if we should be sending access tokens; but we do it
// for all other oauth2 providers, and it may come in handy.
forLoggedInUser: ['services.github'],
forOtherUsers: ['services.github.username']
});
}

View File

@@ -1,4 +0,0 @@
Meteor.loginWithGithub = function(options, callback) {
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Github.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,3 +0,0 @@
if (!Accounts.github) {
Accounts.github = {};
}

View File

@@ -1,9 +0,0 @@
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
// for all other oauth2 providers, and it may come in handy.
forLoggedInUser: ['services.github'],
forOtherUsers: ['services.github.username']
});

View File

@@ -11,7 +11,5 @@ Package.on_use(function(api) {
api.add_files('github_login_button.css', 'client');
api.add_files('github_common.js', ['client', 'server']);
api.add_files('github_server.js', 'server');
api.add_files('github_client.js', 'client');
api.add_files("github.js");
});

View File

@@ -0,0 +1,30 @@
Accounts.oauth.registerService('google');
if (Meteor.isClient) {
Meteor.loginWithGoogle = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Google.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
forLoggedInUser: _.map(
// publish access token since it can be used from the client (if
// transmitted over ssl or on
// localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent
// refresh token probably shouldn't be sent down.
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(Google.whitelistedFields, 'email', 'verified_email'),
function (subfield) { return 'services.google.' + subfield; })
});
}

View File

@@ -1,4 +0,0 @@
Meteor.loginWithGoogle = function(options, callback) {
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Google.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,3 +0,0 @@
if (!Accounts.google) {
Accounts.google = {};
}

View File

@@ -1,17 +0,0 @@
Accounts.oauth.registerService('google');
Accounts.addAutopublishFields({
forLoggedInUser: _.map(
// publish access token since it can be used from the client (if
// transmitted over ssl or on
// localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent
// refresh token probably shouldn't be sent down.
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(Google.whitelistedFields, 'email', 'verified_email'),
function (subfield) { return 'services.google.' + subfield; })
});

View File

@@ -12,7 +12,5 @@ Package.on_use(function(api) {
api.add_files('google_login_button.css', 'client');
api.add_files('google_common.js', ['client', 'server']);
api.add_files('google_server.js', 'server');
api.add_files('google_client.js', 'client');
api.add_files("google.js");
});

View File

@@ -0,0 +1,22 @@
Accounts.oauth.registerService('meetup');
if (Meteor.isClient) {
Meteor.loginWithMeetup = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Meetup.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on
// localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit
forLoggedInUser: ['services.meetup'],
forOtherUsers: ['services.meetup.id']
});
}

View File

@@ -1,4 +0,0 @@
Meteor.loginWithMeetup = function(options, callback) {
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Meetup.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,3 +0,0 @@
if (!Accounts.meetup) {
Accounts.meetup = {};
}

View File

@@ -1,11 +0,0 @@
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
// localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit
forLoggedInUser: ['services.meetup'],
forOtherUsers: ['services.meetup.id']
});

View File

@@ -11,7 +11,5 @@ Package.on_use(function(api) {
api.add_files('meetup_login_button.css', 'client');
api.add_files('meetup_common.js', ['client', 'server']);
api.add_files('meetup_server.js', 'server');
api.add_files('meetup_client.js', 'client');
api.add_files("meetup.js");
});

View File

@@ -19,9 +19,9 @@ Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback) {
Accounts.oauth.credentialRequestCompleteHandler = function(callback) {
return function (credentialTokenOrError) {
if(credentialTokenOrError && credentialTokenOrError instanceof Error) {
callback(credentialTokenOrError);
callback && callback(credentialTokenOrError);
} else {
Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback);
}
};
}
};

View File

@@ -1 +1,24 @@
Accounts.oauth = {};
Accounts.oauth = {};
var services = {};
// Helper for registering OAuth based accounts packages.
// On the server, adds an index to the user collection.
Accounts.oauth.registerService = function (name) {
if (_.has(services, name))
throw new Error("Duplicate service: " + name);
services[name] = true;
if (Meteor.server) {
// 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.serviceNames = function () {
return _.keys(services);
};

View File

@@ -1,24 +1,3 @@
// 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) {

View File

@@ -13,7 +13,7 @@ Package.on_use(function (api) {
api.imply('accounts-base', ['client', 'server']);
api.use('oauth', 'server');
api.add_files('oauth_common.js', ['client', 'server']);
api.add_files('oauth_common.js');
api.add_files('oauth_client.js', 'client');
api.add_files('oauth_server.js', 'server');
});

View File

@@ -3,20 +3,20 @@
// the string "intercept", storing them in an array that can then
// be retrieved using the getInterceptedEmails method
//
var oldEmailSend = Email.send;
var interceptedEmails = {}; // (email address) -> (array of contents)
Email.send = function (options) {
EmailTest.hookSend(function (options) {
var to = options.to;
if (to.indexOf('intercept') === -1) {
oldEmailSend(options);
return true; // go ahead and send
} else {
if (!interceptedEmails[to])
interceptedEmails[to] = [];
interceptedEmails[to].push(options.text);
return false; // skip sending
}
};
});
Meteor.methods({
getInterceptedEmails: function (email) {

View File

@@ -11,16 +11,17 @@ Package.on_use(function(api) {
api.use('random', ['server']);
api.use('check', ['server']);
api.use('underscore');
api.use('livedata', ['client', 'server']);
api.add_files('email_templates.js', 'server');
api.add_files('password_server.js', 'server');
api.add_files('password_client.js', 'client');
api.add_files('password_common.js', ['server', 'client']);
});
Package.on_test(function(api) {
api.use(['accounts-password', 'tinytest', 'test-helpers', 'deps',
'accounts-base', 'random', 'email', 'underscore', 'check']);
'accounts-base', 'random', 'email', 'underscore', 'check',
'livedata']);
api.add_files('password_tests_setup.js', 'server');
api.add_files('password_tests.js', ['client', 'server']);
api.add_files('email_tests_setup.js', 'server');

View File

@@ -8,7 +8,7 @@
// @param password {String}
// @param callback {Function(error|undefined)}
Meteor.loginWithPassword = function (selector, password, callback) {
var srp = new Meteor._srp.Client(password);
var srp = new SRP.Client(password);
var request = srp.startExchange();
if (typeof selector === 'string')
@@ -50,7 +50,7 @@ Accounts.createUser = function (options, callback) {
if (!options.password)
throw new Error("Must set options.password");
var verifier = Meteor._srp.generateVerifier(options.password);
var verifier = SRP.generateVerifier(options.password);
// strip old password, replacing with the verifier object
delete options.password;
options.srp = verifier;
@@ -77,7 +77,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) {
return;
}
var verifier = Meteor._srp.generateVerifier(newPassword);
var verifier = SRP.generateVerifier(newPassword);
if (!oldPassword) {
Meteor.apply('changePassword', [{srp: verifier}], function (error, result) {
@@ -89,7 +89,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) {
}
});
} else { // oldPassword
var srp = new Meteor._srp.Client(oldPassword);
var srp = new SRP.Client(oldPassword);
var request = srp.startExchange();
request.user = {id: Meteor.user()._id};
Meteor.apply('beginPasswordExchange', [request], function (error, result) {
@@ -142,7 +142,7 @@ Accounts.resetPassword = function(token, newPassword, callback) {
if (!newPassword)
throw new Error("Need to pass newPassword");
var verifier = Meteor._srp.generateVerifier(newPassword);
var verifier = SRP.generateVerifier(newPassword);
Accounts.callLoginMethod({
methodName: 'resetPassword',
methodArguments: [token, verifier],

View File

@@ -1 +0,0 @@
Accounts.password = {};

View File

@@ -62,7 +62,7 @@ Meteor.methods({beginPasswordExchange: function (request) {
throw new Meteor.Error(403, "User has no password set");
var verifier = user.services.password.srp;
var srp = new Meteor._srp.Server(verifier);
var srp = new SRP.Server(verifier);
var challenge = srp.issueChallenge({A: request.A});
// save off results in the current session so we can verify them
@@ -82,7 +82,7 @@ Accounts.registerLoginHandler(function (options) {
// we're always called from within a 'login' method, so this should
// be safe.
var currentInvocation = Meteor._CurrentInvocation.get();
var currentInvocation = DDP._CurrentInvocation.get();
var serialized = currentInvocation._sessionData.srpChallenge;
if (!serialized || serialized.M !== options.srp.M)
throw new Meteor.Error(403, "Incorrect password");
@@ -127,7 +127,7 @@ Accounts.registerLoginHandler(function (options) {
// Just check the verifier output when the same identity and salt
// are passed. Don't bother with a full exchange.
var verifier = user.services.password.srp;
var newVerifier = Meteor._srp.generateVerifier(options.password, {
var newVerifier = SRP.generateVerifier(options.password, {
identity: verifier.identity, salt: verifier.salt});
if (verifier.verifier !== newVerifier.verifier)
@@ -155,7 +155,7 @@ Meteor.methods({changePassword: function (options) {
// password. For now, we don't allow changePassword without knowing the old
// password.
M: String,
srp: Match.Optional(Meteor._srp.matchVerifier),
srp: Match.Optional(SRP.matchVerifier),
password: Match.Optional(String)
});
@@ -170,7 +170,7 @@ Meteor.methods({changePassword: function (options) {
var verifier = options.srp;
if (!verifier && options.password) {
verifier = Meteor._srp.generateVerifier(options.password);
verifier = SRP.generateVerifier(options.password);
}
if (!verifier)
throw new Meteor.Error(400, "Invalid verifier");
@@ -192,7 +192,7 @@ Accounts.setPassword = function (userId, newPassword) {
var user = Meteor.users.findOne(userId);
if (!user)
throw new Meteor.Error(403, "User not found");
var newVerifier = Meteor._srp.generateVerifier(newPassword);
var newVerifier = SRP.generateVerifier(newPassword);
Meteor.users.update({_id: user._id}, {
$set: {'services.password.srp': newVerifier}});
@@ -217,6 +217,7 @@ Meteor.methods({forgotPassword: function (options) {
// send the user an email with a link that when opened allows the user
// to set a new password, without the old password.
//
Accounts.sendResetPasswordEmail = function (userId, email) {
// Make sure the user exists, and email is one of their addresses.
var user = Meteor.users.findOne(userId);
@@ -252,8 +253,9 @@ Accounts.sendResetPasswordEmail = function (userId, email) {
// to choose their password. The email must be one of the addresses in the
// user's emails field, or undefined to pick the first email automatically.
//
// This is not called automatically, it must be called manually if you
// This is not called automatically. It must be called manually if you
// want to use enrollment emails.
//
Accounts.sendEnrollmentEmail = function (userId, email) {
// XXX refactor! This is basically identical to sendResetPasswordEmail.
@@ -293,7 +295,7 @@ Accounts.sendEnrollmentEmail = function (userId, email) {
// the users password, and log them in.
Meteor.methods({resetPassword: function (token, newVerifier) {
check(token, String);
check(newVerifier, Meteor._srp.matchVerifier);
check(newVerifier, SRP.matchVerifier);
var user = Meteor.users.findOne({
"services.password.reset.token": ""+token});
@@ -329,6 +331,7 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
// send the user an email with a link that when opened marks that
// address as verified
//
Accounts.sendVerificationEmail = function (userId, address) {
// XXX Also generate a link using which someone can delete this
// account if they own said address but weren't those who created
@@ -428,7 +431,7 @@ var createUser = function (options) {
username: Match.Optional(String),
email: Match.Optional(String),
password: Match.Optional(String),
srp: Match.Optional(Meteor._srp.matchVerifier)
srp: Match.Optional(SRP.matchVerifier)
}));
var username = options.username;
@@ -442,7 +445,7 @@ var createUser = function (options) {
if (options.password) {
if (options.srp)
throw new Meteor.Error(400, "Don't pass both password and srp in options");
options.srp = Meteor._srp.generateVerifier(options.password);
options.srp = SRP.generateVerifier(options.password);
}
var user = {services: {}};
@@ -493,6 +496,7 @@ Meteor.methods({createUser: function (options) {
// which is always empty when called from the createUser method? eg, "admin:
// true", which we want to prevent the client from setting, but which a custom
// method calling Accounts.createUser could set?
//
Accounts.createUser = function (options, callback) {
options = _.clone(options);
options.generateLoginToken = false;

View File

@@ -11,11 +11,8 @@ Package.on_use(function(api) {
api.use('twitter', ['client', 'server']);
api.use('http', ['client', 'server']);
api.use('templating', 'client');
api.add_files('twitter_login_button.css', 'client');
api.add_files('twitter_common.js', ['client', 'server']);
api.add_files('twitter_server.js', 'server');
api.add_files('twitter_client.js', 'client');
api.add_files("twitter.js");
});

View File

@@ -0,0 +1,24 @@
Accounts.oauth.registerService('twitter');
if (Meteor.isClient) {
Meteor.loginWithTwitter = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Twitter.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
var autopublishedFields = _.map(
// don't send access token. https://dev.twitter.com/discussions/5025
Twitter.whitelistedFields.concat(['id', 'screenName']),
function (subfield) { return 'services.twitter.' + subfield; });
Accounts.addAutopublishFields({
forLoggedInUser: autopublishedFields,
forOtherUsers: autopublishedFields
});
}

View File

@@ -1,4 +0,0 @@
Meteor.loginWithTwitter = function(options, callback) {
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Twitter.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,3 +0,0 @@
if (!Accounts.twitter) {
Accounts.twitter = {};
}

View File

@@ -1,11 +0,0 @@
Accounts.oauth.registerService('twitter');
var autopublishedFields = _.map(
// don't send access token. https://dev.twitter.com/discussions/5025
Twitter.whitelistedFields.concat(['id', 'screenName']),
function (subfield) { return 'services.twitter.' + subfield; });
Accounts.addAutopublishFields({
forLoggedInUser: autopublishedFields,
forOtherUsers: autopublishedFields
});

View File

@@ -1,13 +1,9 @@
if (!Accounts.ui)
Accounts.ui = {};
if (!Accounts.ui._options) {
Accounts.ui._options = {
requestPermissions: {},
requestOfflineToken: {}
};
}
Accounts.ui = {};
Accounts.ui._options = {
requestPermissions: {},
requestOfflineToken: {}
};
Accounts.ui.config = function(options) {
// validate options keys
@@ -62,7 +58,7 @@ Accounts.ui.config = function(options) {
}
};
Accounts.ui._passwordSignupFields = function () {
passwordSignupFields = function () {
return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY";
};

View File

@@ -1,6 +1,3 @@
if (!Accounts._loginButtons)
Accounts._loginButtons = {};
// for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
@@ -26,20 +23,105 @@ Template._loginButtons.preserve({
'input[id]': Spark._labelFromIdOrName
});
//
// helpers
//
displayName = function () {
var user = Meteor.user();
if (!user)
return '';
if (user.profile && user.profile.name)
return user.profile.name;
if (user.username)
return user.username;
if (user.emails && user.emails[0] && user.emails[0].address)
return user.emails[0].address;
return '';
};
// returns an array of the login services used by this app. each
// element of the array is an object (eg {name: 'facebook'}), since
// that makes it useful in combination with handlebars {{#each}}.
//
// don't cache the output of this function: if called during startup (before
// oauth packages load) it might not include them all.
//
// NOTE: It is very important to have this return password last
// because of the way we render the different providers in
// login_buttons_dropdown.html
getLoginServices = function () {
var self = this;
// First look for OAuth services.
var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : [];
// Be equally kind to all login services. This also preserves
// backwards-compatibility. (But maybe order should be
// configurable?)
services.sort();
// Add password, if it's there; it must come last.
if (hasPasswordService())
services.push('password');
return _.map(services, function(name) {
return {name: name};
});
};
hasPasswordService = function () {
return !!Package['accounts-password'];
};
dropdown = function () {
return hasPasswordService() || getLoginServices().length > 1;
};
// XXX improve these. should this be in accounts-password instead?
//
// XXX these will become configurable, and will be validated on
// the server as well.
validateUsername = function (username) {
if (username.length >= 3) {
return true;
} else {
loginButtonsSession.errorMessage("Username must be at least 3 characters long");
return false;
}
};
validateEmail = function (email) {
if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
return true;
if (email.indexOf('@') !== -1) {
return true;
} else {
loginButtonsSession.errorMessage("Invalid email");
return false;
}
};
validatePassword = function (password) {
if (password.length >= 6) {
return true;
} else {
loginButtonsSession.errorMessage("Password must be at least 6 characters long");
return false;
}
};
//
// loginButtonLoggedOut template
//
Template._loginButtonsLoggedOut.dropdown = function () {
return Accounts._loginButtons.dropdown();
};
Template._loginButtonsLoggedOut.dropdown = dropdown;
Template._loginButtonsLoggedOut.services = function () {
return Accounts._loginButtons.getLoginServices();
};
Template._loginButtonsLoggedOut.services = getLoginServices;
Template._loginButtonsLoggedOut.singleService = function () {
var services = Accounts._loginButtons.getLoginServices();
var services = getLoginServices();
if (services.length !== 1)
throw new Error(
"Shouldn't be rendering this template with more than one configured service");
@@ -57,9 +139,7 @@ Template._loginButtonsLoggedOut.configurationLoaded = function () {
// decide whether we should show a dropdown rather than a row of
// buttons
Template._loginButtonsLoggedIn.dropdown = function () {
return Accounts._loginButtons.dropdown();
};
Template._loginButtonsLoggedIn.dropdown = dropdown;
@@ -67,9 +147,7 @@ Template._loginButtonsLoggedIn.dropdown = function () {
// loginButtonsLoggedInSingleLogoutButton template
//
Template._loginButtonsLoggedInSingleLogoutButton.displayName = function () {
return Accounts._loginButtons.displayName();
};
Template._loginButtonsLoggedInSingleLogoutButton.displayName = displayName;
@@ -90,113 +168,5 @@ Template._loginButtonsMessages.infoMessage = function () {
// loginButtonsLoggingInPadding template
//
Template._loginButtonsLoggingInPadding.dropdown = function () {
return Accounts._loginButtons.dropdown();
};
Template._loginButtonsLoggingInPadding.dropdown = dropdown;
//
// helpers
//
Accounts._loginButtons.displayName = function () {
var user = Meteor.user();
if (!user)
return '';
if (user.profile && user.profile.name)
return user.profile.name;
if (user.username)
return user.username;
if (user.emails && user.emails[0] && user.emails[0].address)
return user.emails[0].address;
return '';
};
// returns an array of the login services used by this app. each
// element of the array is an object (eg {name: 'facebook'}), since
// that makes it useful in combination with handlebars {{#each}}.
//
// NOTE: It is very important to have this return password last
// because of the way we render the different providers in
// login_buttons_dropdown.html
Accounts._loginButtons.getLoginServices = function () {
var self = this;
var services = [];
// find all methods of the form: `Meteor.loginWithFoo`, where
// `Foo` corresponds to a login service
//
// XXX we should consider having a client-side
// Accounts.oauth.registerService function which records the
// active services and encapsulates boilerplate code now found in
// files such as facebook_client.js. This would have the added
// benefit of allow us to unify facebook_{client,common,server}.js
// into one file, which would encourage people to build more login
// services packages.
_.each(_.keys(Meteor), function(methodName) {
var match;
if ((match = methodName.match(/^loginWith(.*)/))) {
var serviceName = match[1].toLowerCase();
// HACKETY HACK. needed to not match
// Meteor.loginWithToken. See XXX above.
if (Accounts[serviceName])
services.push(match[1].toLowerCase());
}
});
// Be equally kind to all login services. This also preserves
// backwards-compatibility. (But maybe order should be
// configurable?)
services.sort();
// ensure password is last
if (_.contains(services, 'password'))
services = _.without(services, 'password').concat(['password']);
return _.map(services, function(name) {
return {name: name};
});
};
Accounts._loginButtons.hasPasswordService = function () {
return Accounts.password;
};
Accounts._loginButtons.dropdown = function () {
return Accounts._loginButtons.hasPasswordService() || Accounts._loginButtons.getLoginServices().length > 1;
};
// XXX improve these. should this be in accounts-password instead?
//
// XXX these will become configurable, and will be validated on
// the server as well.
Accounts._loginButtons.validateUsername = function (username) {
if (username.length >= 3) {
return true;
} else {
loginButtonsSession.errorMessage("Username must be at least 3 characters long");
return false;
}
};
Accounts._loginButtons.validateEmail = function (email) {
if (Accounts.ui._passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
return true;
if (email.indexOf('@') !== -1) {
return true;
} else {
loginButtonsSession.errorMessage("Invalid email");
return false;
}
};
Accounts._loginButtons.validatePassword = function (password) {
if (password.length >= 6) {
return true;
} else {
loginButtonsSession.errorMessage("Password must be at least 6 characters long");
return false;
}
};

View File

@@ -53,7 +53,7 @@ Template._resetPasswordDialog.events({
var resetPassword = function () {
loginButtonsSession.resetMessages();
var newPassword = document.getElementById('reset-password-new-password').value;
if (!Accounts._loginButtons.validatePassword(newPassword))
if (!validatePassword(newPassword))
return;
Accounts.resetPassword(
@@ -94,7 +94,7 @@ Template._enrollAccountDialog.events({
var enrollAccount = function () {
loginButtonsSession.resetMessages();
var password = document.getElementById('enroll-account-password').value;
if (!Accounts._loginButtons.validatePassword(password))
if (!validatePassword(password))
return;
Accounts.resetPassword(
@@ -141,7 +141,7 @@ Template._loginButtonsMessagesDialog.events({
Template._loginButtonsMessagesDialog.visible = function () {
var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage');
return !Accounts._loginButtons.dropdown() && hasMessage;
return !dropdown() && hasMessage;
};

View File

@@ -26,9 +26,7 @@ Template._loginButtonsLoggedInDropdown.events({
}
});
Template._loginButtonsLoggedInDropdown.displayName = function () {
return Accounts._loginButtons.displayName();
};
Template._loginButtonsLoggedInDropdown.displayName = displayName;
Template._loginButtonsLoggedInDropdown.inChangePasswordFlow = function () {
return loginButtonsSession.get('inChangePasswordFlow');
@@ -175,26 +173,21 @@ Template._loginButtonsLoggedOutDropdown.dropdownVisible = function () {
return loginButtonsSession.get('dropdownVisible');
};
Template._loginButtonsLoggedOutDropdown.hasPasswordService = function () {
return Accounts._loginButtons.hasPasswordService();
};
Template._loginButtonsLoggedOutDropdown.hasPasswordService = hasPasswordService;
// return all login services, with password last
Template._loginButtonsLoggedOutAllServices.services = function () {
return Accounts._loginButtons.getLoginServices();
};
Template._loginButtonsLoggedOutAllServices.services = getLoginServices;
Template._loginButtonsLoggedOutAllServices.isPasswordService = function () {
return this.name === 'password';
};
Template._loginButtonsLoggedOutAllServices.hasOtherServices = function () {
return Accounts._loginButtons.getLoginServices().length > 1;
return getLoginServices().length > 1;
};
Template._loginButtonsLoggedOutAllServices.hasPasswordService = function () {
return Accounts._loginButtons.hasPasswordService();
};
Template._loginButtonsLoggedOutAllServices.hasPasswordService =
hasPasswordService;
Template._loginButtonsLoggedOutPasswordService.fields = function () {
var loginFields = [
@@ -202,15 +195,15 @@ Template._loginButtonsLoggedOutPasswordService.fields = function () {
visible: function () {
return _.contains(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"],
Accounts.ui._passwordSignupFields());
passwordSignupFields());
}},
{fieldName: 'username', fieldLabel: 'Username',
visible: function () {
return Accounts.ui._passwordSignupFields() === "USERNAME_ONLY";
return passwordSignupFields() === "USERNAME_ONLY";
}},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
visible: function () {
return Accounts.ui._passwordSignupFields() === "EMAIL_ONLY";
return passwordSignupFields() === "EMAIL_ONLY";
}},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
visible: function () {
@@ -223,17 +216,17 @@ Template._loginButtonsLoggedOutPasswordService.fields = function () {
visible: function () {
return _.contains(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"],
Accounts.ui._passwordSignupFields());
passwordSignupFields());
}},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
visible: function () {
return _.contains(
["USERNAME_AND_EMAIL", "EMAIL_ONLY"],
Accounts.ui._passwordSignupFields());
passwordSignupFields());
}},
{fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email',
visible: function () {
return Accounts.ui._passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL";
return passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL";
}},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
visible: function () {
@@ -247,7 +240,7 @@ Template._loginButtonsLoggedOutPasswordService.fields = function () {
// the "forgot password" flow.
return _.contains(
["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"],
Accounts.ui._passwordSignupFields());
passwordSignupFields());
}}
];
@@ -273,7 +266,7 @@ Template._loginButtonsLoggedOutPasswordService.showCreateAccountLink = function
Template._loginButtonsLoggedOutPasswordService.showForgotPasswordLink = function () {
return _.contains(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"],
Accounts.ui._passwordSignupFields());
passwordSignupFields());
};
Template._loginButtonsFormField.inputType = function () {
@@ -313,7 +306,7 @@ Template._loginButtonsChangePassword.fields = function () {
// the "forgot password" flow.
return _.contains(
["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"],
Accounts.ui._passwordSignupFields());
passwordSignupFields());
}}
];
};
@@ -357,19 +350,19 @@ var login = function () {
var loginSelector;
if (username !== null) {
if (!Accounts._loginButtons.validateUsername(username))
if (!validateUsername(username))
return;
else
loginSelector = {username: username};
} else if (email !== null) {
if (!Accounts._loginButtons.validateEmail(email))
if (!validateEmail(email))
return;
else
loginSelector = {email: email};
} else if (usernameOrEmail !== null) {
// XXX not sure how we should validate this. but this seems good enough (for now),
// since an email must have at least 3 characters anyways
if (!Accounts._loginButtons.validateUsername(usernameOrEmail))
if (!validateUsername(usernameOrEmail))
return;
else
loginSelector = usernameOrEmail;
@@ -393,7 +386,7 @@ var signup = function () {
var username = trimmedElementValueById('login-username');
if (username !== null) {
if (!Accounts._loginButtons.validateUsername(username))
if (!validateUsername(username))
return;
else
options.username = username;
@@ -401,7 +394,7 @@ var signup = function () {
var email = trimmedElementValueById('login-email');
if (email !== null) {
if (!Accounts._loginButtons.validateEmail(email))
if (!validateEmail(email))
return;
else
options.email = email;
@@ -409,7 +402,7 @@ var signup = function () {
// notably not trimmed. a password could (?) start or end with a space
var password = elementValueById('login-password');
if (!Accounts._loginButtons.validatePassword(password))
if (!validatePassword(password))
return;
else
options.password = password;
@@ -450,7 +443,7 @@ var changePassword = function () {
// notably not trimmed. a password could (?) start or end with a space
var password = elementValueById('login-password');
if (!Accounts._loginButtons.validatePassword(password))
if (!validatePassword(password))
return;
if (!matchPasswordAgainIfPresent())

View File

@@ -27,7 +27,10 @@ var validateKey = function (key) {
var KEY_PREFIX = "Meteor.loginButtons.";
// XXX we should have a better pattern for code private to a package like this one
// XXX This should probably be package scope rather than exported
// (there was even a comment to that effect here from before we had
// namespacing) but accounts-ui-viewer uses it, so leave it as is for
// now
Accounts._loginButtonsSession = {
set: function(key, value) {
validateKey(key);

View File

@@ -3,12 +3,19 @@ Package.describe({
});
Package.on_use(function (api) {
api.use(['deps', 'service-configuration', 'accounts-urls', 'accounts-base',
api.use(['deps', 'service-configuration', 'accounts-base',
'underscore', 'templating',
'handlebars', 'spark', 'session'], 'client');
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
// Allow us to call Accounts.oauth.serviceNames, if there are any OAuth
// services.
api.use('accounts-oauth', {weak: true});
// Allow us to directly test if accounts-password (which doesn't use
// Accounts.oauth.registerService) exists.
api.use('accounts-password', {weak: true});
api.add_files([
'accounts_ui.js',

View File

@@ -1,9 +0,0 @@
Package.describe({
summary: "Generate and consume reset password and verify account URLs",
internal: true
});
Package.on_use(function (api) {
api.add_files('url_client.js', 'client');
api.add_files('url_server.js', 'server');
});

View File

@@ -11,7 +11,5 @@ Package.on_use(function(api) {
api.add_files('weibo_login_button.css', 'client');
api.add_files('weibo_common.js', ['client', 'server']);
api.add_files('weibo_server.js', 'server');
api.add_files('weibo_client.js', 'client');
api.add_files("weibo.js");
});

View File

@@ -0,0 +1,21 @@
Accounts.oauth.registerService('weibo');
if (Meteor.isClient) {
Meteor.loginWithWeibo = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Weibo.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on localhost)
forLoggedInUser: ['services.weibo'],
forOtherUsers: ['services.weibo.screenName']
});
}

View File

@@ -1,4 +0,0 @@
Meteor.loginWithWeibo = function(options, callback) {
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Weibo.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,3 +0,0 @@
if (!Accounts.weibo) {
Accounts.weibo = {};
}

View File

@@ -1,9 +0,0 @@
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)
forLoggedInUser: ['services.weibo'],
forOtherUsers: ['services.weibo.screenName']
});

View File

@@ -13,7 +13,7 @@ var updatingAppcache = false;
var reloadRetry = null;
var appcacheUpdated = false;
Meteor._reload.onMigrate('appcache', function(retry) {
Reload._onMigrate('appcache', function(retry) {
if (appcacheUpdated)
return [true];
@@ -61,7 +61,7 @@ window.applicationCache.addEventListener('obsolete', (function() {
}
else {
appcacheUpdated = true;
Meteor._reload.reload();
Reload._reload();
}
}), false);

View File

@@ -6,7 +6,6 @@ Package.on_use(function (api) {
api.use('webapp', 'server');
api.use('reload', 'client');
api.use('routepolicy', 'server');
api.use('startup', 'client');
api.use('underscore', 'server');
api.add_files('appcache-client.js', 'client');
api.add_files('appcache-server.js', 'server');

View File

@@ -1 +0,0 @@
Meteor._LivedataServer._auditArgumentChecks = true;

View File

@@ -2,7 +2,4 @@ Package.describe({
summary: "Try to detect inadequate input sanitization"
});
Package.on_use(function (api) {
api.use(['livedata'], ['server']);
api.add_files(['audit_argument_checks.js'], 'server');
});
// This package is empty; its presence is detected by livedata.

View File

@@ -1 +0,0 @@
Meteor.default_server.autopublish();

View File

@@ -2,7 +2,5 @@ Package.describe({
summary: "Automatically publish the entire database to all clients"
});
Package.on_use(function (api, where) {
api.use('livedata', 'server');
api.add_files("autopublish.js", "server");
});
// This package is empty; its presence is detected by livedata and
// accounts-base.

View File

@@ -37,8 +37,10 @@
Backbone.VERSION = '0.9.2';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// <METEOR> Commented these lines out; we have _ via api.use.
// var _ = root._;
// if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// </METEOR>
// For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
var $ = root.jQuery || root.Zepto || root.ender;

View File

@@ -2,10 +2,9 @@ Package.describe({
summary: "A minimalist client-side MVC framework"
});
Package.on_use(function (api, where) {
Package.on_use(function (api) {
// XXX Backbone requires either jquery or zepto
api.use(["jquery", "json", "underscore"]);
where = where || ['client', 'server'];
api.add_files("backbone.js", where);
api.add_files("backbone.js");
});

View File

@@ -6,7 +6,6 @@
var currentArgumentChecker = new Meteor.EnvironmentVariable;
// @export check
check = function (value, pattern) {
// Record that check got called, if somebody cared.
var argChecker = currentArgumentChecker.get();
@@ -16,26 +15,20 @@ check = function (value, pattern) {
};
Match = {
// @export Match.Optional
Optional: function (pattern) {
return new Optional(pattern);
},
// @export Match.OneOf
OneOf: function (/*arguments*/) {
return new OneOf(_.toArray(arguments));
},
// @export Match.Any
Any: ['__any__'],
// @export Match.Where
Where: function (condition) {
return new Where(condition);
},
// @export Match.ObjectIncluding
ObjectIncluding: function (pattern) {
return new ObjectIncluding(pattern);
},
// @export Match.Error
// XXX should we record the path down the tree in the error message?
// XXX matchers should know how to describe themselves for errors
Error: Meteor.makeErrorType("Match.Error", function (msg) {
@@ -45,7 +38,6 @@ Match = {
this.sanitizedError = new Meteor.Error(400, "Match failed");
}),
// @export Match.test
// Tests to see if value matches pattern. Unlike check, it merely returns true
// or false (unless an error other than Match.Error was thrown). It does not
// interact with _failIfArgumentsAreNotAllChecked.
@@ -68,7 +60,6 @@ Match = {
// `args` (either directly or in the first level of an array), throws an error
// (using `description` in the message).
//
// @export Match._failIfArgumentsAreNotAllChecked
_failIfArgumentsAreNotAllChecked: function (f, context, args, description) {
var argChecker = new ArgumentChecker(args, description);
var result = currentArgumentChecker.withValue(argChecker, function () {

View File

@@ -6,6 +6,8 @@ Package.describe({
Package.on_use(function (api) {
api.use(['underscore', 'ejson'], ['client', 'server']);
api.export(['check', 'Match']);
api.add_files('match.js', ['client', 'server']);
});

View File

@@ -1,3 +1 @@
### @export COFFEESCRIPT_EXPORTED ###
COFFEESCRIPT_EXPORTED = 123

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: "Used by the coffeescript package's @export tests",
summary: "Used by the coffeescript package's tests",
internal: true
});
Package.on_use(function (api) {
api.use('coffeescript', ['client', 'server']);
api.export('COFFEESCRIPT_EXPORTED');
api.add_files("exporting.coffee", ['client', 'server']);
});

View File

@@ -28,6 +28,8 @@ var stripExportedVars = function (source, exports) {
// up on subsequent lines.)
// XXX relax these assumptions by doing actual JS parsing (eg with jsparse).
// I'd do this now, but there's no easy way to "unparse" a jsparse AST.
// Or alternatively, hack the compiler to allow us to specify unbound
// symbols directly.
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
@@ -83,8 +85,7 @@ var addSharedHeader = function (source, sourceMap) {
// as a var in the package closure, and in "app" mode where it will end up as
// a global.
//
// This ends in a newline in case the first line is a linker @comment, which
// should be at the beginning of a line.
// This ends in a newline to make the source map easier to adjust.
var header = ("__coffeescriptShare = typeof __coffeescriptShare === 'object' " +
"? __coffeescriptShare : {}; " +
"var share = __coffeescriptShare;\n");
@@ -140,15 +141,14 @@ var handler = function (compileStep) {
);
}
var stripped = stripExportedVars(output.js, compileStep.declaredExports);
var sourceWithMap = addSharedHeader(stripped, output.v3SourceMap);
compileStep.addJavaScript({
path: outputFile,
sourcePath: compileStep.inputPath,
data: output.js,
linkerFileTransform: function (sourceWithMap, exports) {
var stripped = stripExportedVars(sourceWithMap.source, exports);
return addSharedHeader(stripped, sourceWithMap.sourceMap);
},
sourceMap: output.v3SourceMap
data: sourceWithMap.source,
sourceMap: sourceWithMap.sourceMap
});
};

Some files were not shown because too many files have changed in this diff Show More