2019-05-29 05:21:06 -04:00
|
|
|
const async = require('async')
|
|
|
|
const PlansLocator = require('./PlansLocator')
|
|
|
|
const _ = require('underscore')
|
|
|
|
const SubscriptionLocator = require('./SubscriptionLocator')
|
|
|
|
const UserFeaturesUpdater = require('./UserFeaturesUpdater')
|
|
|
|
const Settings = require('settings-sharelatex')
|
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
const ReferalFeatures = require('../Referal/ReferalFeatures')
|
|
|
|
const V1SubscriptionManager = require('./V1SubscriptionManager')
|
|
|
|
const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
|
2019-10-05 13:43:21 -04:00
|
|
|
const UserGetter = require('../User/UserGetter')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-11-25 08:29:40 -05:00
|
|
|
const FeaturesUpdater = {
|
2020-01-07 06:03:14 -05:00
|
|
|
refreshFeatures(userId, callback = () => {}) {
|
|
|
|
FeaturesUpdater._computeFeatures(userId, (error, features) => {
|
2019-12-05 08:57:58 -05:00
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2020-01-07 06:03:14 -05:00
|
|
|
logger.log({ userId, features }, 'updating user features')
|
|
|
|
UserFeaturesUpdater.updateFeatures(userId, features, callback)
|
2019-12-05 08:57:58 -05:00
|
|
|
})
|
|
|
|
},
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_computeFeatures(userId, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
const jobs = {
|
|
|
|
individualFeatures(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getIndividualFeatures(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
groupFeatureSets(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getGroupFeatureSets(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
institutionFeatures(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
InstitutionsFeatures.getInstitutionsFeatures(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
v1Features(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getV1Features(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
bonusFeatures(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
ReferalFeatures.getBonusFeatures(userId, cb)
|
2019-10-05 13:43:21 -04:00
|
|
|
},
|
|
|
|
samlFeatures(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getSamlFeatures(userId, cb)
|
2019-12-02 08:51:57 -05:00
|
|
|
},
|
|
|
|
featuresOverrides(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getFeaturesOverrides(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
2020-01-07 06:03:14 -05:00
|
|
|
async.series(jobs, function(err, results) {
|
|
|
|
if (err) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn(
|
2020-01-07 06:03:14 -05:00
|
|
|
{ err, userId },
|
2019-05-29 05:21:06 -04:00
|
|
|
'error getting subscription or group for refreshFeatures'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
const {
|
|
|
|
individualFeatures,
|
|
|
|
groupFeatureSets,
|
|
|
|
institutionFeatures,
|
|
|
|
v1Features,
|
2019-10-05 13:43:21 -04:00
|
|
|
bonusFeatures,
|
2019-12-02 08:51:57 -05:00
|
|
|
samlFeatures,
|
|
|
|
featuresOverrides
|
2019-05-29 05:21:06 -04:00
|
|
|
} = results
|
|
|
|
logger.log(
|
|
|
|
{
|
2020-01-07 06:03:14 -05:00
|
|
|
userId,
|
2019-05-29 05:21:06 -04:00
|
|
|
individualFeatures,
|
|
|
|
groupFeatureSets,
|
|
|
|
institutionFeatures,
|
|
|
|
v1Features,
|
2019-10-05 13:43:21 -04:00
|
|
|
bonusFeatures,
|
2019-12-02 08:51:57 -05:00
|
|
|
samlFeatures,
|
|
|
|
featuresOverrides
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
'merging user features'
|
|
|
|
)
|
|
|
|
const featureSets = groupFeatureSets.concat([
|
|
|
|
individualFeatures,
|
|
|
|
institutionFeatures,
|
|
|
|
v1Features,
|
2019-10-05 13:43:21 -04:00
|
|
|
bonusFeatures,
|
2019-12-02 08:51:57 -05:00
|
|
|
samlFeatures,
|
|
|
|
featuresOverrides
|
2019-05-29 05:21:06 -04:00
|
|
|
])
|
|
|
|
const features = _.reduce(
|
|
|
|
featureSets,
|
|
|
|
FeaturesUpdater._mergeFeatures,
|
|
|
|
Settings.defaultFeatures
|
|
|
|
)
|
2019-12-05 08:57:58 -05:00
|
|
|
callback(null, features)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getIndividualFeatures(userId, callback) {
|
2020-02-03 09:11:23 -05:00
|
|
|
SubscriptionLocator.getUserIndividualSubscription(userId, (err, sub) =>
|
2019-05-29 05:21:06 -04:00
|
|
|
callback(err, FeaturesUpdater._subscriptionToFeatures(sub))
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getGroupFeatureSets(userId, callback) {
|
|
|
|
SubscriptionLocator.getGroupSubscriptionsMemberOf(userId, (err, subs) =>
|
|
|
|
callback(err, (subs || []).map(FeaturesUpdater._subscriptionToFeatures))
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getSamlFeatures(userId, callback) {
|
|
|
|
UserGetter.getUser(userId, (err, user) => {
|
2019-10-05 13:43:21 -04:00
|
|
|
if (err) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
!user ||
|
|
|
|
!Array.isArray(user.samlIdentifiers) ||
|
|
|
|
!user.samlIdentifiers.length
|
|
|
|
) {
|
|
|
|
return callback(null, {})
|
|
|
|
}
|
|
|
|
for (const samlIdentifier of user.samlIdentifiers) {
|
|
|
|
if (samlIdentifier && samlIdentifier.hasEntitlement) {
|
|
|
|
return callback(
|
|
|
|
null,
|
|
|
|
FeaturesUpdater._planCodeToFeatures('professional')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2020-01-07 06:03:14 -05:00
|
|
|
callback(null, {})
|
2019-10-05 13:43:21 -04:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getFeaturesOverrides(userId, callback) {
|
|
|
|
UserGetter.getUser(userId, { featuresOverrides: 1 }, (error, user) => {
|
2019-12-02 08:51:57 -05:00
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
!user ||
|
|
|
|
!user.featuresOverrides ||
|
|
|
|
user.featuresOverrides.length === 0
|
|
|
|
) {
|
|
|
|
return callback(null, {})
|
|
|
|
}
|
|
|
|
let activeFeaturesOverrides = []
|
|
|
|
for (let featuresOverride of user.featuresOverrides) {
|
|
|
|
if (
|
|
|
|
!featuresOverride.expiresAt ||
|
|
|
|
featuresOverride.expiresAt > new Date()
|
|
|
|
) {
|
|
|
|
activeFeaturesOverrides.push(featuresOverride.features)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const features = _.reduce(
|
|
|
|
activeFeaturesOverrides,
|
|
|
|
FeaturesUpdater._mergeFeatures,
|
|
|
|
{}
|
|
|
|
)
|
2020-01-07 06:03:14 -05:00
|
|
|
callback(null, features)
|
2019-12-02 08:51:57 -05:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getV1Features(userId, callback) {
|
|
|
|
V1SubscriptionManager.getPlanCodeFromV1(userId, function(
|
2019-05-29 05:21:06 -04:00
|
|
|
err,
|
|
|
|
planCode,
|
|
|
|
v1Id
|
|
|
|
) {
|
2020-01-07 06:03:14 -05:00
|
|
|
if (err) {
|
|
|
|
if ((err ? err.name : undefined) === 'NotFoundError') {
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(null, [])
|
|
|
|
}
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
callback(
|
2019-05-29 05:21:06 -04:00
|
|
|
err,
|
|
|
|
FeaturesUpdater._mergeFeatures(
|
|
|
|
V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {},
|
|
|
|
FeaturesUpdater._planCodeToFeatures(planCode)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
_mergeFeatures(featuresA, featuresB) {
|
|
|
|
const features = Object.assign({}, featuresA)
|
|
|
|
for (let key in featuresB) {
|
|
|
|
// Special merging logic for non-boolean features
|
|
|
|
if (key === 'compileGroup') {
|
|
|
|
if (
|
|
|
|
features['compileGroup'] === 'priority' ||
|
|
|
|
featuresB['compileGroup'] === 'priority'
|
|
|
|
) {
|
|
|
|
features['compileGroup'] = 'priority'
|
|
|
|
} else {
|
|
|
|
features['compileGroup'] = 'standard'
|
|
|
|
}
|
|
|
|
} else if (key === 'collaborators') {
|
|
|
|
if (
|
|
|
|
features['collaborators'] === -1 ||
|
|
|
|
featuresB['collaborators'] === -1
|
|
|
|
) {
|
|
|
|
features['collaborators'] = -1
|
|
|
|
} else {
|
|
|
|
features['collaborators'] = Math.max(
|
|
|
|
features['collaborators'] || 0,
|
|
|
|
featuresB['collaborators'] || 0
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else if (key === 'compileTimeout') {
|
|
|
|
features['compileTimeout'] = Math.max(
|
|
|
|
features['compileTimeout'] || 0,
|
|
|
|
featuresB['compileTimeout'] || 0
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
// Boolean keys, true is better
|
|
|
|
features[key] = features[key] || featuresB[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return features
|
|
|
|
},
|
|
|
|
|
|
|
|
_subscriptionToFeatures(subscription) {
|
|
|
|
return FeaturesUpdater._planCodeToFeatures(
|
2020-01-07 06:03:14 -05:00
|
|
|
subscription ? subscription.planCode : undefined
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
_planCodeToFeatures(planCode) {
|
2020-01-07 06:03:14 -05:00
|
|
|
if (!planCode) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return {}
|
|
|
|
}
|
|
|
|
const plan = PlansLocator.findLocalPlanInSettings(planCode)
|
2020-01-07 06:03:14 -05:00
|
|
|
if (!plan) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return {}
|
|
|
|
} else {
|
|
|
|
return plan.features
|
|
|
|
}
|
2020-01-07 06:03:14 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
compareFeatures(currentFeatures, expectedFeatures) {
|
|
|
|
currentFeatures = _.clone(currentFeatures)
|
|
|
|
expectedFeatures = _.clone(expectedFeatures)
|
|
|
|
if (_.isEqual(currentFeatures, expectedFeatures)) {
|
|
|
|
return {}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mismatchReasons = {}
|
|
|
|
const featureKeys = [
|
|
|
|
...new Set([
|
|
|
|
...Object.keys(currentFeatures),
|
|
|
|
...Object.keys(expectedFeatures)
|
|
|
|
])
|
|
|
|
]
|
|
|
|
featureKeys.sort().forEach(key => {
|
|
|
|
if (expectedFeatures[key] !== currentFeatures[key]) {
|
|
|
|
mismatchReasons[key] = expectedFeatures[key]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (mismatchReasons.compileTimeout) {
|
|
|
|
// store the compile timeout difference instead of the new compile timeout
|
|
|
|
mismatchReasons.compileTimeout =
|
|
|
|
expectedFeatures.compileTimeout - currentFeatures.compileTimeout
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mismatchReasons.collaborators) {
|
|
|
|
// store the collaborators difference instead of the new number only
|
|
|
|
// replace -1 by 100 to make it clearer
|
|
|
|
if (expectedFeatures.collaborators === -1) {
|
|
|
|
expectedFeatures.collaborators = 100
|
|
|
|
}
|
|
|
|
if (currentFeatures.collaborators === -1) {
|
|
|
|
currentFeatures.collaborators = 100
|
|
|
|
}
|
|
|
|
mismatchReasons.collaborators =
|
|
|
|
expectedFeatures.collaborators - currentFeatures.collaborators
|
|
|
|
}
|
|
|
|
|
|
|
|
return mismatchReasons
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
2019-11-25 08:29:40 -05:00
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
const refreshFeaturesPromise = userId =>
|
2019-11-25 08:29:40 -05:00
|
|
|
new Promise(function(resolve, reject) {
|
|
|
|
FeaturesUpdater.refreshFeatures(
|
2020-01-07 06:03:14 -05:00
|
|
|
userId,
|
2019-11-25 08:29:40 -05:00
|
|
|
(error, features, featuresChanged) => {
|
|
|
|
if (error) {
|
|
|
|
reject(error)
|
|
|
|
} else {
|
|
|
|
resolve({ features, featuresChanged })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
FeaturesUpdater.promises = {
|
|
|
|
refreshFeatures: refreshFeaturesPromise
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = FeaturesUpdater
|