2019-05-29 05:21:06 -04:00
|
|
|
/* eslint-disable
|
|
|
|
camelcase,
|
|
|
|
handle-callback-err,
|
|
|
|
max-len,
|
|
|
|
no-unused-vars,
|
|
|
|
*/
|
|
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
|
|
// Fix any style issues and re-enable lint.
|
|
|
|
/*
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
|
|
* DS207: Consider shorter variations of null checks
|
|
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
|
|
*/
|
|
|
|
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
|
|
|
|
|
|
|
const oneMonthInSeconds = 60 * 60 * 24 * 30
|
|
|
|
|
2019-11-25 08:29:40 -05:00
|
|
|
const FeaturesUpdater = {
|
2019-06-11 09:14:38 -04:00
|
|
|
refreshFeatures(user_id, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error, features, featuresChanged) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
const jobs = {
|
|
|
|
individualFeatures(cb) {
|
|
|
|
return FeaturesUpdater._getIndividualFeatures(user_id, cb)
|
|
|
|
},
|
|
|
|
groupFeatureSets(cb) {
|
|
|
|
return FeaturesUpdater._getGroupFeatureSets(user_id, cb)
|
|
|
|
},
|
|
|
|
institutionFeatures(cb) {
|
|
|
|
return InstitutionsFeatures.getInstitutionsFeatures(user_id, cb)
|
|
|
|
},
|
|
|
|
v1Features(cb) {
|
|
|
|
return FeaturesUpdater._getV1Features(user_id, cb)
|
|
|
|
},
|
|
|
|
bonusFeatures(cb) {
|
|
|
|
return ReferalFeatures.getBonusFeatures(user_id, cb)
|
2019-10-05 13:43:21 -04:00
|
|
|
},
|
|
|
|
samlFeatures(cb) {
|
|
|
|
return FeaturesUpdater._getSamlFeatures(user_id, cb)
|
2019-12-02 08:51:57 -05:00
|
|
|
},
|
|
|
|
featuresOverrides(cb) {
|
|
|
|
return FeaturesUpdater._getFeaturesOverrides(user_id, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return async.series(jobs, function(err, results) {
|
|
|
|
if (err != null) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn(
|
2019-05-29 05:21:06 -04:00
|
|
|
{ err, user_id },
|
|
|
|
'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(
|
|
|
|
{
|
|
|
|
user_id,
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
|
|
|
logger.log({ user_id, features }, 'updating user features')
|
|
|
|
return UserFeaturesUpdater.updateFeatures(user_id, features, callback)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
_getIndividualFeatures(user_id, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error, features) {}
|
|
|
|
}
|
|
|
|
return SubscriptionLocator.getUsersSubscription(user_id, (err, sub) =>
|
|
|
|
callback(err, FeaturesUpdater._subscriptionToFeatures(sub))
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
_getGroupFeatureSets(user_id, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error, featureSets) {}
|
|
|
|
}
|
|
|
|
return SubscriptionLocator.getGroupSubscriptionsMemberOf(
|
|
|
|
user_id,
|
|
|
|
(err, subs) =>
|
|
|
|
callback(err, (subs || []).map(FeaturesUpdater._subscriptionToFeatures))
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2019-10-05 13:43:21 -04:00
|
|
|
_getSamlFeatures(user_id, callback) {
|
|
|
|
UserGetter.getUser(user_id, (err, user) => {
|
|
|
|
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')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return callback(null, {})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-12-02 08:51:57 -05:00
|
|
|
_getFeaturesOverrides(user_id, callback) {
|
|
|
|
UserGetter.getUser(user_id, { featuresOverrides: 1 }, (error, user) => {
|
|
|
|
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,
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
return callback(null, features)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-05-29 05:21:06 -04:00
|
|
|
_getV1Features(user_id, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error, features) {}
|
|
|
|
}
|
|
|
|
return V1SubscriptionManager.getPlanCodeFromV1(user_id, function(
|
|
|
|
err,
|
|
|
|
planCode,
|
|
|
|
v1Id
|
|
|
|
) {
|
|
|
|
if (err != null) {
|
|
|
|
if ((err != null ? err.name : undefined) === 'NotFoundError') {
|
|
|
|
return callback(null, [])
|
|
|
|
}
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return callback(
|
|
|
|
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
|
|
|
|
const value = featuresB[key]
|
|
|
|
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(
|
|
|
|
subscription != null ? subscription.planCode : undefined
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
_planCodeToFeatures(planCode) {
|
|
|
|
if (planCode == null) {
|
|
|
|
return {}
|
|
|
|
}
|
|
|
|
const plan = PlansLocator.findLocalPlanInSettings(planCode)
|
|
|
|
if (plan == null) {
|
|
|
|
return {}
|
|
|
|
} else {
|
|
|
|
return plan.features
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-11-25 08:29:40 -05:00
|
|
|
|
|
|
|
const refreshFeaturesPromise = user_id =>
|
|
|
|
new Promise(function(resolve, reject) {
|
|
|
|
FeaturesUpdater.refreshFeatures(
|
|
|
|
user_id,
|
|
|
|
(error, features, featuresChanged) => {
|
|
|
|
if (error) {
|
|
|
|
reject(error)
|
|
|
|
} else {
|
|
|
|
resolve({ features, featuresChanged })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
FeaturesUpdater.promises = {
|
|
|
|
refreshFeatures: refreshFeaturesPromise
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = FeaturesUpdater
|