diff --git a/History.md b/History.md index acfbf207a1..b45099d206 100644 --- a/History.md +++ b/History.md @@ -45,6 +45,11 @@ * The `meteor-promise` package has been upgraded to version 0.8.5, and the `promise` polyfill package has been upgraded to 8.0.1. +* `Accounts.config` no longer mistakenly allows tokens to expire when + the `loginExpirationInDays` option is set to `null`. + [Issue #5121](https://github.com/meteor/meteor/issues/5121) + [PR #8917](https://github.com/meteor/meteor/pull/8917) + ## v1.5.1, 2017-07-12 * Node has been upgraded to version 4.8.4. diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index a6688ba492..37ec282985 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -213,8 +213,14 @@ export class AccountsCommon { } } - _getTokenLifetimeMs() { - return (this._options.loginExpirationInDays || + // The options argument is only used by tests. + _getTokenLifetimeMs(options) { + options = options || this._options; + if (options.loginExpirationInDays === null) { + // We disable login expiration by returning Infinity + return Infinity; + } + return (options.loginExpirationInDays || DEFAULT_LOGIN_EXPIRATION_DAYS) * 24 * 60 * 60 * 1000; } diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index 5857597221..c12144c830 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -13,6 +13,24 @@ Tinytest.add('accounts - config validates keys', function (test) { }); }); +// test the loginExpirationInDays config + +Tinytest.add( 'accounts - config - token limetime', function (test) { + var config = { loginExpirationInDays: 2 }; + test.equal(Accounts._getTokenLifetimeMs(config), 2 * 24 * 60 * 60 * 1000); +}); + +Tinytest.add( 'accounts - config - unexpiring tokens', function (test) { + var config = { loginExpirationInDays: null }; + test.equal(Accounts._getTokenLifetimeMs(config), Infinity); +}); + +Tinytest.add( 'accounts - config - default token limetime', function(test) { + var DEFAULT_LOGIN_EXPIRATION_DAYS = 90; // copied from accounts_common.js + var config1 = {}; + var config2 = { loginExpirationInDays: DEFAULT_LOGIN_EXPIRATION_DAYS }; + test.equal(Accounts._getTokenLifetimeMs(config1), Accounts._getTokenLifetimeMs(config2)); +}); var idsInValidateNewUser = {}; Accounts.validateNewUser(function (user) { diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index b088b0aafd..93af180f05 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -546,17 +546,17 @@ Meteor.methods({forgotPassword: function (options) { Accounts.sendResetPasswordEmail(user._id, caseSensitiveEmail); }}); -// send the user an email with a link that when opened allows the user -// to set a new password, without the old password. - /** - * @summary Send an email with a link the user can use to reset their password. + * @summary Generates a reset token and saves it into the database. * @locus Server - * @param {String} userId The id of the user to send email to. - * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. + * @param {String} userId The id of the user to generate the reset token for. + * @param {String} email Which address of the user to generate the reset token for. This address must be in the user's `emails` list. If `null`, defaults to the first email in the list. + * @param {String} reason `resetPassword` or `enrollAccount`. + * @param {Object} [extraTokenData] Optional additional data to be added into the token record. + * @returns {Object} Object with {email, user, token} values. * @importFromPackage accounts-base */ -Accounts.sendResetPasswordEmail = function (userId, email) { +Accounts.generateResetToken = function (userId, email, reason, extraTokenData) { // Make sure the user exists, and email is one of their addresses. var user = Meteor.users.findOne(userId); if (!user) { @@ -574,44 +574,146 @@ Accounts.sendResetPasswordEmail = function (userId, email) { } var token = Random.secret(); - var when = new Date(); var tokenRecord = { token: token, email: email, - when: when, - reason: 'reset' + when: new Date() }; - Meteor.users.update(userId, {$set: { - "services.password.reset": tokenRecord + + if (reason === 'resetPassword') { + tokenRecord.reason = 'reset'; + } else if (reason === 'enrollAccount') { + tokenRecord.reason = 'enroll'; + } else if (reason) { + // fallback so that this function can be used for unknown reasons as well + tokenRecord.reason = reason; + } + + if (extraTokenData) { + _.extend(tokenRecord, extraTokenData); + } + + Meteor.users.update({_id: user._id}, {$set: { + 'services.password.reset': tokenRecord }}); + // before passing to template, update user object with new token Meteor._ensure(user, 'services', 'password').reset = tokenRecord; - var resetPasswordUrl = Accounts.urls.resetPassword(token); + return {email, user, token}; +}; - var options = { - to: email, - from: Accounts.emailTemplates.resetPassword.from - ? Accounts.emailTemplates.resetPassword.from(user) - : Accounts.emailTemplates.from, - subject: Accounts.emailTemplates.resetPassword.subject(user) - }; - - if (typeof Accounts.emailTemplates.resetPassword.text === 'function') { - options.text = - Accounts.emailTemplates.resetPassword.text(user, resetPasswordUrl); +/** + * @summary Generates an e-mail verification token and saves it into the database. + * @locus Server + * @param {String} userId The id of the user to generate the e-mail verification token for. + * @param {String} email Which address of the user to generate the e-mail verification token for. This address must be in the user's `emails` list. If `null`, defaults to the first unverified email in the list. + * @param {Object} [extraTokenData] Optional additional data to be added into the token record. + * @returns {Object} Object with {email, user, token} values. + * @importFromPackage accounts-base + */ +Accounts.generateVerificationToken = function (userId, email, extraTokenData) { + // Make sure the user exists, and email is one of their addresses. + var user = Meteor.users.findOne(userId); + if (!user) { + handleError("Can't find user"); } - if (typeof Accounts.emailTemplates.resetPassword.html === 'function') { - options.html = - Accounts.emailTemplates.resetPassword.html(user, resetPasswordUrl); + // pick the first unverified email if we weren't passed an email. + if (!email) { + var emailRecord = _.find(user.emails || [], function (e) { return !e.verified; }); + email = (emailRecord || {}).address; + + if (!email) { + handleError("That user has no unverified email addresses."); + } + } + + // make sure we have a valid email + if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) { + handleError("No such email for user."); + } + + var token = Random.secret(); + var tokenRecord = { + token: token, + // TODO: This should probably be renamed to "email" to match reset token record. + address: email, + when: new Date() + }; + + if (extraTokenData) { + _.extend(tokenRecord, extraTokenData); + } + + Meteor.users.update({_id: user._id}, {$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); + + return {email, user, token}; +}; + +/** + * @summary Creates options for email sending for reset password and enroll account emails. + * You can use this function when customizing a reset password or enroll account email sending. + * @locus Server + * @param {Object} email Which address of the user's to send the email to. + * @param {Object} user The user object to generate options for. + * @param {String} url URL to which user is directed to confirm the email. + * @param {String} reason `resetPassword` or `enrollAccount`. + * @returns {Object} Options which can be passed to `Email.send`. + * @importFromPackage accounts-base + */ +Accounts.generateOptionsForEmail = function (email, user, url, reason) { + var options = { + to: email, + from: Accounts.emailTemplates[reason].from + ? Accounts.emailTemplates[reason].from(user) + : Accounts.emailTemplates.from, + subject: Accounts.emailTemplates[reason].subject(user) + }; + + if (typeof Accounts.emailTemplates[reason].text === 'function') { + options.text = Accounts.emailTemplates[reason].text(user, url); + } + + if (typeof Accounts.emailTemplates[reason].html === 'function') { + options.html = Accounts.emailTemplates[reason].html(user, url); } if (typeof Accounts.emailTemplates.headers === 'object') { options.headers = Accounts.emailTemplates.headers; } + return options; +}; + +// send the user an email with a link that when opened allows the user +// to set a new password, without the old password. + +/** + * @summary Send an email with a link the user can use to reset their password. + * @locus Server + * @param {String} userId The id of the user to send email to. + * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. + * @param {Object} [extraTokenData] Optional additional data to be added into the token record. + * @returns {Object} Object with {email, user, token, url, options} values. + * @importFromPackage accounts-base + */ +Accounts.sendResetPasswordEmail = function (userId, email, extraTokenData) { + const {email: realEmail, user, token} = + Accounts.generateResetToken(userId, email, 'resetPassword', extraTokenData); + const url = Accounts.urls.resetPassword(token); + const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'resetPassword'); Email.send(options); + return {email: realEmail, user, token, url, options}; }; // send the user an email informing them that their account was created, with @@ -627,65 +729,17 @@ Accounts.sendResetPasswordEmail = function (userId, email) { * @locus Server * @param {String} userId The id of the user to send email to. * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. + * @param {Object} [extraTokenData] Optional additional data to be added into the token record. + * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendEnrollmentEmail = function (userId, email) { - // XXX refactor! This is basically identical to sendResetPasswordEmail. - - // Make sure the user exists, and email is in their addresses. - var user = Meteor.users.findOne(userId); - if (!user) { - throw new Error("Can't find user"); - } - // pick the first email if we weren't passed an email. - if (!email && user.emails && user.emails[0]) { - email = user.emails[0].address; - } - // make sure we have a valid 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, - reason: 'enroll' - }; - Meteor.users.update(userId, {$set: { - "services.password.reset": tokenRecord - }}); - - // before passing to template, update user object with new token - Meteor._ensure(user, 'services', 'password').reset = tokenRecord; - - var enrollAccountUrl = Accounts.urls.enrollAccount(token); - - var options = { - to: email, - from: Accounts.emailTemplates.enrollAccount.from - ? Accounts.emailTemplates.enrollAccount.from(user) - : Accounts.emailTemplates.from, - subject: Accounts.emailTemplates.enrollAccount.subject(user) - }; - - if (typeof Accounts.emailTemplates.enrollAccount.text === 'function') { - options.text = - Accounts.emailTemplates.enrollAccount.text(user, enrollAccountUrl); - } - - if (typeof Accounts.emailTemplates.enrollAccount.html === 'function') { - options.html = - Accounts.emailTemplates.enrollAccount.html(user, enrollAccountUrl); - } - - if (typeof Accounts.emailTemplates.headers === 'object') { - options.headers = Accounts.emailTemplates.headers; - } - +Accounts.sendEnrollmentEmail = function (userId, email, extraTokenData) { + const {email: realEmail, user, token} = + Accounts.generateResetToken(userId, email, 'enrollAccount', extraTokenData); + const url = Accounts.urls.enrollAccount(token); + const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'enrollAccount'); Email.send(options); + return {email: realEmail, user, token, url, options}; }; @@ -782,71 +836,21 @@ Meteor.methods({resetPassword: function (token, newPassword) { * @locus Server * @param {String} userId The id of the user to send email to. * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first unverified email in the list. + * @param {Object} [extraTokenData] Optional additional data to be added into the token record. + * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendVerificationEmail = function (userId, address) { +Accounts.sendVerificationEmail = function (userId, email, extraTokenData) { // XXX Also generate a link using which someone can delete this // account if they own said address but weren't those who created // this account. - // Make sure the user exists, and address is one of their addresses. - var user = Meteor.users.findOne(userId); - if (!user) - throw new Error("Can't find user"); - // pick the first unverified address if we weren't passed an address. - if (!address) { - var email = _.find(user.emails || [], - function (e) { return !e.verified; }); - address = (email || {}).address; - - if (!address) { - throw new Error("That user has no unverified email addresses."); - } - } - // make sure we have a valid address - if (!address || !_.contains(_.pluck(user.emails || [], 'address'), address)) - throw new Error("No such email address for user."); - - - var tokenRecord = { - token: Random.secret(), - address: address, - when: new Date()}; - 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); - - var options = { - to: address, - from: Accounts.emailTemplates.verifyEmail.from - ? Accounts.emailTemplates.verifyEmail.from(user) - : Accounts.emailTemplates.from, - subject: Accounts.emailTemplates.verifyEmail.subject(user) - }; - - if (typeof Accounts.emailTemplates.verifyEmail.text === 'function') { - options.text = - Accounts.emailTemplates.verifyEmail.text(user, verifyEmailUrl); - } - - if (typeof Accounts.emailTemplates.verifyEmail.html === 'function') - options.html = - Accounts.emailTemplates.verifyEmail.html(user, verifyEmailUrl); - - if (typeof Accounts.emailTemplates.headers === 'object') { - options.headers = Accounts.emailTemplates.headers; - } - + const {email: realEmail, user, token} = + Accounts.generateVerificationToken(userId, email, extraTokenData); + const url = Accounts.urls.verifyEmail(token); + const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'verifyEmail'); Email.send(options); + return {email: realEmail, user, token, url, options}; }; // Take token from sendVerificationEmail, mark the email as verified, diff --git a/packages/crosswalk/package.js b/packages/crosswalk/package.js index 1dc6923f67..2e7d8b382b 100644 --- a/packages/crosswalk/package.js +++ b/packages/crosswalk/package.js @@ -6,5 +6,5 @@ instead of the System WebView on Android", }); Cordova.depends({ - 'cordova-plugin-crosswalk-webview': '2.2.0' + 'cordova-plugin-crosswalk-webview': '2.3.0' }); diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 2b74334f54..d8d0cb9471 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -94,6 +94,21 @@ var builtinConverters = [ return new Date(obj.$date); } }, + { // RegExp + matchJSONValue: function (obj) { + return _.has(obj, '$regexp') && _.has(obj, '$flags') && _.size(obj) === 2; + }, + matchObject: function (obj) { + return obj instanceof RegExp; + }, + toJSONValue: function (regexp) { + return { $regexp: regexp.source, $flags: regexp.flags }; + }, + fromJSONValue: function (obj) { + //replaces duplicate / invalid flags + return new RegExp(obj.$regexp, obj.$flags.replace(/[^gimuy]/g,'').replace(/(.)(?=.*\1)/g, '')); + } + }, { // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object' // which we match.) matchJSONValue: function (obj) { diff --git a/packages/ejson/ejson_test.js b/packages/ejson/ejson_test.js index eba7c0e31b..fe28c67380 100644 --- a/packages/ejson/ejson_test.js +++ b/packages/ejson/ejson_test.js @@ -185,6 +185,16 @@ Tinytest.add("ejson - parse", function (test) { ); }); +Tinytest.add("ejson - regexp", function (test) { + test.equal(EJSON.stringify(/foo/gi), "{\"$regexp\":\"foo\",\"$flags\":\"gi\"}"); + var d = new RegExp("foo", "gi"); + var obj = { $regexp: "foo", $flags: "gi" }; + + var eObj = EJSON.toJSONValue(obj); + var roundTrip = EJSON.fromJSONValue(eObj); + test.equal(obj, roundTrip); +}); + Tinytest.add("ejson - custom types", function (test) { var testSameConstructors = function (obj, compareWith) { test.equal(obj.constructor, compareWith.constructor); diff --git a/packages/launch-screen/package.js b/packages/launch-screen/package.js index c027b04e60..c6c0749150 100644 --- a/packages/launch-screen/package.js +++ b/packages/launch-screen/package.js @@ -10,7 +10,7 @@ Package.describe({ }); Cordova.depends({ - 'cordova-plugin-splashscreen': '4.0.1' + 'cordova-plugin-splashscreen': '4.0.3' }); Package.onUse(function(api) { diff --git a/packages/logging/package.js b/packages/logging/package.js index f92fc241ea..85e6423d1e 100644 --- a/packages/logging/package.js +++ b/packages/logging/package.js @@ -12,7 +12,7 @@ Npm.strip({ }); Cordova.depends({ - 'cordova-plugin-console': '1.0.5' + 'cordova-plugin-console': '1.0.7' }); Package.onUse(function (api) { diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 840d06c9ed..99720962a0 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -775,8 +775,7 @@ ELEMENT_OPERATORS = { var regexp; if (valueSelector.$options !== undefined) { // Options passed in $options (even the empty string) always overrides - // options in the RegExp object itself. (See also - // Mongo.Collection._rewriteSelector.) + // options in the RegExp object itself. // Be clear that we only support the JS-supported options, not extended // ones (eg, Mongo supports x and s). Ideally we would implement x and s diff --git a/packages/mobile-status-bar/package.js b/packages/mobile-status-bar/package.js index ddf88dc48d..86485b0923 100644 --- a/packages/mobile-status-bar/package.js +++ b/packages/mobile-status-bar/package.js @@ -4,5 +4,5 @@ Package.describe({ }); Cordova.depends({ - 'cordova-plugin-statusbar': '2.2.1' + 'cordova-plugin-statusbar': '2.2.3' }); diff --git a/packages/mongo/collection.js b/packages/mongo/collection.js index b22eb0248a..6a1a6906a4 100644 --- a/packages/mongo/collection.js +++ b/packages/mongo/collection.js @@ -362,8 +362,7 @@ Mongo.Collection._publishCursor = function (cursor, sub, collection) { // protect against dangerous selectors. falsey and {_id: falsey} are both // likely programmer error, and not what you want, particularly for destructive -// operations. JS regexps don't serialize over DDP but can be trivially -// replaced by $regex. If a falsey _id is sent in, a new string _id will be +// operations. If a falsey _id is sent in, a new string _id will be // generated and returned; if a fallbackId is provided, it will be returned // instead. Mongo.Collection._rewriteSelector = (selector, { fallbackId } = {}) => { @@ -382,48 +381,8 @@ Mongo.Collection._rewriteSelector = (selector, { fallbackId } = {}) => { return { _id: fallbackId || Random.id() }; } - var ret = {}; - Object.keys(selector).forEach((key) => { - const value = selector[key]; - // Mongo supports both {field: /foo/} and {field: {$regex: /foo/}} - if (value instanceof RegExp) { - ret[key] = convertRegexpToMongoSelector(value); - } else if (value && value.$regex instanceof RegExp) { - ret[key] = convertRegexpToMongoSelector(value.$regex); - // if value is {$regex: /foo/, $options: ...} then $options - // override the ones set on $regex. - if (value.$options !== undefined) - ret[key].$options = value.$options; - } else if (_.contains(['$or','$and','$nor'], key)) { - // Translate lower levels of $and/$or/$nor - ret[key] = _.map(value, function (v) { - return Mongo.Collection._rewriteSelector(v); - }); - } else { - ret[key] = value; - } - }); - return ret; -}; - -// convert a JS RegExp object to a Mongo {$regex: ..., $options: ...} -// selector -function convertRegexpToMongoSelector(regexp) { - check(regexp, RegExp); // safety belt - - var selector = {$regex: regexp.source}; - var regexOptions = ''; - // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options - // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. - if (regexp.ignoreCase) - regexOptions += 'i'; - if (regexp.multiline) - regexOptions += 'm'; - if (regexOptions) - selector.$options = regexOptions; - return selector; -} +}; // 'insert' immediately returns the inserted document's new _id. // The others return values immediately if you are in a stub, an in-memory diff --git a/packages/mongo/mongo_driver.js b/packages/mongo/mongo_driver.js index ffa5d5d1b0..48f8a8a8e9 100644 --- a/packages/mongo/mongo_driver.js +++ b/packages/mongo/mongo_driver.js @@ -1183,7 +1183,7 @@ MongoConnection.prototype._observeChanges = function ( throw Error("You may not observe a cursor with {fields: {_id: 0}}"); } - var observeKey = JSON.stringify( + var observeKey = EJSON.stringify( _.extend({ordered: ordered}, cursorDescription)); var multiplexer, observeDriver; diff --git a/packages/mongo/mongo_livedata_tests.js b/packages/mongo/mongo_livedata_tests.js index 1df0a927e2..faba62f641 100644 --- a/packages/mongo/mongo_livedata_tests.js +++ b/packages/mongo/mongo_livedata_tests.js @@ -2152,88 +2152,15 @@ _.each(Meteor.isServer ? [true, false] : [true], function (minimongo) { }); // end idGeneration parametrization Tinytest.add('mongo-livedata - rewrite selector', function (test) { - test.equal(Mongo.Collection._rewriteSelector({x: /^o+B/im}), - {x: {$regex: '^o+B', $options: 'im'}}); - test.equal(Mongo.Collection._rewriteSelector({x: {$regex: /^o+B/im}}), - {x: {$regex: '^o+B', $options: 'im'}}); - test.equal(Mongo.Collection._rewriteSelector({x: /^o+B/}), - {x: {$regex: '^o+B'}}); - test.equal(Mongo.Collection._rewriteSelector({x: {$regex: /^o+B/}}), - {x: {$regex: '^o+B'}}); + test.equal(Mongo.Collection._rewriteSelector('foo'), {_id: 'foo'}); - test.equal( - Mongo.Collection._rewriteSelector( - {'$or': [ - {x: /^o/}, - {y: /^p/}, - {z: 'q'}, - {w: {$regex: /^r/}} - ]} - ), - {'$or': [ - {x: {$regex: '^o'}}, - {y: {$regex: '^p'}}, - {z: 'q'}, - {w: {$regex: '^r'}} - ]} - ); - - test.equal( - Mongo.Collection._rewriteSelector( - {'$or': [ - {'$and': [ - {x: /^a/i}, - {y: /^b/}, - {z: {$regex: /^c/i}}, - {w: {$regex: '^[abc]', $options: 'i'}}, // make sure we don't break vanilla selectors - {v: {$regex: /O/, $options: 'i'}}, // $options should override the ones on the RegExp object - {u: {$regex: /O/m, $options: 'i'}} // $options should override the ones on the RegExp object - ]}, - {'$nor': [ - {s: /^d/}, - {t: /^e/i}, - {u: {$regex: /^f/i}}, - // even empty string overrides built-in flags - {v: {$regex: /^g/i, $options: ''}} - ]} - ]} - ), - {'$or': [ - {'$and': [ - {x: {$regex: '^a', $options: 'i'}}, - {y: {$regex: '^b'}}, - {z: {$regex: '^c', $options: 'i'}}, - {w: {$regex: '^[abc]', $options: 'i'}}, - {v: {$regex: 'O', $options: 'i'}}, - {u: {$regex: 'O', $options: 'i'}} - ]}, - {'$nor': [ - {s: {$regex: '^d'}}, - {t: {$regex: '^e', $options: 'i'}}, - {u: {$regex: '^f', $options: 'i'}}, - {v: {$regex: '^g', $options: ''}} - ]} - ]} - ); var oid = new Mongo.ObjectID(); test.equal(Mongo.Collection._rewriteSelector(oid), {_id: oid}); - // Make sure selectors with "length" properties are handled properly - // (verifies issue #8329 has been resolved). - const SomeSelector = function (length) { - this.length = length; - }; - const length = 2; - const testSelector = new SomeSelector(length); - test.equal( - Mongo.Collection._rewriteSelector(testSelector), - { length } - ); - test.matches( Mongo.Collection._rewriteSelector({ _id: null })._id, /^\S+$/, diff --git a/packages/mongo/package.js b/packages/mongo/package.js index 1637f0c82f..398cc6a88d 100644 --- a/packages/mongo/package.js +++ b/packages/mongo/package.js @@ -21,7 +21,7 @@ Npm.strip({ }); Package.onUse(function (api) { - api.use('npm-mongo@2.2.24', 'server'); + api.use('npm-mongo', 'server'); api.use('allow-deny'); api.use([ diff --git a/packages/npm-mongo/.npm/package/npm-shrinkwrap.json b/packages/npm-mongo/.npm/package/npm-shrinkwrap.json index 74c82225ce..d7f6f737ed 100644 --- a/packages/npm-mongo/.npm/package/npm-shrinkwrap.json +++ b/packages/npm-mongo/.npm/package/npm-shrinkwrap.json @@ -32,14 +32,14 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "mongodb": { - "version": "2.2.24", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.24.tgz", - "integrity": "sha1-gPQNbsW97A3ezw+c4BROeUxGRJo=" + "version": "2.2.30", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.30.tgz", + "integrity": "sha1-jM2AH2dsgXIEDC8rR+lgKg1WNKs=" }, "mongodb-core": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.8.tgz", - "integrity": "sha1-sz4DcNClnZe2yx7GEFJ76elcosA=" + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.14.tgz", + "integrity": "sha1-E8uidkImtb49GJkq8Mljzl6g8P0=" }, "process-nextick-args": { "version": "1.0.7", @@ -47,29 +47,34 @@ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, "readable-stream": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", - "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=" + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=" }, "require_optional": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.0.tgz", - "integrity": "sha1-UqhhN6hJco62ClVTNhf4+RT1mr8=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==" }, "resolve-from": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" }, "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==" }, "util-deprecate": { "version": "1.0.2", diff --git a/packages/npm-mongo/package.js b/packages/npm-mongo/package.js index a4a7395c5c..84b27a141b 100644 --- a/packages/npm-mongo/package.js +++ b/packages/npm-mongo/package.js @@ -3,12 +3,12 @@ Package.describe({ summary: "Wrapper around the mongo npm package", - version: '2.2.24', + version: '2.2.30', documentation: null }); Npm.depends({ - mongodb: "2.2.24" + mongodb: "2.2.30" }); Package.onUse(function (api) { diff --git a/packages/oauth/package.js b/packages/oauth/package.js index 4d91f53235..a04c9f7247 100644 --- a/packages/oauth/package.js +++ b/packages/oauth/package.js @@ -57,5 +57,5 @@ Package.onTest(function (api) { }); Cordova.depends({ - 'cordova-plugin-inappbrowser': '1.6.1' + 'cordova-plugin-inappbrowser': '1.7.1' }); diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 2fc2262de2..ac87ec5f40 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -14,9 +14,9 @@ Npm.strip({ }); Cordova.depends({ - 'cordova-plugin-whitelist': '1.3.1', - 'cordova-plugin-wkwebview-engine': '1.1.1', - 'cordova-plugin-meteor-webapp': '1.4.1' + 'cordova-plugin-whitelist': '1.3.2', + 'cordova-plugin-wkwebview-engine': '1.1.3', + 'cordova-plugin-meteor-webapp': '1.4.2' }); Package.onUse(function (api) { diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index f6f9368740..755fa1bc57 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -52,7 +52,7 @@ var packageJson = { pathwatcher: "7.1.0", optimism: "0.3.3", 'lru-cache': '4.0.1', - 'cordova-lib': "6.4.0", + 'cordova-lib': "7.0.1", longjohn: '0.2.11' } }; diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 13195b00dd..1152d045d0 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -1579,8 +1579,31 @@ function doTestCommand(options) { // cleaned up on process exit. Using a temporary app dir means that we can // run multiple "test-packages" commands in parallel without them stomping // on each other. - var testRunnerAppDir = - options['test-app-path'] || files.mkdtemp('meteor-test-run'); + let testRunnerAppDir; + const testAppPath = options['test-app-path']; + if (testAppPath) { + try { + if (files.mkdir_p(testAppPath, 0o700)) { + testRunnerAppDir = testAppPath; + } else { + Console.error( + 'The specified --test-app-path directory could not be used, as ' + + `"${testAppPath}" already exists and it is not a directory.` + ); + return 1; + } + } catch (error) { + Console.error( + 'Unable to create the specified --test-app-path directory of ' + + `"${testAppPath}".` + ); + throw error; + } + } + + if (!testRunnerAppDir) { + testRunnerAppDir = files.mkdtemp('meteor-test-run'); + } // Download packages for our architecture, and for the deploy server's // architecture if we're deploying. @@ -1611,7 +1634,19 @@ function doTestCommand(options) { projectContextOptions.projectDir = testRunnerAppDir; projectContextOptions.projectDirForLocalPackages = options.appDir; - require("./default-npm-deps.js").install(testRunnerAppDir); + try { + require("./default-npm-deps.js").install(testRunnerAppDir); + } catch (error) { + if (error.code === 'EACCES' && options['test-app-path']) { + Console.error( + 'The specified --test-app-path directory of ' + + `"${testRunnerAppDir}" exists, but the current user does not have ` + + `read/write permission in it.` + ); + } + throw error; + } + if (buildmessage.jobHasMessages()) { return; } diff --git a/tools/cordova/index.js b/tools/cordova/index.js index 09837dc13f..4d5281196e 100644 --- a/tools/cordova/index.js +++ b/tools/cordova/index.js @@ -11,8 +11,8 @@ export const CORDOVA_ARCH = "web.cordova"; export const CORDOVA_PLATFORMS = ['ios', 'android']; export const CORDOVA_PLATFORM_VERSIONS = { - 'android': '6.1.1', - 'ios': '4.3.0' + 'android': '6.2.3', + 'ios': '4.4.0' }; const PLATFORM_TO_DISPLAY_NAME_MAP = { diff --git a/tools/cordova/project.js b/tools/cordova/project.js index b9f125d1ac..cecd8853ee 100644 --- a/tools/cordova/project.js +++ b/tools/cordova/project.js @@ -51,29 +51,29 @@ const pinnedPlatformVersions = CORDOVA_PLATFORM_VERSIONS; // Versions are taken from cordova-lib's package.json and should be updated // when we update to a newer version of cordova-lib. const pinnedPluginVersions = { - "cordova-plugin-battery-status": "1.2.2", - "cordova-plugin-camera": "2.3.1", - "cordova-plugin-console": "1.0.5", - "cordova-plugin-contacts": "2.2.1", - "cordova-plugin-device": "1.1.4", - "cordova-plugin-device-motion": "1.2.3", - "cordova-plugin-device-orientation": "1.0.5", - "cordova-plugin-dialogs": "1.3.1", - "cordova-plugin-file": "4.3.1", - "cordova-plugin-file-transfer": "1.6.1", - "cordova-plugin-geolocation": "2.4.1", - "cordova-plugin-globalization": "1.0.5", - "cordova-plugin-inappbrowser": "1.6.1", + "cordova-plugin-battery-status": "1.2.4", + "cordova-plugin-camera": "2.4.1", + "cordova-plugin-console": "1.0.7", + "cordova-plugin-contacts": "2.3.1", + "cordova-plugin-device": "1.1.6", + "cordova-plugin-device-motion": "1.2.5", + "cordova-plugin-device-orientation": "1.0.7", + "cordova-plugin-dialogs": "1.3.3", + "cordova-plugin-file": "4.3.3", + "cordova-plugin-file-transfer": "1.6.3", + "cordova-plugin-geolocation": "2.4.3", + "cordova-plugin-globalization": "1.0.7", + "cordova-plugin-inappbrowser": "1.7.1", "cordova-plugin-legacy-whitelist": "1.1.2", - "cordova-plugin-media": "2.4.1", - "cordova-plugin-media-capture": "1.4.1", - "cordova-plugin-network-information": "1.3.1", - "cordova-plugin-splashscreen": "4.0.1", - "cordova-plugin-statusbar": "2.2.1", - "cordova-plugin-test-framework": "1.1.4", - "cordova-plugin-vibration": "2.1.3", - "cordova-plugin-whitelist": "1.3.1", - "cordova-plugin-wkwebview-engine": "1.1.1" + "cordova-plugin-media": "3.0.1", + "cordova-plugin-media-capture": "1.4.3", + "cordova-plugin-network-information": "1.3.3", + "cordova-plugin-splashscreen": "4.0.3", + "cordova-plugin-statusbar": "2.2.3", + "cordova-plugin-test-framework": "1.1.5", + "cordova-plugin-vibration": "2.1.5", + "cordova-plugin-whitelist": "1.3.2", + "cordova-plugin-wkwebview-engine": "1.1.3" } export class CordovaProject { diff --git a/tools/tests/apps/dynamic-import/.meteor/packages b/tools/tests/apps/dynamic-import/.meteor/packages index 5e0d80da9a..7da6f95217 100644 --- a/tools/tests/apps/dynamic-import/.meteor/packages +++ b/tools/tests/apps/dynamic-import/.meteor/packages @@ -6,21 +6,21 @@ meteor-base@1.1.0 # Packages every Meteor app needs to have mobile-experience@1.0.4 # Packages for a great mobile UX -mongo@1.1.18 # The database Meteor supports right now +mongo@1.1.19 # The database Meteor supports right now blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views reactive-var@1.0.11 # Reactive variable for tracker jquery@1.11.10 # Helpful client-side library tracker@1.1.3 # Meteor's client-side reactive programming library standard-minifier-css@1.3.4 # CSS minifier run for production mode -standard-minifier-js@2.1.0 # JS minifier run for production mode +standard-minifier-js@2.1.1 # JS minifier run for production mode es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.8.0 # Enable ECMAScript2015+ syntax in app code -shell-server@0.2.3 # Server-side component of the `meteor shell` command +ecmascript@0.8.2 # Enable ECMAScript2015+ syntax in app code +shell-server@0.2.4 # Server-side component of the `meteor shell` command autopublish@1.0.7 # Publish all data to the clients (for prototyping) insecure@1.0.7 # Allow all DB writes from clients (for prototyping) -dynamic-import +dynamic-import@0.1.1 dispatch:mocha-phantomjs dispatch:mocha-browser lazy-test-package diff --git a/tools/tests/apps/dynamic-import/.meteor/release b/tools/tests/apps/dynamic-import/.meteor/release index 025f64e707..1e7fc5b564 100644 --- a/tools/tests/apps/dynamic-import/.meteor/release +++ b/tools/tests/apps/dynamic-import/.meteor/release @@ -1 +1 @@ -METEOR@1.5 +METEOR@1.5.1 diff --git a/tools/tests/apps/modules/.meteor/packages b/tools/tests/apps/modules/.meteor/packages index d620c184cb..625b793b0d 100644 --- a/tools/tests/apps/modules/.meteor/packages +++ b/tools/tests/apps/modules/.meteor/packages @@ -6,22 +6,22 @@ meteor-base@1.1.0 # Packages every Meteor app needs to have mobile-experience@1.0.4 # Packages for a great mobile UX -mongo@1.1.18 # The database Meteor supports right now +mongo@1.1.19 # The database Meteor supports right now blaze-html-templates # Compile .html files into Meteor Blaze views session@1.1.7 # Client-side reactive dictionary for your app jquery@1.11.10 # Helpful client-side library tracker@1.1.3 # Meteor's client-side reactive programming library es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.8.0 # Enable ECMAScript2015+ syntax in app code +ecmascript@0.8.2 # Enable ECMAScript2015+ syntax in app code coffeescript modules-test-package dispatch:mocha-phantomjs dispatch:mocha-browser standard-minifier-css@1.3.4 -standard-minifier-js@2.1.0 +standard-minifier-js@2.1.1 client-only-ecmascript modules-test-plugin -shell-server@0.2.3 -dynamic-import +shell-server@0.2.4 +dynamic-import@0.1.1 diff --git a/tools/tests/apps/modules/.meteor/release b/tools/tests/apps/modules/.meteor/release index 025f64e707..1e7fc5b564 100644 --- a/tools/tests/apps/modules/.meteor/release +++ b/tools/tests/apps/modules/.meteor/release @@ -1 +1 @@ -METEOR@1.5 +METEOR@1.5.1 diff --git a/tools/tests/colon-converter-tests.js b/tools/tests/colon-converter-tests.js index c51dceebfe..ab1172f446 100644 --- a/tools/tests/colon-converter-tests.js +++ b/tools/tests/colon-converter-tests.js @@ -74,8 +74,7 @@ if (process.platform !== "win32") { }); var run = s.run("add", packageName); - run.waitSecs(60); - run.matchErr("colons"); + run.matchErrBeforeExit("colons"); }); }); } diff --git a/tools/tests/command-line.js b/tools/tests/command-line.js index 769065b826..6f6a399235 100644 --- a/tools/tests/command-line.js +++ b/tools/tests/command-line.js @@ -596,3 +596,75 @@ selftest.define("old cli tests (converted)", function () { run.expectExit(0); files.unlink(files.pathJoin(s.cwd, 'settings.js')); }); + +// Added to address https://github.com/meteor/meteor/issues/8897. +selftest.define( + 'meteor test-packages --test-app-path directory', + function () { + var s = new Sandbox(); + var run; + + // If test-app-path doesn't exist, it should be created. + var testAppPath = '/tmp/meteor_test_app_path'; + files.rm_recursive(testAppPath); + selftest.expectFalse(files.exists(testAppPath)); + s.createApp('test-app-path-app', 'package-tests', { + dontPrepareApp: true + }); + s.cd('test-app-path-app/packages/say-something', function () { + run = s.run( + 'test-packages', + '--once', + { 'test-app-path': testAppPath }, + './' + ); + run.match('Started'); + selftest.expectTrue(files.exists(testAppPath)); + run.stop(); + files.rm_recursive(testAppPath); + }); + + // If test-app-path already exists, make sure that directory is used. + var testAppPath = '/tmp/meteor_test_app_path'; + files.rm_recursive(testAppPath); + files.mkdir_p(testAppPath); + selftest.expectTrue(files.exists(testAppPath)); + selftest.expectFalse(files.exists(testAppPath + '/.meteor')); + s.createApp('test-app-path-app', 'package-tests', { + dontPrepareApp: true + }); + s.cd('test-app-path-app/packages/say-something', function () { + run = s.run( + 'test-packages', + '--once', + { 'test-app-path': testAppPath }, + './' + ); + run.match('Started'); + selftest.expectTrue(files.exists(testAppPath + '/.meteor')); + run.stop(); + files.rm_recursive(testAppPath); + }); + + // If test-app-path already exists but is a file instead of a directory, + // show a console error message explaining this, and exit. + var testAppPath = '/tmp/meteor_test_app_path'; + files.rm_recursive(testAppPath); + files.writeFile(testAppPath, '<3 meteor'); + selftest.expectTrue(files.exists(testAppPath)); + s.createApp('test-app-path-app', 'package-tests', { + dontPrepareApp: true + }); + s.cd('test-app-path-app/packages/say-something', function () { + run = s.run( + 'test-packages', + '--once', + { 'test-app-path': testAppPath }, + './' + ); + run.matchErr('is not a directory'); + run.expectExit(1); + files.rm_recursive(testAppPath); + }); + } +); diff --git a/tools/tests/package-tests.js b/tools/tests/package-tests.js index 69511d9984..7436d6a1b3 100644 --- a/tools/tests/package-tests.js +++ b/tools/tests/package-tests.js @@ -109,6 +109,7 @@ selftest.define("change packages during hot code push", [], function () { run.waitSecs(5); run.match("myapp"); run.match("proxy"); + run.waitSecs(5); run.match("MongoDB"); run.waitSecs(5); run.match("your app");