Skip to content

Commit b75064f

Browse files
committed
Merge branch 'devel' into batch-plugins
2 parents c09fea8 + 6932f66 commit b75064f

33 files changed

Lines changed: 735 additions & 242 deletions

.eslintignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ tools/safe-pathwatcher.js
7171
tools/selftest.js
7272
tools/service-connection.js
7373
tools/shell-client.js
74-
tools/source-map-retriever-stack.js
7574
tools/stats.js
7675
tools/test-utils.js
7776
tools/tropohouse.js

History.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@
1010
JSON too. #4595
1111

1212

13+
### Meteor Accounts
14+
15+
* `loginWithPassword` now matches username or email in a case insensitive manner. If there are multiple users with a username or email only differing in case, a case sensitive match is required. #550
16+
* `loginWithGithub` now requests `user:email` scope by default, and attempts to fetch the user's emails. If no public email has been set, we use the primary email instead. We also store the complete list of emails. #4545
17+
18+
19+
### DDP
20+
21+
* `sub.ready()` should return true inside that subscription's `onReady`
22+
callback. #4614
23+
24+
### Livequery
25+
26+
* Improved server performance by reducing overhead of processing oplog after
27+
database writes. Improvements are most noticeable in case when a method is
28+
doing a lot of writes on collections with plenty of active observers. #4694
29+
30+
1331
## in progress: v.1.1.1
1432

1533
### Blaze
@@ -185,6 +203,8 @@
185203

186204
### Other bug fixes and improvements
187205

206+
* The `spiderable` package now reports the URL it's trying to fetch on failure.
207+
188208
* Upgraded dependencies:
189209

190210
- uglify-js: 2.4.20 (from 2.4.17)

docs/client/full-api/api/accounts.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ will be logged out.
134134

135135
{{> autoApiBox "Meteor.loginWithPassword"}}
136136

137+
If there are multiple users with a username or email only differing in case, a case sensitive match is required. Although `createUser` won't let you create users with ambiguous usernames or emails, this could happen with existing databases or if you modify the users collection directly.
138+
137139
This function is provided by the `accounts-password` package. See the
138140
[Passwords](#accounts_passwords) section below.
139141

docs/client/full-api/api/passwords.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ id.
2626

2727
On the client, you must pass `password` and at least one of `username` or
2828
`email` — enough information for the user to be able to log in again
29-
later. On the server, you do not need to specify `password`, but the user will
29+
later. If there are existing users with a username or email only differing in case, `createUser` will fail. On the server, you do not need to specify `password`, but the user will
3030
not be able to log in until it has a password (eg, set with
3131
[`Accounts.setPassword`](#accounts_setpassword)).
3232

docs/client/full-api/concepts.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,9 @@ <h2 id="usingpackages">Using packages</h2>
752752
app, `meteor list` and `meteor add` also search the `packages` directory at the
753753
top of your app. You can also use the `packages` directory to break your app
754754
into subpackages for your convenience, or to test packages that you might want
755-
to publish. See [Writing Packages](#writingpackages).
755+
to publish. See [Writing Packages](#writingpackages). If you wish to add
756+
packages outside of your app's folder structure, set the environment variable
757+
`PACKAGE_DIRS` to a colon-delimited list of paths.
756758

757759
{{/markdown}}
758760
</template>

meteor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/bash
22

3-
BUNDLE_VERSION=0.4.38
3+
BUNDLE_VERSION=0.5.0
44

55
# OS Check. Put here because here is where we download the precompiled
66
# bundles that are arch specific.

packages/accounts-base/accounts_server.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ AccountsServer = function AccountsServer(server) {
5151
];
5252

5353
this._deleteSavedTokensForAllUsersOnStartup();
54+
55+
this._skipCaseInsensitiveChecksForTest = {};
5456
};
5557

5658
Meteor._inherits(AccountsServer, AccountsCommon);
@@ -1256,21 +1258,14 @@ Ap.validateNewUser = function (func) {
12561258
this._validateNewUserHooks.push(func);
12571259
};
12581260

1259-
// XXX Find a better place for this utility function
1260-
// Like Perl's quotemeta: quotes all regexp metacharacters. See
1261-
// https://github.com/substack/quotemeta/blob/master/index.js
1262-
var quotemeta = function (str) {
1263-
return String(str).replace(/(\W)/g, '\\$1');
1264-
};
1265-
12661261
// Helper function: returns false if email does not match company domain from
12671262
// the configuration.
12681263
Ap._testEmailDomain = function (email) {
12691264
var domain = this._options.restrictCreationByEmailDomain;
12701265
return !domain ||
12711266
(_.isFunction(domain) && domain(email)) ||
12721267
(_.isString(domain) &&
1273-
(new RegExp('@' + quotemeta(domain) + '$', 'i')).test(email));
1268+
(new RegExp('@' + Meteor._escapeRegExp(domain) + '$', 'i')).test(email));
12741269
};
12751270

12761271
// Validate new user's email or Google/Facebook/GitHub account's email

packages/accounts-password/password_client.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111
/**
1212
* @summary Log the user in with a password.
1313
* @locus Client
14-
* @param {Object | String} user Either a string interpreted as a username or an email; or an object with a single key: `email`, `username` or `id`.
14+
* @param {Object | String} user
15+
* Either a string interpreted as a username or an email; or an object with a
16+
* single key: `email`, `username` or `id`. Username or email match in a case
17+
* insensitive manner.
1518
* @param {String} password The user's password.
16-
* @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure.
19+
* @param {Function} [callback] Optional callback.
20+
* Called with no arguments on success, or with a single `Error` argument
21+
* on failure.
1722
*/
1823
Meteor.loginWithPassword = function (selector, password, callback) {
1924
if (typeof selector === 'string')

packages/accounts-password/password_server.js

Lines changed: 124 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,30 +77,93 @@ var checkPassword = Accounts._checkPassword;
7777
/// LOGIN
7878
///
7979

80-
// Users can specify various keys to identify themselves with.
81-
// @param user {Object} with one of `id`, `username`, or `email`.
82-
// @returns A selector to pass to mongo to get the user record.
83-
84-
var selectorFromUserQuery = function (user) {
85-
if (user.id)
86-
return {_id: user.id};
87-
else if (user.username)
88-
return {username: user.username};
89-
else if (user.email)
90-
return {"emails.address": user.email};
91-
throw new Error("shouldn't happen (validation missed something)");
92-
};
93-
94-
var findUserFromUserQuery = function (user) {
95-
var selector = selectorFromUserQuery(user);
96-
97-
var user = Meteor.users.findOne(selector);
98-
if (!user)
99-
throw new Meteor.Error(403, "User not found");
80+
// Attempts to find a user from a user query.
81+
// First tries to match username or email case sensitively; if that fails, it
82+
// tries case insensitively; but if more than one user matches the case
83+
// insensitive search, it returns null
84+
// @param query {Object} with one of `id`, `username`, or `email`.
85+
// @returns A user if found, else null
86+
var findUserFromQuery = function (query) {
87+
var user = null;
88+
89+
if (query.id) {
90+
user = Meteor.users.findOne({ _id: query.id });
91+
} else {
92+
var fieldName;
93+
var fieldValue;
94+
if (query.username) {
95+
fieldName = 'username';
96+
fieldValue = query.username;
97+
} else if (query.email) {
98+
fieldName = 'emails.address';
99+
fieldValue = query.email;
100+
} else {
101+
throw new Error("shouldn't happen (validation missed something)");
102+
}
103+
var selector = {};
104+
selector[fieldName] = fieldValue;
105+
user = Meteor.users.findOne(selector);
106+
// If user is not found, try a case insensitive lookup
107+
if (!user) {
108+
selector = selectorForFastCaseInsensitiveLookup(fieldName, fieldValue);
109+
var candidateUsers = Meteor.users.find(selector).fetch();
110+
// No match if multiple candidates are found
111+
if (candidateUsers.length === 1) {
112+
user = candidateUsers[0];
113+
} else {
114+
console.error('Found multiple users with ' + fieldName + ' = ' + fieldValue + ' only differing in case. Requiring case sensitive login.');
115+
}
116+
}
117+
}
100118

101119
return user;
102120
};
103121

122+
123+
// Generates a MongoDB selector that can be used to perform a fast case
124+
// insensitive lookup for the given fieldName and string. Since MongoDB does
125+
// not support case insensitive indexes, and case insensitive regex queries
126+
// are slow, we construct a set of prefix selectors for all permutations of
127+
// the first 4 characters ourselves. We first attempt to matching against
128+
// these, and because 'prefix expression' regex queries do use indexes (see
129+
// http://docs.mongodb.org/v2.6/reference/operator/query/regex/#index-use),
130+
// this has been found to greatly improve performance (from 1200ms to 5ms in a
131+
// test with 1.000.000 users).
132+
var selectorForFastCaseInsensitiveLookup = function (fieldName, string) {
133+
// Performance seems to improve up to 4 prefix characters
134+
var prefix = string.substring(0, Math.min(string.length, 4));
135+
var orClause = _.map(generateCasePermutationsForString(prefix),
136+
function (prefixPermutation) {
137+
var selector = {};
138+
selector[fieldName] =
139+
new RegExp('^' + Meteor._escapeRegExp(prefixPermutation));
140+
return selector;
141+
});
142+
var caseInsensitiveClause = {};
143+
caseInsensitiveClause[fieldName] =
144+
new RegExp('^' + Meteor._escapeRegExp(string) + '$', 'i')
145+
return {$and: [{$or: orClause}, caseInsensitiveClause]};
146+
}
147+
148+
// Generates permutations of all case variations of a given string.
149+
var generateCasePermutationsForString = function (string) {
150+
var permutations = [''];
151+
for (var i = 0; i < string.length; i++) {
152+
var ch = string.charAt(i);
153+
permutations = _.flatten(_.map(permutations, function (prefix) {
154+
var lowerCaseChar = ch.toLowerCase();
155+
var upperCaseChar = ch.toUpperCase();
156+
// Don't add unneccesary permutations when ch is not a letter
157+
if (lowerCaseChar === upperCaseChar) {
158+
return [prefix + ch];
159+
} else {
160+
return [prefix + lowerCaseChar, prefix + upperCaseChar];
161+
}
162+
}));
163+
}
164+
return permutations;
165+
}
166+
104167
// XXX maybe this belongs in the check package
105168
var NonEmptyString = Match.Where(function (x) {
106169
check(x, String);
@@ -147,7 +210,9 @@ Accounts.registerLoginHandler("password", function (options) {
147210
});
148211

149212

150-
var user = findUserFromUserQuery(options.user);
213+
var user = findUserFromQuery(options.user);
214+
if (!user)
215+
throw new Meteor.Error(403, "User not found");
151216

152217
if (!user.services || !user.services.password ||
153218
!(user.services.password.bcrypt || user.services.password.srp))
@@ -211,7 +276,9 @@ Accounts.registerLoginHandler("password", function (options) {
211276
password: passwordValidator
212277
});
213278

214-
var user = findUserFromUserQuery(options.user);
279+
var user = findUserFromQuery(options.user);
280+
if (!user)
281+
throw new Meteor.Error(403, "User not found");
215282

216283
// Check to see if another simultaneous login has already upgraded
217284
// the user record to bcrypt.
@@ -672,7 +739,7 @@ Meteor.methods({verifyEmail: function (token) {
672739
{_id: user._id,
673740
'emails.address': tokenRecord.address},
674741
{$set: {'emails.$.verified': true},
675-
$pull: {'services.email.verificationTokens': {token: token}}});
742+
$pull: {'services.email.verificationTokens': {address: tokenRecord.address}}});
676743

677744
return {userId: user._id};
678745
}
@@ -715,7 +782,40 @@ var createUser = function (options) {
715782
if (email)
716783
user.emails = [{address: email, verified: false}];
717784

718-
return Accounts.insertUserDoc(options, user);
785+
// Check if there is no other user with a username or email only differing
786+
// in case.
787+
var performCaseInsensitiveCheck = function () {
788+
// Some tests need the ability to add users with the same case insensitive
789+
// username or email, hence the _skipCaseInsensitiveChecksForTest check
790+
791+
if (username &&
792+
!_.has(Accounts._skipCaseInsensitiveChecksForTest, username) &&
793+
Meteor.users.find(selectorForFastCaseInsensitiveLookup(
794+
"username", username)).count() > 1) {
795+
throw new Meteor.Error(403, "Username already exists.");
796+
}
797+
798+
if (email &&
799+
!_.has(Accounts._skipCaseInsensitiveChecksForTest, email) &&
800+
Meteor.users.find(selectorForFastCaseInsensitiveLookup(
801+
"emails.address", email)).count() > 1) {
802+
throw new Meteor.Error(403, "Email already exists.");
803+
}
804+
}
805+
806+
// Perform a case insensitive check before insert
807+
performCaseInsensitiveCheck();
808+
var userId = Accounts.insertUserDoc(options, user);
809+
// Perform another check after insert, in case a matching user has been
810+
// inserted in the meantime
811+
try {
812+
performCaseInsensitiveCheck();
813+
} catch (ex) {
814+
// Remove inserted user if the check fails
815+
Meteor.users.remove(userId);
816+
throw ex;
817+
}
818+
return userId;
719819
};
720820

721821
// method for create user. Requests come from the client.

0 commit comments

Comments
 (0)