From f7b6aa47a6d23c39cb1698305aaeca487c8adc0c Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 28 May 2014 13:23:04 +1000 Subject: [PATCH 01/88] Added a `userEmail` option to MD accounts oauth. --- .../meteor-developer.js | 2 +- .../meteor-developer/meteor_developer_client.js | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/accounts-meteor-developer/meteor-developer.js b/packages/accounts-meteor-developer/meteor-developer.js index 55e131b798..79ac4e4c2c 100644 --- a/packages/accounts-meteor-developer/meteor-developer.js +++ b/packages/accounts-meteor-developer/meteor-developer.js @@ -10,7 +10,7 @@ if (Meteor.isClient) { var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); - MeteorDeveloperAccounts.requestCredential(credentialRequestCompleteCallback); + MeteorDeveloperAccounts.requestCredential(options, credentialRequestCompleteCallback); }; } else { Accounts.addAutopublishFields({ diff --git a/packages/meteor-developer/meteor_developer_client.js b/packages/meteor-developer/meteor_developer_client.js index 239afec978..82d7ca7c98 100644 --- a/packages/meteor-developer/meteor_developer_client.js +++ b/packages/meteor-developer/meteor_developer_client.js @@ -4,7 +4,13 @@ MeteorDeveloperAccounts = {}; // @param credentialRequestCompleteCallback {Function} Callback function to call on // completion. Takes one argument, credentialToken on success, or Error on // error. -var requestCredential = function (credentialRequestCompleteCallback) { +var requestCredential = function (options, credentialRequestCompleteCallback) { + // support a callback without options + if (! credentialRequestCompleteCallback && typeof options === "function") { + credentialRequestCompleteCallback = options; + options = null; + } + var config = ServiceConfiguration.configurations.findOne({ service: 'meteor-developer' }); @@ -20,8 +26,12 @@ var requestCredential = function (credentialRequestCompleteCallback) { METEOR_DEVELOPER_URL + "/oauth2/authorize?" + "state=" + credentialToken + "&response_type=code&" + - "client_id=" + config.clientId + - "&redirect_uri=" + Meteor.absoluteUrl("_oauth/meteor-developer?close"); + "client_id=" + config.clientId; + + if (options && options.userEmail) + loginUrl += '&user_email=' + encodeURIComponent(options.userEmail); + + loginUrl += "&redirect_uri=" + Meteor.absoluteUrl("_oauth/meteor-developer?close"); OAuth.showPopup( loginUrl, From b567046300c78d9b1d5f5b5f32c75bee6e20cc2f Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Sat, 31 May 2014 13:28:29 -0400 Subject: [PATCH 02/88] Migrate from SRP to bcrypt. See the spec https://meteor.hackpad.com/SRP-bcrypt-J5mdBojeVfe for details. --- packages/accounts-password/package.js | 1 + packages/accounts-password/password_client.js | 128 ++++---- packages/accounts-password/password_server.js | 279 +++++++++--------- packages/accounts-password/password_tests.js | 37 ++- .../accounts-password/password_tests_setup.js | 18 ++ packages/sha/.gitignore | 1 + packages/sha/package.js | 9 + packages/{srp => sha}/sha256.js | 44 ++- packages/srp/package.js | 4 +- packages/srp/srp.js | 12 +- 10 files changed, 284 insertions(+), 249 deletions(-) create mode 100644 packages/sha/.gitignore create mode 100644 packages/sha/package.js rename packages/{srp => sha}/sha256.js (96%) diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index f53f8c2d10..95f5804885 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -7,6 +7,7 @@ Package.on_use(function(api) { // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('srp', ['client', 'server']); + api.use('sha', ['client', 'server']); api.use('email', ['server']); api.use('random', ['server']); api.use('check', ['server']); diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 96be0b1092..4d87fbeb69 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -1,3 +1,16 @@ +// The server requested an upgrade from the old SRP password format, +// so supply the needed SRP identity to login. +var srpUpgradePath = function (selector, password, identity, callback) { + Accounts.callLoginMethod({ + methodArguments: [{ + user: selector, + srp: SHA256(identity + ":" + password), + hashedPassword: SHA256(password) + }], + userCallback: callback + }); +}; + // Attempt to log in with a password. // // @param selector {String|Object} One of the following: @@ -8,41 +21,36 @@ // @param password {String} // @param callback {Function(error|undefined)} Meteor.loginWithPassword = function (selector, password, callback) { - var srp = new SRP.Client(password); - var request = srp.startExchange(); - if (typeof selector === 'string') if (selector.indexOf('@') === -1) selector = {username: selector}; else selector = {email: selector}; - request.user = selector; - - // Normally, we only set Meteor.loggingIn() to true within - // Accounts.callLoginMethod, but we'd also like it to be true during the - // password exchange. So we set it to true here, and clear it on error; in - // the non-error case, it gets cleared by callLoginMethod. - Accounts._setLoggingIn(true); - Accounts.connection.apply( - 'beginPasswordExchange', [request], function (error, result) { - if (error || !result) { - Accounts._setLoggingIn(false); - error = error || - new Error("No result from call to beginPasswordExchange"); - callback && callback(error); - return; + Accounts.callLoginMethod({ + methodArguments: [{ + user: selector, + hashedPassword: SHA256(password), + }], + userCallback: function (error, result) { + if (error && error.error === 400 && error.reason === 'old password format') { + var details; + try { + details = EJSON.parse(error.details); + } + catch (e) { + } + if (!(details && details.format === 'srp')) + callback(new Error("unknown old password format")); + else + srpUpgradePath(selector, password, details.identity, callback); } - - var response = srp.respondToChallenge(result); - Accounts.callLoginMethod({ - methodArguments: [{srp: response}], - validateResult: function (result) { - if (!srp.verifyConfirmation({HAMK: result.HAMK})) - throw new Error("Server is cheating!"); - }, - userCallback: callback}); - }); + else if (error) + callback(error); + else + callback(); + } + }); }; @@ -52,10 +60,10 @@ Accounts.createUser = function (options, callback) { if (!options.password) throw new Error("Must set options.password"); - var verifier = SRP.generateVerifier(options.password); - // strip old password, replacing with the verifier object + + // Replace password with the hashed password. + options.hashedPassword = SHA256(options.password); delete options.password; - options.srp = verifier; Accounts.callLoginMethod({ methodName: 'createUser', @@ -79,49 +87,18 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { return; } - var verifier = SRP.generateVerifier(newPassword); - - if (!oldPassword) { - Accounts.connection.apply( - 'changePassword', [{srp: verifier}], function (error, result) { - if (error || !result) { - callback && callback( - error || new Error("No result from changePassword.")); - } else { - callback && callback(); - } - }); - } else { // oldPassword - var srp = new SRP.Client(oldPassword); - var request = srp.startExchange(); - request.user = {id: Meteor.user()._id}; - Accounts.connection.apply( - 'beginPasswordExchange', [request], function (error, result) { - if (error || !result) { - callback && callback( - error || new Error("No result from call to beginPasswordExchange")); - return; - } - - var response = srp.respondToChallenge(result); - response.srp = verifier; - Accounts.connection.apply( - 'changePassword', [response],function (error, result) { - if (error || !result) { - callback && callback( - error || new Error("No result from changePassword.")); - } else { - if (!srp.verifyConfirmation(result)) { - // Monkey business! - callback && - callback(new Error("Old password verification failed.")); - } else { - callback && callback(); - } - } - }); - }); - } + Accounts.connection.apply( + 'changePassword', + [oldPassword ? SHA256(oldPassword) : null, SHA256(newPassword)], + function (error, result) { + if (error || !result) { + callback && callback( + error || new Error("No result from changePassword.")); + } else { + callback && callback(); + } + } + ); }; // Sends an email to a user with a link that can be used to reset @@ -148,10 +125,9 @@ Accounts.resetPassword = function(token, newPassword, callback) { if (!newPassword) throw new Error("Need to pass newPassword"); - var verifier = SRP.generateVerifier(newPassword); Accounts.callLoginMethod({ methodName: 'resetPassword', - methodArguments: [token, verifier], + methodArguments: [token, SHA256(newPassword)], userCallback: callback}); }; diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index a758da4a1c..88981cfc5f 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -1,3 +1,32 @@ +/// BCRYPT + +var bcrypt = Npm.require('bcrypt'); +var bcryptHash = Meteor._wrapAsync(bcrypt.hash); +var bcryptCompare = Meteor._wrapAsync(bcrypt.compare); + +// Salt the password that was hashed on the client for storage in the +// database. +// +var saltPassword = function (hashedPassword) { + return bcryptHash(hashedPassword, 10); +}; + + +// Check whether the provided hashed password matches the salted +// password in the database user record. +// +var checkPassword = function (user, hashedPassword) { + var result = { + userId: user._id + }; + + if (! bcryptCompare(hashedPassword, user.services.password.bcrypt)) + result.error = new Meteor.Error(403, "Incorrect password"); + + return result; +}; + + /// /// LOGIN /// @@ -16,6 +45,16 @@ var selectorFromUserQuery = function (user) { throw new Error("shouldn't happen (validation missed something)"); }; +var findUserFromUserQuery = function (user) { + var selector = selectorFromUserQuery(user); + + var user = Meteor.users.findOne(selector); + if (!user) + throw new Meteor.Error(403, "User not found"); + + return user; +}; + // XXX maybe this belongs in the check package var NonEmptyString = Match.Where(function (x) { check(x, String); @@ -33,101 +72,84 @@ var userQueryValidator = Match.Where(function (user) { return true; }); -// Step 1 of SRP password exchange. This puts an `M` value in the -// session data for this connection. If a client later sends the same -// `M` value to a method on this connection, it proves they know the -// password for this user. We can then prove we know the password to -// them by sending our `HAMK` value. -// -// @param request {Object} with fields: -// user: either {username: (username)}, {email: (email)}, or {id: (userId)} -// A: hex encoded int. the client's public key for this exchange -// @returns {Object} with fields: -// identity: random string ID -// salt: random string ID -// B: hex encoded int. server's public key for this exchange -Meteor.methods({beginPasswordExchange: function (request) { - var self = this; - try { - check(request, { - user: userQueryValidator, - A: String - }); - var selector = selectorFromUserQuery(request.user); +// Handler to login with a password. +Accounts.registerLoginHandler("password", function (options) { + if (!options.hashedPassword || options.srp) + return undefined; // don't handle - var user = Meteor.users.findOne(selector); - if (!user) - throw new Meteor.Error(403, "User not found"); + check(options, { + user: userQueryValidator, + hashedPassword: String + }); - if (!user.services || !user.services.password || - !user.services.password.srp) - throw new Meteor.Error(403, "User has no password set"); + var user = findUserFromUserQuery(options.user); - var verifier = user.services.password.srp; - var srp = new SRP.Server(verifier); - var challenge = srp.issueChallenge({A: request.A}); + if (!user.services || !user.services.password || + !(user.services.password.bcrypt || user.services.password.srp)) + throw new Meteor.Error(403, "User has no password set"); - } catch (err) { - // Report login failure if the method fails, so that login hooks are - // called. If the method succeeds, login hooks will be called when - // the second step method ('login') is called. If a user calls - // 'beginPasswordExchange' but then never calls the second step - // 'login' method, no login hook will fire. - // The validate login hooks can mutate the exception to be thrown. - var attempt = Accounts._reportLoginFailure(self, 'beginPasswordExchange', arguments, { - type: 'password', - error: err, - userId: user && user._id - }); - throw attempt.error; + if (!user.services.password.bcrypt) { + // Tell the client to use the SRP upgrade process. + throw new Meteor.Error(400, "old password format", EJSON.stringify({ + format: 'srp', + identity: user.services.password.srp.identity + })); } - // Save results so we can verify them later. - Accounts._setAccountData(this.connection.id, 'srpChallenge', - { userId: user._id, M: srp.M, HAMK: srp.HAMK } - ); - return challenge; -}}); + return checkPassword(user, options.hashedPassword); +}); -// Handler to login with password via SRP. Checks the `M` value set by -// beginPasswordExchange. +// Handler to login using the SRP upgrade path. Accounts.registerLoginHandler("password", function (options) { - if (!options.srp) + if (!options.srp || !options.hashedPassword) return undefined; // don't handle - check(options.srp, {M: String}); - // we're always called from within a 'login' method, so this should - // be safe. - var currentInvocation = DDP._CurrentInvocation.get(); - var serialized = Accounts._getAccountData(currentInvocation.connection.id, 'srpChallenge'); - if (!serialized || serialized.M !== options.srp.M) + check(options, { + user: userQueryValidator, + srp: String, + hashedPassword: String + }); + + var user = findUserFromUserQuery(options.user); + + if (!(user.services && user.services.password && user.services.password.srp)) + throw new Meteor.Error(403, "User has no password set"); + + var v1 = user.services.password.srp.verifier; + var v2 = SRP.generateVerifier( + null, + { + hashedIdentityAndPassword: options.srp, + salt: user.services.password.srp.salt + } + ).verifier; + if (v1 !== v2) return { - userId: serialized && serialized.userId, + userId: user._id, error: new Meteor.Error(403, "Incorrect password") }; - // Only can use challenges once. - Accounts._setAccountData(currentInvocation.connection.id, 'srpChallenge', undefined); - var userId = serialized.userId; - var user = Meteor.users.findOne(userId); - // Was the user deleted since the start of this challenge? - if (!user) - return { - userId: userId, - error: new Meteor.Error(403, "User not found") - }; + // Upgrade to bcrypt on successful login. + var salted = saltPassword(options.hashedPassword); + Meteor.users.update( + user._id, + { + $unset: { 'services.password.srp': 1 }, + $set: { 'services.password.bcrypt': salted } + } + ); - return { - userId: userId, - options: {HAMK: serialized.HAMK} - }; + return {userId: user._id}; }); // Handler to login with plaintext password. // // The meteor client doesn't use this, it is for other DDP clients who -// haven't implemented SRP. Since it sends the password in plaintext -// over the wire, it should only be run over SSL! +// haven't implemented hashing passwords. Since it sends the password +// in plaintext over the wire, it should only be run over SSL! +// +// XXX The above comment suggests regular logins without SSL *are* +// secure? // // Also, it might be nice if servers could turn this off. Or maybe it // should be opt-in, not opt-out? Accounts.config option? @@ -137,31 +159,15 @@ Accounts.registerLoginHandler("password", function (options) { check(options, {user: userQueryValidator, password: String}); - var selector = selectorFromUserQuery(options.user); - var user = Meteor.users.findOne(selector); - if (!user) - throw new Meteor.Error(403, "User not found"); + var user = findUserFromUserQuery(options.user); - if (!user.services || !user.services.password || - !user.services.password.srp) + if (!user.services || !user.services.password || !user.services.password.bcrypt) return { userId: user._id, error: new Meteor.Error(403, "User has no password set") }; - // 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 = SRP.generateVerifier(options.password, { - identity: verifier.identity, salt: verifier.salt}); - - if (verifier.verifier !== newVerifier.verifier) - return { - userId: user._id, - error: new Meteor.Error(403, "Incorrect password") - }; - - return {userId: user._id}; + return checkPassword(user, SHA256(options.password)) }); @@ -170,34 +176,26 @@ Accounts.registerLoginHandler("password", function (options) { /// // Let the user change their own password if they know the old -// password. Checks the `M` value set by beginPasswordExchange. -Meteor.methods({changePassword: function (options) { +// password. +Meteor.methods({changePassword: function (oldPassword, newPassword) { + check(oldPassword, String); + check(newPassword, String); + if (!this.userId) throw new Meteor.Error(401, "Must be logged in"); - check(options, { - // If options.M is set, it means we went through a challenge with the old - // password. For now, we don't allow changePassword without knowing the old - // password. - M: String, - srp: Match.Optional(SRP.matchVerifier), - password: Match.Optional(String) - }); - var serialized = Accounts._getAccountData(this.connection.id, 'srpChallenge'); - if (!serialized || serialized.M !== options.M) - throw new Meteor.Error(403, "Incorrect password"); - if (serialized.userId !== this.userId) - // No monkey business! - throw new Meteor.Error(403, "Incorrect password"); - // Only can use challenges once. - Accounts._setAccountData(this.connection.id, 'srpChallenge', undefined); + var user = Meteor.users.findOne(this.userId); + if (!user) + throw new Meteor.Error(403, "User not found"); - var verifier = options.srp; - if (!verifier && options.password) { - verifier = SRP.generateVerifier(options.password); - } - if (!verifier) - throw new Meteor.Error(400, "Invalid verifier"); + if (!user.services || !user.services.password || !user.services.password.bcrypt) + throw new Meteor.Error(403, "User has no password set"); + + var result = checkPassword(user, oldPassword); + if (result.error) + throw result.error; + + var salted = saltPassword(newPassword); // It would be better if this removed ALL existing tokens and replaced // the token for the current connection with a new one, but that would @@ -207,17 +205,14 @@ Meteor.methods({changePassword: function (options) { Meteor.users.update( { _id: this.userId }, { - $set: { 'services.password.srp': verifier }, + $set: { 'services.password.bcrypt': salted }, $pull: { 'services.resume.loginTokens': { hashedToken: { $ne: currentToken } } } } ); - var ret = {passwordChanged: true}; - if (serialized) - ret.HAMK = serialized.HAMK; - return ret; + return {passwordChanged: true}; }}); @@ -226,10 +221,12 @@ Accounts.setPassword = function (userId, newPassword) { var user = Meteor.users.findOne(userId); if (!user) throw new Meteor.Error(403, "User not found"); - var newVerifier = SRP.generateVerifier(newPassword); - Meteor.users.update({_id: user._id}, { - $set: {'services.password.srp': newVerifier}}); + Meteor.users.update( + {_id: user._id}, + { $unset: {'services.password.srp': 1}, + $set: {'services.password.bcrypt': saltPassword(SHA256(newPassword))} } + ); }; @@ -342,7 +339,7 @@ Accounts.sendEnrollmentEmail = function (userId, email) { // Take token from sendResetPasswordEmail or sendEnrollmentEmail, change // the users password, and log them in. -Meteor.methods({resetPassword: function (token, newVerifier) { +Meteor.methods({resetPassword: function (token, newPassword) { var self = this; return Accounts._loginMethod( self, @@ -351,10 +348,10 @@ Meteor.methods({resetPassword: function (token, newVerifier) { "password", function () { check(token, String); - check(newVerifier, SRP.matchVerifier); + check(newPassword, String); var user = Meteor.users.findOne({ - "services.password.reset.token": ""+token}); + "services.password.reset.token": token}); if (!user) throw new Meteor.Error(403, "Token expired"); var email = user.services.password.reset.email; @@ -364,6 +361,8 @@ Meteor.methods({resetPassword: function (token, newVerifier) { error: new Meteor.Error(403, "Token has invalid email address") }; + var salted = saltPassword(newPassword); + // NOTE: We're about to invalidate tokens on the user, who we might be // logged in as. Make sure to avoid logging ourselves out if this // happens. But also make sure not to leave the connection in a state @@ -376,7 +375,7 @@ Meteor.methods({resetPassword: function (token, newVerifier) { try { // Update the user record by: - // - Changing the password verifier to the new one + // - Changing the password to the new one // - Forgetting about the reset token that was just used // - Verifying their email, since they got the password reset via email. var affectedRecords = Meteor.users.update( @@ -385,9 +384,10 @@ Meteor.methods({resetPassword: function (token, newVerifier) { 'emails.address': email, 'services.password.reset.token': token }, - {$set: {'services.password.srp': newVerifier, + {$set: {'services.password.bcrypt': salted, 'emails.$.verified': true}, - $unset: {'services.password.reset': 1}}); + $unset: {'services.password.reset': 1, + 'services.password.srp': 1}}); if (affectedRecords !== 1) return { userId: user._id, @@ -529,7 +529,8 @@ var createUser = function (options) { username: Match.Optional(String), email: Match.Optional(String), password: Match.Optional(String), - srp: Match.Optional(SRP.matchVerifier) + srp: Match.Optional(SRP.matchVerifier), + hashedPassword: Match.Optional(String) })); var username = options.username; @@ -541,14 +542,16 @@ var createUser = function (options) { // client that didn't implement SRP could send this. This should // only be done over SSL. if (options.password) { - if (options.srp) - throw new Meteor.Error(400, "Don't pass both password and srp in options"); - options.srp = SRP.generateVerifier(options.password); + if (options.hashedPassword) + throw new Meteor.Error(400, "Don't pass both password and hashedPassword in options"); + options.hashedPassword = SHA256(options.password); } var user = {services: {}}; - if (options.srp) - user.services.password = {srp: options.srp}; // XXX validate verifier + if (options.hashedPassword) { + var salted = saltPassword(options.hashedPassword); + user.services.password = { bcrypt: salted }; + } if (username) user.username = username; if (email) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index bd1ce9a6d1..c6618c756a 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -732,6 +732,30 @@ if (Meteor.isClient) (function () { } ]); + testAsyncMulti("passwords - srp to bcrypt upgrade", [ + logoutStep, + // Create user with old SRP credentials in the database. + function (test, expect) { + Meteor.call("testCreateSRPUser", expect(function (error) { + test.isFalse(error); + })); + }, + // We are able to login with the old style credentials in the database. + function (test, expect) { + Meteor.loginWithPassword('srptestuser', 'abcdef', function (error) { + console.log('error', error); + test.isFalse(error); + }); + }, + logoutStep, + // After the upgrade to bcrypt we're still able to login. + function (test, expect) { + Meteor.loginWithPassword('srptestuser', 'abcdef', function (error) { + test.isFalse(error); + }); + }, + logoutStep + ]); }) (); @@ -778,16 +802,15 @@ if (Meteor.isServer) (function () { // set a new password. Accounts.setPassword(userId, 'new password'); user = Meteor.users.findOne(userId); - var oldVerifier = user.services.password.srp; - test.isTrue(user.services.password.srp); + var oldSaltedHash = user.services.password.bcrypt; + test.isTrue(oldSaltedHash); - // reset with the same password, see we get a different verifier + // reset with the same password, see we get a different salted hash Accounts.setPassword(userId, 'new password'); user = Meteor.users.findOne(userId); - var newVerifier = user.services.password.srp; - test.notEqual(oldVerifier.salt, newVerifier.salt); - test.notEqual(oldVerifier.identity, newVerifier.identity); - test.notEqual(oldVerifier.verifier, newVerifier.verifier); + var newSaltedHash = user.services.password.bcrypt; + test.isTrue(newSaltedHash); + test.notEqual(oldSaltedHash, newSaltedHash); // cleanup Meteor.users.remove(userId); diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index e9d110c936..0ecbb79fa5 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -115,3 +115,21 @@ Meteor.methods({ Meteor.users.remove({ "username": username }); } }); + + +// Create a user that had previously logged in with SRP. + +Meteor.methods({ + testCreateSRPUser: function () { + Meteor.users.remove({username: 'srptestuser'}); + var userId = Accounts.createUser({username: 'srptestuser'}); + Meteor.users.update( + userId, + { '$set': { 'services.password.srp': { + "identity" : "iPNrshUEcpOSO5fRDu7o4RRDc9OJBCGGljYpcXCuyg9", + "salt" : "Dk3lFggdEtcHU3aKm6Odx7sdcaIrMskQxBbqtBtFzt6", + "verifier" : "2e8bce266b1357edf6952cc56d979db19f699ced97edfb2854b95972f820b0c7006c1a18e98aad40edf3fe111b87c52ef7dd06b320ce452d01376df2d560fdc4d8e74f7a97bca1f67b3cfaef34dee34dd6c76571c247d762624dc166dab5499da06bc9358528efa75bf74e2e7f5a80d09e60acf8856069ae5cfb080f2239ee76" + } } } + ); + } +}); diff --git a/packages/sha/.gitignore b/packages/sha/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/sha/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/sha/package.js b/packages/sha/package.js new file mode 100644 index 0000000000..fbf93e6c8d --- /dev/null +++ b/packages/sha/package.js @@ -0,0 +1,9 @@ +Package.describe({ + summary: "SHA256 implementation", + internal: true +}); + +Package.on_use(function (api) { + api.export('SHA256'); + api.add_files(['sha256.js'], ['client', 'server']); +}); diff --git a/packages/srp/sha256.js b/packages/sha/sha256.js similarity index 96% rename from packages/srp/sha256.js rename to packages/sha/sha256.js index 4743264b4e..675f87ef1a 100644 --- a/packages/srp/sha256.js +++ b/packages/sha/sha256.js @@ -1,7 +1,5 @@ /// METEOR WRAPPER // -// XXX this should get packaged and moved into the Meteor.crypto -// namespace, along with other hash functions. SHA256 = (function () { @@ -14,18 +12,18 @@ SHA256 = (function () { * Original code by Angel Marin, Paul Johnston. * **/ - + function SHA256(s){ - + var chrsz = 8; var hexcase = 0; - + function safe_add (x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); } - + function S (X, n) { return ( X >>> n ) | (X << (32 - n)); } function R (X, n) { return ( X >>> n ); } function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); } @@ -34,17 +32,17 @@ function SHA256(s){ function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); } function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); } function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); } - + function core_sha256 (m, l) { var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2); var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19); var W = new Array(64); var a, b, c, d, e, f, g, h, i, j; var T1, T2; - + m[l >> 5] |= 0x80 << (24 - l % 32); m[((l + 64 >> 9) << 4) + 15] = l; - + for ( var i = 0; i> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } - + } - + return utftext; } - + function binb2hex (binarray) { var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; var str = ""; @@ -134,10 +132,10 @@ function SHA256(s){ } return str; } - + s = Utf8Encode(s); return binb2hex(core_sha256(str2binb(s), s.length * chrsz)); - + } /// METEOR WRAPPER diff --git a/packages/srp/package.js b/packages/srp/package.js index f304b8dccc..e9388f918c 100644 --- a/packages/srp/package.js +++ b/packages/srp/package.js @@ -4,10 +4,10 @@ Package.describe({ }); Package.on_use(function (api) { - api.use(['random', 'check'], ['client', 'server']); + api.use(['random', 'check', 'sha'], ['client', 'server']); api.use('underscore'); api.export('SRP'); - api.add_files(['biginteger.js', 'sha256.js', 'srp.js'], + api.add_files(['biginteger.js', 'srp.js'], ['client', 'server']); }); diff --git a/packages/srp/srp.js b/packages/srp/srp.js index 099848c569..ec1724fcc4 100644 --- a/packages/srp/srp.js +++ b/packages/srp/srp.js @@ -8,6 +8,7 @@ SRP = {}; * options is optional and can include: * - identity: String. The SRP username to user. Mostly this is passed * in for testing. Random UUID if not provided. + * - hashedIdentityAndPassword: combined identity and password, already hashed, for the SRP to bcrypt upgrade path. * - salt: String. A salt to use. Mostly this is passed in for * testing. Random UUID if not provided. * - SRP parameters (see _defaults and paramsFromOptions below) @@ -15,14 +16,19 @@ SRP = {}; SRP.generateVerifier = function (password, options) { var params = paramsFromOptions(options); - var identity = (options && options.identity) || Random.secret(); var salt = (options && options.salt) || Random.secret(); - var x = params.hash(salt + params.hash(identity + ":" + password)); + var identity; + var hashedIdentityAndPassword = options && options.hashedIdentityAndPassword; + if (!hashedIdentityAndPassword) { + identity = (options && options.identity) || Random.secret(); + hashedIdentityAndPassword = params.hash(identity + ":" + password); + } + + var x = params.hash(salt + hashedIdentityAndPassword); var xi = new BigInteger(x, 16); var v = params.g.modPow(xi, params.N); - return { identity: identity, salt: salt, From 644dde0382e77c21efbfc273018076be3e52c55c Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Sat, 31 May 2014 16:30:32 -0400 Subject: [PATCH 03/88] account data is no longer used for the srp challenge --- packages/accounts-base/accounts_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 4bb346a578..46d904ec08 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -544,7 +544,7 @@ Meteor.methods({ /// ACCOUNT DATA /// -// connectionId -> {connection, loginToken, srpChallenge} +// connectionId -> {connection, loginToken} var accountData = {}; // HACK: This is used by 'meteor-accounts' to get the loginToken for a From ef697a6fa7b7ed5cacafea2be3231d9de4151380 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Mon, 2 Jun 2014 19:42:12 -0400 Subject: [PATCH 04/88] code review items --- packages/accounts-password/password_client.js | 12 +- packages/accounts-password/password_server.js | 94 +++---- packages/accounts-password/password_tests.js | 22 +- .../accounts-password/password_tests_setup.js | 8 + packages/srp/package.js | 7 - packages/srp/srp.js | 253 +----------------- packages/srp/srp_tests.js | 115 -------- 7 files changed, 73 insertions(+), 438 deletions(-) delete mode 100644 packages/srp/srp_tests.js diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 4d87fbeb69..80c755698e 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -33,22 +33,22 @@ Meteor.loginWithPassword = function (selector, password, callback) { hashedPassword: SHA256(password), }], userCallback: function (error, result) { - if (error && error.error === 400 && error.reason === 'old password format') { + if (error && error.error === 400 && + error.reason === 'old password format') { var details; try { details = EJSON.parse(error.details); - } - catch (e) { - } + } catch (e) {} if (!(details && details.format === 'srp')) callback(new Error("unknown old password format")); else srpUpgradePath(selector, password, details.identity, callback); } - else if (error) + else if (error) { callback(error); - else + } else { callback(); + } } }); }; diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 88981cfc5f..20ca0cfe20 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -7,20 +7,20 @@ var bcryptCompare = Meteor._wrapAsync(bcrypt.compare); // Salt the password that was hashed on the client for storage in the // database. // -var saltPassword = function (hashedPassword) { - return bcryptHash(hashedPassword, 10); +var hashPassword = function (password) { + return bcryptHash(password, 10 /* number of rounds */); }; // Check whether the provided hashed password matches the salted // password in the database user record. // -var checkPassword = function (user, hashedPassword) { +var checkPassword = function (user, password) { var result = { userId: user._id }; - if (! bcryptCompare(hashedPassword, user.services.password.bcrypt)) + if (! bcryptCompare(password, user.services.password.bcrypt)) result.error = new Meteor.Error(403, "Incorrect password"); return result; @@ -73,14 +73,27 @@ var userQueryValidator = Match.Where(function (user) { }); // Handler to login with a password. +// +// The Meteor client uses options.hashedPassword, the password hashed +// with SHA256. +// +// For other DDP clients which don't have access to SHA, the handler +// also accepts the plaintext password in options.plaintextPassword. +// +// (It might be nice if servers could turn the plaintext password +// option off. Or maybe it should be opt-in, not opt-out? +// Accounts.config option?) +// +// Note that neither password option is secure without SSL. +// Accounts.registerLoginHandler("password", function (options) { - if (!options.hashedPassword || options.srp) + if (!(options.hashedPassword || options.plaintextPassword) || options.srp) return undefined; // don't handle - check(options, { - user: userQueryValidator, - hashedPassword: String - }); + check(options, Match.OneOf( + {user: userQueryValidator, hashedPassword: String}, + {user: userQueryValidator, plaintextPassword: String} + )); var user = findUserFromUserQuery(options.user); @@ -96,22 +109,30 @@ Accounts.registerLoginHandler("password", function (options) { })); } - return checkPassword(user, options.hashedPassword); + return checkPassword( + user, + options.hashedPassword || SHA256(options.plaintextPassword) + ); }); // Handler to login using the SRP upgrade path. Accounts.registerLoginHandler("password", function (options) { - if (!options.srp || !options.hashedPassword) + if (!options.srp || !(options.hashedPassword || options.plaintextPassword)) return undefined; // don't handle - check(options, { - user: userQueryValidator, - srp: String, - hashedPassword: String - }); + check(options, Match.OneOf( + {user: userQueryValidator, srp: String, hashedPassword: String}, + {user: userQueryValidator, srp: String, plaintextPassword: String} + )); + var password = options.hashedPassword || SHA256(options.plaintextPassword); var user = findUserFromUserQuery(options.user); + // Check to see if another simultaneous login has already upgraded + // the user record to bcrypt. + if (user.services && user.services.password && user.services.password.bcrypt) + return checkPassword(user, password); + if (!(user.services && user.services.password && user.services.password.srp)) throw new Meteor.Error(403, "User has no password set"); @@ -130,7 +151,7 @@ Accounts.registerLoginHandler("password", function (options) { }; // Upgrade to bcrypt on successful login. - var salted = saltPassword(options.hashedPassword); + var salted = hashPassword(password); Meteor.users.update( user._id, { @@ -142,34 +163,6 @@ Accounts.registerLoginHandler("password", function (options) { return {userId: user._id}; }); -// Handler to login with plaintext password. -// -// The meteor client doesn't use this, it is for other DDP clients who -// haven't implemented hashing passwords. Since it sends the password -// in plaintext over the wire, it should only be run over SSL! -// -// XXX The above comment suggests regular logins without SSL *are* -// secure? -// -// Also, it might be nice if servers could turn this off. Or maybe it -// should be opt-in, not opt-out? Accounts.config option? -Accounts.registerLoginHandler("password", function (options) { - if (!options.password || !options.user) - return undefined; // don't handle - - check(options, {user: userQueryValidator, password: String}); - - var user = findUserFromUserQuery(options.user); - - if (!user.services || !user.services.password || !user.services.password.bcrypt) - return { - userId: user._id, - error: new Meteor.Error(403, "User has no password set") - }; - - return checkPassword(user, SHA256(options.password)) -}); - /// /// CHANGING @@ -195,7 +188,7 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { if (result.error) throw result.error; - var salted = saltPassword(newPassword); + var salted = hashPassword(newPassword); // It would be better if this removed ALL existing tokens and replaced // the token for the current connection with a new one, but that would @@ -217,7 +210,7 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { // Force change the users password. -Accounts.setPassword = function (userId, newPassword) { +Accounts.setPassword = function (userId, newPlaintextPassword) { var user = Meteor.users.findOne(userId); if (!user) throw new Meteor.Error(403, "User not found"); @@ -225,7 +218,7 @@ Accounts.setPassword = function (userId, newPassword) { Meteor.users.update( {_id: user._id}, { $unset: {'services.password.srp': 1}, - $set: {'services.password.bcrypt': saltPassword(SHA256(newPassword))} } + $set: {'services.password.bcrypt': hashPassword(SHA256(newPlaintextPassword))} } ); }; @@ -361,7 +354,7 @@ Meteor.methods({resetPassword: function (token, newPassword) { error: new Meteor.Error(403, "Token has invalid email address") }; - var salted = saltPassword(newPassword); + var salted = hashPassword(newPassword); // NOTE: We're about to invalidate tokens on the user, who we might be // logged in as. Make sure to avoid logging ourselves out if this @@ -529,7 +522,6 @@ var createUser = function (options) { username: Match.Optional(String), email: Match.Optional(String), password: Match.Optional(String), - srp: Match.Optional(SRP.matchVerifier), hashedPassword: Match.Optional(String) })); @@ -549,7 +541,7 @@ var createUser = function (options) { var user = {services: {}}; if (options.hashedPassword) { - var salted = saltPassword(options.hashedPassword); + var salted = hashPassword(options.hashedPassword); user.services.password = { bcrypt: salted }; } if (username) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index c6618c756a..1dd33fdc3f 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -137,7 +137,7 @@ if (Meteor.isClient) (function () { function (test, expect) { Accounts.callLoginMethod({ // wrong password - methodArguments: [{user: {username: this.username}, password: 'wrong'}], + methodArguments: [{user: {username: this.username}, plaintextPassword: 'wrong'}], userCallback: expect(function (error) { test.isTrue(error); test.isFalse(Meteor.user()); @@ -147,7 +147,7 @@ if (Meteor.isClient) (function () { Accounts.callLoginMethod({ // right password methodArguments: [{user: {username: this.username}, - password: this.password}], + plaintextPassword: this.password}], userCallback: loggedInAs(this.username, test, expect) }); }, @@ -212,7 +212,7 @@ if (Meteor.isClient) (function () { self.secondConn = DDP.connect(Meteor.absoluteUrl()); self.secondConn.call('login', - { user: { username: self.username }, password: self.password }, + { user: { username: self.username }, plaintextPassword: self.password }, expect(function (err, result) { test.isFalse(err); self.secondConn.setUserId(result.id); @@ -742,17 +742,21 @@ if (Meteor.isClient) (function () { }, // We are able to login with the old style credentials in the database. function (test, expect) { - Meteor.loginWithPassword('srptestuser', 'abcdef', function (error) { - console.log('error', error); + Meteor.loginWithPassword('srptestuser', 'abcdef', expect(function (error) { test.isFalse(error); - }); + })); + }, + function (test, expect) { + Meteor.call("testSRPUpgrade", expect(function (error) { + test.isFalse(error); + })); }, logoutStep, // After the upgrade to bcrypt we're still able to login. function (test, expect) { - Meteor.loginWithPassword('srptestuser', 'abcdef', function (error) { + Meteor.loginWithPassword('srptestuser', 'abcdef', expect(function (error) { test.isFalse(error); - }); + })); }, logoutStep ]); @@ -846,7 +850,7 @@ if (Meteor.isServer) (function () { }); var result = clientConn.call('login', { user: {username: username}, - password: 'password' + plaintextPassword: 'password' }); test.isTrue(result); var token = Accounts._getAccountData(serverConn.id, 'loginToken'); diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index 0ecbb79fa5..d1418b81f6 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -131,5 +131,13 @@ Meteor.methods({ "verifier" : "2e8bce266b1357edf6952cc56d979db19f699ced97edfb2854b95972f820b0c7006c1a18e98aad40edf3fe111b87c52ef7dd06b320ce452d01376df2d560fdc4d8e74f7a97bca1f67b3cfaef34dee34dd6c76571c247d762624dc166dab5499da06bc9358528efa75bf74e2e7f5a80d09e60acf8856069ae5cfb080f2239ee76" } } } ); + }, + + testSRPUpgrade: function () { + var user = Meteor.users.findOne({username: 'srptestuser'}); + if (user.services && user.services.password && user.services.password.srp) + throw new Error("srp wasn't removed"); + if (!(user.services && user.services.password && user.services.password.bcrypt)) + throw new Error("bcrypt wasn't added"); } }); diff --git a/packages/srp/package.js b/packages/srp/package.js index e9388f918c..5f344b6204 100644 --- a/packages/srp/package.js +++ b/packages/srp/package.js @@ -10,10 +10,3 @@ Package.on_use(function (api) { api.add_files(['biginteger.js', 'srp.js'], ['client', 'server']); }); - -Package.on_test(function (api) { - api.use('tinytest'); - api.use('srp', ['client', 'server']); - api.use('underscore'); - api.add_files(['srp_tests.js'], ['client', 'server']); -}); diff --git a/packages/srp/srp.js b/packages/srp/srp.js index ec1724fcc4..4268691f7a 100644 --- a/packages/srp/srp.js +++ b/packages/srp/srp.js @@ -1,6 +1,7 @@ -SRP = {}; +// This package contains just enough of the original SRP code to +// support the backwards-compatibility upgrade path. -/////// PUBLIC CLIENT +SRP = {}; /** * Generate a new SRP verifier. Password is the plaintext password. @@ -44,249 +45,6 @@ SRP.matchVerifier = { }; -/** - * Generate a new SRP client object. Password is the plaintext password. - * - * options is optional and can include: - * - a: client's private ephemeral value. String or - * BigInteger. Normally, this is picked randomly, but it can be - * passed in for testing. - * - SRP parameters (see _defaults and paramsFromOptions below) - */ -SRP.Client = function (password, options) { - var self = this; - self.params = paramsFromOptions(options); - self.password = password; - - // shorthand - var N = self.params.N; - var g = self.params.g; - - // construct public and private keys. - var a, A; - if (options && options.a) { - if (typeof options.a === "string") - a = new BigInteger(options.a, 16); - else if (options.a instanceof BigInteger) - a = options.a; - else - throw new Error("Invalid parameter: a"); - - A = g.modPow(a, N); - - if (A.mod(N) === 0) - throw new Error("Invalid parameter: a: A mod N == 0."); - - } else { - while (!A || A.mod(N) === 0) { - a = randInt(); - A = g.modPow(a, N); - } - } - - self.a = a; - self.A = A; - self.Astr = A.toString(16); -}; - - -/** - * Initiate an SRP exchange. - * - * returns { A: 'client public ephemeral key. hex encoded integer.' } - */ -SRP.Client.prototype.startExchange = function () { - var self = this; - - return { - A: self.Astr - }; -}; - -/** - * Respond to the server's challenge with a proof of password. - * - * challenge is an object with - * - B: server public ephemeral key. hex encoded integer. - * - identity: user's identity (SRP username). - * - salt: user's salt. - * - * returns { M: 'client proof of password. hex encoded integer.' } - * throws an error if it got an invalid challenge. - */ -SRP.Client.prototype.respondToChallenge = function (challenge) { - var self = this; - - // shorthand - var N = self.params.N; - var g = self.params.g; - var k = self.params.k; - var H = self.params.hash; - - // XXX check for missing / bad parameters. - self.identity = challenge.identity; - self.salt = challenge.salt; - self.Bstr = challenge.B; - self.B = new BigInteger(self.Bstr, 16); - - if (self.B.mod(N) === 0) - throw new Error("Server sent invalid key: B mod N == 0."); - - var u = new BigInteger(H(self.Astr + self.Bstr), 16); - var x = new BigInteger( - H(self.salt + H(self.identity + ":" + self.password)), 16); - - var kgx = k.multiply(g.modPow(x, N)); - var aux = self.a.add(u.multiply(x)); - var S = self.B.subtract(kgx).modPow(aux, N); - var M = H(self.Astr + self.Bstr + S.toString(16)); - var HAMK = H(self.Astr + M + S.toString(16)); - - self.S = S; - self.HAMK = HAMK; - - return { - M: M - }; -}; - - -/** - * Verify server's confirmation message. - * - * confirmation is an object with - * - HAMK: server's proof of password. - * - * returns true or false. - */ -SRP.Client.prototype.verifyConfirmation = function (confirmation) { - var self = this; - - return (self.HAMK && (confirmation.HAMK === self.HAMK)); -}; - - - -/////// PUBLIC SERVER - - -/** - * Generate a new SRP server object. - * - * options is optional and can include: - * - b: server's private ephemeral value. String or - * BigInteger. Normally, this is picked randomly, but it can be - * passed in for testing. - * - SRP parameters (see _defaults and paramsFromOptions below) - */ -SRP.Server = function (verifier, options) { - var self = this; - self.params = paramsFromOptions(options); - self.verifier = verifier; - - // shorthand - var N = self.params.N; - var g = self.params.g; - var k = self.params.k; - var v = new BigInteger(self.verifier.verifier, 16); - - // construct public and private keys. - var b, B; - if (options && options.b) { - if (typeof options.b === "string") - b = new BigInteger(options.b, 16); - else if (options.b instanceof BigInteger) - b = options.b; - else - throw new Error("Invalid parameter: b"); - - B = k.multiply(v).add(g.modPow(b, N)).mod(N); - - if (B.mod(N) === 0) - throw new Error("Invalid parameter: b: B mod N == 0."); - - } else { - while (!B || B.mod(N) === 0) { - b = randInt(); - B = k.multiply(v).add(g.modPow(b, N)).mod(N); - } - } - - self.b = b; - self.B = B; - self.Bstr = B.toString(16); - -}; - - -/** - * Issue a challenge to the client. - * - * Takes a request from the client containing: - * - A: hex encoded int. - * - * Returns a challenge with: - * - B: server public ephemeral key. hex encoded integer. - * - identity: user's identity (SRP username). - * - salt: user's salt. - * - * Throws an error if issued a bad request. - */ -SRP.Server.prototype.issueChallenge = function (request) { - var self = this; - - // XXX check for missing / bad parameters. - self.Astr = request.A; - self.A = new BigInteger(self.Astr, 16); - - if (self.A.mod(self.params.N) === 0) - throw new Error("Client sent invalid key: A mod N == 0."); - - // shorthand - var N = self.params.N; - var H = self.params.hash; - - // Compute M and HAMK in advance. Don't send to client yet. - var u = new BigInteger(H(self.Astr + self.Bstr), 16); - var v = new BigInteger(self.verifier.verifier, 16); - var avu = self.A.multiply(v.modPow(u, N)); - self.S = avu.modPow(self.b, N); - self.M = H(self.Astr + self.Bstr + self.S.toString(16)); - self.HAMK = H(self.Astr + self.M + self.S.toString(16)); - - return { - identity: self.verifier.identity, - salt: self.verifier.salt, - B: self.Bstr - }; -}; - - -/** - * Verify a response from the client and return confirmation. - * - * Takes a challenge response from the client containing: - * - M: client proof of password. hex encoded int. - * - * Returns a confirmation if the client's proof is good: - * - HAMK: server proof of password. hex encoded integer. - * OR null if the client's proof doesn't match. - */ -SRP.Server.prototype.verifyResponse = function (response) { - var self = this; - - if (response.M !== self.M) - return null; - - return { - HAMK: self.HAMK - }; -}; - - - -/////// INTERNAL - /** * Default parameter values for SRP. * @@ -337,8 +95,3 @@ var paramsFromOptions = function (options) { return ret; }; - - -var randInt = function () { - return new BigInteger(Random.hexString(36), 16); -}; diff --git a/packages/srp/srp_tests.js b/packages/srp/srp_tests.js deleted file mode 100644 index d1ea3edc35..0000000000 --- a/packages/srp/srp_tests.js +++ /dev/null @@ -1,115 +0,0 @@ -Tinytest.add("srp - good exchange", function(test) { - var password = 'hi there!'; - var verifier = SRP.generateVerifier(password); - - var C = new SRP.Client(password); - var S = new SRP.Server(verifier); - - var request = C.startExchange(); - var challenge = S.issueChallenge(request); - var response = C.respondToChallenge(challenge); - var confirmation = S.verifyResponse(response); - - test.isTrue(confirmation); - test.isTrue(C.verifyConfirmation(confirmation)); - -}); - -Tinytest.add("srp - bad exchange", function(test) { - var verifier = SRP.generateVerifier('one password'); - - var C = new SRP.Client('another password'); - var S = new SRP.Server(verifier); - - var request = C.startExchange(); - var challenge = S.issueChallenge(request); - var response = C.respondToChallenge(challenge); - var confirmation = S.verifyResponse(response); - - test.isFalse(confirmation); -}); - - -Tinytest.add("srp - fixed values", function(test) { - // Test exact values during the exchange. We have to be very careful - // about changing the SRP code, because changes could render - // people's existing user database unusable. This test is - // intentionally brittle to catch change that could affect the - // validity of user passwords. - - var identity = "b73d9af9-4e74-4ce0-879c-484828b08436"; - var salt = "85f8b9d3-744a-487d-8982-a50e4c9f552a"; - var password = "95109251-3d8a-4777-bdec-44ffe8d86dfb"; - var a = "dc99c646fa4cb7c24314bb6f4ca2d391297acd0dacb0430a13bbf1e37dcf8071"; - var b = "cf878e00c9f2b6aa48a10f66df9706e64fef2ca399f396d65f5b0a27cb8ae237"; - - var verifier = SRP.generateVerifier( - password, {identity: identity, salt: salt}); - - var C = new SRP.Client(password, {a: a}); - var S = new SRP.Server(verifier, {b: b}); - - var request = C.startExchange(); - test.equal(request.A, "8a75aa61471a92d4c3b5d53698c910af5ef013c42799876c40612d1d5e0dc41d01f669bc022fadcd8a704030483401a1b86b8670191bd9dfb1fb506dd11c688b2f08e9946756263954db2040c1df1894af7af5f839c9215bb445268439157e65e8f100469d575d5d0458e19e8bd4dd4ea2c0b30b1b3f4f39264de4ec596e0bb7"); - - var challenge = S.issueChallenge(request); - test.equal(challenge.B, "77ab0a40ef428aa2fa2bc257c905f352c7f75fbcfdb8761393c9dc0f730bbb0270ba9f837545b410c955c3f761494b329ad23c6efdec7e63509e538c2f68a3526e072550a11dac46017718362205e0c698b5bed67d6ff475aa92c191ca169f865c81a1a577373c449b98df720c7b7ff50536f9919d781e698025fd7164932ba7"); - - var response = C.respondToChallenge(challenge); - test.equal(response.M, "8705d31bb61497279adf44eef6c167dcb7e03aa7a42102c1ea7e73025fbd4cd9"); - - var confirmation = S.verifyResponse(response); - test.equal(confirmation.HAMK, "07a0f200392fa9a084db7acc2021fbc174bfb36956b46835cc12506b68b27bba"); - - test.isTrue(C.verifyConfirmation(confirmation)); -}); - - -Tinytest.add("srp - options", function(test) { - // test that all options are respected. - // - // Note, all test strings here should be hex, because the 'hash' - // function needs to output numbers. - - var baseOptions = { - hash: function (x) { return x; }, - N: 'b', - g: '2', - k: '1' - }; - var verifierOptions = _.extend({ - identity: 'a', - salt: 'b' - }, baseOptions); - var clientOptions = _.extend({ - a: "2" - }, baseOptions); - var serverOptions = _.extend({ - b: "2" - }, baseOptions); - - var verifier = SRP.generateVerifier('c', verifierOptions);; - - test.equal(verifier.identity, 'a'); - test.equal(verifier.salt, 'b'); - test.equal(verifier.verifier, '3'); - - var C = new SRP.Client('c', clientOptions); - var S = new SRP.Server(verifier, serverOptions); - - var request = C.startExchange(); - test.equal(request.A, '4'); - - var challenge = S.issueChallenge(request); - test.equal(challenge.identity, 'a'); - test.equal(challenge.salt, 'b'); - test.equal(challenge.B, '7'); - - var response = C.respondToChallenge(challenge); - test.equal(response.M, '471'); - - var confirmation = S.verifyResponse(response); - test.isTrue(confirmation); - test.equal(confirmation.HAMK, '44711'); - -}); From 338ede126602d5195b57febadad0416b8a36ea1c Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Wed, 4 Jun 2014 10:55:03 -0400 Subject: [PATCH 05/88] code review II --- packages/accounts-password/password_client.js | 4 ++-- packages/accounts-password/password_server.js | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 80c755698e..9e15fa2a62 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -5,7 +5,7 @@ var srpUpgradePath = function (selector, password, identity, callback) { methodArguments: [{ user: selector, srp: SHA256(identity + ":" + password), - hashedPassword: SHA256(password) + password: SHA256(password) }], userCallback: callback }); @@ -30,7 +30,7 @@ Meteor.loginWithPassword = function (selector, password, callback) { Accounts.callLoginMethod({ methodArguments: [{ user: selector, - hashedPassword: SHA256(password), + password: SHA256(password), }], userCallback: function (error, result) { if (error && error.error === 400 && diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 20ca0cfe20..80eeba07e7 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -4,15 +4,15 @@ var bcrypt = Npm.require('bcrypt'); var bcryptHash = Meteor._wrapAsync(bcrypt.hash); var bcryptCompare = Meteor._wrapAsync(bcrypt.compare); -// Salt the password that was hashed on the client for storage in the -// database. +// Use bcrypt to hash the password (which was already hashed on the +// client) for storage in the database. // var hashPassword = function (password) { return bcryptHash(password, 10 /* number of rounds */); }; -// Check whether the provided hashed password matches the salted +// Check whether the provided hashed password matches the bcrypt'ed // password in the database user record. // var checkPassword = function (user, password) { @@ -74,7 +74,7 @@ var userQueryValidator = Match.Where(function (user) { // Handler to login with a password. // -// The Meteor client uses options.hashedPassword, the password hashed +// The Meteor client uses options.password, the password hashed // with SHA256. // // For other DDP clients which don't have access to SHA, the handler @@ -87,11 +87,11 @@ var userQueryValidator = Match.Where(function (user) { // Note that neither password option is secure without SSL. // Accounts.registerLoginHandler("password", function (options) { - if (!(options.hashedPassword || options.plaintextPassword) || options.srp) + if (!(options.password || options.plaintextPassword) || options.srp) return undefined; // don't handle check(options, Match.OneOf( - {user: userQueryValidator, hashedPassword: String}, + {user: userQueryValidator, password: String}, {user: userQueryValidator, plaintextPassword: String} )); @@ -111,20 +111,20 @@ Accounts.registerLoginHandler("password", function (options) { return checkPassword( user, - options.hashedPassword || SHA256(options.plaintextPassword) + options.password || SHA256(options.plaintextPassword) ); }); // Handler to login using the SRP upgrade path. Accounts.registerLoginHandler("password", function (options) { - if (!options.srp || !(options.hashedPassword || options.plaintextPassword)) + if (!options.srp || !(options.password || options.plaintextPassword)) return undefined; // don't handle check(options, Match.OneOf( - {user: userQueryValidator, srp: String, hashedPassword: String}, + {user: userQueryValidator, srp: String, password: String}, {user: userQueryValidator, srp: String, plaintextPassword: String} )); - var password = options.hashedPassword || SHA256(options.plaintextPassword); + var password = options.password || SHA256(options.plaintextPassword); var user = findUserFromUserQuery(options.user); From 254075587c6a726a19e5ad58a5f9f38441d14a75 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 4 Jun 2014 15:06:00 -0700 Subject: [PATCH 06/88] Nick code review items (mostly comments) --- packages/accounts-password/password_client.js | 41 ++++++++++++------- packages/accounts-password/password_server.js | 22 ++++++++-- packages/accounts-password/password_tests.js | 17 +++++--- .../accounts-password/password_tests_setup.js | 10 +++-- packages/srp/package.js | 12 ++++++ packages/srp/srp.js | 4 ++ packages/srp/srp_tests.js | 19 +++++++++ 7 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 packages/srp/srp_tests.js diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 9e15fa2a62..130dd8c71e 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -1,16 +1,3 @@ -// The server requested an upgrade from the old SRP password format, -// so supply the needed SRP identity to login. -var srpUpgradePath = function (selector, password, identity, callback) { - Accounts.callLoginMethod({ - methodArguments: [{ - user: selector, - srp: SHA256(identity + ":" + password), - password: SHA256(password) - }], - userCallback: callback - }); -}; - // Attempt to log in with a password. // // @param selector {String|Object} One of the following: @@ -30,11 +17,23 @@ Meteor.loginWithPassword = function (selector, password, callback) { Accounts.callLoginMethod({ methodArguments: [{ user: selector, - password: SHA256(password), + password: SHA256(password) }], userCallback: function (error, result) { if (error && error.error === 400 && error.reason === 'old password format') { + // The "reason" string should match the error thrown in the + // password login handler in password_server.js. + + // XXX COMPAT WITH 0.8.1.3 + // If this user's last login was with a previous version of + // Meteor that used SRP, then the server throws this error to + // indicate that we should try again. The error includes the + // user's SRP identity. We provide a value derived from the + // identity and the password to prove to the server that we know + // the password without requiring a full SRP flow, as well as + // SHA256(password), which the server bcrypts and stores in + // place of the old SRP information for this user. var details; try { details = EJSON.parse(error.details); @@ -53,6 +52,20 @@ Meteor.loginWithPassword = function (selector, password, callback) { }); }; +// The server requested an upgrade from the old SRP password format, +// so supply the needed SRP identity to login. +var srpUpgradePath = function (selector, plaintextPassword, + identity, callback) { + Accounts.callLoginMethod({ + methodArguments: [{ + user: selector, + srp: SHA256(identity + ":" + plaintextPassword), + password: SHA256(plaintextPassword) + }], + userCallback: callback + }); +}; + // Attempt to log in as a new user. Accounts.createUser = function (options, callback) { diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 80eeba07e7..16da372f4b 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -4,8 +4,8 @@ var bcrypt = Npm.require('bcrypt'); var bcryptHash = Meteor._wrapAsync(bcrypt.hash); var bcryptCompare = Meteor._wrapAsync(bcrypt.compare); -// Use bcrypt to hash the password (which was already hashed on the -// client) for storage in the database. +// Use bcrypt to hash the password (which was already hashed with SHA256 +// on the client) for storage in the database. // var hashPassword = function (password) { return bcryptHash(password, 10 /* number of rounds */); @@ -115,7 +115,21 @@ Accounts.registerLoginHandler("password", function (options) { ); }); -// Handler to login using the SRP upgrade path. +// Handler to login using the SRP upgrade path. To use this login +// handler, the client must provide: +// - srp: H(identity + ":" + password) +// - plaintextPassword or password (which is the SHA256 of the password) +// +// We use `options.srp` to verify that the client knows the correct +// password without doing a full SRP flow. Once we've checked that, we +// upgrade the user to bcrypt and remove the SRP information from the +// user document. +// +// The client ends up using this login handler after trying the normal +// login handler (above), which throws an error telling the client to +// try the SRP upgrade path. +// +// XXX COMPAT WITH 0.8.1.3 Accounts.registerLoginHandler("password", function (options) { if (!options.srp || !(options.password || options.plaintextPassword)) return undefined; // don't handle @@ -217,7 +231,7 @@ Accounts.setPassword = function (userId, newPlaintextPassword) { Meteor.users.update( {_id: user._id}, - { $unset: {'services.password.srp': 1}, + { $unset: {'services.password.srp': 1}, // XXX COMPAT WITH 0.8.1.3 $set: {'services.password.bcrypt': hashPassword(SHA256(newPlaintextPassword))} } ); }; diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 1dd33fdc3f..c43f709f0a 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -736,29 +736,36 @@ if (Meteor.isClient) (function () { logoutStep, // Create user with old SRP credentials in the database. function (test, expect) { - Meteor.call("testCreateSRPUser", expect(function (error) { + var self = this; + Meteor.call("testCreateSRPUser", expect(function (error, result) { test.isFalse(error); + self.username = result; })); }, // We are able to login with the old style credentials in the database. function (test, expect) { - Meteor.loginWithPassword('srptestuser', 'abcdef', expect(function (error) { + Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) { test.isFalse(error); })); }, function (test, expect) { - Meteor.call("testSRPUpgrade", expect(function (error) { + Meteor.call("testSRPUpgrade", this.username, expect(function (error) { test.isFalse(error); })); }, logoutStep, // After the upgrade to bcrypt we're still able to login. function (test, expect) { - Meteor.loginWithPassword('srptestuser', 'abcdef', expect(function (error) { + Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) { test.isFalse(error); })); }, - logoutStep + logoutStep, + function (test, expect) { + Meteor.call("removeUser", this.username, expect(function (error) { + test.isFalse(error); + })); + } ]); }) (); diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index d1418b81f6..0993a706b6 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -121,8 +121,9 @@ Meteor.methods({ Meteor.methods({ testCreateSRPUser: function () { - Meteor.users.remove({username: 'srptestuser'}); - var userId = Accounts.createUser({username: 'srptestuser'}); + var username = Random.id(); + Meteor.users.remove({username: username}); + var userId = Accounts.createUser({username: username}); Meteor.users.update( userId, { '$set': { 'services.password.srp': { @@ -131,10 +132,11 @@ Meteor.methods({ "verifier" : "2e8bce266b1357edf6952cc56d979db19f699ced97edfb2854b95972f820b0c7006c1a18e98aad40edf3fe111b87c52ef7dd06b320ce452d01376df2d560fdc4d8e74f7a97bca1f67b3cfaef34dee34dd6c76571c247d762624dc166dab5499da06bc9358528efa75bf74e2e7f5a80d09e60acf8856069ae5cfb080f2239ee76" } } } ); + return username; }, - testSRPUpgrade: function () { - var user = Meteor.users.findOne({username: 'srptestuser'}); + testSRPUpgrade: function (username) { + var user = Meteor.users.findOne({username: username}); if (user.services && user.services.password && user.services.password.srp) throw new Error("srp wasn't removed"); if (!(user.services && user.services.password && user.services.password.bcrypt)) diff --git a/packages/srp/package.js b/packages/srp/package.js index 5f344b6204..674a14437a 100644 --- a/packages/srp/package.js +++ b/packages/srp/package.js @@ -1,3 +1,8 @@ +// XXX COMPAT WITH 0.8.1.3 +// This package is replaced by the use of bcrypt in accounts-password, +// but we are leaving in some of the code to allow existing user +// databases to be upgraded from SRP to bcrypt. + Package.describe({ summary: "Library for Secure Remote Password (SRP) exchanges", internal: true @@ -10,3 +15,10 @@ Package.on_use(function (api) { api.add_files(['biginteger.js', 'srp.js'], ['client', 'server']); }); + +Package.on_test(function (api) { + api.use('tinytest'); + api.use('srp', ['client', 'server']); + api.use('underscore'); + api.add_files(['srp_tests.js'], ['client', 'server']); +}); diff --git a/packages/srp/srp.js b/packages/srp/srp.js index 4268691f7a..1fd7344007 100644 --- a/packages/srp/srp.js +++ b/packages/srp/srp.js @@ -1,5 +1,9 @@ // This package contains just enough of the original SRP code to // support the backwards-compatibility upgrade path. +// +// An SRP (and possibly also accounts-srp) package should eventually be +// available in Atmosphere so that users can continue to use SRP if they +// want to. SRP = {}; diff --git a/packages/srp/srp_tests.js b/packages/srp/srp_tests.js new file mode 100644 index 0000000000..597b4cf4df --- /dev/null +++ b/packages/srp/srp_tests.js @@ -0,0 +1,19 @@ +Tinytest.add("srp - fixed values", function(test) { + // Test exact values outputted by `generateVerifier`. We have to be very + // careful about changing the SRP code, because changes could render + // people's existing user database unusable. This test is + // intentionally brittle to catch change that could affect the + // validity of user passwords. + + var identity = "b73d9af9-4e74-4ce0-879c-484828b08436"; + var salt = "85f8b9d3-744a-487d-8982-a50e4c9f552a"; + var password = "95109251-3d8a-4777-bdec-44ffe8d86dfb"; + var a = "dc99c646fa4cb7c24314bb6f4ca2d391297acd0dacb0430a13bbf1e37dcf8071"; + var b = "cf878e00c9f2b6aa48a10f66df9706e64fef2ca399f396d65f5b0a27cb8ae237"; + + var verifier = SRP.generateVerifier( + password, {identity: identity, salt: salt}); + test.equal(verifier.identity, identity); + test.equal(verifier.salt, salt); + test.equal(verifier.verifier, "56778b720d20b2e306f04e47180fb94335b88a6052808483acb0e85612606f9f1d8d5a3c6b85e0c7bfec7f08c07bdfbd0d40b032f517871dd8afd045b0f24e2edc05ccdc47b19f35d2eb9f7670521a38c1b358fcee63f052a1aedbb1282d3b92c7a554f8523f3379c2fbc6885be8227fbd426ad6960c3839809f8c94d80a6c51"); +}); From 55cecde4043dcfe37d355928a19b4c58a402eaaf Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 4 Jun 2014 15:23:35 -0700 Subject: [PATCH 07/88] Make an error message nicer --- packages/accounts-password/password_client.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 130dd8c71e..a3447f17a8 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -39,7 +39,9 @@ Meteor.loginWithPassword = function (selector, password, callback) { details = EJSON.parse(error.details); } catch (e) {} if (!(details && details.format === 'srp')) - callback(new Error("unknown old password format")); + callback(new Meteor.Error(400, + "Password is old. Please reset your " + + "password.")); else srpUpgradePath(selector, password, details.identity, callback); } From e1669f464ca6bb8edf6a6f4f26209009b9e26ad0 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 4 Jun 2014 18:24:42 -0700 Subject: [PATCH 08/88] Remove 'plaintextPassword'. 'password' can always be either a string or an object (indicating that it's been hashed already with something). When the server receives a string, it hashes it with SHA256 before bcrypt. --- packages/accounts-password/password_client.js | 18 ++- packages/accounts-password/password_server.js | 131 ++++++++++++------ packages/accounts-password/password_tests.js | 8 +- 3 files changed, 102 insertions(+), 55 deletions(-) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index a3447f17a8..2a3a6e088a 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -17,7 +17,7 @@ Meteor.loginWithPassword = function (selector, password, callback) { Accounts.callLoginMethod({ methodArguments: [{ user: selector, - password: SHA256(password) + password: hashPassword(password) }], userCallback: function (error, result) { if (error && error.error === 400 && @@ -54,6 +54,13 @@ Meteor.loginWithPassword = function (selector, password, callback) { }); }; +var hashPassword = function (password) { + return { + digest: SHA256(password), + algorithm: "sha-256" + }; +}; + // The server requested an upgrade from the old SRP password format, // so supply the needed SRP identity to login. var srpUpgradePath = function (selector, plaintextPassword, @@ -62,7 +69,7 @@ var srpUpgradePath = function (selector, plaintextPassword, methodArguments: [{ user: selector, srp: SHA256(identity + ":" + plaintextPassword), - password: SHA256(plaintextPassword) + password: hashPassword(plaintextPassword) }], userCallback: callback }); @@ -77,8 +84,7 @@ Accounts.createUser = function (options, callback) { throw new Error("Must set options.password"); // Replace password with the hashed password. - options.hashedPassword = SHA256(options.password); - delete options.password; + options.password = hashPassword(options.password); Accounts.callLoginMethod({ methodName: 'createUser', @@ -104,7 +110,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { Accounts.connection.apply( 'changePassword', - [oldPassword ? SHA256(oldPassword) : null, SHA256(newPassword)], + [oldPassword ? hashPassword(oldPassword) : null, hashPassword(newPassword)], function (error, result) { if (error || !result) { callback && callback( @@ -142,7 +148,7 @@ Accounts.resetPassword = function(token, newPassword, callback) { Accounts.callLoginMethod({ methodName: 'resetPassword', - methodArguments: [token, SHA256(newPassword)], + methodArguments: [token, hashPassword(newPassword)], userCallback: callback}); }; diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 16da372f4b..800aecd71e 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -4,24 +4,68 @@ var bcrypt = Npm.require('bcrypt'); var bcryptHash = Meteor._wrapAsync(bcrypt.hash); var bcryptCompare = Meteor._wrapAsync(bcrypt.compare); -// Use bcrypt to hash the password (which was already hashed with SHA256 -// on the client) for storage in the database. +// User records have a 'services.password.bcrypt' field on them to hold +// their hashed passwords (unless they have a 'services.password.srp' +// field, in which case they will be upgraded to bcrypt the next time +// they log in). +// +// When the client sends a password to the server, it can either be a +// string (the plaintext password) or an object with keys 'digest' and +// 'algorithm' (must be "sha-256" for now). The Meteor client always sends +// password objects { digest: *, algorithm: "sha-256" }, but DDP clients +// that don't have access to SHA can just send plaintext passwords as +// strings. +// +// When the server receives a plaintext password as a string, it always +// hashes it with SHA256 before passing it into bcrypt. When the server +// receives a password as an object, it asserts that the algorithm is +// "sha-256" and then passes the digest to bcrypt. + + +// Given a 'password' from the client, extract the string that we should +// bcrypt. 'password' can be one of: +// - String (the plaintext password) +// - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256". +// +var getPasswordString = function (password) { + if (typeof password === "string") { + password = SHA256(password); + } else { // 'password' is an object + if (password.algorithm !== "sha-256") { + throw new Error("Invalid password hash algorithm. " + + "Only 'sha-256' is allowed."); + } + password = password.digest; + } + return password; +}; + +// Use bcrypt to hash the password for storage in the database. +// `password` can be a string (in which case it will be run through +// SHA256 before bcrypt) or an object with properties `digest` and +// `algorithm` (in which case we bcrypt `password.digest`). // var hashPassword = function (password) { + password = getPasswordString(password); return bcryptHash(password, 10 /* number of rounds */); }; - -// Check whether the provided hashed password matches the bcrypt'ed -// password in the database user record. +// Check whether the provided password matches the bcrypt'ed password in +// the database user record. `password` can be a string (in which case +// it will be run through SHA256 before bcrypt) or an object with +// properties `digest` and `algorithm` (in which case we bcrypt +// `password.digest`). // var checkPassword = function (user, password) { var result = { userId: user._id }; - if (! bcryptCompare(password, user.services.password.bcrypt)) + password = getPasswordString(password); + + if (! bcryptCompare(password, user.services.password.bcrypt)) { result.error = new Meteor.Error(403, "Incorrect password"); + } return result; }; @@ -72,13 +116,18 @@ var userQueryValidator = Match.Where(function (user) { return true; }); +var passwordValidator = Match.OneOf( + String, + { digest: String, algorithm: String } +); + // Handler to login with a password. // -// The Meteor client uses options.password, the password hashed -// with SHA256. +// The Meteor client sets options.password to an object with keys +// 'digest' (set to SHA256(password)) and 'algorithm' ("sha-256"). // // For other DDP clients which don't have access to SHA, the handler -// also accepts the plaintext password in options.plaintextPassword. +// also accepts the plaintext password in options.password as a string. // // (It might be nice if servers could turn the plaintext password // option off. Or maybe it should be opt-in, not opt-out? @@ -87,13 +136,14 @@ var userQueryValidator = Match.Where(function (user) { // Note that neither password option is secure without SSL. // Accounts.registerLoginHandler("password", function (options) { - if (!(options.password || options.plaintextPassword) || options.srp) + if (! options.password || options.srp) return undefined; // don't handle - check(options, Match.OneOf( - {user: userQueryValidator, password: String}, - {user: userQueryValidator, plaintextPassword: String} - )); + check(options, { + user: userQueryValidator, + password: passwordValidator + }); + var user = findUserFromUserQuery(options.user); @@ -111,14 +161,14 @@ Accounts.registerLoginHandler("password", function (options) { return checkPassword( user, - options.password || SHA256(options.plaintextPassword) + options.password ); }); // Handler to login using the SRP upgrade path. To use this login // handler, the client must provide: // - srp: H(identity + ":" + password) -// - plaintextPassword or password (which is the SHA256 of the password) +// - password: a string or an object with properties 'digest' and 'algorithm' // // We use `options.srp` to verify that the client knows the correct // password without doing a full SRP flow. Once we've checked that, we @@ -131,21 +181,21 @@ Accounts.registerLoginHandler("password", function (options) { // // XXX COMPAT WITH 0.8.1.3 Accounts.registerLoginHandler("password", function (options) { - if (!options.srp || !(options.password || options.plaintextPassword)) + if (!options.srp || !options.password) return undefined; // don't handle - check(options, Match.OneOf( - {user: userQueryValidator, srp: String, password: String}, - {user: userQueryValidator, srp: String, plaintextPassword: String} - )); - var password = options.password || SHA256(options.plaintextPassword); + check(options, { + user: userQueryValidator, + srp: String, + password: passwordValidator + }); var user = findUserFromUserQuery(options.user); // Check to see if another simultaneous login has already upgraded // the user record to bcrypt. if (user.services && user.services.password && user.services.password.bcrypt) - return checkPassword(user, password); + return checkPassword(user, options.password); if (!(user.services && user.services.password && user.services.password.srp)) throw new Meteor.Error(403, "User has no password set"); @@ -165,7 +215,7 @@ Accounts.registerLoginHandler("password", function (options) { }; // Upgrade to bcrypt on successful login. - var salted = hashPassword(password); + var salted = hashPassword(options.password); Meteor.users.update( user._id, { @@ -185,8 +235,8 @@ Accounts.registerLoginHandler("password", function (options) { // Let the user change their own password if they know the old // password. Meteor.methods({changePassword: function (oldPassword, newPassword) { - check(oldPassword, String); - check(newPassword, String); + check(oldPassword, passwordValidator); + check(newPassword, passwordValidator); if (!this.userId) throw new Meteor.Error(401, "Must be logged in"); @@ -202,7 +252,7 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { if (result.error) throw result.error; - var salted = hashPassword(newPassword); + var hashed = hashPassword(newPassword); // It would be better if this removed ALL existing tokens and replaced // the token for the current connection with a new one, but that would @@ -212,7 +262,7 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { Meteor.users.update( { _id: this.userId }, { - $set: { 'services.password.bcrypt': salted }, + $set: { 'services.password.bcrypt': hashed }, $pull: { 'services.resume.loginTokens': { hashedToken: { $ne: currentToken } } } @@ -232,7 +282,7 @@ Accounts.setPassword = function (userId, newPlaintextPassword) { Meteor.users.update( {_id: user._id}, { $unset: {'services.password.srp': 1}, // XXX COMPAT WITH 0.8.1.3 - $set: {'services.password.bcrypt': hashPassword(SHA256(newPlaintextPassword))} } + $set: {'services.password.bcrypt': hashPassword(newPlaintextPassword)} } ); }; @@ -355,7 +405,7 @@ Meteor.methods({resetPassword: function (token, newPassword) { "password", function () { check(token, String); - check(newPassword, String); + check(newPassword, passwordValidator); var user = Meteor.users.findOne({ "services.password.reset.token": token}); @@ -368,7 +418,7 @@ Meteor.methods({resetPassword: function (token, newPassword) { error: new Meteor.Error(403, "Token has invalid email address") }; - var salted = hashPassword(newPassword); + var hashed = hashPassword(newPassword); // NOTE: We're about to invalidate tokens on the user, who we might be // logged in as. Make sure to avoid logging ourselves out if this @@ -391,7 +441,7 @@ Meteor.methods({resetPassword: function (token, newPassword) { 'emails.address': email, 'services.password.reset.token': token }, - {$set: {'services.password.bcrypt': salted, + {$set: {'services.password.bcrypt': hashed, 'emails.$.verified': true}, $unset: {'services.password.reset': 1, 'services.password.srp': 1}}); @@ -535,8 +585,7 @@ var createUser = function (options) { check(options, Match.ObjectIncluding({ username: Match.Optional(String), email: Match.Optional(String), - password: Match.Optional(String), - hashedPassword: Match.Optional(String) + password: Match.Optional(passwordValidator) })); var username = options.username; @@ -544,20 +593,12 @@ var createUser = function (options) { if (!username && !email) throw new Meteor.Error(400, "Need to set a username or email"); - // Raw password. The meteor client doesn't send this, but a DDP - // client that didn't implement SRP could send this. This should - // only be done over SSL. + var user = {services: {}}; if (options.password) { - if (options.hashedPassword) - throw new Meteor.Error(400, "Don't pass both password and hashedPassword in options"); - options.hashedPassword = SHA256(options.password); + var hashed = hashPassword(options.password); + user.services.password = { bcrypt: hashed }; } - var user = {services: {}}; - if (options.hashedPassword) { - var salted = hashPassword(options.hashedPassword); - user.services.password = { bcrypt: salted }; - } if (username) user.username = username; if (email) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index c43f709f0a..d79654e23f 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -137,7 +137,7 @@ if (Meteor.isClient) (function () { function (test, expect) { Accounts.callLoginMethod({ // wrong password - methodArguments: [{user: {username: this.username}, plaintextPassword: 'wrong'}], + methodArguments: [{user: {username: this.username}, password: 'wrong'}], userCallback: expect(function (error) { test.isTrue(error); test.isFalse(Meteor.user()); @@ -147,7 +147,7 @@ if (Meteor.isClient) (function () { Accounts.callLoginMethod({ // right password methodArguments: [{user: {username: this.username}, - plaintextPassword: this.password}], + password: this.password}], userCallback: loggedInAs(this.username, test, expect) }); }, @@ -212,7 +212,7 @@ if (Meteor.isClient) (function () { self.secondConn = DDP.connect(Meteor.absoluteUrl()); self.secondConn.call('login', - { user: { username: self.username }, plaintextPassword: self.password }, + { user: { username: self.username }, password: self.password }, expect(function (err, result) { test.isFalse(err); self.secondConn.setUserId(result.id); @@ -857,7 +857,7 @@ if (Meteor.isServer) (function () { }); var result = clientConn.call('login', { user: {username: username}, - plaintextPassword: 'password' + password: 'password' }); test.isTrue(result); var token = Accounts._getAccountData(serverConn.id, 'loginToken'); From d61a676ac3c98cbc6776b49275b0d29b5ce4cbcd Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 9 Jun 2014 20:43:40 -0700 Subject: [PATCH 09/88] Extract number of bcrypt rounds to top of file --- packages/accounts-password/password_server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 800aecd71e..699f7755b2 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -22,6 +22,8 @@ var bcryptCompare = Meteor._wrapAsync(bcrypt.compare); // "sha-256" and then passes the digest to bcrypt. +var BCRYPT_ROUNDS = 10; + // Given a 'password' from the client, extract the string that we should // bcrypt. 'password' can be one of: // - String (the plaintext password) @@ -47,7 +49,7 @@ var getPasswordString = function (password) { // var hashPassword = function (password) { password = getPasswordString(password); - return bcryptHash(password, 10 /* number of rounds */); + return bcryptHash(password, BCRYPT_ROUNDS); }; // Check whether the provided password matches the bcrypt'ed password in From dbca6908f36c590d0d50dc67d653ae12935d12b5 Mon Sep 17 00:00:00 2001 From: Cangit Date: Fri, 6 Jun 2014 16:41:02 +0200 Subject: [PATCH 10/88] Don't decrement the line number in LESS error reporting. LESS fixed their math problem in 1.6.0 --- packages/less/plugin/compile-less.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/less/plugin/compile-less.js b/packages/less/plugin/compile-less.js index 5b00d99293..7eccb5e42f 100644 --- a/packages/less/plugin/compile-less.js +++ b/packages/less/plugin/compile-less.js @@ -44,7 +44,7 @@ Plugin.registerSourceHandler("less", function (compileStep) { compileStep.error({ message: "Less compiler error: " + e.message, sourcePath: e.filename || compileStep.inputPath, - line: e.line - 1, // dunno why, but it matches + line: e.line, column: e.column + 1 }); return; From 5ab3fbc17eb2ea4b5729d57aaecbd5449b3b9a73 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Tue, 10 Jun 2014 11:05:43 -0700 Subject: [PATCH 11/88] Allow external CSS style attributes to interop with Blaze style attributes. This fixes cases where jQuery calls .hide() or .show() on an element which also has a Blaze defined dynamic attribute. --- History.md | 2 + packages/ui/attrs.js | 82 ++++++++++++++++++++++++++++--------- packages/ui/render_tests.js | 38 +++++++++++++++++ 3 files changed, 103 insertions(+), 19 deletions(-) diff --git a/History.md b/History.md index 50ded1a9e7..ff8cbaebb3 100644 --- a/History.md +++ b/History.md @@ -54,6 +54,8 @@ the client, don't cache the return value of `cursor.count()` (consistently with the server behavior). `cursor.rewind()` is now a no-op. #2114 +* Allow externally applied CSS style attributes to interop with Blaze dynamic style attributes. + * Upgraded dependencies: - node: 0.10.28 (from 0.10.26) - uglify-js: 2.4.13 (from 2.4.7) diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index a9427c8654..83aaae37e7 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -48,51 +48,93 @@ AttributeHandler.extend = function (options) { return subType; }; -// Extended below to support both regular and SVG elements -var BaseClassHandler = AttributeHandler.extend({ +// Apply the diff between the tokens of "oldValue" and "value" to "element." +// Extended below to support classes, SVG elements and styles. +var BaseTokenHandler = AttributeHandler.extend({ update: function (element, oldValue, value) { - if (!this.getCurrentValue || !this.setValue) - throw new Error("Missing methods in subclass of 'BaseClassHandler'"); + if (!this.getCurrentValue || !this.setValue || + !this.tokenize || !this.stringify) + throw new Error("Missing methods in subclass of 'BaseTokenHandler'"); - var oldClasses = oldValue ? _.compact(oldValue.split(' ')) : []; - var newClasses = value ? _.compact(value.split(' ')) : []; + var oldTokens = oldValue ? _.compact(this.tokenize(oldValue)) : []; + var newTokens = value ? _.compact(this.tokenize(value)) : []; // the current classes on the element, which we will mutate. - var classes = _.compact(this.getCurrentValue(element).split(' ')); + + var tokenString = this.getCurrentValue(element); + var tokens = tokenString ? _.compact(this.tokenize(tokenString)) : []; // optimize this later (to be asymptotically faster) if necessary - for (var i = 0; i < oldClasses.length; i++) { - var c = oldClasses[i]; - if (! _.contains(newClasses, c)) - classes = _.without(classes, c); + for (var i = 0; i < oldTokens.length; i++) { + var c = oldTokens[i]; + if (! _.contains(newTokens, c)) + tokens = _.without(tokens, c); } - for (var i = 0; i < newClasses.length; i++) { - var c = newClasses[i]; - if ((! _.contains(oldClasses, c)) && - (! _.contains(classes, c))) - classes.push(c); + for (var i = 0; i < newTokens.length; i++) { + var c = newTokens[i]; + if ((! _.contains(oldTokens, c)) && + (! _.contains(tokens, c))) + tokens.push(c); } - this.setValue(element, classes.join(' ')); + this.setValue(element, this.stringify(tokens)); } }); -var ClassHandler = BaseClassHandler.extend({ +var ClassHandler = BaseTokenHandler.extend({ // @param rawValue {String} getCurrentValue: function (element) { return element.className; }, setValue: function (element, className) { element.className = className; + }, + tokenize: function (attrString) { + return attrString.split(' '); + }, + stringify: function (tokens) { + return tokens.join(' '); } }); -var SVGClassHandler = BaseClassHandler.extend({ +var SVGClassHandler = BaseTokenHandler.extend({ getCurrentValue: function (element) { return element.className.baseVal; }, setValue: function (element, className) { element.setAttribute('class', className); + }, + tokenize: function (attrString) { + return attrString.split(' '); + }, + stringify: function (tokens) { + return tokens.join(' '); + } +}); + +var StyleHandler = BaseTokenHandler.extend({ + getCurrentValue: function (element) { + return element.getAttribute("style") || ''; + }, + setValue: function (element, style) { + element.setAttribute("style", style); + }, + tokenize: function (attrString) { + var tokens = []; + + // Regex for parsing a css attribute declaration, taken from css-parse. + var regex = /(\*?[-#\/\*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*:\s*((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)[;\s]*/g; + var match = regex.exec(attrString); + while (match) { + var token = match[1] + ":" + match[2]; + tokens.push(token); + match = regex.exec(attrString); + } + + return tokens; + }, + stringify: function (tokens) { + return tokens.join('; ') + ';'; } }); @@ -230,6 +272,8 @@ makeAttributeHandler = function (elem, name, value) { } else { return new ClassHandler(name, value); } + } else if (name === 'style') { + return new StyleHandler(name, value); } else if ((elem.tagName === 'OPTION' && name === 'selected') || (elem.tagName === 'INPUT' && name === 'checked')) { return new BooleanHandler(name, value); diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 605cc0e4c1..06f7687cc3 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -268,6 +268,44 @@ Tinytest.add("ui - render - reactive attributes", function (test) { test.equal(R.numListeners(), 0); })(); + // Test styles. + (function () { + // Test the case where there is a semicolon in the css attribute. + var R = ReactiveVar({'style': ['foo:"a;aa"; bar: b;'], + id: 'foo'}); + + var spanCode = SPAN({$dynamic: [function () { return R.get(); }]}); + + test.equal(toHTML(spanCode), ''); + + test.equal(R.numListeners(), 0); + + var div = document.createElement("DIV"); + materialize(spanCode, div); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + test.equal(R.numListeners(), 1); + + var span = div.firstChild; + test.equal(span.nodeName, 'SPAN'); + console.log(span.getAttribute("style")); + span.setAttribute("style", 'jquery-style: hidden; ' + span.getAttribute("style")); + + R.set({'style': 'foo:"a;zz;aa"', id: 'bar'}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(R.numListeners(), 1); + + R.set({}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(R.numListeners(), 1); + + $(div).remove(); + + test.equal(R.numListeners(), 0); + })(); + // Test `null`, `undefined`, and `[]` attributes (function () { var R = ReactiveVar({id: 'foo', From c346568cefede0452d662f68e046e947631e8d64 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Tue, 10 Jun 2014 11:33:33 -0700 Subject: [PATCH 12/88] Update StyleAttributeHandler to prevent conflicts in css styles. This fix will ensure that Blaze and external javascript cannot both set the same property on an element. If both Blaze and external javascript modify the same property, then the most recent modification will win. --- packages/ui/attrs.js | 97 +++++++++++++++++++------------------ packages/ui/render_tests.js | 47 +++++++++++++++--- 2 files changed, 91 insertions(+), 53 deletions(-) diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index 83aaae37e7..bc6e0e1f6f 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -48,40 +48,42 @@ AttributeHandler.extend = function (options) { return subType; }; -// Apply the diff between the tokens of "oldValue" and "value" to "element." +/// Apply the diff between the attributes of "oldValue" and "value" to "element." +// +// Each subclass must implement a parseValue method which takes a string +// as an input and returns a dict of attributes. The keys of the dict +// are unique identifiers (ie. css properties in the case of styles), and the +// values are the entire attribute which will be injected into the element. +// // Extended below to support classes, SVG elements and styles. -var BaseTokenHandler = AttributeHandler.extend({ + +var DiffingAttributeHandler = AttributeHandler.extend({ update: function (element, oldValue, value) { - if (!this.getCurrentValue || !this.setValue || - !this.tokenize || !this.stringify) - throw new Error("Missing methods in subclass of 'BaseTokenHandler'"); + if (!this.getCurrentValue || !this.setValue || !this.parseValue) + throw new Error("Missing methods in subclass of 'DiffingAttributeHandler'"); - var oldTokens = oldValue ? _.compact(this.tokenize(oldValue)) : []; - var newTokens = value ? _.compact(this.tokenize(value)) : []; + var oldAttrsMap = oldValue ? this.parseValue(oldValue) : {}; + var newAttrsMap = value ? this.parseValue(value) : {}; - // the current classes on the element, which we will mutate. + // the current attributes on the element, which we will mutate. - var tokenString = this.getCurrentValue(element); - var tokens = tokenString ? _.compact(this.tokenize(tokenString)) : []; + var attrString = this.getCurrentValue(element); + var attrsMap = attrString ? this.parseValue(attrString) : {}; - // optimize this later (to be asymptotically faster) if necessary - for (var i = 0; i < oldTokens.length; i++) { - var c = oldTokens[i]; - if (! _.contains(newTokens, c)) - tokens = _.without(tokens, c); - } - for (var i = 0; i < newTokens.length; i++) { - var c = newTokens[i]; - if ((! _.contains(oldTokens, c)) && - (! _.contains(tokens, c))) - tokens.push(c); - } + _.each(_.keys(oldAttrsMap), function (t) { + if (! (t in newAttrsMap)) + delete attrsMap[t]; + }); - this.setValue(element, this.stringify(tokens)); + _.each(_.keys(newAttrsMap), function (t) { + attrsMap[t] = newAttrsMap[t]; + }); + + this.setValue(element, _.values(attrsMap).join(' ')); } }); -var ClassHandler = BaseTokenHandler.extend({ +var ClassHandler = DiffingAttributeHandler.extend({ // @param rawValue {String} getCurrentValue: function (element) { return element.className; @@ -89,52 +91,55 @@ var ClassHandler = BaseTokenHandler.extend({ setValue: function (element, className) { element.className = className; }, - tokenize: function (attrString) { - return attrString.split(' '); - }, - stringify: function (tokens) { - return tokens.join(' '); + parseValue: function (attrString) { + var tokens = {}; + + _.each(attrString.split(' '), function(token) { + if (token) + tokens[token] = token; + }); + return tokens; } }); -var SVGClassHandler = BaseTokenHandler.extend({ +var SVGClassHandler = ClassHandler.extend({ getCurrentValue: function (element) { return element.className.baseVal; }, setValue: function (element, className) { element.setAttribute('class', className); - }, - tokenize: function (attrString) { - return attrString.split(' '); - }, - stringify: function (tokens) { - return tokens.join(' '); } }); -var StyleHandler = BaseTokenHandler.extend({ +var StyleHandler = DiffingAttributeHandler.extend({ getCurrentValue: function (element) { return element.getAttribute("style") || ''; }, setValue: function (element, style) { element.setAttribute("style", style); }, - tokenize: function (attrString) { - var tokens = []; - // Regex for parsing a css attribute declaration, taken from css-parse. - var regex = /(\*?[-#\/\*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*:\s*((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)[;\s]*/g; + // Parse a string to produce a map from property to attribute string. + // + // Example: + // "color:red; foo:12px" produces a token {color: "color:red", foo:"foo:12px"} + parseValue: function (attrString) { + var tokens = {}; + + // Regex for parsing a css attribute declaration, taken from css-parse: + // https://github.com/reworkcss/css-parse/blob/7cef3658d0bba872cde05a85339034b187cb3397/index.js#L219 + var regex = /(\*?[-#\/\*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*:\s*(?:\'(?:\\\'|.)*?\'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+[;\s]*/g; var match = regex.exec(attrString); while (match) { - var token = match[1] + ":" + match[2]; - tokens.push(token); + // match[0] = entire matching string + // match[1] = css property + // Prefix the token to prevent conflicts with existing properties. + tokens[' ' + match[1]] = match[0].trim(); + match = regex.exec(attrString); } return tokens; - }, - stringify: function (tokens) { - return tokens.join('; ') + ';'; } }); diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 06f7687cc3..3c0ac08834 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -271,7 +271,7 @@ Tinytest.add("ui - render - reactive attributes", function (test) { // Test styles. (function () { // Test the case where there is a semicolon in the css attribute. - var R = ReactiveVar({'style': ['foo:"a;aa"; bar: b;'], + var R = ReactiveVar({'style': 'foo:"a;aa"; bar: b;', id: 'foo'}); var spanCode = SPAN({$dynamic: [function () { return R.get(); }]}); @@ -282,23 +282,22 @@ Tinytest.add("ui - render - reactive attributes", function (test) { var div = document.createElement("DIV"); materialize(spanCode, div); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R.numListeners(), 1); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); - console.log(span.getAttribute("style")); - span.setAttribute("style", 'jquery-style: hidden; ' + span.getAttribute("style")); + span.setAttribute("style", 'jquery-style: hidden;' + span.getAttribute("style")); R.set({'style': 'foo:"a;zz;aa"', id: 'bar'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R.numListeners(), 1); R.set({}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R.numListeners(), 1); $(div).remove(); @@ -306,6 +305,40 @@ Tinytest.add("ui - render - reactive attributes", function (test) { test.equal(R.numListeners(), 0); })(); + // Test that identical styles are successfully overwritten. + (function () { + var R = ReactiveVar({'style': 'foo:a;'}); + + var spanCode = SPAN({$dynamic: [function () { return R.get(); }]}); + + var div = document.createElement("DIV"); + materialize(spanCode, div); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + var span = div.firstChild; + test.equal(span.nodeName, 'SPAN'); + span.setAttribute("style", 'foo:b;'); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + R.set({'style': 'foo:c;'}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + // Test malformed styles + R.set({'style': 'foo:a; bar::d;:e; baz:c;'}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + // Test strange styles + R.set({'style': 'constructor:a; __proto__:b; foo:c;'}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + + R.set({}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), ''); + })(); + // Test `null`, `undefined`, and `[]` attributes (function () { var R = ReactiveVar({id: 'foo', @@ -642,4 +675,4 @@ Tinytest.add("ui - UI.render _nestInCurrentComputation flag", function (test) { test.equal(firstComputation.stopped, false); } }); -}); +}); \ No newline at end of file From 832f3bab0208db9a27837d6da08472d550d020ba Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Tue, 10 Jun 2014 14:51:39 -0700 Subject: [PATCH 13/88] Test the interpretation of event map selectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you listen on “div p”, for example, both the div and the p have to be in the template. --- packages/spacebars-tests/template_tests.html | 28 ++++++++++ packages/spacebars-tests/template_tests.js | 54 ++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index 61b8e4a592..6b75bee13b 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -664,6 +664,34 @@ Hi there! click me + + + + + + + + diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 6bd4909d89..479bd0ff59 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -1730,6 +1730,60 @@ Tinytest.add( } ); +// Make sure that if you bind an event on "div p", for example, +// both the div and the p need to be in the template. jQuery's +// `$(elem).find(...)` works this way, but the browser's +// querySelector doesn't. +Tinytest.add( + "spacebars - template - event map selector scope", + function (test) { + var tmpl = Template.spacebars_test_event_selectors1; + var tmpl2 = Template.spacebars_test_event_selectors2; + var buf = []; + tmpl2.events({ + 'click div p': function (evt) { buf.push(evt.currentTarget.className); } + }); + + var div = renderToDiv(tmpl); + document.body.appendChild(div); + test.equal(buf.join(), ''); + clickIt(div.querySelector('.p1')); + test.equal(buf.join(), ''); + clickIt(div.querySelector('.p2')); + test.equal(buf.join(), 'p2'); + document.body.removeChild(div); + } +); + +if (document.addEventListener) { + // see note about non-bubbling events in the "capuring events" + // templating test for why we use the VIDEO tag. (It would be + // nice to get rid of the network dependency, though.) + // We skip this test in IE 8. + Tinytest.add( + "spacebars - template - event map selector scope (capturing)", + function (test) { + var tmpl = Template.spacebars_test_event_selectors_capturing1; + var tmpl2 = Template.spacebars_test_event_selectors_capturing2; + var buf = []; + tmpl2.events({ + 'play div video': function (evt) { buf.push(evt.currentTarget.className); } + }); + + var div = renderToDiv(tmpl); + document.body.appendChild(div); + test.equal(buf.join(), ''); + simulateEvent(div.querySelector(".video1"), + "play", {}, {bubbles: false}); + test.equal(buf.join(), ''); + simulateEvent(div.querySelector(".video2"), + "play", {}, {bubbles: false}); + test.equal(buf.join(), 'video2'); + document.body.removeChild(div); + } + ); +} + Tinytest.add("spacebars - template - tables", function (test) { var tmpl1 = Template.spacebars_test_tables1; From ab6091ef43608af744c5c90de89f28afc4646c6c Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 10 Jun 2014 15:11:23 -0700 Subject: [PATCH 14/88] Add docs for Meteor.loginWithMDA userEmail option. --- docs/client/api.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/client/api.js b/docs/client/api.js index b9aba4c706..c2686b3aac 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1133,6 +1133,11 @@ Template.api.loginWithExternalService = { 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." + }, + { + name: "userEmail", + type: "String", + descr: "An email address that the external service will use to pre-fill the login prompt. Currently only supported with Meteor developer accounts." } ] }; From 1ce98b5e025af40f39b1f39c8eef85a2de5d690a Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 10 Jun 2014 15:39:06 -0700 Subject: [PATCH 15/88] Add History entry for userEmail option --- History.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/History.md b/History.md index ff8cbaebb3..f7401772d5 100644 --- a/History.md +++ b/History.md @@ -56,6 +56,8 @@ * Allow externally applied CSS style attributes to interop with Blaze dynamic style attributes. +* Add `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. + * Upgraded dependencies: - node: 0.10.28 (from 0.10.26) - uglify-js: 2.4.13 (from 2.4.7) From 31a0db621fbbc3dc2cd8c9eb0640789d0299a276 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 10 Jun 2014 16:04:19 -0700 Subject: [PATCH 16/88] Add History entry for bcrypt --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index f7401772d5..6da2f37fd8 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,8 @@ ## v.NEXT +* Migrate from SRP to bcrypt in `accounts-password`. Users will be + transparently upgraded when they log in. + * The `findAll` method on template instances now returns a vanilla array, not a jQuery object. The `$` method continues to return a jQuery object. #2039 From b3d7434e0635620bc6f1fd4f913670bdd56b36b6 Mon Sep 17 00:00:00 2001 From: Tim Phillips Date: Wed, 28 May 2014 10:49:48 -0700 Subject: [PATCH 17/88] Add command line h1 to docs Remove unused template --- docs/client/api.html | 5 ----- docs/client/commandline.html | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index d2208a75f3..adc97f191e 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -3163,8 +3163,3 @@ code can read `data.txt` by running: {{/each}} - - - diff --git a/docs/client/commandline.html b/docs/client/commandline.html index 27e28ccef4..bc36e6fef5 100644 --- a/docs/client/commandline.html +++ b/docs/client/commandline.html @@ -2,7 +2,7 @@
{{#markdown}} -{{#api_section "commandline"}}Command line{{/api_section}} +

Command line

From 4acfc5c6fffee42842b074ae36c5b3b1b739cf99 Mon Sep 17 00:00:00 2001 From: Frederico Carvalho Date: Wed, 4 Jun 2014 16:32:51 +1000 Subject: [PATCH 18/88] before passing 'user' to email template functions, update with new token information --- packages/accounts-password/password_server.js | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 699f7755b2..396beff8e6 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -322,13 +322,16 @@ Accounts.sendResetPasswordEmail = function (userId, email) { var token = Random.secret(); var when = new Date(); + var tokenRecord = { + token: token, + email: email, + when: when + }; Meteor.users.update(userId, {$set: { - "services.password.reset": { - token: token, - email: email, - when: when - } + "services.password.reset": tokenRecord }}); + // before passing to template, update user object with new token + user.services.password.reset = tokenRecord; var resetPasswordUrl = Accounts.urls.resetPassword(token); @@ -368,16 +371,18 @@ Accounts.sendEnrollmentEmail = function (userId, email) { if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) throw new Error("No such email for user."); - var token = Random.secret(); var when = new Date(); + var tokenRecord = { + token: token, + email: email, + when: when + }; Meteor.users.update(userId, {$set: { - "services.password.reset": { - token: token, - email: email, - when: when - } + "services.password.reset": tokenRecord }}); + // before passing to template, update user object with new token + user.services.password.reset = tokenRecord; var enrollAccountUrl = Accounts.urls.enrollAccount(token); @@ -501,6 +506,8 @@ Accounts.sendVerificationEmail = function (userId, address) { Meteor.users.update( {_id: userId}, {$push: {'services.email.verificationTokens': tokenRecord}}); + // before passing to template, update user object with new token + user.services.email.verificationTokens.push(tokenRecord); var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token); From ab5c2513cd807f22866caa5a014b82e6ae8d96bd Mon Sep 17 00:00:00 2001 From: "J. Bruni" Date: Fri, 6 Jun 2014 08:52:43 -0300 Subject: [PATCH 19/88] Show meaningful deploy error message for long hostname https://github.com/meteor/meteor/issues/1208#issuecomment-45158503 --- tools/deploy.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/deploy.js b/tools/deploy.js index d5061b3b91..4805125b62 100644 --- a/tools/deploy.js +++ b/tools/deploy.js @@ -280,6 +280,14 @@ var printUnauthorizedMessage = function () { // stripping 'http://' or a trailing '/' if present) and return it. If // not, print an error message to stderr and return null. var canonicalizeSite = function (site) { + + if (site.length > 63) { + process.stdout.write( +"Maximum hostname length currently supported is 63 characters.\n" + +"Please, try again with a shorter URL for your site.\n"); + return false; + } + var url = site; if (!url.match(':\/\/')) url = 'http://' + url; From 31a5f4014ff9180b5bc25551df314cc4b36e2a08 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 10 Jun 2014 17:10:34 -0700 Subject: [PATCH 20/88] Improve comment and message Make sure that it shows you that it added '.meteor.com'. --- tools/deploy.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/deploy.js b/tools/deploy.js index 4805125b62..0d017fb0c4 100644 --- a/tools/deploy.js +++ b/tools/deploy.js @@ -280,11 +280,18 @@ var printUnauthorizedMessage = function () { // stripping 'http://' or a trailing '/' if present) and return it. If // not, print an error message to stderr and return null. var canonicalizeSite = function (site) { - + // There are actually two different bugs here. One is that the meteor deploy + // server does not support apps whose total site length is greater than 63 + // (because of how it generates Mongo database names); that can be fixed on + // the server. After that, this check will be too strong, but we still will + // want to check that each *component* of the hostname is at most 63 + // characters (url.parse will do something very strange if a component is + // larger than 63, which is the maximum legal length). if (site.length > 63) { process.stdout.write( -"Maximum hostname length currently supported is 63 characters.\n" + -"Please, try again with a shorter URL for your site.\n"); +"The maximum hostname length currently supported is 63 characters.\n" + +site + " is too long.\n" + +"Please try again with a shorter URL for your site.\n"); return false; } From f65451e196b4115034e01a2c75d8c4b902fb310b Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Wed, 4 Jun 2014 01:56:46 +0200 Subject: [PATCH 21/88] Upgrade Stylus from 0.42.3 to 0.46.3 --- History.md | 3 ++- .../plugin/compileStylus/npm-shrinkwrap.json | 19 ++++++++++++------- packages/stylus/package.js | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/History.md b/History.md index 6da2f37fd8..a3155c1157 100644 --- a/History.md +++ b/History.md @@ -66,8 +66,9 @@ - uglify-js: 2.4.13 (from 2.4.7) - sockjs server: 0.3.9 (from 0.3.8) - websocket-driver: 0.3.4 (from 0.3.2) + - stylus: 0.46.3 (from 0.42.3) -Patches contributed by GitHub users awwx, subhog +Patches contributed by GitHub users awwx, mquandalle, subhog ## v.0.8.1.3 diff --git a/packages/stylus/.npm/plugin/compileStylus/npm-shrinkwrap.json b/packages/stylus/.npm/plugin/compileStylus/npm-shrinkwrap.json index f1651f24a7..b04c343ccb 100644 --- a/packages/stylus/.npm/plugin/compileStylus/npm-shrinkwrap.json +++ b/packages/stylus/.npm/plugin/compileStylus/npm-shrinkwrap.json @@ -23,7 +23,7 @@ } }, "stylus": { - "version": "0.42.3", + "version": "0.46.3", "dependencies": { "css-parse": { "version": "1.7.0" @@ -32,16 +32,24 @@ "version": "0.3.5" }, "debug": { - "version": "0.7.4" + "version": "1.0.1", + "dependencies": { + "ms": { + "version": "0.6.2" + } + } }, "sax": { "version": "0.5.8" }, "glob": { - "version": "3.2.9", + "version": "3.2.11", "dependencies": { + "inherits": { + "version": "2.0.1" + }, "minimatch": { - "version": "0.2.14", + "version": "0.3.0", "dependencies": { "lru-cache": { "version": "2.5.0" @@ -50,9 +58,6 @@ "version": "1.0.0" } } - }, - "inherits": { - "version": "2.0.1" } } } diff --git a/packages/stylus/package.js b/packages/stylus/package.js index c3b995389c..371d6528e6 100644 --- a/packages/stylus/package.js +++ b/packages/stylus/package.js @@ -8,7 +8,7 @@ Package._transitional_registerBuildPlugin({ sources: [ 'plugin/compile-stylus.js' ], - npmDependencies: { stylus: "0.42.3", nib: "1.0.2" } + npmDependencies: { stylus: "0.46.3", nib: "1.0.2" } }); Package.on_test(function (api) { From 89d8f411567e43b398b3334966f3708c3895288a Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 10 Jun 2014 20:59:19 -0700 Subject: [PATCH 22/88] Update History --- History.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/History.md b/History.md index a3155c1157..5005f4302c 100644 --- a/History.md +++ b/History.md @@ -24,16 +24,6 @@ `browser-policy-content` and you don't want your app to send this header, then call `BrowserPolicy.content.allowContentTypeSniffing()`. -* Fix memory leak (introduced in 0.8.1) by making sure to unregister - sessions at the server when they are closed due to heartbeat timeout. - -* Fix hardcoded Twitter URL in `oauth1` package. This fixes a regression - in 0.8.0.1 that broke Atmosphere packages that do OAuth1 - logins. #2154. - -* Add `credentialSecret` argument to `Google.retrieveCredential`, which - was forgotten in a previous release. - * Fix a Blaze memory leak by cleaning up event handlers when a template instance is destroyed. #1997 @@ -57,10 +47,81 @@ the client, don't cache the return value of `cursor.count()` (consistently with the server behavior). `cursor.rewind()` is now a no-op. #2114 -* Allow externally applied CSS style attributes to interop with Blaze dynamic style attributes. +* Allow externally applied CSS style attributes to interop with Blaze + dynamic style attributes. * Add `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. +* Fix uninformative error message when deploying to long hostnames. #1208 + +* Ensure that the user object has updated token information on it before + it is passed to email template functions. #2210 + +* Remove an obsolete hack in reporting line numbers for LESS errors. #2216 + +* Fix a bug where helpers used by {{#with}} were still re-running when + their reactive data sources change after they have been removed from + the DOM. + +* Avoid exceptions when accessing localStorage in certain Internet + Explorer configurations. #1291, #1688. + +* Add `UI._templateInstance()` for accessing the current template + instance from within a block helper. + +* Stop not updating form controls if they're focused. If a field is + edited by one user while another user is focused on it, it will just + lose its value but maintain its focus. #1965 + +* Add tentative API for registering hooks to run when Blaze intends to + insert, move, or remove DOM elements. XXX more detail + +* Export the function that serves the HTTP response at the end of an + OAuth flow as `OAuth._endOfLoginResponse`. This function can be + overridden to make the OAuth popup flow work in certain mobile + environments where `window.opener` is not supported. + +* Remove support for OAuth redirect URLs where a `redirect` query + parameter. This OAuth flow was never documented and never fully + worked. + +* Add `_nestInCurrentComputation` option to `UI.render`, fixing a bug in + {{#each}} when an item is added inside a computation that subsequently + gets invalidated. #2156 + +* Fix bug where "=" was not allowed in helper arguments. #2157 + +* Fix bug when a template tag immediately follows a Spacebars block + comment. #2175 + +* Make `handle.ready()` reactively stop, where `handle` is a + subscription handle. + +* Increase a buffer size to avoid failing when running MongoDB due to a + large number of processes running on the machine, and fix the error + message when the failure does occur. #2158 + +* Fix an error message from `audit-argument-checks` after login. + +* Add --directory flag to `meteor bundle`. Setting this flag outputs a + directory rather than a tarball. + +* Make the DDP server send an error if the client sends a connect + message with a missing or malformed `support` field. #2125 + +* Fix missing `jquery` dependency in the `amplify` package. #2113 + +* Ban inserting EJSON custom types as documents. #2095 + +* Clarify a `meteor mongo` error message when using the MONGO_URL + environment variable. #1256 + +* XXX 1e4838ccd38c2df142591a67d675ac38eb8a5630 #2106 + +* XXX df2820ffd92 + +* XXX 00157d8aed23fc290fb985fef73b1c293fa24e63 + * Upgraded dependencies: - node: 0.10.28 (from 0.10.26) - uglify-js: 2.4.13 (from 2.4.7) From 5e0c9923d441bef51f208db49b1f2c9debd90381 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 10 Jun 2014 21:10:50 -0700 Subject: [PATCH 23/88] Update mailmap/history --- .mailmap | 11 ++++++++++- History.md | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.mailmap b/.mailmap index 42f74158ab..efd15a1605 100644 --- a/.mailmap +++ b/.mailmap @@ -12,6 +12,7 @@ GITHUB: aldeed GITHUB: AlexeyMK GITHUB: apendua GITHUB: arbesfeld +GITHUB: Cangit GITHUB: DenisGorbachev GITHUB: EOT GITHUB: FooBarWidget @@ -20,15 +21,21 @@ GITHUB: OyoKooN GITHUB: RobertLowe GITHUB: ansman GITHUB: awwx +GITHUB: babenzele GITHUB: cmather GITHUB: codeinthehole GITHUB: dandv GITHUB: davegonzalez +GITHUB: ducdigital GITHUB: emgee3 +GITHUB: felixrabe +GITHUB: FredericoC GITHUB: icellan GITHUB: jacott GITHUB: jfhamlin +GITHUB: jbruni GITHUB: justinsb +GITHUB: kentonv GITHUB: marcandre GITHUB: mart-jansink GITHUB: meawoppl @@ -47,7 +54,10 @@ GITHUB: rgould GITHUB: ryw GITHUB: rzymek GITHUB: sdarnell +GITHUB: subhog +GITHUB: tbjers GITHUB: timhaines +GITHUB: tmeasday GITHUB: yeputons GITHUB: zol @@ -65,4 +75,3 @@ METEOR: sixolet METEOR: Slava METEOR: stubailo METEOR: ekatek - diff --git a/History.md b/History.md index 5005f4302c..29376ff15b 100644 --- a/History.md +++ b/History.md @@ -129,7 +129,9 @@ - websocket-driver: 0.3.4 (from 0.3.2) - stylus: 0.46.3 (from 0.42.3) -Patches contributed by GitHub users awwx, mquandalle, subhog +Patches contributed by GitHub users awwx, babenzele, Cangit, dandv, +ducdigital, emgee3, felixrabe, FredericoC, jbruni, kentonv, mizzao, +mquandalle, subhog, tbjers, tmeasday. ## v.0.8.1.3 From 81dbf20ea3a1a0ddc79cf10076a6816b1babed10 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 11 Jun 2014 10:11:20 -0700 Subject: [PATCH 24/88] Ensure that `user` fields exist before setting them. Follow-up to 4acfc5c6f --- packages/accounts-password/password_server.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 396beff8e6..d157dd1b53 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -381,7 +381,9 @@ Accounts.sendEnrollmentEmail = function (userId, email) { Meteor.users.update(userId, {$set: { "services.password.reset": tokenRecord }}); + // before passing to template, update user object with new token + Meteor._ensure(user, "services", "password"); user.services.password.reset = tokenRecord; var enrollAccountUrl = Accounts.urls.enrollAccount(token); @@ -506,7 +508,12 @@ Accounts.sendVerificationEmail = function (userId, address) { Meteor.users.update( {_id: userId}, {$push: {'services.email.verificationTokens': tokenRecord}}); + // before passing to template, update user object with new token + Meteor._ensure(user, "services", "email"); + if (! user.services.email.verificationTokens) { + user.services.email.verificationTokens = []; + } user.services.email.verificationTokens.push(tokenRecord); var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token); From 03aadebf558b17f0e2ea0f5a40dcfd773be63e33 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 11 Jun 2014 12:30:30 -0700 Subject: [PATCH 25/88] followup to 4acfc5c6 --- packages/accounts-password/password_server.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 396beff8e6..a0fbc5b504 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -331,7 +331,7 @@ Accounts.sendResetPasswordEmail = function (userId, email) { "services.password.reset": tokenRecord }}); // before passing to template, update user object with new token - user.services.password.reset = tokenRecord; + Meteor._ensure(user, 'services', 'password').reset = tokenRecord; var resetPasswordUrl = Accounts.urls.resetPassword(token); @@ -382,7 +382,7 @@ Accounts.sendEnrollmentEmail = function (userId, email) { "services.password.reset": tokenRecord }}); // before passing to template, update user object with new token - user.services.password.reset = tokenRecord; + Meteor._ensure(user, 'services', 'password').reset = tokenRecord; var enrollAccountUrl = Accounts.urls.enrollAccount(token); @@ -507,6 +507,10 @@ Accounts.sendVerificationEmail = function (userId, address) { {_id: userId}, {$push: {'services.email.verificationTokens': tokenRecord}}); // before passing to template, update user object with new token + Meteor._ensure(user, 'services', 'email'); + if (!user.services.email.verificationTokens) { + user.services.email.verificationTokens = []; + } user.services.email.verificationTokens.push(tokenRecord); var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token); From c87613378b2e39cf6c88d03e1fe67934618912d1 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 10 Jun 2014 14:58:34 -0700 Subject: [PATCH 26/88] Use 'protocol' property to detect javascript: URLs. This strategy works in Safari 4, unlike what we were doing previously. --- packages/ui/attrs.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index bc6e0e1f6f..8a4ad7e1a2 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -225,24 +225,20 @@ if (Meteor.isClient) { var anchorForNormalization = document.createElement('A'); } -var normalizeUrl = function (url) { +var getUrlProtocol = function (url) { if (Meteor.isClient) { anchorForNormalization.href = url; - return anchorForNormalization.href; + return (anchorForNormalization.protocol || "").toLowerCase(); } else { - throw new Error('normalizeUrl not implemented on the server'); + throw new Error('getUrlProtocol not implemented on the server'); } }; // UrlHandler is an attribute handler for all HTML attributes that take // URL values. It disallows javascript: URLs, unless // UI._allowJavascriptUrls() has been called. To detect javascript: -// urls, we set the attribute and then reads the attribute out of the -// DOM, in order to avoid writing our own URL normalization code. (We -// don't want to be fooled by ' javascript:alert(1)' or -// 'jAvAsCrIpT:alert(1)'.) In future, when the URL interface is more -// widely supported, we can use that, which will be -// cleaner. https://developer.mozilla.org/en-US/docs/Web/API/URL +// urls, we set the attribute on a dummy anchor element and then read +// out the 'protocol' property of the attribute. var origUpdate = AttributeHandler.prototype.update; var UrlHandler = AttributeHandler.extend({ update: function (element, oldValue, value) { @@ -252,8 +248,7 @@ var UrlHandler = AttributeHandler.extend({ if (UI._javascriptUrlsAllowed()) { origUpdate.apply(self, args); } else { - var isJavascriptProtocol = - (normalizeUrl(value).indexOf('javascript:') === 0); + var isJavascriptProtocol = (getUrlProtocol(value) === "javascript:"); if (isJavascriptProtocol) { Meteor._debug("URLs that use the 'javascript:' protocol are not " + "allowed in URL attribute values. " + From 8b9a61b39c27c1e5449a1004bbaaf06d7f8bfd1a Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 10 Jun 2014 14:58:34 -0700 Subject: [PATCH 27/88] Use 'protocol' property to detect javascript: URLs. This strategy works in Safari 4, unlike what we were doing previously. --- packages/ui/attrs.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index bc6e0e1f6f..8a4ad7e1a2 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -225,24 +225,20 @@ if (Meteor.isClient) { var anchorForNormalization = document.createElement('A'); } -var normalizeUrl = function (url) { +var getUrlProtocol = function (url) { if (Meteor.isClient) { anchorForNormalization.href = url; - return anchorForNormalization.href; + return (anchorForNormalization.protocol || "").toLowerCase(); } else { - throw new Error('normalizeUrl not implemented on the server'); + throw new Error('getUrlProtocol not implemented on the server'); } }; // UrlHandler is an attribute handler for all HTML attributes that take // URL values. It disallows javascript: URLs, unless // UI._allowJavascriptUrls() has been called. To detect javascript: -// urls, we set the attribute and then reads the attribute out of the -// DOM, in order to avoid writing our own URL normalization code. (We -// don't want to be fooled by ' javascript:alert(1)' or -// 'jAvAsCrIpT:alert(1)'.) In future, when the URL interface is more -// widely supported, we can use that, which will be -// cleaner. https://developer.mozilla.org/en-US/docs/Web/API/URL +// urls, we set the attribute on a dummy anchor element and then read +// out the 'protocol' property of the attribute. var origUpdate = AttributeHandler.prototype.update; var UrlHandler = AttributeHandler.extend({ update: function (element, oldValue, value) { @@ -252,8 +248,7 @@ var UrlHandler = AttributeHandler.extend({ if (UI._javascriptUrlsAllowed()) { origUpdate.apply(self, args); } else { - var isJavascriptProtocol = - (normalizeUrl(value).indexOf('javascript:') === 0); + var isJavascriptProtocol = (getUrlProtocol(value) === "javascript:"); if (isJavascriptProtocol) { Meteor._debug("URLs that use the 'javascript:' protocol are not " + "allowed in URL attribute values. " + From 83132a921f659a9abccea0e853437c5f5932a70d Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Wed, 11 Jun 2014 17:25:31 -0700 Subject: [PATCH 28/88] Remove unecessary import from less_tests --- packages/less/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/less/package.js b/packages/less/package.js index 75e5a1b580..aed84f460d 100644 --- a/packages/less/package.js +++ b/packages/less/package.js @@ -14,6 +14,6 @@ Package._transitional_registerBuildPlugin({ Package.on_test(function (api) { api.use(['test-helpers', 'tinytest', 'less', 'templating']); api.add_files(['less_tests.less', 'less_tests.js', 'less_tests.html', - 'less_tests.import.less', 'less_tests_empty.less'], + 'less_tests_empty.less'], 'client'); }); From e43d13e9f0b5dddfc193226fffab4ec30232c2e8 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 11 Jun 2014 17:46:46 -0700 Subject: [PATCH 29/88] organization pass on history --- History.md | 150 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 85 insertions(+), 65 deletions(-) diff --git a/History.md b/History.md index 29376ff15b..dd64f586df 100644 --- a/History.md +++ b/History.md @@ -1,71 +1,50 @@ ## v.NEXT + +## v0.8.2 + +#### Meteor Accounts + * Migrate from SRP to bcrypt in `accounts-password`. Users will be transparently upgraded when they log in. +* Show the display name of the currently logged-in user after following + a verification link or password reset link in `accounts-ui`. + +* Add `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. + +* Ensure that the user object has updated token information on it before + it is passed to email template functions. #2210 + +* Export the function that serves the HTTP response at the end of an + OAuth flow as `OAuth._endOfLoginResponse`. This function can be + overridden to make the OAuth popup flow work in certain mobile + environments where `window.opener` is not supported. + +* Remove support for OAuth redirect URLs where a `redirect` query + parameter. This OAuth flow was never documented and never fully + worked. + + +#### Blaze + +* Allow externally applied CSS style attributes to interop with Blaze + dynamic style attributes. + * The `findAll` method on template instances now returns a vanilla array, not a jQuery object. The `$` method continues to return a jQuery object. #2039 -* Speed up updates of NPM modules by upgrading Node to include our fix for - https://github.com/npm/npm/issues/3265 instead of passing `--force` to - `npm install`. - -* Always rebuild on changes to npm-shrinkwrap.json files. #1648 - -* Run server tests from multiple clients serially instead of in - parallel. This allows testing features that modify global server - state. #2088 - -* Add Content-Type headers on JavaScript and CSS resources. - -* Add `X-Content-Type-Options: nosniff` header to - `browser-policy-content`'s default policy. If you are using - `browser-policy-content` and you don't want your app to send this - header, then call `BrowserPolicy.content.allowContentTypeSniffing()`. - * Fix a Blaze memory leak by cleaning up event handlers when a template instance is destroyed. #1997 -* Allow `check` to work on the server outside of a Fiber. #2136 - -* EJSON custom type conversion functions should not be permitted to yield. #2136 - -* The legacy polling observe driver handles errors communicating with MongoDB - better and no longer gets "stuck" in some circumstances. - * Add {{> UI.dynamic}} to make it easier to dynamically render a template with a data context. XXX Update "Using Blaze" wiki page. -* Show the display name of the currently logged-in user after following - a verification link or password reset link in `accounts-ui`. - -* Use `Meteor.absoluteUrl()` to compute the redirect URI in `force-ssl` - instead of the host header. - -* Automatically rewind cursors before calls to `fetch`, `forEach`, or `map`. On - the client, don't cache the return value of `cursor.count()` (consistently - with the server behavior). `cursor.rewind()` is now a no-op. #2114 - -* Allow externally applied CSS style attributes to interop with Blaze - dynamic style attributes. - -* Add `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. - -* Fix uninformative error message when deploying to long hostnames. #1208 - -* Ensure that the user object has updated token information on it before - it is passed to email template functions. #2210 - -* Remove an obsolete hack in reporting line numbers for LESS errors. #2216 - * Fix a bug where helpers used by {{#with}} were still re-running when their reactive data sources change after they have been removed from the DOM. -* Avoid exceptions when accessing localStorage in certain Internet - Explorer configurations. #1291, #1688. - * Add `UI._templateInstance()` for accessing the current template instance from within a block helper. @@ -76,15 +55,6 @@ * Add tentative API for registering hooks to run when Blaze intends to insert, move, or remove DOM elements. XXX more detail -* Export the function that serves the HTTP response at the end of an - OAuth flow as `OAuth._endOfLoginResponse`. This function can be - overridden to make the OAuth popup flow work in certain mobile - environments where `window.opener` is not supported. - -* Remove support for OAuth redirect URLs where a `redirect` query - parameter. This OAuth flow was never documented and never fully - worked. - * Add `_nestInCurrentComputation` option to `UI.render`, fixing a bug in {{#each}} when an item is added inside a computation that subsequently gets invalidated. #2156 @@ -94,18 +64,71 @@ * Fix bug when a template tag immediately follows a Spacebars block comment. #2175 -* Make `handle.ready()` reactively stop, where `handle` is a - subscription handle. + +### Tool + +* Speed up updates of NPM modules by upgrading Node to include our fix for + https://github.com/npm/npm/issues/3265 instead of passing `--force` to + `npm install`. + +* Always rebuild on changes to npm-shrinkwrap.json files. #1648 + +* Fix uninformative error message when deploying to long hostnames. #1208 * Increase a buffer size to avoid failing when running MongoDB due to a large number of processes running on the machine, and fix the error message when the failure does occur. #2158 -* Fix an error message from `audit-argument-checks` after login. - * Add --directory flag to `meteor bundle`. Setting this flag outputs a directory rather than a tarball. +* Clarify a `meteor mongo` error message when using the MONGO_URL + environment variable. #1256 + + +### Testing + +* Run server tests from multiple clients serially instead of in + parallel. This allows testing features that modify global server + state. #2088 + + +### Security + +* Add Content-Type headers on JavaScript and CSS resources. + +* Add `X-Content-Type-Options: nosniff` header to + `browser-policy-content`'s default policy. If you are using + `browser-policy-content` and you don't want your app to send this + header, then call `BrowserPolicy.content.allowContentTypeSniffing()`. + +* Use `Meteor.absoluteUrl()` to compute the redirect URI in `force-ssl` + instead of the host header. + + +### Miscellaneous + +* Allow `check` to work on the server outside of a Fiber. #2136 + +* EJSON custom type conversion functions should not be permitted to yield. #2136 + +* The legacy polling observe driver handles errors communicating with MongoDB + better and no longer gets "stuck" in some circumstances. + +* Automatically rewind cursors before calls to `fetch`, `forEach`, or `map`. On + the client, don't cache the return value of `cursor.count()` (consistently + with the server behavior). `cursor.rewind()` is now a no-op. #2114 + +* Remove an obsolete hack in reporting line numbers for LESS errors. #2216 + +* Avoid exceptions when accessing localStorage in certain Internet + Explorer configurations. #1291, #1688. + +* Make `handle.ready()` reactively stop, where `handle` is a + subscription handle. + +* Fix an error message from `audit-argument-checks` after login. + * Make the DDP server send an error if the client sends a connect message with a missing or malformed `support` field. #2125 @@ -113,9 +136,6 @@ * Ban inserting EJSON custom types as documents. #2095 -* Clarify a `meteor mongo` error message when using the MONGO_URL - environment variable. #1256 - * XXX 1e4838ccd38c2df142591a67d675ac38eb8a5630 #2106 * XXX df2820ffd92 From 2554228e493876272d7a36173acad5d02448bfda Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 11 Jun 2014 17:50:38 -0700 Subject: [PATCH 30/88] History tweaks --- History.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/History.md b/History.md index dd64f586df..70e4210ffa 100644 --- a/History.md +++ b/History.md @@ -13,7 +13,7 @@ * Add `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. -* Ensure that the user object has updated token information on it before +* Ensure that the user object has updated token information before it is passed to email template functions. #2210 * Export the function that serves the HTTP response at the end of an @@ -21,14 +21,14 @@ overridden to make the OAuth popup flow work in certain mobile environments where `window.opener` is not supported. -* Remove support for OAuth redirect URLs where a `redirect` query +* Remove support for OAuth redirect URLs with a `redirect` query parameter. This OAuth flow was never documented and never fully worked. #### Blaze -* Allow externally applied CSS style attributes to interop with Blaze +* Allow externally applied CSS style attributes to interoperate with Blaze dynamic style attributes. * The `findAll` method on template instances now returns a vanilla @@ -65,7 +65,10 @@ comment. #2175 -### Tool +#### Command-line tool + +* Add --directory flag to `meteor bundle`. Setting this flag outputs a + directory rather than a tarball. * Speed up updates of NPM modules by upgrading Node to include our fix for https://github.com/npm/npm/issues/3265 instead of passing `--force` to @@ -79,21 +82,18 @@ large number of processes running on the machine, and fix the error message when the failure does occur. #2158 -* Add --directory flag to `meteor bundle`. Setting this flag outputs a - directory rather than a tarball. - * Clarify a `meteor mongo` error message when using the MONGO_URL environment variable. #1256 -### Testing +#### Testing * Run server tests from multiple clients serially instead of in parallel. This allows testing features that modify global server state. #2088 -### Security +#### Security * Add Content-Type headers on JavaScript and CSS resources. @@ -102,11 +102,11 @@ `browser-policy-content` and you don't want your app to send this header, then call `BrowserPolicy.content.allowContentTypeSniffing()`. -* Use `Meteor.absoluteUrl()` to compute the redirect URI in `force-ssl` - instead of the host header. +* Use `Meteor.absoluteUrl()` to compute the redirect URL in the `force-ssl` + package (instead of the host header). -### Miscellaneous +#### Miscellaneous * Allow `check` to work on the server outside of a Fiber. #2136 From 8d1086aa7453aa8617365b2f8a2028e58600c3cb Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Thu, 12 Jun 2014 19:14:49 -0700 Subject: [PATCH 31/88] Add spiderable test to deploy-examples --- scripts/admin/deploy-examples.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/admin/deploy-examples.sh b/scripts/admin/deploy-examples.sh index 8377680bbe..6c720b762e 100755 --- a/scripts/admin/deploy-examples.sh +++ b/scripts/admin/deploy-examples.sh @@ -59,3 +59,13 @@ cd .. # meteor root echo -n "* Configuring OAuth for $PREFIX-parties.meteor.com... " meteor --release $RELEASE mongo $PREFIX-parties.meteor.com >> $LOG 2>&1 < scripts/admin/configure_parties.js echo DONE + +echo -n "* Testing spiderable on $PREFIX-todos.meteor.com... " +(curl "http://$PREFIX-todos.meteor.com?_escaped_fragment_=1" 2> $LOG | + (grep Lovelace >> $LOG 2>&1)) +if [ $? -eq 0 ]; then + echo DONE +else + echo FAILED + exit 1 +fi From eb4ad2c56643f1b4e7619e62208f57c36aa1d64f Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Thu, 12 Jun 2014 19:19:13 -0700 Subject: [PATCH 32/88] Remove deploy-examples. Moving it to automated-unit-tests --- scripts/admin/configure_parties.js | 12 ----- scripts/admin/deploy-examples.sh | 71 ------------------------------ 2 files changed, 83 deletions(-) delete mode 100644 scripts/admin/configure_parties.js delete mode 100755 scripts/admin/deploy-examples.sh diff --git a/scripts/admin/configure_parties.js b/scripts/admin/configure_parties.js deleted file mode 100644 index d60677747d..0000000000 --- a/scripts/admin/configure_parties.js +++ /dev/null @@ -1,12 +0,0 @@ -db.meteor_accounts_loginServiceConfiguration.insert({ - "service" : "facebook", - "appId" : "137758583064594", - "secret" : "3915c1077d25e56fc6444498c6a7984d", - "_id" : "gjpDD9vwGw2tF45ww" -}); -db.meteor_accounts_loginServiceConfiguration.insert({ - "service" : "twitter", - "consumerKey" : "4HF4e0BhNRR7WwC9WqhRBLPRK", - "secret" : "VSzSnLU2W0dT64a9XGVqKhYo90yAu9pQIJ6McTtIRyRzVopHvT", - "_id" : "FCXK6RmNhKyhjSBQk" -}); diff --git a/scripts/admin/deploy-examples.sh b/scripts/admin/deploy-examples.sh deleted file mode 100755 index 6c720b762e..0000000000 --- a/scripts/admin/deploy-examples.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash -set -e - -if [ -z $1 ]; then - echo "This script is to be used in advance of running automated QA on Rainforest" - echo - echo "Usage: ./deploy-example.sh RELEASE" - exit 1 -fi - -RELEASE=$1 - -cd `dirname "$0"`/../.. -METEOR_ROOT=`pwd` -LOG="$METEOR_ROOT/rainforestqa-deploy.log" -rm $LOG &> /dev/null || true - -# Store the original contents in ~/.meteorsession, which contain the -# credentials for the currently logged-in user. Restore that file if -# this script exits. -METEORSESSION_RESTORE="$METEOR_ROOT/.meteorsession-restore" -cp ~/.meteorsession "$METEORSESSION_RESTORE" -function cleanup { - echo "Logs can be found at $METEOR_ROOT/rainforestqa-deploy.log" - cp "$METEORSESSION_RESTORE" ~/.meteorsession - rm "$METEORSESSION_RESTORE" - rm -rf rainforestqa-tmp -} -trap cleanup EXIT - -# Now, login as rainforestqa. This way, anyone can access apps -# deployed by this script. -(echo rainforestqa; echo rainforestqa;) | meteor login - -PREFIX=rainforest-test -EXAMPLES=`meteor create --list --release $RELEASE | grep '^ ' | cut -c 3-` - -# This is where we'll create the example app to be deployed -rm -rf rainforestqa-tmp || true -mkdir rainforestqa-tmp -cd rainforestqa-tmp - -# Deploy all example apps -for EXAMPLE in $EXAMPLES ; do - SITE=$PREFIX-$EXAMPLE.meteor.com - - # `|| true` so that the script doesn't fail if the the app doesn't exist - meteor deploy -D $SITE >> $LOG 2>&1 || true - meteor create --example $EXAMPLE --release $RELEASE $EXAMPLE >> $LOG 2>&1 - cd $EXAMPLE - echo -n "* Deploying $EXAMPLE to $SITE... " - meteor deploy $SITE >> $LOG 2>&1 - echo DONE - cd .. -done - -# Configure OAuth on parties -cd .. # meteor root -echo -n "* Configuring OAuth for $PREFIX-parties.meteor.com... " -meteor --release $RELEASE mongo $PREFIX-parties.meteor.com >> $LOG 2>&1 < scripts/admin/configure_parties.js -echo DONE - -echo -n "* Testing spiderable on $PREFIX-todos.meteor.com... " -(curl "http://$PREFIX-todos.meteor.com?_escaped_fragment_=1" 2> $LOG | - (grep Lovelace >> $LOG 2>&1)) -if [ $? -eq 0 ]; then - echo DONE -else - echo FAILED - exit 1 -fi From 0950952c26bf018bbfc8930d36c115fc72007ca6 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 11 Jun 2014 12:17:31 -0700 Subject: [PATCH 33/88] partial fix to ui reactive attributes style diffing test It's failing in IE10, maybe other IEs also --- packages/test-helpers/canonicalize_html.js | 35 +++++++++++++++------- packages/ui/render_tests.js | 10 +++---- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js index ba1868d0a3..9f6d369a78 100644 --- a/packages/test-helpers/canonicalize_html.js +++ b/packages/test-helpers/canonicalize_html.js @@ -31,12 +31,15 @@ canonicalizeHtml = function(html) { attrs = attrs.replace(/\s+/g, ' '); // quote unquoted attribute values, as in `type=checkbox`. This // will do the wrong thing if there's an `=` in an attribute value. - attrs = attrs.replace(/(\w)=([^" >/]+)/g, '$1="$2"'); - // for the purpose of splitting attributes in a string like - // 'a="b" c="d"', assume they are separated by a single space - // and values are double-quoted, but allow for spaces inside - // the quotes. Split on space following quote. - var attrList = attrs.replace(/" /g, '"\u0000').split('\u0000'); + attrs = attrs.replace(/(\w)=([^'" >/]+)/g, '$1="$2"'); + + // for the purpose of splitting attributes in a string like 'a="b" + // c="d"', assume they are separated by a single space and values + // are double- or single-quoted, but allow for spaces inside the + // quotes. Split on space following quote. + var attrList = attrs.replace(/(\w)='([^']+)' /g, "$1='$2'\u0000"); + attrList = attrList.replace(/(\w)="([^"]+)" /g, '$1="$2"\u0000'); + attrList = attrList.split("\u0000"); // put attributes in alphabetical order attrList.sort(); @@ -59,11 +62,21 @@ canonicalizeHtml = function(html) { if (key === 'sizset') continue; var value = a[1]; - value = value.replace(/["'`]/g, '"'); - // this check is probably made unreachable by a regex above - // that quotes unquoted attribute values - if (value.charAt(0) !== '"') - value = '"'+value+'"'; + + // make sure the attribute is doubled-quoted + if (value.charAt(0) === '"') { + // Do nothing + } else { + if (value.charAt(0) !== "'") { + // attribute is unquoted. should be unreachable because of + // regex above. + value = '"' + value + '"'; + } else { + // attribute is single-quoted. make it double-quoted. + value = value.replace(/\"/g, """); + } + value = value.replace(/["'`]/g, '"'); + } tagContents.push(key+'='+value); } return '<'+tagContents.join(' ')+'>'; diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 3c0ac08834..b2599ec647 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -271,18 +271,18 @@ Tinytest.add("ui - render - reactive attributes", function (test) { // Test styles. (function () { // Test the case where there is a semicolon in the css attribute. - var R = ReactiveVar({'style': 'foo:"a;aa"; bar: b;', + var R = ReactiveVar({'style': 'foo: "a;aa"; bar: b;', id: 'foo'}); var spanCode = SPAN({$dynamic: [function () { return R.get(); }]}); - test.equal(toHTML(spanCode), ''); + test.equal(toHTML(spanCode), ''); test.equal(R.numListeners(), 0); var div = document.createElement("DIV"); materialize(spanCode, div); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R.numListeners(), 1); @@ -292,7 +292,7 @@ Tinytest.add("ui - render - reactive attributes", function (test) { R.set({'style': 'foo:"a;zz;aa"', id: 'bar'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML, true), ''); test.equal(R.numListeners(), 1); R.set({}); @@ -675,4 +675,4 @@ Tinytest.add("ui - UI.render _nestInCurrentComputation flag", function (test) { test.equal(firstComputation.stopped, false); } }); -}); \ No newline at end of file +}); From 15804badf468b95b144727da484595ef1d26e36b Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 11 Jun 2014 13:51:30 -0700 Subject: [PATCH 34/88] Remove forgotten debugging argument --- packages/ui/render_tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index b2599ec647..7993624cf3 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -292,7 +292,7 @@ Tinytest.add("ui - render - reactive attributes", function (test) { R.set({'style': 'foo:"a;zz;aa"', id: 'bar'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML, true), ''); + test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R.numListeners(), 1); R.set({}); From 4f20802cf9476fae94629946667672eb39535405 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Wed, 11 Jun 2014 15:06:39 -0700 Subject: [PATCH 35/88] Fix test expectations --- packages/ui/render_tests.js | 41 ++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 7993624cf3..91ec366575 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -15,6 +15,11 @@ var HR = HTML.HR; var TEXTAREA = HTML.TEXTAREA; var INPUT = HTML.INPUT; +var isIE = function () { + var myNav = navigator.userAgent.toLowerCase(); + return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false; +}; + Tinytest.add("ui - render - basic", function (test) { var run = function (input, expectedInnerHTML, expectedHTML, expectedCode) { var div = document.createElement("DIV"); @@ -285,14 +290,13 @@ Tinytest.add("ui - render - reactive attributes", function (test) { test.equal(canonicalizeHtml(div.innerHTML), ''); test.equal(R.numListeners(), 1); - var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); - span.setAttribute("style", 'jquery-style: hidden;' + span.getAttribute("style")); + span.setAttribute("style", span.getAttribute("style") + 'jquery-style: hidden;'); - R.set({'style': 'foo:"a;zz;aa"', id: 'bar'}); + R.set({'style': 'foo: "a;zz;aa";', id: 'bar'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML, true), ''); test.equal(R.numListeners(), 1); R.set({}); @@ -307,36 +311,41 @@ Tinytest.add("ui - render - reactive attributes", function (test) { // Test that identical styles are successfully overwritten. (function () { - var R = ReactiveVar({'style': 'foo:a;'}); + + var R = ReactiveVar({'style': 'foo: a;'}); var spanCode = SPAN({$dynamic: [function () { return R.get(); }]}); var div = document.createElement("DIV"); + document.body.appendChild(div); materialize(spanCode, div); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), ''); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); - span.setAttribute("style", 'foo:b;'); - test.equal(canonicalizeHtml(div.innerHTML), ''); + span.setAttribute("style", 'foo: b;'); + test.equal(canonicalizeHtml(div.innerHTML), ''); - R.set({'style': 'foo:c;'}); + R.set({'style': 'foo: c;'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), ''); - // Test malformed styles - R.set({'style': 'foo:a; bar::d;:e; baz:c;'}); + // XXX test malformed styles - different expectations in IE from Chrome + R.set({'style': 'foo: a; bar::d;:e; baz: c;'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), + isIE() ? '' : ''); // Test strange styles - R.set({'style': 'constructor:a; __proto__:b; foo:c;'}); + R.set({'style': 'constructor: a; __proto__: b; foo: c;'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), ''); + // XXX test clearing styles - different expectations in IE from Chrome R.set({}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), ''); + test.equal(canonicalizeHtml(div.innerHTML), + isIE() ? '' : ''); })(); // Test `null`, `undefined`, and `[]` attributes From 961965a7c5812ea323afb26c442c0d57232da3bf Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Wed, 11 Jun 2014 15:25:34 -0700 Subject: [PATCH 36/88] fix merge --- packages/ui/render_tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 91ec366575..4fa60165f3 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -296,7 +296,7 @@ Tinytest.add("ui - render - reactive attributes", function (test) { R.set({'style': 'foo: "a;zz;aa";', id: 'bar'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML, true), ''); + test.equal(canonicalizeHtml(div.innerHTML, true), ''); test.equal(R.numListeners(), 1); R.set({}); From a6ecfb3825da96b27424bff00a4766a16313f3e0 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 11 Jun 2014 23:11:01 -0700 Subject: [PATCH 37/88] Use $.trim in StyleHandler when String.trim isn't available. (Safari 4) --- packages/ui/attrs.js | 6 +++++- packages/ui/package.js | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index 8a4ad7e1a2..99992dfa53 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -134,7 +134,11 @@ var StyleHandler = DiffingAttributeHandler.extend({ // match[0] = entire matching string // match[1] = css property // Prefix the token to prevent conflicts with existing properties. - tokens[' ' + match[1]] = match[0].trim(); + + // XXX No `String.trim` on Safari 4. Swap out $.trim if we want to + // remove strong dep on jquery. + tokens[' ' + match[1]] = match[0].trim ? + match[0].trim() : $.trim(match[0]); match = regex.exec(attrString); } diff --git a/packages/ui/package.js b/packages/ui/package.js index 11d0c040ce..2f50cf3942 100644 --- a/packages/ui/package.js +++ b/packages/ui/package.js @@ -6,6 +6,9 @@ Package.describe({ Package.on_use(function (api) { api.export(['UI', 'Handlebars']); api.use('jquery'); // should be a weak dep, by having multiple "DOM backends" + // XXX StyleHandler uses $.trim since Safari 4 doesn't support + // `String.trim`. We should just replace this with our own `trim` if + // we want to make jquery a weak dep. api.use('deps'); api.use('random'); api.use('ejson'); From dd034b3f0b1d67b24613b8f865dc317d67a0bc1c Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Thu, 12 Jun 2014 15:16:26 -0700 Subject: [PATCH 38/88] Maintain old behavior of plaintext password handler. This is for backwards compatibility with old standalone DDP clients (like the Meteor command line tool). Note that we are not maintaining back-compat with old standalone DDP clients that implemented SRP. --- packages/accounts-password/password_server.js | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index d157dd1b53..842abb4f0d 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -154,11 +154,30 @@ Accounts.registerLoginHandler("password", function (options) { throw new Meteor.Error(403, "User has no password set"); if (!user.services.password.bcrypt) { - // Tell the client to use the SRP upgrade process. - throw new Meteor.Error(400, "old password format", EJSON.stringify({ - format: 'srp', - identity: user.services.password.srp.identity - })); + if (typeof options.password === "string") { + // The client has presented a plaintext password, and the user is + // not upgraded to bcrypt yet. We don't attempt to tell the client + // to upgrade to bcrypt, because it might be a standalone DDP + // client doesn't know how to do such a thing. + var verifier = user.services.password.srp; + var newVerifier = SRP.generateVerifier(options.password, { + identity: verifier.identity, salt: verifier.salt}); + + if (verifier.verifier !== newVerifier.verifier) { + return { + userId: user._id, + error: new Meteor.Error(403, "Incorrect password") + }; + } + + return {userId: user._id}; + } else { + // Tell the client to use the SRP upgrade process. + throw new Meteor.Error(400, "old password format", EJSON.stringify({ + format: 'srp', + identity: user.services.password.srp.identity + })); + } } return checkPassword( From cde31045a2a5547ec8ddd463f2a308e4422a1c18 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 13 Jun 2014 14:51:36 -0700 Subject: [PATCH 39/88] Don't swallow the error from the method invocation in case when user didn't submit a callback. --- packages/livedata/livedata_connection.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index b3468cad37..004d4e7c28 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -784,8 +784,11 @@ _.extend(Connection.prototype, { if (Meteor.isClient) { // On the client, we don't have fibers, so we can't block. The // only thing we can do is to return undefined and discard the - // result of the RPC. - callback = function () {}; + // result of the RPC. If an error occurred then print the error + // to the console. + callback = function (err) { + err && Meteor._debug("Error from Method invocation:", err.stack); + }; } else { // On the server, make the function synchronous. Throw on // errors, return on success. From f6b8549adcf51d70316355ad6e67c1ded9da5876 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 13 Jun 2014 15:23:25 -0700 Subject: [PATCH 40/88] Remove useless stacktrace from error reporting, add the method name --- packages/livedata/livedata_connection.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 004d4e7c28..33e2c560bb 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -787,7 +787,8 @@ _.extend(Connection.prototype, { // result of the RPC. If an error occurred then print the error // to the console. callback = function (err) { - err && Meteor._debug("Error from Method invocation:", err.stack); + err && Meteor._debug("Error from Method '" + name + "' invocation:", + err.message); }; } else { // On the server, make the function synchronous. Throw on From 4a5f2d544c2da561c4dc60605b736bc40d22d5fd Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 13 Jun 2014 15:28:07 -0700 Subject: [PATCH 41/88] Change wording --- packages/livedata/livedata_connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 33e2c560bb..5502857651 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -787,7 +787,7 @@ _.extend(Connection.prototype, { // result of the RPC. If an error occurred then print the error // to the console. callback = function (err) { - err && Meteor._debug("Error from Method '" + name + "' invocation:", + err && Meteor._debug("Error invoking Method '" + name + "':", err.message); }; } else { From 566302f1cb8d20acbc84e7b293cde153d584c03e Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Sat, 14 Jun 2014 14:23:08 -0700 Subject: [PATCH 42/88] Export the number of bcrypt rounds and checkPassword --- packages/accounts-password/password_server.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 842abb4f0d..b054bb1f76 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -22,7 +22,7 @@ var bcryptCompare = Meteor._wrapAsync(bcrypt.compare); // "sha-256" and then passes the digest to bcrypt. -var BCRYPT_ROUNDS = 10; +Accounts._bcryptRounds = 10; // Given a 'password' from the client, extract the string that we should // bcrypt. 'password' can be one of: @@ -49,7 +49,7 @@ var getPasswordString = function (password) { // var hashPassword = function (password) { password = getPasswordString(password); - return bcryptHash(password, BCRYPT_ROUNDS); + return bcryptHash(password, Accounts._bcryptRounds); }; // Check whether the provided password matches the bcrypt'ed password in @@ -58,7 +58,7 @@ var hashPassword = function (password) { // properties `digest` and `algorithm` (in which case we bcrypt // `password.digest`). // -var checkPassword = function (user, password) { +Accounts._checkPassword = function (user, password) { var result = { userId: user._id }; @@ -71,7 +71,7 @@ var checkPassword = function (user, password) { return result; }; - +var checkPassword = Accounts._checkPassword; /// /// LOGIN From c8a5800cf04502d59a746f29c3d5f5ef1a5a1462 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Sun, 15 Jun 2014 15:55:09 -0700 Subject: [PATCH 43/88] boot.js passes the absolute path to the loaded source relative paths confuse node-inspector and possibly other tools --- tools/server/boot.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/server/boot.js b/tools/server/boot.js index 2076c92c2a..012899605c 100644 --- a/tools/server/boot.js +++ b/tools/server/boot.js @@ -151,7 +151,10 @@ Fiber(function () { // \n is necessary in case final line is a //-comment var wrapped = "(function(Npm, Assets){" + code + "\n})"; - var func = require('vm').runInThisContext(wrapped, fileInfo.path, true); + // It is safer to use the absolute path as different tooling, such as + // node-inspector, can get confused on relative urls. + var absoluteFilePath = __dirname + "/" + fileInfo.path; + var func = require('vm').runInThisContext(wrapped, absoluteFilePath, true); func.call(global, Npm, Assets); // Coffeescript }); From d59ab1850d1196bc57be63e88137d9a637ad3809 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Sun, 15 Jun 2014 16:02:22 -0700 Subject: [PATCH 44/88] Reference source-map in the server-side code's source It is not needed for mapped stack-traces but is necessary for node-inspector. --- tools/bundler.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/bundler.js b/tools/bundler.js index 2244bf0de6..e6df343fe9 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -1141,25 +1141,29 @@ _.extend(JsImage.prototype, { if (! item.targetPath) throw new Error("No targetPath?"); - var loadPath = builder.writeToGeneratedFilename( - item.targetPath, - { data: new Buffer(item.source, 'utf8') }); var loadItem = { - path: loadPath, node_modules: item.nodeModulesDirectory ? item.nodeModulesDirectory.preferredBundlePath : undefined }; if (item.sourceMap) { + // Reference the source map in the source. Looked up later by node-inspector. + var sourceMapBaseName = item.targetPath + ".map"; + var sourceMapFileName = path.basename(sourceMapBaseName); + item.source = item.source.concat("\n//# sourceMappingURL=" + sourceMapFileName + "\n"); + // Write the source map. - // XXX this code is very similar to saveAsUnipackage. loadItem.sourceMap = builder.writeToGeneratedFilename( - item.targetPath + '.map', + sourceMapBaseName, { data: new Buffer(item.sourceMap, 'utf8') } ); loadItem.sourceMapRoot = item.sourceMapRoot; } + loadItem.path = builder.writeToGeneratedFilename( + item.targetPath, + { data: new Buffer(item.source, 'utf8') }); + if (!_.isEmpty(item.assets)) { // For package code, static assets go inside a directory inside // assets/packages specific to this package. Application assets (e.g. those From 7336bf901bba49f8447d57ffaefc5d5837bbc2ac Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 16 Jun 2014 18:13:00 -0700 Subject: [PATCH 45/88] bump some self-test timeouts --- tools/tests/login.js | 7 +++++++ tools/tests/run.js | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/tests/login.js b/tools/tests/login.js index 876eeffe7c..0e793bf3f5 100644 --- a/tools/tests/login.js +++ b/tools/tests/login.js @@ -19,6 +19,7 @@ selftest.define("login", ['net'], function () { // even if you are already logged in. for (var i = 0; i < 2; i++) { run = s.run("login"); + run.waitSecs(commandTimeoutSecs); run.matchErr("Username:"); run.write("test\n"); run.matchErr("Password:"); @@ -31,6 +32,7 @@ selftest.define("login", ['net'], function () { // Leaving username blank, or getting the password wrong, doesn't // reprompt. It also doesn't log you out. run = s.run("login"); + run.waitSecs(commandTimeoutSecs); run.matchErr("Username:"); run.write("\n"); run.matchErr("Password:"); @@ -40,6 +42,7 @@ selftest.define("login", ['net'], function () { run.expectExit(1); run = s.run("login"); + run.waitSecs(commandTimeoutSecs); run.matchErr("Username:"); run.write("test\n"); run.matchErr("Password:"); @@ -49,6 +52,7 @@ selftest.define("login", ['net'], function () { run.expectExit(1); run = s.run('login'); + run.waitSecs(commandTimeoutSecs); run.matchErr("Username:"); run.write("test\n"); run.matchErr("Password:"); @@ -80,6 +84,7 @@ selftest.define("login", ['net'], function () { // Test login failure run = s.run("login"); + run.waitSecs(commandTimeoutSecs); run.matchErr("Username:"); run.write("test\n"); run.matchErr("Password:"); @@ -91,6 +96,7 @@ selftest.define("login", ['net'], function () { // Logging in with a capitalized username should work (usernames are // case-insensitive). run = s.run("login"); + run.waitSecs(commandTimeoutSecs); run.matchErr("Username:"); run.write("TeSt\n"); run.matchErr("Password:"); @@ -107,6 +113,7 @@ selftest.define("login", ['net'], function () { // Logging in with a capitalized password should NOT work (can't be // too safe...) run = s.run("login"); + run.waitSecs(commandTimeoutSecs); run.matchErr("Username:"); run.write("test\n"); run.matchErr("Password:"); diff --git a/tools/tests/run.js b/tools/tests/run.js index 80e4544544..cc114a024e 100644 --- a/tools/tests/run.js +++ b/tools/tests/run.js @@ -180,7 +180,7 @@ selftest.define("run --once", function () { s.cd("onceapp"); s.set("RUN_ONCE_OUTCOME", "mongo"); run = s.run("--once"); - run.waitSecs(15); + run.waitSecs(30); run.expectExit(86); }); @@ -199,7 +199,7 @@ selftest.define("run errors", function () { var run = s.run("-p", proxyPort); _.times(3, function () { - run.waitSecs(3); + run.waitSecs(30); run.match("Unexpected mongo exit code 48. Restarting."); }); run.waitSecs(3); From 1180597f0628487d5d33c9182fb345adc403d6ee Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 16 Jun 2014 11:48:26 -0700 Subject: [PATCH 46/88] bump dev bundle; upgrade node --- docs/client/concepts.html | 2 +- meteor | 2 +- scripts/generate-dev-bundle.sh | 4 ++-- tools/bundler.js | 2 +- tools/main.js | 2 +- tools/server/boot.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 0ed41a18b3..e73cd7a8f8 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -740,7 +740,7 @@ To get started, run 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.10 and a MongoDB server. (The current release of Meteor has been tested with Node -0.10.28; older versions contain a serious bug that can cause production servers +0.10.29; older versions contain a serious bug that can cause production servers to stall.) 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 diff --git a/meteor b/meteor index 0e8875133c..93ddb8cc13 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.3.37 +BUNDLE_VERSION=0.3.38 # 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 3346bb6259..d0738cad4f 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -74,9 +74,9 @@ cd build git clone https://github.com/joyent/node.git cd node # When upgrading node versions, also update the values of MIN_NODE_VERSION at -# the top of tools/meteor.js and tools/server/boot.js, and the text in +# the top of tools/main.js and tools/server/boot.js, and the text in # docs/client/concepts.html and the README in tools/bundler.js. -git checkout v0.10.28 +git checkout v0.10.29 ./configure --prefix="$DIR" make -j4 diff --git a/tools/bundler.js b/tools/bundler.js index 2244bf0de6..af3f96aa21 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -1536,7 +1536,7 @@ var writeSiteArchive = function (targets, outputPath, options) { builder.write('README', { data: new Buffer( "This is a Meteor application bundle. It has only one dependency:\n" + -"Node.js 0.10.28 or newer, plus the 'fibers' module. To run the application:\n" + +"Node.js 0.10.29 or newer, plus the 'fibers' module. To run the application:\n" + "\n" + " $ rm -r programs/server/node_modules/fibers\n" + " $ npm install fibers@1.0.1\n" + diff --git a/tools/main.js b/tools/main.js index f7cc871a4b..af83746a1c 100644 --- a/tools/main.js +++ b/tools/main.js @@ -330,7 +330,7 @@ Fiber(function () { // Check required Node version. // This code is duplicated in tools/server/boot.js. - var MIN_NODE_VERSION = 'v0.10.28'; + var MIN_NODE_VERSION = 'v0.10.29'; if (require('semver').lt(process.version, MIN_NODE_VERSION)) { process.stderr.write( 'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n'); diff --git a/tools/server/boot.js b/tools/server/boot.js index 2076c92c2a..72ebf71f1f 100644 --- a/tools/server/boot.js +++ b/tools/server/boot.js @@ -6,7 +6,7 @@ var _ = require('underscore'); var sourcemap_support = require('source-map-support'); // This code is duplicated in tools/main.js. -var MIN_NODE_VERSION = 'v0.10.28'; +var MIN_NODE_VERSION = 'v0.10.29'; if (require('semver').lt(process.version, MIN_NODE_VERSION)) { process.stderr.write( From 36004dc4351f643de409be715096214b489da1bb Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 16 Jun 2014 19:20:19 -0700 Subject: [PATCH 47/88] Use the filepath from builder in the sourceMappingURL ref --- tools/bundler.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/bundler.js b/tools/bundler.js index e6df343fe9..104482654b 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -1149,14 +1149,15 @@ _.extend(JsImage.prototype, { if (item.sourceMap) { // Reference the source map in the source. Looked up later by node-inspector. var sourceMapBaseName = item.targetPath + ".map"; - var sourceMapFileName = path.basename(sourceMapBaseName); - item.source = item.source.concat("\n//# sourceMappingURL=" + sourceMapFileName + "\n"); // Write the source map. loadItem.sourceMap = builder.writeToGeneratedFilename( sourceMapBaseName, { data: new Buffer(item.sourceMap, 'utf8') } ); + + var sourceMapFileName = path.basename(loadItem.sourceMap); + item.source = item.source.concat("\n//# sourceMappingURL=" + sourceMapFileName + "\n"); loadItem.sourceMapRoot = item.sourceMapRoot; } From f6084927f9c8166301a46f065d6fb32615df8c9c Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 16 Jun 2014 19:22:10 -0700 Subject: [PATCH 48/88] Prefer += operator for strings concatenation to String#concat --- tools/bundler.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/bundler.js b/tools/bundler.js index 104482654b..b13a4b99e4 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -1147,7 +1147,8 @@ _.extend(JsImage.prototype, { }; if (item.sourceMap) { - // Reference the source map in the source. Looked up later by node-inspector. + // Reference the source map in the source. Looked up later by + // node-inspector. var sourceMapBaseName = item.targetPath + ".map"; // Write the source map. @@ -1157,7 +1158,7 @@ _.extend(JsImage.prototype, { ); var sourceMapFileName = path.basename(loadItem.sourceMap); - item.source = item.source.concat("\n//# sourceMappingURL=" + sourceMapFileName + "\n"); + item.source += "\n//# sourceMappingURL=" + sourceMapFileName + "\n"; loadItem.sourceMapRoot = item.sourceMapRoot; } From 21c4caf2f703f182cb79956edd2c23c762bf20c3 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 16 Jun 2014 19:22:33 -0700 Subject: [PATCH 49/88] Resolve paths with path.resolve --- tools/server/boot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/server/boot.js b/tools/server/boot.js index 012899605c..93c3673e92 100644 --- a/tools/server/boot.js +++ b/tools/server/boot.js @@ -153,7 +153,7 @@ Fiber(function () { // It is safer to use the absolute path as different tooling, such as // node-inspector, can get confused on relative urls. - var absoluteFilePath = __dirname + "/" + fileInfo.path; + var absoluteFilePath = path.resolve(__dirname, fileInfo.path); var func = require('vm').runInThisContext(wrapped, absoluteFilePath, true); func.call(global, Npm, Assets); // Coffeescript }); From 7258643fbf9d0adb41ca6eb6391a92ff44028b49 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 16 Jun 2014 19:32:58 -0700 Subject: [PATCH 50/88] For files with source maps, use absolute paths map the stack-trace. If no source map found, don't bother. --- tools/server/boot.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/server/boot.js b/tools/server/boot.js index 93c3673e92..16b48d1df6 100644 --- a/tools/server/boot.js +++ b/tools/server/boot.js @@ -57,7 +57,7 @@ _.each(serverJson.load, function (fileInfo) { parsedSourceMap.sourceRoot = path.join( fileInfo.sourceMapRoot, parsedSourceMap.sourceRoot || ''); } - parsedSourceMaps[fileInfo.path] = parsedSourceMap; + parsedSourceMaps[path.resolve(__dirname, fileInfo.path)] = parsedSourceMap; } }); @@ -154,7 +154,9 @@ Fiber(function () { // It is safer to use the absolute path as different tooling, such as // node-inspector, can get confused on relative urls. var absoluteFilePath = path.resolve(__dirname, fileInfo.path); - var func = require('vm').runInThisContext(wrapped, absoluteFilePath, true); + var scriptPath = + parsedSourceMaps[absoluteFilePath] ? absoluteFilePath : fileInfo.path; + var func = require('vm').runInThisContext(wrapped, scriptPath, true); func.call(global, Npm, Assets); // Coffeescript }); From 4dfcba8bdc8e64a1a5becf2d088dc8527bdd0dc5 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 16 Jun 2014 19:36:52 -0700 Subject: [PATCH 51/88] Update the comment --- tools/server/boot.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/server/boot.js b/tools/server/boot.js index 16b48d1df6..59676c9704 100644 --- a/tools/server/boot.js +++ b/tools/server/boot.js @@ -151,8 +151,9 @@ Fiber(function () { // \n is necessary in case final line is a //-comment var wrapped = "(function(Npm, Assets){" + code + "\n})"; - // It is safer to use the absolute path as different tooling, such as - // node-inspector, can get confused on relative urls. + // It is safer to use the absolute path when source map is present as + // different tooling, such as node-inspector, can get confused on relative + // urls. var absoluteFilePath = path.resolve(__dirname, fileInfo.path); var scriptPath = parsedSourceMaps[absoluteFilePath] ? absoluteFilePath : fileInfo.path; From 0a7b1b6acbc6f68701bf13e93a6495a60c9ba611 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Tue, 17 Jun 2014 12:26:48 -0700 Subject: [PATCH 52/88] Adds UI._parentData(n) method in template helpers. This method returns the parent data context which surrounds the helper call. This mirrors the effect of {{..}} in Spacebars. So UI._parentData(2) is equivalent to {{../..}}. --- packages/spacebars-tests/template_tests.html | 18 +++++++++ packages/spacebars-tests/template_tests.js | 39 ++++++++++++++++++++ packages/ui/base.js | 21 +++++++++++ 3 files changed, 78 insertions(+) diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index 6b75bee13b..bf8752c606 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -790,3 +790,21 @@ Hi there! {{/with}}
+ + + + \ No newline at end of file diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 479bd0ff59..f4ac2acbbf 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2170,3 +2170,42 @@ Tinytest.add( test.equal(helperCalled, false); } ); + +Tinytest.add( + "spacebars - access parent data contexts from helper", + function (test) { + var childTmpl = Template.spacebars_test_template_parent_data_helper_child; + var parentTmpl = Template.spacebars_test_template_parent_data_helper; + var rv = new ReactiveVar(0); + + childTmpl.a = ["a"]; + childTmpl.b = new ReactiveVar("b"); + childTmpl.c = ["c"]; + + childTmpl.foo = function () { + var data = UI._parentData(rv.get()); + return data.get === undefined ? data : data.get(); + }; + + var div = renderToDiv(parentTmpl); + test.equal(canonicalizeHtml(div.innerHTML), "d"); + + rv.set(1); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "b"); + + // Test UI._parentData() reactivity + + childTmpl.b.set("bNew"); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "bNew"); + + rv.set(2); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "a"); + + rv.set(3); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "parent"); + } +); \ No newline at end of file diff --git a/packages/ui/base.js b/packages/ui/base.js index ad4f70d3e2..433cf84e64 100644 --- a/packages/ui/base.js +++ b/packages/ui/base.js @@ -368,3 +368,24 @@ UI._templateInstance = function () { } return currentTemplateInstance; }; + +// Returns the data context of the parent which is 'numLevels' above the +// component. Same behavior as {{../..}} in a template, with 'numLevels' +// occurrences of '..'. +UI._parentData = function (numLevels) { + var component = currentComponent.get(); + while (component && numLevels >= 0) { + // Decrement numLevels every time we find a new data context. Break + // once we have reached numLevels < 0. + if (component.data !== undefined && --numLevels < 0) { + break; + } + component = component.parent; + } + + if (! component) { + return null; + } + + return getComponentData(component); +}; \ No newline at end of file From 25dc9d93f4802972fcec6cf4a54e3e64bd845106 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Tue, 17 Jun 2014 13:49:21 -0700 Subject: [PATCH 53/88] Update History.md with 'UI._parentData(n)'. --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 29376ff15b..790c10b8e0 100644 --- a/History.md +++ b/History.md @@ -69,6 +69,9 @@ * Add `UI._templateInstance()` for accessing the current template instance from within a block helper. +* Add 'UI._parentData(n)' for accessing parent data contexts from + within a block helper. + * Stop not updating form controls if they're focused. If a field is edited by one user while another user is focused on it, it will just lose its value but maintain its focus. #1965 From 759d5797ad3ad78cc33ed1b530e7ce219c8b13fe Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Tue, 17 Jun 2014 13:55:53 -0700 Subject: [PATCH 54/88] Fix quotes in History.md --- History.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/History.md b/History.md index 790c10b8e0..2423ca5593 100644 --- a/History.md +++ b/History.md @@ -69,7 +69,7 @@ * Add `UI._templateInstance()` for accessing the current template instance from within a block helper. -* Add 'UI._parentData(n)' for accessing parent data contexts from +* Add `UI._parentData(n)` for accessing parent data contexts from within a block helper. * Stop not updating form controls if they're focused. If a field is From 6b486301602b44201aee9752ec721fb4d979dde2 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Tue, 17 Jun 2014 12:26:48 -0700 Subject: [PATCH 55/88] Adds UI._parentData(n) method in template helpers. This method returns the parent data context which surrounds the helper call. This mirrors the effect of {{..}} in Spacebars. So UI._parentData(2) is equivalent to {{../..}}. --- packages/spacebars-tests/template_tests.html | 18 +++++++++ packages/spacebars-tests/template_tests.js | 39 ++++++++++++++++++++ packages/ui/base.js | 21 +++++++++++ 3 files changed, 78 insertions(+) diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index 6b75bee13b..bf8752c606 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -790,3 +790,21 @@ Hi there! {{/with}} + + + + \ No newline at end of file diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 479bd0ff59..f4ac2acbbf 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2170,3 +2170,42 @@ Tinytest.add( test.equal(helperCalled, false); } ); + +Tinytest.add( + "spacebars - access parent data contexts from helper", + function (test) { + var childTmpl = Template.spacebars_test_template_parent_data_helper_child; + var parentTmpl = Template.spacebars_test_template_parent_data_helper; + var rv = new ReactiveVar(0); + + childTmpl.a = ["a"]; + childTmpl.b = new ReactiveVar("b"); + childTmpl.c = ["c"]; + + childTmpl.foo = function () { + var data = UI._parentData(rv.get()); + return data.get === undefined ? data : data.get(); + }; + + var div = renderToDiv(parentTmpl); + test.equal(canonicalizeHtml(div.innerHTML), "d"); + + rv.set(1); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "b"); + + // Test UI._parentData() reactivity + + childTmpl.b.set("bNew"); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "bNew"); + + rv.set(2); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "a"); + + rv.set(3); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), "parent"); + } +); \ No newline at end of file diff --git a/packages/ui/base.js b/packages/ui/base.js index ad4f70d3e2..433cf84e64 100644 --- a/packages/ui/base.js +++ b/packages/ui/base.js @@ -368,3 +368,24 @@ UI._templateInstance = function () { } return currentTemplateInstance; }; + +// Returns the data context of the parent which is 'numLevels' above the +// component. Same behavior as {{../..}} in a template, with 'numLevels' +// occurrences of '..'. +UI._parentData = function (numLevels) { + var component = currentComponent.get(); + while (component && numLevels >= 0) { + // Decrement numLevels every time we find a new data context. Break + // once we have reached numLevels < 0. + if (component.data !== undefined && --numLevels < 0) { + break; + } + component = component.parent; + } + + if (! component) { + return null; + } + + return getComponentData(component); +}; \ No newline at end of file From c25017edf07e507556c45787aaac2d986fc7ad33 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Tue, 17 Jun 2014 13:49:21 -0700 Subject: [PATCH 56/88] Update History.md with 'UI._parentData(n)'. --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 70e4210ffa..a6030a5e3a 100644 --- a/History.md +++ b/History.md @@ -48,6 +48,9 @@ * Add `UI._templateInstance()` for accessing the current template instance from within a block helper. +* Add 'UI._parentData(n)' for accessing parent data contexts from + within a block helper. + * Stop not updating form controls if they're focused. If a field is edited by one user while another user is focused on it, it will just lose its value but maintain its focus. #1965 From ae0fab33d2b560b78f2429154c11a5ec7a276a3d Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Tue, 17 Jun 2014 13:55:53 -0700 Subject: [PATCH 57/88] Fix quotes in History.md --- History.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/History.md b/History.md index a6030a5e3a..ecc96433d8 100644 --- a/History.md +++ b/History.md @@ -48,7 +48,7 @@ * Add `UI._templateInstance()` for accessing the current template instance from within a block helper. -* Add 'UI._parentData(n)' for accessing parent data contexts from +* Add `UI._parentData(n)` for accessing parent data contexts from within a block helper. * Stop not updating form controls if they're focused. If a field is From 40e45cab001e58835ff10e045a72bbead1c28c18 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 17 Jun 2014 15:33:00 -0700 Subject: [PATCH 58/88] more detail in History --- History.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/History.md b/History.md index ecc96433d8..e0ec79b0af 100644 --- a/History.md +++ b/History.md @@ -5,13 +5,30 @@ #### Meteor Accounts -* Migrate from SRP to bcrypt in `accounts-password`. Users will be - transparently upgraded when they log in. +* Switch `accounts-password` to use bcrypt to store passwords on the + server. (Previous versions of Meteor used a protocol called SRP.) + Users will be transparently transitioned when they log in. This + transition is one-way, so you cannot downgrade a production app once + you upgrade to 0.8.2. If you are maintaining an authenticating DDP + client: + - Clients that use the plaintext password login handler (i.e. call + the `login` method with argument `{ password: }`) will continue to work, but users will not be + transitioned from SRP to bcrypt when logging in with this login + handler. + - Clients that use SRP will no longer work. These clients should + instead directly call the `login` method, as in + `Meteor.loginWithPassword`. The argument to the `login` method + can be either: + - `{ password: <plaintext password> }`, or + - `{ password: { digest: <password hash>, algorithm: "sha-256" } }`, + where the password hash is the hex-encoded SHA256 hash of the + plaintext password. * Show the display name of the currently logged-in user after following a verification link or password reset link in `accounts-ui`. -* Add `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. +* Add a `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. * Ensure that the user object has updated token information before it is passed to email template functions. #2210 From b84c7662f3adebb6067e5d4eb434bdd4a6538a6c Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Tue, 17 Jun 2014 16:58:54 -0700 Subject: [PATCH 59/88] Fix UI._templateInstance() to look at surrounding template. Previously we were using the current component to retrieve the template instance, but what we actually want is the template instance of the surrounding component that is a template. --- packages/spacebars-tests/template_tests.html | 4 +-- packages/spacebars-tests/template_tests.js | 19 ++++++++++--- packages/ui/base.js | 28 ++++++++++++++++---- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index bf8752c606..4477b18c64 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -780,7 +780,7 @@ Hi there! </template> <template name="spacebars_test_template_instance_helper"> - {{foo}} + {{#with true}}{{foo}}{{/with}} </template> <template name="spacebars_test_with_cleanup"> @@ -807,4 +807,4 @@ Hi there! {{/if}} {{/with}} {{/each}} -</template> \ No newline at end of file +</template> diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index f4ac2acbbf..e610da4652 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2115,6 +2115,17 @@ Tinytest.add( } ); +// XXX This is for traversing empty text nodes and should be removed +// on blaze-refactor. +var getSiblingText = function (node, siblingNum) { + var sibling = node; + for (var i = 0; i < siblingNum; i++) { + if (sibling) + sibling = sibling.nextSibling; + } + return $(sibling).text(); +}; + Tinytest.add( "spacebars - access template instance from helper, " + "template instance is kept up-to-date", @@ -2132,11 +2143,13 @@ Tinytest.add( rv.set("first"); Deps.flush(); // `nextSibling` because the first node is an empty text node. - test.equal($(instanceFromHelper.firstNode.nextSibling).text(), "first"); + test.equal(getSiblingText(instanceFromHelper.firstNode, 4), + "first"); rv.set("second"); Deps.flush(); - test.equal($(instanceFromHelper.firstNode.nextSibling).text(), "second"); + test.equal(getSiblingText(instanceFromHelper.firstNode, 4), + "second"); // UI._templateInstance() should throw when called from not within a // helper. @@ -2208,4 +2221,4 @@ Tinytest.add( Deps.flush(); test.equal(canonicalizeHtml(div.innerHTML), "parent"); } -); \ No newline at end of file +); diff --git a/packages/ui/base.js b/packages/ui/base.js index 433cf84e64..c1c49adc98 100644 --- a/packages/ui/base.js +++ b/packages/ui/base.js @@ -206,6 +206,16 @@ findComponentWithProp = function (id, comp) { return null; }; +var findHelperHostComponent = function (comp) { + while (comp) { + if (comp.__helperHost) { + return comp; + } + comp = comp.parent; + } + return null; +}; + findComponentWithHelper = function (id, comp) { while (comp) { if (comp.__helperHost) { @@ -354,17 +364,25 @@ UI._javascriptUrlsAllowed = function () { }; UI._templateInstance = function () { - var component = currentComponent.get(); - if (! component) { + var currentComp = currentComponent.get(); + if (! currentComp) { throw new Error("You can only call UI._templateInstance() from within" + " a helper function."); } + // Find the enclosing component that is a template. (`currentComp` + // could be, for example, an #if or #with, and we want the component + // that is the surrounding template.) + var template = findHelperHostComponent(currentComp); + if (! template) { + throw new Error("Current component is not inside a template?"); + } + // Lazily update the template instance for this helper, and do it only // once. if (! currentTemplateInstance) { - updateTemplateInstance(component); - currentTemplateInstance = component.templateInstance; + updateTemplateInstance(template); + currentTemplateInstance = template.templateInstance; } return currentTemplateInstance; }; @@ -388,4 +406,4 @@ UI._parentData = function (numLevels) { } return getComponentData(component); -}; \ No newline at end of file +}; From 7d9c1da4447cb199eae9831660e3b7be650efe21 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Tue, 17 Jun 2014 21:09:14 -0700 Subject: [PATCH 60/88] Fix removeNode indentation --- packages/ui/domrange.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ui/domrange.js b/packages/ui/domrange.js index dee0e3311a..dc958eaa11 100644 --- a/packages/ui/domrange.js +++ b/packages/ui/domrange.js @@ -7,12 +7,12 @@ var DomBackend = UI.DomBackend; var removeNode = function (n) { - if (n.nodeType === 1 && - n.parentNode._uihooks && n.parentNode._uihooks.removeElement) { - n.parentNode._uihooks.removeElement(n); - } else { + if (n.nodeType === 1 && + n.parentNode._uihooks && n.parentNode._uihooks.removeElement) { + n.parentNode._uihooks.removeElement(n); + } else { n.parentNode.removeChild(n); - } + } }; var insertNode = function (n, parent, next) { From 86aeec461ae1a129acf86ccce1d006bc29bb78f2 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Tue, 17 Jun 2014 22:32:21 -0700 Subject: [PATCH 61/88] Fix canonicalizeHtml regexes. Allows empty, quoted attributes (e.g. <div foo="">) --- packages/test-helpers/canonicalize_html.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js index 9f6d369a78..477b1bfa0f 100644 --- a/packages/test-helpers/canonicalize_html.js +++ b/packages/test-helpers/canonicalize_html.js @@ -37,8 +37,8 @@ canonicalizeHtml = function(html) { // c="d"', assume they are separated by a single space and values // are double- or single-quoted, but allow for spaces inside the // quotes. Split on space following quote. - var attrList = attrs.replace(/(\w)='([^']+)' /g, "$1='$2'\u0000"); - attrList = attrList.replace(/(\w)="([^"]+)" /g, '$1="$2"\u0000'); + var attrList = attrs.replace(/(\w)='([^']*)' /g, "$1='$2'\u0000"); + attrList = attrList.replace(/(\w)="([^"]*)" /g, '$1="$2"\u0000'); attrList = attrList.split("\u0000"); // put attributes in alphabetical order attrList.sort(); From 722b666ab652ced399987e6a4c527be9496f17e8 Mon Sep 17 00:00:00 2001 From: Fredric Endrerud <fredricendrerud@gmail.com> Date: Wed, 11 Jun 2014 12:39:36 +0200 Subject: [PATCH 62/88] Upgrade Less from 1.6.1 to 1.7.1 --- .../plugin/compileLess/npm-shrinkwrap.json | 21 +++++++++++-------- packages/less/package.js | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/less/.npm/plugin/compileLess/npm-shrinkwrap.json b/packages/less/.npm/plugin/compileLess/npm-shrinkwrap.json index f2a90d9a9f..94ab2fc445 100644 --- a/packages/less/.npm/plugin/compileLess/npm-shrinkwrap.json +++ b/packages/less/.npm/plugin/compileLess/npm-shrinkwrap.json @@ -1,13 +1,16 @@ { "dependencies": { "less": { - "version": "1.6.1", + "version": "1.7.1", "dependencies": { + "graceful-fs": { + "version": "2.0.3" + }, "mime": { "version": "1.2.11" }, "request": { - "version": "2.33.0", + "version": "2.34.0", "dependencies": { "qs": { "version": "0.6.6" @@ -16,7 +19,7 @@ "version": "5.0.0" }, "forever-agent": { - "version": "0.5.0" + "version": "0.5.2" }, "node-uuid": { "version": "1.4.1" @@ -25,12 +28,12 @@ "version": "0.12.1", "dependencies": { "punycode": { - "version": "1.2.3" + "version": "1.2.4" } } }, "form-data": { - "version": "0.1.2", + "version": "0.1.3", "dependencies": { "combined-stream": { "version": "0.0.4", @@ -41,7 +44,7 @@ } }, "async": { - "version": "0.2.10" + "version": "0.9.0" } } }, @@ -91,15 +94,15 @@ "version": "0.3.5" }, "clean-css": { - "version": "2.0.7", + "version": "2.1.8", "dependencies": { "commander": { - "version": "2.0.0" + "version": "2.1.0" } } }, "source-map": { - "version": "0.1.31", + "version": "0.1.34", "dependencies": { "amdefine": { "version": "0.1.0" diff --git a/packages/less/package.js b/packages/less/package.js index aed84f460d..08b6a3efff 100644 --- a/packages/less/package.js +++ b/packages/less/package.js @@ -8,7 +8,7 @@ Package._transitional_registerBuildPlugin({ sources: [ 'plugin/compile-less.js' ], - npmDependencies: {"less": "1.6.1"} + npmDependencies: {"less": "1.7.1"} }); Package.on_test(function (api) { From 5b2c55fe330c345b44ee9d2bb2f44ed6d094b16e Mon Sep 17 00:00:00 2001 From: David Glasser <glasser@meteor.com> Date: Wed, 18 Jun 2014 16:15:43 -0700 Subject: [PATCH 63/88] Update history --- History.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/History.md b/History.md index 2423ca5593..618bad1e8e 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +## v.REALLY NEXT + +* Upgraded dependencies: + - less: 1.7.1 (from 1.6.1) + + ## v.NEXT * Migrate from SRP to bcrypt in `accounts-password`. Users will be From d180f9b02a131cd8eefe783c34d1e36e2f7287ef Mon Sep 17 00:00:00 2001 From: David Glasser <glasser@meteor.com> Date: Wed, 18 Jun 2014 16:53:59 -0700 Subject: [PATCH 64/88] Fix "Email already exists" error with MongoDB 2.6 Fixes #2238 --- packages/accounts-base/accounts_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 46d904ec08..95c78c7cf3 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -1023,7 +1023,7 @@ Accounts.insertUserDoc = function (options, user) { // XXX string parsing sucks, maybe // https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day if (e.name !== 'MongoError') throw e; - var match = e.err.match(/^E11000 duplicate key error index: ([^ ]+)/); + var match = e.err.match(/E11000 duplicate key error index: ([^ ]+)/); if (!match) throw e; if (match[1].indexOf('$emails.address') !== -1) throw new Meteor.Error(403, "Email already exists."); From dc2b03aee634cf5b99cb6023b957707b41ff91d6 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Wed, 18 Jun 2014 16:45:47 -0700 Subject: [PATCH 65/88] Tentative maybe fix for UI hooks on nested domranges. --- packages/spacebars-tests/template_tests.html | 14 +++++++++ packages/spacebars-tests/template_tests.js | 33 ++++++++++++++++++++ packages/ui/domrange.js | 4 +-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index 4477b18c64..0964ba4b9f 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -779,6 +779,20 @@ Hi there! </div> </template> +<template name="spacebars_test_ui_hooks_nested"> + {{#if foo}} + {{> spacebars_test_ui_hooks_nested_sub}} + {{/if}} +</template> + +<template name="spacebars_test_ui_hooks_nested_sub"> + <div> + {{#with true}} + <p>hello</p> + {{/with}} + </div> +</template> + <template name="spacebars_test_template_instance_helper"> {{#with true}}{{foo}}{{/with}} </template> diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index e610da4652..599917ca1c 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2093,6 +2093,39 @@ Tinytest.add( } ); +Tinytest.add( + "spacebars - ui hooks - nested domranges", + function (test) { + var tmpl = Template.spacebars_test_ui_hooks_nested; + var rv = new ReactiveVar(true); + + tmpl.foo = function () { + return rv.get(); + }; + + var subtmpl = Template.spacebars_test_ui_hooks_nested_sub; + var uiHookCalled = false; + subtmpl.rendered = function () { + this.firstNode.parentNode._uihooks = { + removeElement: function (node) { + uiHookCalled = true; + } + }; + }; + + var div = renderToDiv(tmpl); + document.body.appendChild(div); + Deps.flush(); + + var htmlBeforeRemove = canonicalizeHtml(div.innerHTML); + rv.set(false); + Deps.flush(); + test.isTrue(uiHookCalled); + var htmlAfterRemove = canonicalizeHtml(div.innerHTML); + test.equal(htmlBeforeRemove, htmlAfterRemove); + } +); + Tinytest.add( "spacebars - access template instance from helper", function (test) { diff --git a/packages/ui/domrange.js b/packages/ui/domrange.js index dc958eaa11..1b46b5cae1 100644 --- a/packages/ui/domrange.js +++ b/packages/ui/domrange.js @@ -11,7 +11,7 @@ var removeNode = function (n) { n.parentNode._uihooks && n.parentNode._uihooks.removeElement) { n.parentNode._uihooks.removeElement(n); } else { - n.parentNode.removeChild(n); + DomBackend.removeElement(n); } }; @@ -154,7 +154,7 @@ var nodeRemoved = function (node, elementsAlreadyRemoved) { if (node.nodeType === 1) { // ELEMENT var comps = DomRange.getComponents(node); for (var i = 0, N = comps.length; i < N; i++) - rangeRemoved(comps[i]); + rangeRemoved(comps[i], elementsAlreadyRemoved); if (! elementsAlreadyRemoved) DomBackend.removeElement(node); From 4bec4877e37ea5548c72636825b0579d404ccecb Mon Sep 17 00:00:00 2001 From: David Glasser <glasser@meteor.com> Date: Wed, 18 Jun 2014 17:02:03 -0700 Subject: [PATCH 66/88] after startup, Meteor.startup(c) should call c now This was the client behavior and is now the server behavior as well. Fixes #2239. --- packages/meteor/helpers_test.js | 9 +++++++++ packages/meteor/startup_server.js | 7 ++++++- tools/server/boot.js | 12 +++++++++--- tools/unipackage.js | 9 +++++++-- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/meteor/helpers_test.js b/packages/meteor/helpers_test.js index ec5afd3859..14d083f085 100644 --- a/packages/meteor/helpers_test.js +++ b/packages/meteor/helpers_test.js @@ -75,3 +75,12 @@ Tinytest.add("environment - helpers", function (test) { Meteor._delete(x, "a"); test.equal(x, {}); }); + +Tinytest.add("environment - startup", function (test) { + // After startup, Meteor.startup should call the callback immediately. + var called = false; + Meteor.startup(function () { + called = true; + }); + test.isTrue(called); +}); diff --git a/packages/meteor/startup_server.js b/packages/meteor/startup_server.js index af0ca8126c..2a08396ac1 100644 --- a/packages/meteor/startup_server.js +++ b/packages/meteor/startup_server.js @@ -1,3 +1,8 @@ Meteor.startup = function (callback) { - __meteor_bootstrap__.startup_hooks.push(callback); + if (__meteor_bootstrap__.startupHooks) { + __meteor_bootstrap__.startupHooks.push(callback); + } else { + // We already started up. Just call it now. + callback(); + } }; diff --git a/tools/server/boot.js b/tools/server/boot.js index ecba19b232..ac5d08b92f 100644 --- a/tools/server/boot.js +++ b/tools/server/boot.js @@ -23,7 +23,7 @@ var configJson = // Set up environment __meteor_bootstrap__ = { - startup_hooks: [], + startupHooks: [], serverDir: serverDir, configJson: configJson }; __meteor_runtime_config__ = { meteorRelease: configJson.meteorRelease }; @@ -161,8 +161,14 @@ Fiber(function () { func.call(global, Npm, Assets); // Coffeescript }); - // run the user startup hooks. - _.each(__meteor_bootstrap__.startup_hooks, function (x) { x(); }); + // run the user startup hooks. other calls to startup() during this can still + // add hooks to the end. + while (__meteor_bootstrap__.startupHooks.length) { + var hook = __meteor_bootstrap__.startupHooks.shift(); + hook(); + } + // Setting this to null tells Meteor.startup to call hooks immediately. + __meteor_bootstrap__.startupHooks = null; // find and run main() // XXX hack. we should know the package that contains main. diff --git a/tools/unipackage.js b/tools/unipackage.js index 53da2f00ad..b4854329f7 100644 --- a/tools/unipackage.js +++ b/tools/unipackage.js @@ -66,7 +66,7 @@ var load = function (options) { // will get refactored before too long. Note that // __meteor_bootstrap__.require is no longer provided. var env = { - __meteor_bootstrap__: { startup_hooks: [] }, + __meteor_bootstrap__: { startupHooks: [] }, __meteor_runtime_config__: { meteorRelease: options.release } }; @@ -83,7 +83,12 @@ var load = function (options) { ret = image.load(env); // Run any user startup hooks. - _.each(env.__meteor_bootstrap__.startup_hooks, function (x) { x(); }); + while (env.__meteor_bootstrap__.startupHooks.length) { + var hook = env.__meteor_bootstrap__.startupHooks.shift(); + hook(); + } + // Setting this to null tells Meteor.startup to call hooks immediately. + env.__meteor_bootstrap__.startupHooks = null; }); if (messages.hasMessages()) { From 3c6c8e591ce4592aaf5350b3375e35435f843667 Mon Sep 17 00:00:00 2001 From: Joe Gallo <gallo.j@gmail.com> Date: Wed, 18 Jun 2014 17:36:51 -0700 Subject: [PATCH 67/88] Only show duplicate id warning if really from _id Fixes #1980 --- packages/observe-sequence/observe_sequence.js | 3 ++- packages/observe-sequence/observe_sequence_tests.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/observe-sequence/observe_sequence.js b/packages/observe-sequence/observe_sequence.js index 7af183f084..e4c1b4dea2 100644 --- a/packages/observe-sequence/observe_sequence.js +++ b/packages/observe-sequence/observe_sequence.js @@ -116,7 +116,8 @@ ObserveSequence = { var idString = idStringify(id); if (idsUsed[idString]) { - warn("duplicate id " + id + " in", seq); + if (typeof item === 'object' && '_id' in item) + warn("duplicate id " + id + " in", seq); id = Random.id(); } else { idsUsed[idString] = true; diff --git a/packages/observe-sequence/observe_sequence_tests.js b/packages/observe-sequence/observe_sequence_tests.js index f950c1492c..ca4bc2e7cd 100644 --- a/packages/observe-sequence/observe_sequence_tests.js +++ b/packages/observe-sequence/observe_sequence_tests.js @@ -483,7 +483,7 @@ Tinytest.add('observe sequence - number arrays', function (test) { {removedAt: [{NOT: 1}, 1, 1]}, {addedAt: [3, 3, 1, 2]}, {addedAt: [{NOT: 3}, 3, 3, null]} - ], /*numExpectedWarnings = */2); + ]); }); Tinytest.add('observe sequence - cursor to other cursor, same collection', function (test) { From b31629de4ef44784d9da481275d252f4b7b18136 Mon Sep 17 00:00:00 2001 From: Slava Kim <slava@meteor.com> Date: Wed, 18 Jun 2014 18:35:12 -0700 Subject: [PATCH 68/88] Fix incorrect arguments to observeSequence#changedAt callback --- packages/ui/each.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/each.js b/packages/ui/each.js index 6a93a73a42..ea3ab262c3 100644 --- a/packages/ui/each.js +++ b/packages/ui/each.js @@ -104,7 +104,7 @@ UI.EachImpl = Component.extend({ LocalCollection._idStringify(id), beforeId && LocalCollection._idStringify(beforeId)); }, - changedAt: function (id, newItem, atIndex) { + changedAt: function (id, newItem, oldItem, atIndex) { range.get(LocalCollection._idStringify(id)).component.data.$set(newItem); } }); From 80ee5c19eec8c19ecb3fe0c700ef3d2a86c5a19b Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Thu, 19 Jun 2014 09:40:28 -0700 Subject: [PATCH 69/88] Add comment to `findHelperHostComponent` --- packages/ui/base.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ui/base.js b/packages/ui/base.js index c1c49adc98..fe18942dd4 100644 --- a/packages/ui/base.js +++ b/packages/ui/base.js @@ -206,6 +206,9 @@ findComponentWithProp = function (id, comp) { return null; }; +// Look up the component's chain of parents until we find one with +// `__helperHost` set (a component that can have helpers defined on it, +// i.e. a template). var findHelperHostComponent = function (comp) { while (comp) { if (comp.__helperHost) { From b845f8e7ebb8c69f7e4a02f1684ebc777077c66e Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Thu, 19 Jun 2014 09:51:33 -0700 Subject: [PATCH 70/88] history pass --- History.md | 54 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/History.md b/History.md index e0ec79b0af..7f5430fdeb 100644 --- a/History.md +++ b/History.md @@ -26,7 +26,7 @@ plaintext password. * Show the display name of the currently logged-in user after following - a verification link or password reset link in `accounts-ui`. + an email verification link or password reset link in `accounts-ui`. * Add a `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. @@ -45,8 +45,36 @@ #### Blaze -* Allow externally applied CSS style attributes to interoperate with Blaze - dynamic style attributes. +* Blaze now tracks individual CSS rules in `style` attributes and won't + overwrite changes to them made by other JavaScript libraries. + +* Add {{> UI.dynamic}} to make it easier to dynamically render a + template with a data context. XXX Update "Using Blaze" wiki page. + +* Add `UI._templateInstance()` for accessing the current template + instance from within a block helper. + +* Add `UI._parentData(n)` for accessing parent data contexts from + within a block helper. + +* Add preliminary API for registering hooks to run when Blaze intends to + insert, move, or remove DOM elements. For example, you can use these + hooks to animate nodes as they are inserted, moved, or removed. To use + them, you can set the `_uihooks` property on a container DOM + element. `_uihooks` is an object that can have any subset of the + following three properties: + + - `insertElement: function (node, next)`: called when Blaze intends + to insert the DOM element `node` before the element `next` + - `moveElement: function (node, next)`: called when Blaze intends to + move the DOM element `node` before the element `next` + - `removeElement: function (node)`: called when Blaze intends to + remove the DOM element `node` + + Note that when you set one of these functions on a container + element, Blaze will not do the actual operation; it's your + responsibility to actually insert, move, or remove the node (by + calling `$(node).remove()`, for example). * The `findAll` method on template instances now returns a vanilla array, not a jQuery object. The `$` method continues to @@ -55,26 +83,14 @@ * Fix a Blaze memory leak by cleaning up event handlers when a template instance is destroyed. #1997 -* Add {{> UI.dynamic}} to make it easier to dynamically render a - template with a data context. XXX Update "Using Blaze" wiki page. - * Fix a bug where helpers used by {{#with}} were still re-running when - their reactive data sources change after they have been removed from + their reactive data sources changed after they had been removed from the DOM. -* Add `UI._templateInstance()` for accessing the current template - instance from within a block helper. - -* Add `UI._parentData(n)` for accessing parent data contexts from - within a block helper. - * Stop not updating form controls if they're focused. If a field is edited by one user while another user is focused on it, it will just lose its value but maintain its focus. #1965 -* Add tentative API for registering hooks to run when Blaze intends to - insert, move, or remove DOM elements. XXX more detail - * Add `_nestInCurrentComputation` option to `UI.render`, fixing a bug in {{#each}} when an item is added inside a computation that subsequently gets invalidated. #2156 @@ -156,11 +172,7 @@ * Ban inserting EJSON custom types as documents. #2095 -* XXX 1e4838ccd38c2df142591a67d675ac38eb8a5630 #2106 - -* XXX df2820ffd92 - -* XXX 00157d8aed23fc290fb985fef73b1c293fa24e63 +* Fix incorrect URL rewrites in stylesheets. #2106 * Upgraded dependencies: - node: 0.10.28 (from 0.10.26) From 4f75911a0ade8179c40755c88abb41095f3a39ea Mon Sep 17 00:00:00 2001 From: Avital Oliver <avital@thewe.net> Date: Fri, 20 Jun 2014 11:53:51 -0700 Subject: [PATCH 71/88] Guard against accidental usage of jQuery objects in UI.insert --- packages/ui/render.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/render.js b/packages/ui/render.js index 31da03bda3..d3f3bec957 100644 --- a/packages/ui/render.js +++ b/packages/ui/render.js @@ -170,6 +170,11 @@ UI.namedEmboxValue = function (name, funcOrValue, equals) { //////////////////////////////////////// UI.insert = function (renderedTemplate, parentElement, nextNode) { + // parentElement must be a DOM node. in particular, can't be the + // result of a call to `$`. Can't do `check(parentElement, Node` + // since 'Node' is undefined in IE8. + check(parentElement.nodeType, Number); + if (! renderedTemplate.dom) throw new Error("Expected template rendered with UI.render"); From 4ec9cb5847be61226538c066523d42bc2d143fd8 Mon Sep 17 00:00:00 2001 From: David Greenspan <dgreenspan@alummit.edu> Date: Fri, 20 Jun 2014 12:08:08 -0700 Subject: [PATCH 72/88] Prevent a test from leaving a DIV behind --- packages/spacebars-tests/template_tests.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 599917ca1c..da45b0bed9 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2123,6 +2123,7 @@ Tinytest.add( test.isTrue(uiHookCalled); var htmlAfterRemove = canonicalizeHtml(div.innerHTML); test.equal(htmlBeforeRemove, htmlAfterRemove); + document.body.removeChild(div); } ); From 35f1dc45dd602514771697a2e45e71e6841682ed Mon Sep 17 00:00:00 2001 From: Avital Oliver <avital@thewe.net> Date: Fri, 20 Jun 2014 12:13:04 -0700 Subject: [PATCH 73/88] Improve error when passing a jQuery object to UI.insert Also add a test for this behavior --- packages/ui/render.js | 7 ++++--- packages/ui/render_tests.js | 13 ++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/ui/render.js b/packages/ui/render.js index d3f3bec957..0259f1b459 100644 --- a/packages/ui/render.js +++ b/packages/ui/render.js @@ -171,9 +171,10 @@ UI.namedEmboxValue = function (name, funcOrValue, equals) { UI.insert = function (renderedTemplate, parentElement, nextNode) { // parentElement must be a DOM node. in particular, can't be the - // result of a call to `$`. Can't do `check(parentElement, Node` - // since 'Node' is undefined in IE8. - check(parentElement.nodeType, Number); + // result of a call to `$`. Can't check if `parentElement instanceof + // Node` since 'Node' is undefined in IE8. + if (typeof parentElement.nodeType !== 'number') + throw new Error("'parentElement' must be a DOM node"); if (! renderedTemplate.dom) throw new Error("Expected template rendered with UI.render"); diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 3c0ac08834..e3ba814fcd 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -618,6 +618,17 @@ Tinytest.add("ui - UI.render", function (test) { document.body.removeChild(div); }); +Tinytest.add("ui - UI.insert fails on jQuery objects", function (test) { + var tmpl = UI.Component.extend({ + render: function () { + return SPAN(); + } + }); + test.throws(function () { + UI.insert(UI.render(tmpl), $('body')); + }, /must be a DOM node/); +}); + Tinytest.add("ui - UI.getDataContext", function (test) { var div = document.createElement("DIV"); @@ -675,4 +686,4 @@ Tinytest.add("ui - UI.render _nestInCurrentComputation flag", function (test) { test.equal(firstComputation.stopped, false); } }); -}); \ No newline at end of file +}); From 2ea77a940902146d91c854440f49ca847a6cde3f Mon Sep 17 00:00:00 2001 From: Avital Oliver <avital@thewe.net> Date: Fri, 20 Jun 2014 12:17:58 -0700 Subject: [PATCH 74/88] Even better checks for the arguments to UI.insert --- packages/ui/render.js | 4 +++- packages/ui/render_tests.js | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ui/render.js b/packages/ui/render.js index 0259f1b459..3867219d93 100644 --- a/packages/ui/render.js +++ b/packages/ui/render.js @@ -173,8 +173,10 @@ UI.insert = function (renderedTemplate, parentElement, nextNode) { // parentElement must be a DOM node. in particular, can't be the // result of a call to `$`. Can't check if `parentElement instanceof // Node` since 'Node' is undefined in IE8. - if (typeof parentElement.nodeType !== 'number') + if (! parentElement || typeof parentElement.nodeType !== 'number') throw new Error("'parentElement' must be a DOM node"); + if (nextNode && typeof nextNode.nodeType !== 'number') // 'nextNode' is optional + throw new Error("'nextNode' must be a DOM node"); if (! renderedTemplate.dom) throw new Error("Expected template rendered with UI.render"); diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index e3ba814fcd..915eff7600 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -626,7 +626,10 @@ Tinytest.add("ui - UI.insert fails on jQuery objects", function (test) { }); test.throws(function () { UI.insert(UI.render(tmpl), $('body')); - }, /must be a DOM node/); + }, /'parentElement' must be a DOM node/); + test.throws(function () { + UI.insert(UI.render(tmpl), document.body, $('body')); + }, /'nextNode' must be a DOM node/); }); Tinytest.add("ui - UI.getDataContext", function (test) { From 085441524fc3f4badbbee1dfe825fe3128dc95ef Mon Sep 17 00:00:00 2001 From: David Greenspan <dgreenspan@alummit.edu> Date: Fri, 20 Jun 2014 12:34:23 -0700 Subject: [PATCH 75/88] =?UTF-8?q?Separate=20element=20removal=20and=20?= =?UTF-8?q?=E2=80=9Cteardown=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DomRange now never removes elements except through the removeElement UI hook. If you write a hook that prevents removal, teardown still happens (e.g. templates stop updating). This code also provides the basis for stopping updates to part of the DOM by triggering teardown without removal. Before, DomBackend.removeElement would both trigger teardown and actually deparent the element. Now we have DomBackend.tearDownElement, which just triggers the jQuery teardown (which in turn triggers finalization of DomRanges that have been inserted in the DOM tree). The flag to {node,members,range}Removed is now named “alreadyTornDown” and documented. Its purpose is to prevent redundant teardown walks through the DOM. --- packages/ui/dombackend.js | 10 +++++++-- packages/ui/dombackend_tests.js | 34 ++++++++++++++--------------- packages/ui/domrange.js | 38 +++++++++++++++++++++------------ packages/ui/domrange_tests.js | 7 ++++-- packages/ui/render.js | 2 +- 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/packages/ui/dombackend.js b/packages/ui/dombackend.js index 7800723175..d94d8fbcc0 100644 --- a/packages/ui/dombackend.js +++ b/packages/ui/dombackend.js @@ -28,7 +28,7 @@ if (Meteor.isClient) { // Causes `elem` (a DOM element) to be detached from its parent, if any. // Whether or not `elem` was detached, causes any callbacks registered - // with `onRemoveElement` on `elem` and its descendants to fire. + // with `onElementTeardown` on `elem` and its descendants to fire. // Not for use on non-element nodes. // // This method is modeled after the behavior of jQuery's `$(elem).remove()`, @@ -37,11 +37,17 @@ if (Meteor.isClient) { $jq(elem).remove(); }; + DomBackend.tearDownElement = function (elem) { + var elems = Array.prototype.slice.call(elem.getElementsByTagName('*')); + elems.push(elem); + $jq.cleanData(elems); + }; + // Registers a callback function to be called when the given element or // one of its ancestors is removed from the DOM via the backend library. // The callback function is called at most once, and it receives the element // in question as an argument. - DomBackend.onRemoveElement = function (elem, func) { + DomBackend.onElementTeardown = function (elem, func) { if (! elem[REMOVAL_CALLBACKS_PROPERTY_NAME]) { elem[REMOVAL_CALLBACKS_PROPERTY_NAME] = []; diff --git a/packages/ui/dombackend_tests.js b/packages/ui/dombackend_tests.js index 1b3583b192..5799e3c030 100644 --- a/packages/ui/dombackend_tests.js +++ b/packages/ui/dombackend_tests.js @@ -35,16 +35,16 @@ var isDetachedSingleNode = function (test, node) { }; Tinytest.add("ui - DomBackend - element removal", function (test) { - // Test that calling removeElement on a detached element calls onRemoveElement + // Test that calling removeElement on a detached element calls onElementTeardown // on it and its descendents. For jQuery, `removeElement` runs `$(elem).remove()`, // so it tests detecting a jQuery removal, as well as the stronger condition // that clean-up still happens on the DOM tree in the detached case. runDivSpanBTest(function (div, span, b, buf, func1, func2, func3, func4) { - DomBackend.onRemoveElement(div, func1); - DomBackend.onRemoveElement(span, func2); - DomBackend.onRemoveElement(b, func3); + DomBackend.onElementTeardown(div, func1); + DomBackend.onElementTeardown(span, func2); + DomBackend.onElementTeardown(b, func3); // test second callback on same element - DomBackend.onRemoveElement(div, func4); + DomBackend.onElementTeardown(div, func4); DomBackend.removeElement(div); // "remove" the (parentless) DIV @@ -59,10 +59,10 @@ Tinytest.add("ui - DomBackend - element removal", function (test) { // Test that `removeElement` actually removes the element // (and fires appropriate callbacks). runDivSpanBTest(function (div, span, b, buf, func1, func2, func3, func4) { - DomBackend.onRemoveElement(div, func1); - DomBackend.onRemoveElement(span, func2); - DomBackend.onRemoveElement(b, func3); - DomBackend.onRemoveElement(div, func4); + DomBackend.onElementTeardown(div, func1); + DomBackend.onElementTeardown(span, func2); + DomBackend.onElementTeardown(b, func3); + DomBackend.onElementTeardown(div, func4); DomBackend.removeElement(span); // remove the SPAN @@ -83,10 +83,10 @@ Tinytest.add("ui - DomBackend - element removal (jQuery)", function (test) { // Test with `$(elem).remove()`. runDivSpanBTest(function (div, span, b, buf, func1, func2, func3, func4) { - DomBackend.onRemoveElement(div, func1); - DomBackend.onRemoveElement(span, func2); - DomBackend.onRemoveElement(b, func3); - DomBackend.onRemoveElement(div, func4); + DomBackend.onElementTeardown(div, func1); + DomBackend.onElementTeardown(span, func2); + DomBackend.onElementTeardown(b, func3); + DomBackend.onElementTeardown(div, func4); $(span).remove(); // remove the SPAN @@ -103,10 +103,10 @@ Tinytest.add("ui - DomBackend - element removal (jQuery)", function (test) { // Test that `$(elem).detach()` is NOT considered a removal. runDivSpanBTest(function (div, span, b, buf, func1, func2, func3, func4) { - DomBackend.onRemoveElement(div, func1); - DomBackend.onRemoveElement(span, func2); - DomBackend.onRemoveElement(b, func3); - DomBackend.onRemoveElement(div, func4); + DomBackend.onElementTeardown(div, func1); + DomBackend.onElementTeardown(span, func2); + DomBackend.onElementTeardown(b, func3); + DomBackend.onElementTeardown(div, func4); $(span).detach(); // detach the SPAN diff --git a/packages/ui/domrange.js b/packages/ui/domrange.js index 1b46b5cae1..a875b759de 100644 --- a/packages/ui/domrange.js +++ b/packages/ui/domrange.js @@ -11,7 +11,7 @@ var removeNode = function (n) { n.parentNode._uihooks && n.parentNode._uihooks.removeElement) { n.parentNode._uihooks.removeElement(n); } else { - DomBackend.removeElement(n); + n.parentNode.removeChild(n); } }; @@ -110,8 +110,8 @@ var rangeParented = function (range) { range._rangeDict = rangeDict; // get jQuery to tell us when this node is removed - DomBackend.onRemoveElement(parentNode, function () { - rangeRemoved(range, true /* elementsAlreadyRemoved */); + DomBackend.onElementTeardown(parentNode, function () { + rangeRemoved(range, true /* alreadyTornDown */); }); } @@ -128,7 +128,7 @@ var rangeParented = function (range) { } }; -var rangeRemoved = function (range, elementsAlreadyRemoved) { +var rangeRemoved = function (range, alreadyTornDown) { if (! range.isRemoved) { range.isRemoved = true; @@ -146,29 +146,39 @@ var rangeRemoved = function (range, elementsAlreadyRemoved) { if (range.removed) range.removed(); - membersRemoved(range, elementsAlreadyRemoved); + membersRemoved(range, alreadyTornDown); } }; -var nodeRemoved = function (node, elementsAlreadyRemoved) { +var nodeRemoved = function (node, alreadyTornDown) { if (node.nodeType === 1) { // ELEMENT var comps = DomRange.getComponents(node); for (var i = 0, N = comps.length; i < N; i++) - rangeRemoved(comps[i], elementsAlreadyRemoved); + rangeRemoved(comps[i], true /* alreadyTornDown */); - if (! elementsAlreadyRemoved) - DomBackend.removeElement(node); + // `alreadyTornDown` is an optimization so that we don't + // tear down the same elements multiple times when tearing + // down a tree of DomRanges and elements, leading to asymptotic + // inefficiency. + // + // When jQuery removes an element or DomBackend.tearDownElement + // is called, the DOM is "cleaned" recursively, calling all + // onElementTearDown handlers on the entire DOM subtree. + // Since the entire subtree is already walked, we don't want to + // also walk the subtrees of each DomRange for teardown purposes. + if (! alreadyTornDown) + DomBackend.tearDownElement(node); } }; -var membersRemoved = function (range, elementsAlreadyRemoved) { +var membersRemoved = function (range, alreadyTornDown) { var members = range.members; for (var k in members) { var mem = members[k]; if (mem instanceof DomRange) - rangeRemoved(mem, elementsAlreadyRemoved); + rangeRemoved(mem, alreadyTornDown); else - nodeRemoved(mem, elementsAlreadyRemoved); + nodeRemoved(mem, alreadyTornDown); } }; @@ -230,7 +240,7 @@ _extend(DomRange.prototype, { for (var i = 0, N = nodes.length; i < N; i++) removeNode(nodes[i]); - membersRemoved(this, true /* elementsAlreadyRemoved */); + membersRemoved(this); this.members = {}; }, @@ -341,7 +351,7 @@ _extend(DomRange.prototype, { removeNode(this.start); removeNode(this.end); this.owner = null; - rangeRemoved(this, true /* elementsAlreadyRemoved */); + rangeRemoved(this); return; } diff --git a/packages/ui/domrange_tests.js b/packages/ui/domrange_tests.js index fb042f2dea..2fd7f2b519 100644 --- a/packages/ui/domrange_tests.js +++ b/packages/ui/domrange_tests.js @@ -25,7 +25,8 @@ var inDocument = function (range, func) { try { func(range); } finally { - document.body.removeChild(onscreen); + if (onscreen.parentNode === document.body) + document.body.removeChild(onscreen); } }; @@ -862,7 +863,7 @@ Tinytest.add("ui - DomRange - structural removal", function (test) { test.isTrue(e.isRemoved); - for (var scenario = 0; scenario < 2; scenario++) { + for (var scenario = 0; scenario < 3; scenario++) { var f = new DomRange; var g = document.createElement("DIV"); var h = new DomRange; @@ -882,6 +883,8 @@ Tinytest.add("ui - DomRange - structural removal", function (test) { r.removeAll(); else if (scenario === 1) r.remove('f'); + else if (scenario === 2) + $(r.parentNode()).remove(); test.isTrue(f.isRemoved); test.isTrue(h.isRemoved); test.isTrue(k.isRemoved); diff --git a/packages/ui/render.js b/packages/ui/render.js index 31da03bda3..58d30787dc 100644 --- a/packages/ui/render.js +++ b/packages/ui/render.js @@ -382,7 +382,7 @@ var materialize = function (node, parent, before, parentComponent) { reportUIException(e); } }); - UI.DomBackend.onRemoveElement(elem, function () { + UI.DomBackend.onElementTeardown(elem, function () { attrComp.stop(); }); } From 02c0c4de68d96f0060318caca94f4dc2c3f6ea61 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld <arbesfeld@gmail.com> Date: Fri, 20 Jun 2014 13:28:23 -0700 Subject: [PATCH 76/88] IE8 fix for StyleAttrHandler. --- packages/test-helpers/canonicalize_html.js | 12 ++++++++++++ packages/ui/attrs.js | 16 ++++++++++++++-- packages/ui/render_tests.js | 16 +++++++++++----- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js index 477b1bfa0f..8e7d606fe6 100644 --- a/packages/test-helpers/canonicalize_html.js +++ b/packages/test-helpers/canonicalize_html.js @@ -77,6 +77,18 @@ canonicalizeHtml = function(html) { } value = value.replace(/["'`]/g, '"'); } + + // Encode quotes and double quotes in the attribute. + var attr = value.slice(1, -1); + attr = attr.replace(/\"/g, "&quot;"); + attr = attr.replace(/\'/g, "&quot;"); + value = '"' + attr + '"'; + + // Ensure that styles end with ';' + if (key === 'style' && value.slice(-2) !== ';"' && value !== '""') { + value = value.slice(0, -1) + ';"'; + } + tagContents.push(key+'='+value); } return '<'+tagContents.join(' ')+'>'; diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index 99992dfa53..0cb0572ae8 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -113,10 +113,22 @@ var SVGClassHandler = ClassHandler.extend({ var StyleHandler = DiffingAttributeHandler.extend({ getCurrentValue: function (element) { - return element.getAttribute("style") || ''; + var style = element.getAttribute('style'); + if (! style) + return ''; + // IE sometimes removes the last semicolon on the style attribute, so we + // add it back if it does not already exist. + if (style.slice(-1) !== ';') { + style += ';'; + } + return style; }, setValue: function (element, style) { - element.setAttribute("style", style); + if (style === '') { + element.removeAttribute('style'); + } else { + element.setAttribute('style', style); + } }, // Parse a string to produce a map from property to attribute string. diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 4fa60165f3..3351a12a13 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -292,7 +292,11 @@ Tinytest.add("ui - render - reactive attributes", function (test) { test.equal(R.numListeners(), 1); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); - span.setAttribute("style", span.getAttribute("style") + 'jquery-style: hidden;'); + + if (isIE()) + span.style.cssText = 'jquery-style: hidden;'; + else + span.setAttribute('style', span.getAttribute('style') + 'jquery-style: hidden;'); R.set({'style': 'foo: "a;zz;aa";', id: 'bar'}); Deps.flush(); @@ -330,7 +334,7 @@ Tinytest.add("ui - render - reactive attributes", function (test) { Deps.flush(); test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c;"></span>'); - // XXX test malformed styles - different expectations in IE from Chrome + // test malformed styles - different expectations in IE from Chrome R.set({'style': 'foo: a; bar::d;:e; baz: c;'}); Deps.flush(); test.equal(canonicalizeHtml(div.innerHTML), @@ -341,11 +345,13 @@ Tinytest.add("ui - render - reactive attributes", function (test) { Deps.flush(); test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c; constructor: a; __proto__: b;"></span>'); - // XXX test clearing styles - different expectations in IE from Chrome R.set({}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), - isIE() ? '<span></span>' : '<span style=""></span>'); + test.equal(canonicalizeHtml(div.innerHTML), '<span></span>'); + + R.set({'style': 'foo: bar;'}); + Deps.flush(); + test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: bar;"></span>'); })(); // Test `null`, `undefined`, and `[]` attributes From 42b0cad48f28348fa980753321d1fc2292c83282 Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld <arbesfeld@gmail.com> Date: Fri, 20 Jun 2014 13:51:57 -0700 Subject: [PATCH 77/88] Allow styles to not have a trailing semicolon. Fix cannonicalize_html to remove trailing semicolons from styles. --- packages/test-helpers/canonicalize_html.js | 6 +++--- packages/ui/attrs.js | 10 +-------- packages/ui/render_tests.js | 25 ++++++++++------------ 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js index 8e7d606fe6..6a5ed5f4a9 100644 --- a/packages/test-helpers/canonicalize_html.js +++ b/packages/test-helpers/canonicalize_html.js @@ -84,9 +84,9 @@ canonicalizeHtml = function(html) { attr = attr.replace(/\'/g, "&quot;"); value = '"' + attr + '"'; - // Ensure that styles end with ';' - if (key === 'style' && value.slice(-2) !== ';"' && value !== '""') { - value = value.slice(0, -1) + ';"'; + // Ensure that styles do not end with a semicolon. + if (key === 'style') { + value = value.replace(/;\"$/, '"'); } tagContents.push(key+'='+value); diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index 0cb0572ae8..a15e2e7a6e 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -113,15 +113,7 @@ var SVGClassHandler = ClassHandler.extend({ var StyleHandler = DiffingAttributeHandler.extend({ getCurrentValue: function (element) { - var style = element.getAttribute('style'); - if (! style) - return ''; - // IE sometimes removes the last semicolon on the style attribute, so we - // add it back if it does not already exist. - if (style.slice(-1) !== ';') { - style += ';'; - } - return style; + return element.getAttribute('style'); }, setValue: function (element, style) { if (style === '') { diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 3351a12a13..0a61fff6e9 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -287,25 +287,22 @@ Tinytest.add("ui - render - reactive attributes", function (test) { var div = document.createElement("DIV"); materialize(spanCode, div); - test.equal(canonicalizeHtml(div.innerHTML), '<span id="foo" style="foo: &quot;a;aa&quot;; bar: b;"></span>'); + test.equal(canonicalizeHtml(div.innerHTML), '<span id="foo" style="foo: &quot;a;aa&quot;; bar: b"></span>'); test.equal(R.numListeners(), 1); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); - if (isIE()) - span.style.cssText = 'jquery-style: hidden;'; - else - span.setAttribute('style', span.getAttribute('style') + 'jquery-style: hidden;'); + span.setAttribute('style', span.getAttribute('style') + '; jquery-style: hidden'); R.set({'style': 'foo: "a;zz;aa";', id: 'bar'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML, true), '<span id="bar" style="foo: &quot;a;zz;aa&quot;; jquery-style: hidden;"></span>'); + test.equal(canonicalizeHtml(div.innerHTML, true), '<span id="bar" style="foo: &quot;a;zz;aa&quot;; jquery-style: hidden"></span>'); test.equal(R.numListeners(), 1); R.set({}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), '<span style="jquery-style: hidden;"></span>'); + test.equal(canonicalizeHtml(div.innerHTML), '<span style="jquery-style: hidden"></span>'); test.equal(R.numListeners(), 1); $(div).remove(); @@ -323,27 +320,27 @@ Tinytest.add("ui - render - reactive attributes", function (test) { var div = document.createElement("DIV"); document.body.appendChild(div); materialize(spanCode, div); - test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: a;"></span>'); + test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: a"></span>'); var span = div.firstChild; test.equal(span.nodeName, 'SPAN'); span.setAttribute("style", 'foo: b;'); - test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: b;"></span>'); + test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: b"></span>'); R.set({'style': 'foo: c;'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c;"></span>'); + test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c"></span>'); // test malformed styles - different expectations in IE from Chrome R.set({'style': 'foo: a; bar::d;:e; baz: c;'}); Deps.flush(); test.equal(canonicalizeHtml(div.innerHTML), - isIE() ? '<span style="foo: a; baz: c;"></span>' : '<span style="foo: a; bar::d; baz: c;"></span>'); + isIE() ? '<span style="foo: a; baz: c"></span>' : '<span style="foo: a; bar::d; baz: c"></span>'); // Test strange styles - R.set({'style': 'constructor: a; __proto__: b; foo: c;'}); + R.set({'style': ' foo: c; constructor: a; __proto__: b;'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c; constructor: a; __proto__: b;"></span>'); + test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c; constructor: a; __proto__: b"></span>'); R.set({}); Deps.flush(); @@ -351,7 +348,7 @@ Tinytest.add("ui - render - reactive attributes", function (test) { R.set({'style': 'foo: bar;'}); Deps.flush(); - test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: bar;"></span>'); + test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: bar"></span>'); })(); // Test `null`, `undefined`, and `[]` attributes From 137129b35804ff7011fa271b374436282a24ab53 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Thu, 19 Jun 2014 14:45:49 -0700 Subject: [PATCH 78/88] Upgrade from SRP to bcrypt on password change --- packages/accounts-password/password_client.js | 86 ++++++++++++++----- packages/accounts-password/password_server.js | 25 +++++- packages/accounts-password/password_tests.js | 45 ++++++++++ .../accounts-password/password_tests_setup.js | 8 ++ 4 files changed, 139 insertions(+), 25 deletions(-) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 2a3a6e088a..2cd4731f61 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -34,16 +34,11 @@ Meteor.loginWithPassword = function (selector, password, callback) { // the password without requiring a full SRP flow, as well as // SHA256(password), which the server bcrypts and stores in // place of the old SRP information for this user. - var details; - try { - details = EJSON.parse(error.details); - } catch (e) {} - if (!(details && details.format === 'srp')) - callback(new Meteor.Error(400, - "Password is old. Please reset your " + - "password.")); - else - srpUpgradePath(selector, password, details.identity, callback); + srpUpgradePath({ + upgradeError: error, + userSelector: selector, + plaintextPassword: password + }, callback); } else if (error) { callback(error); @@ -61,18 +56,32 @@ var hashPassword = function (password) { }; }; +// XXX COMPAT WITH 0.8.1.3 // The server requested an upgrade from the old SRP password format, -// so supply the needed SRP identity to login. -var srpUpgradePath = function (selector, plaintextPassword, - identity, callback) { - Accounts.callLoginMethod({ - methodArguments: [{ - user: selector, - srp: SHA256(identity + ":" + plaintextPassword), - password: hashPassword(plaintextPassword) - }], - userCallback: callback - }); +// so supply the needed SRP identity to login. Options: +// - upgradeError: the error object that the server returned to tell +// us to upgrade from SRP to bcrypt. +// - userSelector: selector to retrieve the user object +// - plaintextPassword: the password as a string +var srpUpgradePath = function (options, callback) { + var details; + try { + details = EJSON.parse(options.upgradeError.details); + } catch (e) {} + if (!(details && details.format === 'srp')) { + callback(new Meteor.Error(400, + "Password is old. Please reset your " + + "password.")); + } else { + Accounts.callLoginMethod({ + methodArguments: [{ + user: options.userSelector, + srp: SHA256(details.identity + ":" + options.plaintextPassword), + password: hashPassword(options.plaintextPassword) + }], + userCallback: callback + }); + } }; @@ -113,8 +122,39 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { [oldPassword ? hashPassword(oldPassword) : null, hashPassword(newPassword)], function (error, result) { if (error || !result) { - callback && callback( - error || new Error("No result from changePassword.")); + if (error && error.error === 400 && + error.reason === 'old password format') { + // XXX COMPAT WITH 0.8.1.3 + // The server is telling us to upgrade from SRP to bcrypt, as + // in Meteor.loginWithPassword. + var userSelector = {}; + if (Meteor.user().username) { + userSelector = { username: Meteor.user().username }; + } else if (Meteor.user().emails && Meteor.user().emails.length) { + userSelector = { email: Meteor.user().emails[0].address }; + } else { + callback(new Error( + "Cannot upgrade password format without " + + "username or email address")); + return; + } + + srpUpgradePath({ + upgradeError: error, + userSelector: userSelector, + plaintextPassword: oldPassword + }, function (err) { + if (err) { + callback(err); + } else { + Accounts.changePassword(oldPassword, newPassword, callback); + } + }); + } else { + // A normal error, not an error telling us to upgrade to bcrypt + callback && callback( + error || new Error("No result from changePassword.")); + } } else { callback && callback(); } diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index b054bb1f76..39d84518c0 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -254,7 +254,20 @@ Accounts.registerLoginHandler("password", function (options) { /// // Let the user change their own password if they know the old -// password. +// password. `oldPassword` and `newPassword` should be objects with keys +// `digest` and `algorithm` (representing the SHA256 of the password). +// +// XXX COMPAT WITH 0.8.1.3 +// Like the login method, if the user hasn't been upgraded from SRP to +// bcrypt yet, then this method will throw an 'old password format' +// error. The client should call the SRP upgrade login handler and then +// retry this method again. +// +// UNLIKE the login method, there is no way to avoid getting SRP upgrade +// errors thrown. The reasoning for this is that clients using this +// method directly will need to be updated anyway because we no longer +// support the SRP flow that they would have been doing to use this +// method previously. Meteor.methods({changePassword: function (oldPassword, newPassword) { check(oldPassword, passwordValidator); check(newPassword, passwordValidator); @@ -266,9 +279,17 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { if (!user) throw new Meteor.Error(403, "User not found"); - if (!user.services || !user.services.password || !user.services.password.bcrypt) + if (!user.services || !user.services.password || + (!user.services.password.bcrypt && !user.services.password.srp)) throw new Meteor.Error(403, "User has no password set"); + if (! user.services.password.bcrypt) { + throw new Meteor.Error(400, "old password format", EJSON.stringify({ + format: 'srp', + identity: user.services.password.srp.identity + })); + } + var result = checkPassword(user, oldPassword); if (result.error) throw result.error; diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index d79654e23f..e1e51d4aac 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -767,6 +767,51 @@ if (Meteor.isClient) (function () { })); } ]); + + testAsyncMulti("passwords - srp to bcrypt upgrade via password change", [ + logoutStep, + // Create user with old SRP credentials in the database. + function (test, expect) { + var self = this; + Meteor.call("testCreateSRPUser", expect(function (error, result) { + test.isFalse(error); + self.username = result; + })); + }, + // Log in with the plaintext password handler, which should NOT upgrade us to bcrypt. + function (test, expect) { + Accounts.callLoginMethod({ + methodName: "login", + methodArguments: [ { user: { username: this.username }, password: "abcdef" } ], + userCallback: expect(function (err) { + test.isFalse(err); + }) + }); + }, + function (test, expect) { + Meteor.call("testNoSRPUpgrade", this.username, expect(function (error) { + test.isFalse(error); + })); + }, + // Changing our password should upgrade us to bcrypt. + function (test, expect) { + Accounts.changePassword("abcdef", "abcdefg", expect(function (error) { + test.isFalse(error); + })); + }, + function (test, expect) { + Meteor.call("testSRPUpgrade", this.username, expect(function (error) { + test.isFalse(error); + })); + }, + // And after the upgrade we should be able to change our password again. + function (test, expect) { + Accounts.changePassword("abcdefg", "abcdef", expect(function (error) { + test.isFalse(error); + })); + }, + logoutStep + ]); }) (); diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index 0993a706b6..fa4432f097 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -141,5 +141,13 @@ Meteor.methods({ throw new Error("srp wasn't removed"); if (!(user.services && user.services.password && user.services.password.bcrypt)) throw new Error("bcrypt wasn't added"); + }, + + testNoSRPUpgrade: function (username) { + var user = Meteor.users.findOne({username: username}); + if (user.services && user.services.password && user.services.password.bcrypt) + throw new Error("bcrypt was added"); + if (user.services && user.services.password && ! user.services.password.srp) + throw new Error("srp was removed"); } }); From 84774800b8a4ec8fe9a0f9a9ac571e83d23eb039 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Thu, 19 Jun 2014 14:47:49 -0700 Subject: [PATCH 79/88] Add a comment --- packages/accounts-password/password_client.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 2cd4731f61..255dbd3d9d 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -147,6 +147,8 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { if (err) { callback(err); } else { + // Now that we've successfully migrated from srp to + // bcrypt, try changing the password again. Accounts.changePassword(oldPassword, newPassword, callback); } }); From 924f51a0ed02ad1ed46d170fc9b3d46ffad24fa2 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Thu, 19 Jun 2014 15:10:40 -0700 Subject: [PATCH 80/88] Add a changePassword test --- packages/accounts-password/password_tests.js | 92 ++++++++++--------- .../accounts-password/password_tests_setup.js | 37 ++++++-- 2 files changed, 76 insertions(+), 53 deletions(-) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index e1e51d4aac..9cc41dc305 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -768,50 +768,54 @@ if (Meteor.isClient) (function () { } ]); - testAsyncMulti("passwords - srp to bcrypt upgrade via password change", [ - logoutStep, - // Create user with old SRP credentials in the database. - function (test, expect) { - var self = this; - Meteor.call("testCreateSRPUser", expect(function (error, result) { - test.isFalse(error); - self.username = result; - })); - }, - // Log in with the plaintext password handler, which should NOT upgrade us to bcrypt. - function (test, expect) { - Accounts.callLoginMethod({ - methodName: "login", - methodArguments: [ { user: { username: this.username }, password: "abcdef" } ], - userCallback: expect(function (err) { - test.isFalse(err); - }) - }); - }, - function (test, expect) { - Meteor.call("testNoSRPUpgrade", this.username, expect(function (error) { - test.isFalse(error); - })); - }, - // Changing our password should upgrade us to bcrypt. - function (test, expect) { - Accounts.changePassword("abcdef", "abcdefg", expect(function (error) { - test.isFalse(error); - })); - }, - function (test, expect) { - Meteor.call("testSRPUpgrade", this.username, expect(function (error) { - test.isFalse(error); - })); - }, - // And after the upgrade we should be able to change our password again. - function (test, expect) { - Accounts.changePassword("abcdefg", "abcdef", expect(function (error) { - test.isFalse(error); - })); - }, - logoutStep - ]); + _.each([true, false], function (email) { + testAsyncMulti("passwords - srp to bcrypt upgrade via password change, " + + "user with " + (email ? "email" : "username"), [ + logoutStep, + // Create user with old SRP credentials in the database. + function (test, expect) { + var self = this; + Meteor.call("testCreateSRPUser", email, expect(function (error, result) { + test.isFalse(error); + self[email ? "email" : "username"] = result; + })); + }, + // Log in with the plaintext password handler, which should NOT upgrade us to bcrypt. + function (test, expect) { + var selector = email ? { email: this.email } : { username: this.username }; + Accounts.callLoginMethod({ + methodName: "login", + methodArguments: [ { user: selector, password: "abcdef" } ], + userCallback: expect(function (err) { + test.isFalse(err); + }) + }); + }, + function (test, expect) { + Meteor.call("testNoSRPUpgrade", email ? this.email : this.username, expect(function (error) { + test.isFalse(error); + })); + }, + // Changing our password should upgrade us to bcrypt. + function (test, expect) { + Accounts.changePassword("abcdef", "abcdefg", expect(function (error) { + test.isFalse(error); + })); + }, + function (test, expect) { + Meteor.call("testSRPUpgrade", email ? this.email : this.username, expect(function (error) { + test.isFalse(error); + })); + }, + // And after the upgrade we should be able to change our password again. + function (test, expect) { + Accounts.changePassword("abcdefg", "abcdef", expect(function (error) { + test.isFalse(error); + })); + }, + logoutStep + ]); + }); }) (); diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index fa4432f097..0c58f76a67 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -120,10 +120,17 @@ Meteor.methods({ // Create a user that had previously logged in with SRP. Meteor.methods({ - testCreateSRPUser: function () { - var username = Random.id(); - Meteor.users.remove({username: username}); - var userId = Accounts.createUser({username: username}); + testCreateSRPUser: function (email) { + var userId; + if (email) { + email = Random.id() + "@example.com"; + Meteor.users.remove({ "emails.address": email }); + userId = Accounts.createUser({ email: email }); + } else { + var username = Random.id(); + Meteor.users.remove({username: username}); + userId = Accounts.createUser({username: username}); + } Meteor.users.update( userId, { '$set': { 'services.password.srp': { @@ -132,19 +139,31 @@ Meteor.methods({ "verifier" : "2e8bce266b1357edf6952cc56d979db19f699ced97edfb2854b95972f820b0c7006c1a18e98aad40edf3fe111b87c52ef7dd06b320ce452d01376df2d560fdc4d8e74f7a97bca1f67b3cfaef34dee34dd6c76571c247d762624dc166dab5499da06bc9358528efa75bf74e2e7f5a80d09e60acf8856069ae5cfb080f2239ee76" } } } ); - return username; + return email || username; }, - testSRPUpgrade: function (username) { - var user = Meteor.users.findOne({username: username}); + testSRPUpgrade: function (usernameOrEmail) { + var selector; + if (usernameOrEmail.indexOf("@") !== -1) { + selector = { "emails.address": usernameOrEmail }; + } else { + selector = { username: usernameOrEmail }; + } + var user = Meteor.users.findOne(selector); if (user.services && user.services.password && user.services.password.srp) throw new Error("srp wasn't removed"); if (!(user.services && user.services.password && user.services.password.bcrypt)) throw new Error("bcrypt wasn't added"); }, - testNoSRPUpgrade: function (username) { - var user = Meteor.users.findOne({username: username}); + testNoSRPUpgrade: function (usernameOrEmail) { + var selector; + if (usernameOrEmail.indexOf("@") !== -1) { + selector = { "emails.address": usernameOrEmail }; + } else { + selector = { username: usernameOrEmail }; + } + var user = Meteor.users.findOne(selector); if (user.services && user.services.password && user.services.password.bcrypt) throw new Error("bcrypt was added"); if (user.services && user.services.password && ! user.services.password.srp) From 76cfdd44a62fe852b29faef670794a42d4d718c4 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Fri, 20 Jun 2014 16:11:18 -0700 Subject: [PATCH 81/88] Use id as user selector instead of username or email --- packages/accounts-password/password_client.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 255dbd3d9d..ff391fc505 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -126,22 +126,9 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { error.reason === 'old password format') { // XXX COMPAT WITH 0.8.1.3 // The server is telling us to upgrade from SRP to bcrypt, as - // in Meteor.loginWithPassword. - var userSelector = {}; - if (Meteor.user().username) { - userSelector = { username: Meteor.user().username }; - } else if (Meteor.user().emails && Meteor.user().emails.length) { - userSelector = { email: Meteor.user().emails[0].address }; - } else { - callback(new Error( - "Cannot upgrade password format without " + - "username or email address")); - return; - } - srpUpgradePath({ upgradeError: error, - userSelector: userSelector, + userSelector: { id: Meteor.user()._id }, plaintextPassword: oldPassword }, function (err) { if (err) { From ea231729de3f2b3a0b73cf845dc9c5673c05acd3 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Fri, 20 Jun 2014 16:11:59 -0700 Subject: [PATCH 82/88] Revert "Add a changePassword test" This reverts commit 7e519e11b7b8376d92fdec2492732e75abcda350. This commit added a test that upgraded from srp to bcrypt via changePassword for a user with only an email address, in addition to the test that existed for a user with a username. Having two such tests is now silly because Meteor.changePassword no longer has different code paths for email/username users. --- packages/accounts-password/password_tests.js | 92 +++++++++---------- .../accounts-password/password_tests_setup.js | 37 ++------ 2 files changed, 53 insertions(+), 76 deletions(-) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 9cc41dc305..e1e51d4aac 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -768,54 +768,50 @@ if (Meteor.isClient) (function () { } ]); - _.each([true, false], function (email) { - testAsyncMulti("passwords - srp to bcrypt upgrade via password change, " + - "user with " + (email ? "email" : "username"), [ - logoutStep, - // Create user with old SRP credentials in the database. - function (test, expect) { - var self = this; - Meteor.call("testCreateSRPUser", email, expect(function (error, result) { - test.isFalse(error); - self[email ? "email" : "username"] = result; - })); - }, - // Log in with the plaintext password handler, which should NOT upgrade us to bcrypt. - function (test, expect) { - var selector = email ? { email: this.email } : { username: this.username }; - Accounts.callLoginMethod({ - methodName: "login", - methodArguments: [ { user: selector, password: "abcdef" } ], - userCallback: expect(function (err) { - test.isFalse(err); - }) - }); - }, - function (test, expect) { - Meteor.call("testNoSRPUpgrade", email ? this.email : this.username, expect(function (error) { - test.isFalse(error); - })); - }, - // Changing our password should upgrade us to bcrypt. - function (test, expect) { - Accounts.changePassword("abcdef", "abcdefg", expect(function (error) { - test.isFalse(error); - })); - }, - function (test, expect) { - Meteor.call("testSRPUpgrade", email ? this.email : this.username, expect(function (error) { - test.isFalse(error); - })); - }, - // And after the upgrade we should be able to change our password again. - function (test, expect) { - Accounts.changePassword("abcdefg", "abcdef", expect(function (error) { - test.isFalse(error); - })); - }, - logoutStep - ]); - }); + testAsyncMulti("passwords - srp to bcrypt upgrade via password change", [ + logoutStep, + // Create user with old SRP credentials in the database. + function (test, expect) { + var self = this; + Meteor.call("testCreateSRPUser", expect(function (error, result) { + test.isFalse(error); + self.username = result; + })); + }, + // Log in with the plaintext password handler, which should NOT upgrade us to bcrypt. + function (test, expect) { + Accounts.callLoginMethod({ + methodName: "login", + methodArguments: [ { user: { username: this.username }, password: "abcdef" } ], + userCallback: expect(function (err) { + test.isFalse(err); + }) + }); + }, + function (test, expect) { + Meteor.call("testNoSRPUpgrade", this.username, expect(function (error) { + test.isFalse(error); + })); + }, + // Changing our password should upgrade us to bcrypt. + function (test, expect) { + Accounts.changePassword("abcdef", "abcdefg", expect(function (error) { + test.isFalse(error); + })); + }, + function (test, expect) { + Meteor.call("testSRPUpgrade", this.username, expect(function (error) { + test.isFalse(error); + })); + }, + // And after the upgrade we should be able to change our password again. + function (test, expect) { + Accounts.changePassword("abcdefg", "abcdef", expect(function (error) { + test.isFalse(error); + })); + }, + logoutStep + ]); }) (); diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index 0c58f76a67..fa4432f097 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -120,17 +120,10 @@ Meteor.methods({ // Create a user that had previously logged in with SRP. Meteor.methods({ - testCreateSRPUser: function (email) { - var userId; - if (email) { - email = Random.id() + "@example.com"; - Meteor.users.remove({ "emails.address": email }); - userId = Accounts.createUser({ email: email }); - } else { - var username = Random.id(); - Meteor.users.remove({username: username}); - userId = Accounts.createUser({username: username}); - } + testCreateSRPUser: function () { + var username = Random.id(); + Meteor.users.remove({username: username}); + var userId = Accounts.createUser({username: username}); Meteor.users.update( userId, { '$set': { 'services.password.srp': { @@ -139,31 +132,19 @@ Meteor.methods({ "verifier" : "2e8bce266b1357edf6952cc56d979db19f699ced97edfb2854b95972f820b0c7006c1a18e98aad40edf3fe111b87c52ef7dd06b320ce452d01376df2d560fdc4d8e74f7a97bca1f67b3cfaef34dee34dd6c76571c247d762624dc166dab5499da06bc9358528efa75bf74e2e7f5a80d09e60acf8856069ae5cfb080f2239ee76" } } } ); - return email || username; + return username; }, - testSRPUpgrade: function (usernameOrEmail) { - var selector; - if (usernameOrEmail.indexOf("@") !== -1) { - selector = { "emails.address": usernameOrEmail }; - } else { - selector = { username: usernameOrEmail }; - } - var user = Meteor.users.findOne(selector); + testSRPUpgrade: function (username) { + var user = Meteor.users.findOne({username: username}); if (user.services && user.services.password && user.services.password.srp) throw new Error("srp wasn't removed"); if (!(user.services && user.services.password && user.services.password.bcrypt)) throw new Error("bcrypt wasn't added"); }, - testNoSRPUpgrade: function (usernameOrEmail) { - var selector; - if (usernameOrEmail.indexOf("@") !== -1) { - selector = { "emails.address": usernameOrEmail }; - } else { - selector = { username: usernameOrEmail }; - } - var user = Meteor.users.findOne(selector); + testNoSRPUpgrade: function (username) { + var user = Meteor.users.findOne({username: username}); if (user.services && user.services.password && user.services.password.bcrypt) throw new Error("bcrypt was added"); if (user.services && user.services.password && ! user.services.password.srp) From 367516a966d8e154be1c558d0596cfddffc6d2c8 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Fri, 20 Jun 2014 16:16:37 -0700 Subject: [PATCH 83/88] Replace accidentally deleted comment line --- packages/accounts-password/password_client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index ff391fc505..24278c3d2f 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -126,6 +126,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { error.reason === 'old password format') { // XXX COMPAT WITH 0.8.1.3 // The server is telling us to upgrade from SRP to bcrypt, as + // in Meteor.loginWithPassword. srpUpgradePath({ upgradeError: error, userSelector: { id: Meteor.user()._id }, From abd92999a4470efc4ee0abfafa6b251e1863517e Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Fri, 20 Jun 2014 16:44:12 -0700 Subject: [PATCH 84/88] Meteor.user()._id -> Meteor.userId() --- packages/accounts-password/password_client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 24278c3d2f..bc04cc8754 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -129,7 +129,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { // in Meteor.loginWithPassword. srpUpgradePath({ upgradeError: error, - userSelector: { id: Meteor.user()._id }, + userSelector: { id: Meteor.userId() }, plaintextPassword: oldPassword }, function (err) { if (err) { From 69455d4df7f66fd600bc353a8b112fce40c8e93e Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Sun, 22 Jun 2014 08:27:42 -0700 Subject: [PATCH 85/88] Use _.toArray to make IE8 happy --- packages/ui/dombackend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/dombackend.js b/packages/ui/dombackend.js index d94d8fbcc0..2d291b7a26 100644 --- a/packages/ui/dombackend.js +++ b/packages/ui/dombackend.js @@ -38,7 +38,7 @@ if (Meteor.isClient) { }; DomBackend.tearDownElement = function (elem) { - var elems = Array.prototype.slice.call(elem.getElementsByTagName('*')); + var elems = _.toArray(elem.getElementsByTagName('*')); elems.push(elem); $jq.cleanData(elems); }; From 2b87aa1f00a347778ce3f0938231b196e59e5afb Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Sun, 22 Jun 2014 16:31:13 -0700 Subject: [PATCH 86/88] Do feature detection instead of browser detection for ui test. The previous `isIE()` check returned false for IE11. --- packages/ui/render_tests.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index 0a61fff6e9..d2ffd56c5d 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -15,11 +15,6 @@ var HR = HTML.HR; var TEXTAREA = HTML.TEXTAREA; var INPUT = HTML.INPUT; -var isIE = function () { - var myNav = navigator.userAgent.toLowerCase(); - return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false; -}; - Tinytest.add("ui - render - basic", function (test) { var run = function (input, expectedInnerHTML, expectedHTML, expectedCode) { var div = document.createElement("DIV"); @@ -209,6 +204,15 @@ Tinytest.add("ui - render - closures", function (test) { }); +// IE strips malformed styles like "bar::d" from the `style` +// attribute. We detect this to adjust expectations for the StyleHandler +// test below. +var malformedStylesAllowed = function () { + var div = document.createElement("div"); + div.setAttribute("style", "bar::d;"); + return (div.getAttribute("style") === "bar::d;"); +}; + Tinytest.add("ui - render - closure GC", function (test) { // test that removing parent element removes listeners and stops autoruns. (function () { @@ -331,11 +335,14 @@ Tinytest.add("ui - render - reactive attributes", function (test) { Deps.flush(); test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c"></span>'); - // test malformed styles - different expectations in IE from Chrome + // test malformed styles - different expectations in IE (which + // strips malformed styles) from other browsers R.set({'style': 'foo: a; bar::d;:e; baz: c;'}); Deps.flush(); test.equal(canonicalizeHtml(div.innerHTML), - isIE() ? '<span style="foo: a; baz: c"></span>' : '<span style="foo: a; bar::d; baz: c"></span>'); + malformedStylesAllowed() ? + '<span style="foo: a; bar::d; baz: c"></span>' : + '<span style="foo: a; baz: c"></span>'); // Test strange styles R.set({'style': ' foo: c; constructor: a; __proto__: b;'}); From fba413d5976afcd97ff22bf47577b437575744df Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Mon, 23 Jun 2014 08:04:59 -0700 Subject: [PATCH 87/88] update banner, notices, History --- History.md | 7 ++++--- scripts/admin/banner.txt | 8 +++++--- scripts/admin/notices.json | 11 +++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/History.md b/History.md index 7f5430fdeb..5b08a51be2 100644 --- a/History.md +++ b/History.md @@ -26,9 +26,10 @@ plaintext password. * Show the display name of the currently logged-in user after following - an email verification link or password reset link in `accounts-ui`. + an email verification link or a password reset link in `accounts-ui`. -* Add a `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`. +* Add a `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount` + to pre-fill the user's email address in the OAuth popup. * Ensure that the user object has updated token information before it is passed to email template functions. #2210 @@ -49,7 +50,7 @@ overwrite changes to them made by other JavaScript libraries. * Add {{> UI.dynamic}} to make it easier to dynamically render a - template with a data context. XXX Update "Using Blaze" wiki page. + template with a data context. * Add `UI._templateInstance()` for accessing the current template instance from within a block helper. diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index 748c205ee0..c72d6b8ddb 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -1,5 +1,7 @@ -=> Meteor 0.8.1.3: Fixes a security flaw in the `spiderable` package and - minor regressions from 0.8.1. +=> Meteor 0.8.2: Switch `accounts-password` to use bcrypt on the + server. User accounts will seamlessly transition to bcrypt on the + next login, but this transition is one-way, so you cannot downgrade a + production app once you upgrade to 0.8.2. This release is being downloaded in the background. Update your - project to Meteor 0.8.1.3 by running 'meteor update'. + project to Meteor 0.8.2 by running 'meteor update'. diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index ab738757bc..3b002c98b5 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -138,6 +138,17 @@ { "release": "0.8.1.3" }, + { + "release": "0.8.2", + "packageNotices": { + "accounts-password": [ + "Transition to bcrypt for password storage on the server.", + "You do not need to make any changes to your app, but you will", + "not be able to downgrade production apps after you update them", + "to 0.8.2." + ] + } + }, { "release": "NEXT" } From 5cba1289e83e3a1ddd06d459bb62798dd6bbf6f0 Mon Sep 17 00:00:00 2001 From: Emily Stark <emily@meteor.com> Date: Mon, 23 Jun 2014 08:13:51 -0700 Subject: [PATCH 88/88] Update docs and examples --- docs/.meteor/release | 2 +- docs/lib/release-override.js | 2 +- examples/clock/.meteor/release | 2 +- examples/leaderboard/.meteor/release | 2 +- examples/parties/.meteor/release | 2 +- examples/todos/.meteor/release | 2 +- examples/wordplay/.meteor/release | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/.meteor/release b/docs/.meteor/release index db5f2c74b7..100435be13 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/docs/lib/release-override.js b/docs/lib/release-override.js index 2b39cfa5e4..d0fc420710 100644 --- a/docs/lib/release-override.js +++ b/docs/lib/release-override.js @@ -1,5 +1,5 @@ // While galaxy apps are on their own special meteor releases, override // Meteor.release here. if (Meteor.isClient) { - Meteor.release = Meteor.release ? "0.8.1.3" : undefined; + Meteor.release = Meteor.release ? "0.8.2" : undefined; } diff --git a/examples/clock/.meteor/release b/examples/clock/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/clock/.meteor/release +++ b/examples/clock/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/examples/leaderboard/.meteor/release b/examples/leaderboard/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/leaderboard/.meteor/release +++ b/examples/leaderboard/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/examples/parties/.meteor/release b/examples/parties/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/parties/.meteor/release +++ b/examples/parties/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/examples/todos/.meteor/release b/examples/todos/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/todos/.meteor/release +++ b/examples/todos/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/examples/wordplay/.meteor/release b/examples/wordplay/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/wordplay/.meteor/release +++ b/examples/wordplay/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2