Merge pull request #2113 from overleaf/em-project-details-handler-async
Move ProjectDetailsHandler to async/await GitOrigin-RevId: 5ec9343d6203850641174cc728db9a2e56f44775
This commit is contained in:
committed by
sharelatex
parent
797595bb1a
commit
abae7ef2a3
@@ -13,7 +13,6 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let CollaboratorsHandler
|
||||
const UserCreator = require('../User/UserCreator')
|
||||
const { Project } = require('../../models/Project')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
@@ -29,8 +28,9 @@ const EmailHelper = require('../Helpers/EmailHelper')
|
||||
const ProjectEditorHandler = require('../Project/ProjectEditorHandler')
|
||||
const Sources = require('../Authorization/Sources')
|
||||
const { ObjectId } = require('mongojs')
|
||||
const { promisifyAll } = require('../../util/promises')
|
||||
|
||||
module.exports = CollaboratorsHandler = {
|
||||
const CollaboratorsHandler = {
|
||||
getMemberIdsWithPrivilegeLevels(project_id, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, members) {}
|
||||
@@ -665,3 +665,8 @@ module.exports = CollaboratorsHandler = {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
CollaboratorsHandler.promises = promisifyAll(CollaboratorsHandler, {
|
||||
without: ['getMemberIdsWithPrivilegeLevelsFromFields']
|
||||
})
|
||||
module.exports = CollaboratorsHandler
|
||||
|
||||
@@ -238,8 +238,7 @@ async function undeleteProject(project_id) {
|
||||
let restored = new Project(deletedProject.project)
|
||||
|
||||
// if we're undeleting, we want the document to show up
|
||||
const generateUniqueName = promisify(ProjectDetailsHandler.generateUniqueName)
|
||||
restored.name = await generateUniqueName(
|
||||
restored.name = await ProjectDetailsHandler.promises.generateUniqueName(
|
||||
deletedProject.deleterData.deletedProjectOwnerId,
|
||||
restored.name + ' (Restored)'
|
||||
)
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
/* 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
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const _ = require('underscore')
|
||||
const ProjectGetter = require('./ProjectGetter')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const { Project } = require('../../models/Project')
|
||||
const { ObjectId } = require('mongojs')
|
||||
const logger = require('logger-sharelatex')
|
||||
const tpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
|
||||
const _ = require('underscore')
|
||||
const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
|
||||
const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const ProjectTokenGenerator = require('./ProjectTokenGenerator')
|
||||
@@ -27,411 +11,276 @@ const ProjectEntityHandler = require('./ProjectEntityHandler')
|
||||
const ProjectHelper = require('./ProjectHelper')
|
||||
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
||||
const settings = require('settings-sharelatex')
|
||||
const { promisify } = require('util')
|
||||
const { callbackify } = require('util')
|
||||
|
||||
const ProjectDetailsHandler = {
|
||||
getDetails(project_id, callback) {
|
||||
return ProjectGetter.getProject(
|
||||
project_id,
|
||||
{
|
||||
name: true,
|
||||
description: true,
|
||||
compiler: true,
|
||||
features: true,
|
||||
owner_ref: true,
|
||||
overleaf: true
|
||||
},
|
||||
function(err, project) {
|
||||
if (err != null) {
|
||||
logger.warn({ err, project_id }, 'error getting project')
|
||||
return callback(err)
|
||||
}
|
||||
if (project == null) {
|
||||
return callback(new Errors.NotFoundError('project not found'))
|
||||
}
|
||||
return UserGetter.getUser(project.owner_ref, function(err, user) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
const details = {
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
compiler: project.compiler,
|
||||
features:
|
||||
(user != null ? user.features : undefined) ||
|
||||
settings.defaultFeatures
|
||||
}
|
||||
const MAX_PROJECT_NAME_LENGTH = 150
|
||||
|
||||
if (project.overleaf != null) {
|
||||
details.overleaf = project.overleaf
|
||||
}
|
||||
|
||||
logger.log({ project_id, details }, 'getting project details')
|
||||
return callback(err, details)
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
getProjectDescription(project_id, callback) {
|
||||
return ProjectGetter.getProject(
|
||||
project_id,
|
||||
{ description: true },
|
||||
(err, project) =>
|
||||
callback(err, project != null ? project.description : undefined)
|
||||
)
|
||||
},
|
||||
|
||||
setProjectDescription(project_id, description, callback) {
|
||||
const conditions = { _id: project_id }
|
||||
const update = { description }
|
||||
logger.log(
|
||||
{ conditions, update, project_id, description },
|
||||
'setting project description'
|
||||
)
|
||||
return Project.update(conditions, update, function(err) {
|
||||
if (err != null) {
|
||||
logger.warn({ err }, 'something went wrong setting project description')
|
||||
}
|
||||
return callback(err)
|
||||
})
|
||||
},
|
||||
|
||||
transferOwnership(project_id, user_id, suffix, callback) {
|
||||
if (suffix == null) {
|
||||
suffix = ''
|
||||
}
|
||||
if (typeof suffix === 'function') {
|
||||
callback = suffix
|
||||
suffix = ''
|
||||
}
|
||||
return ProjectGetter.getProject(
|
||||
project_id,
|
||||
{ owner_ref: true, name: true },
|
||||
function(err, project) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
if (project == null) {
|
||||
return callback(new Errors.NotFoundError('project not found'))
|
||||
}
|
||||
if (project.owner_ref === user_id) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
return UserGetter.getUser(user_id, function(err, user) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
if (user == null) {
|
||||
return callback(new Errors.NotFoundError('user not found'))
|
||||
}
|
||||
|
||||
// we make sure the user to which the project is transferred is not a collaborator for the project,
|
||||
// this prevents any conflict during unique name generation
|
||||
return CollaboratorsHandler.removeUserFromProject(
|
||||
project_id,
|
||||
user_id,
|
||||
function(err) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
return ProjectDetailsHandler.generateUniqueName(
|
||||
user_id,
|
||||
project.name + suffix,
|
||||
function(err, name) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
return Project.update(
|
||||
{ _id: project_id },
|
||||
{
|
||||
$set: {
|
||||
owner_ref: user_id,
|
||||
name
|
||||
}
|
||||
},
|
||||
function(err) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
return ProjectEntityHandler.flushProjectToThirdPartyDataStore(
|
||||
project_id,
|
||||
callback
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
renameProject(project_id, newName, callback) {
|
||||
if (callback == null) {
|
||||
callback = function() {}
|
||||
}
|
||||
return ProjectDetailsHandler.validateProjectName(newName, function(error) {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
logger.log({ project_id, newName }, 'renaming project')
|
||||
return ProjectGetter.getProject(project_id, { name: true }, function(
|
||||
err,
|
||||
project
|
||||
) {
|
||||
if (err != null || project == null) {
|
||||
logger.warn(
|
||||
{ err, project_id },
|
||||
'error getting project or could not find it todo project rename'
|
||||
)
|
||||
return callback(err)
|
||||
}
|
||||
const oldProjectName = project.name
|
||||
return Project.update(
|
||||
{ _id: project_id },
|
||||
{ name: newName },
|
||||
(err, project) => {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
return tpdsUpdateSender.moveEntity(
|
||||
{
|
||||
project_id,
|
||||
project_name: oldProjectName,
|
||||
newProjectName: newName
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
MAX_PROJECT_NAME_LENGTH: 150,
|
||||
validateProjectName(name, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
if (name == null || name.length === 0) {
|
||||
return callback(
|
||||
new Errors.InvalidNameError('Project name cannot be blank')
|
||||
)
|
||||
} else if (name.length > ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH) {
|
||||
return callback(new Errors.InvalidNameError('Project name is too long'))
|
||||
} else if (name.indexOf('/') > -1) {
|
||||
return callback(
|
||||
new Errors.InvalidNameError('Project name cannot contain / characters')
|
||||
)
|
||||
} else if (name.indexOf('\\') > -1) {
|
||||
return callback(
|
||||
new Errors.InvalidNameError('Project name cannot contain \\ characters')
|
||||
)
|
||||
} else {
|
||||
return callback()
|
||||
}
|
||||
},
|
||||
|
||||
generateUniqueName(user_id, name, suffixes, callback) {
|
||||
if (suffixes == null) {
|
||||
suffixes = []
|
||||
}
|
||||
if (callback == null) {
|
||||
callback = function(error, newName) {}
|
||||
}
|
||||
if (arguments.length === 3 && typeof suffixes === 'function') {
|
||||
// make suffixes an optional argument
|
||||
callback = suffixes
|
||||
suffixes = []
|
||||
}
|
||||
return ProjectDetailsHandler.ensureProjectNameIsUnique(
|
||||
user_id,
|
||||
name,
|
||||
suffixes,
|
||||
callback
|
||||
)
|
||||
},
|
||||
|
||||
// FIXME: we should put a lock around this to make it completely safe, but we would need to do that at
|
||||
// the point of project creation, rather than just checking the name at the start of the import.
|
||||
// If we later move this check into ProjectCreationHandler we can ensure all new projects are created
|
||||
// with a unique name. But that requires thinking through how we would handle incoming projects from
|
||||
// dropbox for example.
|
||||
ensureProjectNameIsUnique(user_id, name, suffixes, callback) {
|
||||
if (suffixes == null) {
|
||||
suffixes = []
|
||||
}
|
||||
if (callback == null) {
|
||||
callback = function(error, name, changed) {}
|
||||
}
|
||||
return ProjectGetter.findAllUsersProjects(user_id, { name: 1 }, function(
|
||||
error,
|
||||
allUsersProjectNames
|
||||
) {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
// allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]}
|
||||
// collect all of the names and flatten them into a single array
|
||||
const projectNameList = _.pluck(
|
||||
_.flatten(_.values(allUsersProjectNames)),
|
||||
'name'
|
||||
)
|
||||
return ProjectHelper.ensureNameIsUnique(
|
||||
projectNameList,
|
||||
name,
|
||||
suffixes,
|
||||
ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH,
|
||||
callback
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
fixProjectName(name) {
|
||||
if (name === '' || !name) {
|
||||
name = 'Untitled'
|
||||
}
|
||||
if (name.indexOf('/') > -1) {
|
||||
// v2 does not allow / in a project name
|
||||
name = name.replace(/\//g, '-')
|
||||
}
|
||||
if (name.indexOf('\\') > -1) {
|
||||
// backslashes in project name will prevent syncing to dropbox
|
||||
name = name.replace(/\\/g, '')
|
||||
}
|
||||
if (name.length > ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH) {
|
||||
name = name.substr(0, ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH)
|
||||
}
|
||||
return name
|
||||
},
|
||||
|
||||
setPublicAccessLevel(project_id, newAccessLevel, callback) {
|
||||
if (callback == null) {
|
||||
callback = function() {}
|
||||
}
|
||||
logger.log({ project_id, level: newAccessLevel }, 'set public access level')
|
||||
// DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer
|
||||
// be passed here. Remove after token-based access has been live for a while
|
||||
if (
|
||||
project_id != null &&
|
||||
newAccessLevel != null &&
|
||||
_.include(
|
||||
[
|
||||
PublicAccessLevels.READ_ONLY,
|
||||
PublicAccessLevels.READ_AND_WRITE,
|
||||
PublicAccessLevels.PRIVATE,
|
||||
PublicAccessLevels.TOKEN_BASED
|
||||
],
|
||||
newAccessLevel
|
||||
)
|
||||
) {
|
||||
return Project.update(
|
||||
{ _id: project_id },
|
||||
{ publicAccesLevel: newAccessLevel },
|
||||
err => callback(err)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
ensureTokensArePresent(project_id, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(err, tokens) {}
|
||||
}
|
||||
return ProjectGetter.getProject(project_id, { tokens: 1 }, function(
|
||||
err,
|
||||
project
|
||||
) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
if (
|
||||
project.tokens != null &&
|
||||
project.tokens.readOnly != null &&
|
||||
project.tokens.readAndWrite != null
|
||||
) {
|
||||
logger.log({ project_id }, 'project already has tokens')
|
||||
return callback(null, project.tokens)
|
||||
} else {
|
||||
logger.log(
|
||||
{
|
||||
project_id,
|
||||
has_tokens: project.tokens != null,
|
||||
has_readOnly:
|
||||
__guard__(
|
||||
project != null ? project.tokens : undefined,
|
||||
x => x.readOnly
|
||||
) != null,
|
||||
has_readAndWrite:
|
||||
__guard__(
|
||||
project != null ? project.tokens : undefined,
|
||||
x1 => x1.readAndWrite
|
||||
) != null
|
||||
},
|
||||
'generating tokens for project'
|
||||
)
|
||||
return ProjectDetailsHandler._generateTokens(project, function(err) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
return Project.update(
|
||||
{ _id: project_id },
|
||||
{ $set: { tokens: project.tokens } },
|
||||
function(err) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
return callback(null, project.tokens)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
_generateTokens(project, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(err) {}
|
||||
}
|
||||
if (!project.tokens) {
|
||||
project.tokens = {}
|
||||
}
|
||||
const { tokens } = project
|
||||
if (tokens.readAndWrite == null) {
|
||||
const { token, numericPrefix } = ProjectTokenGenerator.readAndWriteToken()
|
||||
tokens.readAndWrite = token
|
||||
tokens.readAndWritePrefix = numericPrefix
|
||||
}
|
||||
if (tokens.readOnly == null) {
|
||||
return ProjectTokenGenerator.generateUniqueReadOnlyToken(function(
|
||||
err,
|
||||
token
|
||||
) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
tokens.readOnly = token
|
||||
return callback()
|
||||
})
|
||||
} else {
|
||||
return callback()
|
||||
}
|
||||
module.exports = {
|
||||
MAX_PROJECT_NAME_LENGTH,
|
||||
getDetails: callbackify(getDetails),
|
||||
getProjectDescription: callbackify(getProjectDescription),
|
||||
setProjectDescription: callbackify(setProjectDescription),
|
||||
transferOwnership: callbackify(transferOwnership),
|
||||
renameProject: callbackify(renameProject),
|
||||
validateProjectName: callbackify(validateProjectName),
|
||||
generateUniqueName: callbackify(generateUniqueName),
|
||||
setPublicAccessLevel: callbackify(setPublicAccessLevel),
|
||||
ensureTokensArePresent: callbackify(ensureTokensArePresent),
|
||||
fixProjectName,
|
||||
promises: {
|
||||
getDetails,
|
||||
getProjectDescription,
|
||||
setProjectDescription,
|
||||
transferOwnership,
|
||||
renameProject,
|
||||
validateProjectName,
|
||||
generateUniqueName,
|
||||
setPublicAccessLevel,
|
||||
ensureTokensArePresent
|
||||
}
|
||||
}
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
async function getDetails(projectId) {
|
||||
let project
|
||||
try {
|
||||
project = await ProjectGetter.promises.getProject(projectId, {
|
||||
name: true,
|
||||
description: true,
|
||||
compiler: true,
|
||||
features: true,
|
||||
owner_ref: true,
|
||||
overleaf: true
|
||||
})
|
||||
} catch (err) {
|
||||
logger.warn({ err, projectId }, 'error getting project')
|
||||
throw err
|
||||
}
|
||||
if (project == null) {
|
||||
throw new Errors.NotFoundError('project not found')
|
||||
}
|
||||
const user = await UserGetter.promises.getUser(project.owner_ref)
|
||||
const details = {
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
compiler: project.compiler,
|
||||
features:
|
||||
user != null && user.features != null
|
||||
? user.features
|
||||
: settings.defaultFeatures
|
||||
}
|
||||
if (project.overleaf != null) {
|
||||
details.overleaf = project.overleaf
|
||||
}
|
||||
logger.log({ projectId, details }, 'getting project details')
|
||||
return details
|
||||
}
|
||||
|
||||
const promises = {
|
||||
generateUniqueName: promisify(ProjectDetailsHandler.generateUniqueName)
|
||||
async function getProjectDescription(projectId) {
|
||||
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||
description: true
|
||||
})
|
||||
if (project == null) {
|
||||
return undefined
|
||||
}
|
||||
return project.description
|
||||
}
|
||||
|
||||
ProjectDetailsHandler.promises = promises
|
||||
async function setProjectDescription(projectId, description) {
|
||||
const conditions = { _id: projectId }
|
||||
const update = { description }
|
||||
logger.log(
|
||||
{ conditions, update, projectId, description },
|
||||
'setting project description'
|
||||
)
|
||||
try {
|
||||
await Project.update(conditions, update).exec()
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'something went wrong setting project description')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProjectDetailsHandler
|
||||
async function transferOwnership(projectId, userId, suffix = '') {
|
||||
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||
owner_ref: true,
|
||||
name: true
|
||||
})
|
||||
if (project == null) {
|
||||
throw new Errors.NotFoundError('project not found')
|
||||
}
|
||||
if (project.owner_ref === userId) {
|
||||
return
|
||||
}
|
||||
const user = await UserGetter.promises.getUser(userId)
|
||||
if (user == null) {
|
||||
throw new Errors.NotFoundError('user not found')
|
||||
}
|
||||
|
||||
// we make sure the user to which the project is transferred is not a collaborator for the project,
|
||||
// this prevents any conflict during unique name generation
|
||||
await CollaboratorsHandler.promises.removeUserFromProject(projectId, userId)
|
||||
const name = await generateUniqueName(userId, project.name + suffix)
|
||||
await Project.update(
|
||||
{ _id: projectId },
|
||||
{
|
||||
$set: {
|
||||
owner_ref: userId,
|
||||
name
|
||||
}
|
||||
}
|
||||
).exec()
|
||||
await ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore(
|
||||
projectId
|
||||
)
|
||||
}
|
||||
|
||||
async function renameProject(projectId, newName) {
|
||||
await validateProjectName(newName)
|
||||
logger.log({ projectId, newName }, 'renaming project')
|
||||
let project
|
||||
try {
|
||||
project = await ProjectGetter.promises.getProject(projectId, { name: true })
|
||||
} catch (err) {
|
||||
logger.warn({ err, projectId }, 'error getting project')
|
||||
throw err
|
||||
}
|
||||
if (project == null) {
|
||||
logger.warn({ projectId }, 'could not find project to rename')
|
||||
return
|
||||
}
|
||||
const oldProjectName = project.name
|
||||
await Project.update({ _id: projectId }, { name: newName }).exec()
|
||||
await TpdsUpdateSender.promises.moveEntity({
|
||||
project_id: projectId,
|
||||
project_name: oldProjectName,
|
||||
newProjectName: newName
|
||||
})
|
||||
}
|
||||
|
||||
async function validateProjectName(name) {
|
||||
if (name == null || name.length === 0) {
|
||||
throw new Errors.InvalidNameError('Project name cannot be blank')
|
||||
}
|
||||
if (name.length > MAX_PROJECT_NAME_LENGTH) {
|
||||
throw new Errors.InvalidNameError('Project name is too long')
|
||||
}
|
||||
if (name.indexOf('/') > -1) {
|
||||
throw new Errors.InvalidNameError(
|
||||
'Project name cannot contain / characters'
|
||||
)
|
||||
}
|
||||
if (name.indexOf('\\') > -1) {
|
||||
throw new Errors.InvalidNameError(
|
||||
'Project name cannot contain \\ characters'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: we should put a lock around this to make it completely safe, but we would need to do that at
|
||||
// the point of project creation, rather than just checking the name at the start of the import.
|
||||
// If we later move this check into ProjectCreationHandler we can ensure all new projects are created
|
||||
// with a unique name. But that requires thinking through how we would handle incoming projects from
|
||||
// dropbox for example.
|
||||
async function generateUniqueName(userId, name, suffixes = []) {
|
||||
const allUsersProjectNames = await ProjectGetter.promises.findAllUsersProjects(
|
||||
userId,
|
||||
{ name: 1 }
|
||||
)
|
||||
// allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]}
|
||||
// collect all of the names and flatten them into a single array
|
||||
const projectNameList = _.pluck(
|
||||
_.flatten(_.values(allUsersProjectNames)),
|
||||
'name'
|
||||
)
|
||||
const uniqueName = await ProjectHelper.promises.ensureNameIsUnique(
|
||||
projectNameList,
|
||||
name,
|
||||
suffixes,
|
||||
MAX_PROJECT_NAME_LENGTH
|
||||
)
|
||||
return uniqueName
|
||||
}
|
||||
|
||||
function fixProjectName(name) {
|
||||
if (name === '' || !name) {
|
||||
name = 'Untitled'
|
||||
}
|
||||
if (name.indexOf('/') > -1) {
|
||||
// v2 does not allow / in a project name
|
||||
name = name.replace(/\//g, '-')
|
||||
}
|
||||
if (name.indexOf('\\') > -1) {
|
||||
// backslashes in project name will prevent syncing to dropbox
|
||||
name = name.replace(/\\/g, '')
|
||||
}
|
||||
if (name.length > MAX_PROJECT_NAME_LENGTH) {
|
||||
name = name.substr(0, MAX_PROJECT_NAME_LENGTH)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
async function setPublicAccessLevel(projectId, newAccessLevel) {
|
||||
logger.log({ projectId, level: newAccessLevel }, 'set public access level')
|
||||
// DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer
|
||||
// be passed here. Remove after token-based access has been live for a while
|
||||
if (
|
||||
projectId != null &&
|
||||
newAccessLevel != null &&
|
||||
_.include(
|
||||
[
|
||||
PublicAccessLevels.READ_ONLY,
|
||||
PublicAccessLevels.READ_AND_WRITE,
|
||||
PublicAccessLevels.PRIVATE,
|
||||
PublicAccessLevels.TOKEN_BASED
|
||||
],
|
||||
newAccessLevel
|
||||
)
|
||||
) {
|
||||
await Project.update(
|
||||
{ _id: projectId },
|
||||
{ publicAccesLevel: newAccessLevel }
|
||||
).exec()
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTokensArePresent(projectId) {
|
||||
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||
tokens: 1
|
||||
})
|
||||
if (
|
||||
project.tokens != null &&
|
||||
project.tokens.readOnly != null &&
|
||||
project.tokens.readAndWrite != null
|
||||
) {
|
||||
logger.log({ projectId }, 'project already has tokens')
|
||||
return project.tokens
|
||||
}
|
||||
const hasTokens = project.tokens != null
|
||||
const hasReadOnly = hasTokens && project.tokens.readOnly != null
|
||||
const hasReadAndWrite = hasTokens && project.tokens.readAndWrite != null
|
||||
logger.log(
|
||||
{ projectId, hasTokens, hasReadOnly, hasReadAndWrite },
|
||||
'generating tokens for project'
|
||||
)
|
||||
await _generateTokens(project)
|
||||
await Project.update(
|
||||
{ _id: projectId },
|
||||
{ $set: { tokens: project.tokens } }
|
||||
).exec()
|
||||
return project.tokens
|
||||
}
|
||||
|
||||
async function _generateTokens(project, callback) {
|
||||
if (!project.tokens) {
|
||||
project.tokens = {}
|
||||
}
|
||||
const { tokens } = project
|
||||
if (tokens.readAndWrite == null) {
|
||||
const { token, numericPrefix } = ProjectTokenGenerator.readAndWriteToken()
|
||||
tokens.readAndWrite = token
|
||||
tokens.readAndWritePrefix = numericPrefix
|
||||
}
|
||||
if (tokens.readOnly == null) {
|
||||
tokens.readOnly = await ProjectTokenGenerator.promises.generateUniqueReadOnlyToken()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let ProjectEntityHandler, self
|
||||
const _ = require('underscore')
|
||||
const async = require('async')
|
||||
const path = require('path')
|
||||
@@ -25,8 +24,9 @@ const Errors = require('../Errors/Errors')
|
||||
const { Project } = require('../../models/Project')
|
||||
const ProjectGetter = require('./ProjectGetter')
|
||||
const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
|
||||
const { promisifyAll } = require('../../util/promises')
|
||||
|
||||
module.exports = ProjectEntityHandler = self = {
|
||||
const ProjectEntityHandler = {
|
||||
getAllDocs(project_id, callback) {
|
||||
logger.log({ project_id }, 'getting all docs for project')
|
||||
|
||||
@@ -46,7 +46,10 @@ module.exports = ProjectEntityHandler = self = {
|
||||
docContents[docContent._id] = docContent
|
||||
}
|
||||
|
||||
return self._getAllFolders(project_id, function(error, folders) {
|
||||
return ProjectEntityHandler._getAllFolders(project_id, function(
|
||||
error,
|
||||
folders
|
||||
) {
|
||||
if (folders == null) {
|
||||
folders = {}
|
||||
}
|
||||
@@ -79,7 +82,10 @@ module.exports = ProjectEntityHandler = self = {
|
||||
|
||||
getAllFiles(project_id, callback) {
|
||||
logger.log({ project_id }, 'getting all files for project')
|
||||
return self._getAllFolders(project_id, function(err, folders) {
|
||||
return ProjectEntityHandler._getAllFolders(project_id, function(
|
||||
err,
|
||||
folders
|
||||
) {
|
||||
if (folders == null) {
|
||||
folders = {}
|
||||
}
|
||||
@@ -104,13 +110,16 @@ module.exports = ProjectEntityHandler = self = {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
return self.getAllEntitiesFromProject(project, callback)
|
||||
return ProjectEntityHandler.getAllEntitiesFromProject(project, callback)
|
||||
})
|
||||
},
|
||||
|
||||
getAllEntitiesFromProject(project, callback) {
|
||||
logger.log({ project }, 'getting all entities for project')
|
||||
return self._getAllFoldersFromProject(project, function(err, folders) {
|
||||
return ProjectEntityHandler._getAllFoldersFromProject(project, function(
|
||||
err,
|
||||
folders
|
||||
) {
|
||||
if (folders == null) {
|
||||
folders = {}
|
||||
}
|
||||
@@ -147,13 +156,16 @@ module.exports = ProjectEntityHandler = self = {
|
||||
if (project == null) {
|
||||
return callback(Errors.NotFoundError('no project'))
|
||||
}
|
||||
return self.getAllDocPathsFromProject(project, callback)
|
||||
return ProjectEntityHandler.getAllDocPathsFromProject(project, callback)
|
||||
})
|
||||
},
|
||||
|
||||
getAllDocPathsFromProject(project, callback) {
|
||||
logger.log({ project }, 'getting all docs for project')
|
||||
return self._getAllFoldersFromProject(project, function(err, folders) {
|
||||
return ProjectEntityHandler._getAllFoldersFromProject(project, function(
|
||||
err,
|
||||
folders
|
||||
) {
|
||||
if (folders == null) {
|
||||
folders = {}
|
||||
}
|
||||
@@ -191,7 +203,10 @@ module.exports = ProjectEntityHandler = self = {
|
||||
return callback(error)
|
||||
}
|
||||
const requests = []
|
||||
return self.getAllDocs(project_id, function(error, docs) {
|
||||
return ProjectEntityHandler.getAllDocs(project_id, function(
|
||||
error,
|
||||
docs
|
||||
) {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
@@ -211,7 +226,10 @@ module.exports = ProjectEntityHandler = self = {
|
||||
)
|
||||
))(docPath, doc)
|
||||
}
|
||||
return self.getAllFiles(project_id, function(error, files) {
|
||||
return ProjectEntityHandler.getAllFiles(project_id, function(
|
||||
error,
|
||||
files
|
||||
) {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
@@ -268,7 +286,7 @@ module.exports = ProjectEntityHandler = self = {
|
||||
if (project == null) {
|
||||
return callback(Errors.NotFoundError('no project'))
|
||||
}
|
||||
return self._getAllFoldersFromProject(project, callback)
|
||||
return ProjectEntityHandler._getAllFoldersFromProject(project, callback)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -295,3 +313,6 @@ module.exports = ProjectEntityHandler = self = {
|
||||
return callback(null, folders)
|
||||
}
|
||||
}
|
||||
|
||||
ProjectEntityHandler.promises = promisifyAll(ProjectEntityHandler)
|
||||
module.exports = ProjectEntityHandler
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let ProjectGetter
|
||||
const mongojs = require('../../infrastructure/mongojs')
|
||||
const metrics = require('metrics-sharelatex')
|
||||
const { db } = mongojs
|
||||
const { ObjectId } = mongojs
|
||||
const async = require('async')
|
||||
const { promisifyAll } = require('../../util/promises')
|
||||
const { Project } = require('../../models/Project')
|
||||
const logger = require('logger-sharelatex')
|
||||
const LockManager = require('../../infrastructure/LockManager')
|
||||
const { DeletedProject } = require('../../models/DeletedProject')
|
||||
|
||||
module.exports = ProjectGetter = {
|
||||
const ProjectGetter = {
|
||||
EXCLUDE_DEPTH: 8,
|
||||
|
||||
getProjectWithoutDocLines(project_id, callback) {
|
||||
@@ -221,3 +221,6 @@ module.exports = ProjectGetter = {
|
||||
;['getProject', 'getProjectWithoutDocLines'].map(method =>
|
||||
metrics.timeAsyncMethod(ProjectGetter, method, 'mongo.ProjectGetter', logger)
|
||||
)
|
||||
|
||||
ProjectGetter.promises = promisifyAll(ProjectGetter)
|
||||
module.exports = ProjectGetter
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let ProjectHelper
|
||||
const ENGINE_TO_COMPILER_MAP = {
|
||||
latex_dvipdf: 'latex',
|
||||
pdflatex: 'pdflatex',
|
||||
@@ -19,8 +18,9 @@ const ENGINE_TO_COMPILER_MAP = {
|
||||
lualatex: 'lualatex'
|
||||
}
|
||||
const { ObjectId } = require('../../infrastructure/mongojs')
|
||||
const { promisify } = require('util')
|
||||
|
||||
module.exports = ProjectHelper = {
|
||||
const ProjectHelper = {
|
||||
compilerFromV1Engine(engine) {
|
||||
return ENGINE_TO_COMPILER_MAP[engine]
|
||||
},
|
||||
@@ -58,13 +58,13 @@ module.exports = ProjectHelper = {
|
||||
suffixes = []
|
||||
}
|
||||
if (callback == null) {
|
||||
callback = function(error, name, changed) {}
|
||||
callback = function(error, name) {}
|
||||
}
|
||||
const allNames = new Set(nameList)
|
||||
const isUnique = x => !allNames.has(x)
|
||||
// check if the supplied name is already unique
|
||||
if (isUnique(name)) {
|
||||
return callback(null, name, false)
|
||||
return callback(null, name)
|
||||
}
|
||||
// the name already exists, try adding the user-supplied suffixes to generate a unique name
|
||||
for (let suffix of Array.from(suffixes)) {
|
||||
@@ -74,7 +74,7 @@ module.exports = ProjectHelper = {
|
||||
maxLength
|
||||
)
|
||||
if (isUnique(candidateName)) {
|
||||
return callback(null, candidateName, true)
|
||||
return callback(null, candidateName)
|
||||
}
|
||||
}
|
||||
// if there are no (more) suffixes, use a numeric one
|
||||
@@ -84,7 +84,7 @@ module.exports = ProjectHelper = {
|
||||
maxLength
|
||||
)
|
||||
if (uniqueName != null) {
|
||||
return callback(null, uniqueName, true)
|
||||
return callback(null, uniqueName)
|
||||
} else {
|
||||
return callback(
|
||||
new Error(`Failed to generate a unique name for: ${name}`)
|
||||
@@ -129,3 +129,8 @@ module.exports = ProjectHelper = {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
ProjectHelper.promises = {
|
||||
ensureNameIsUnique: promisify(ProjectHelper.ensureNameIsUnique)
|
||||
}
|
||||
module.exports = ProjectHelper
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let ProjectTokenGenerator
|
||||
const crypto = require('crypto')
|
||||
const V1Api = require('../V1/V1Api')
|
||||
const Async = require('async')
|
||||
const logger = require('logger-sharelatex')
|
||||
const { promisify } = require('util')
|
||||
|
||||
// This module mirrors the token generation in Overleaf (`random_token.rb`),
|
||||
// for the purposes of implementing token-based project access, like the
|
||||
// 'unlisted-projects' feature in Overleaf
|
||||
|
||||
module.exports = ProjectTokenGenerator = {
|
||||
const ProjectTokenGenerator = {
|
||||
// (From Overleaf `random_token.rb`)
|
||||
// Letters (not numbers! see generate_token) used in tokens. They're all
|
||||
// consonants, to avoid embarassing words (I can't think of any that use only
|
||||
@@ -105,3 +105,10 @@ module.exports = ProjectTokenGenerator = {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProjectTokenGenerator.promises = {
|
||||
generateUniqueReadOnlyToken: promisify(
|
||||
ProjectTokenGenerator.generateUniqueReadOnlyToken
|
||||
)
|
||||
}
|
||||
module.exports = ProjectTokenGenerator
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let TpdsUpdateSender, tpdsUrl
|
||||
let tpdsUrl
|
||||
const settings = require('settings-sharelatex')
|
||||
const logger = require('logger-sharelatex')
|
||||
const path = require('path')
|
||||
@@ -22,6 +22,7 @@ const keys = require('../../infrastructure/Keys')
|
||||
const metrics = require('metrics-sharelatex')
|
||||
const request = require('request')
|
||||
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
||||
const { promisifyAll } = require('../../util/promises')
|
||||
|
||||
const buildPath = function(user_id, project_name, filePath) {
|
||||
let projectPath = path.join(project_name, '/', filePath)
|
||||
@@ -44,7 +45,7 @@ if (settings.apis.thirdPartyDataStore.linode_url != null) {
|
||||
tpdsUrl = settings.apis.thirdPartyDataStore.url
|
||||
}
|
||||
|
||||
module.exports = TpdsUpdateSender = {
|
||||
const TpdsUpdateSender = {
|
||||
_enqueue(group, method, job, callback) {
|
||||
if (!tpdsworkerEnabled()) {
|
||||
return callback()
|
||||
@@ -321,3 +322,6 @@ var mergeProjectNameAndPath = function(project_name, path) {
|
||||
const fullPath = `/${project_name}/${path}`
|
||||
return fullPath
|
||||
}
|
||||
|
||||
TpdsUpdateSender.promises = promisifyAll(TpdsUpdateSender)
|
||||
module.exports = TpdsUpdateSender
|
||||
|
||||
@@ -11,17 +11,17 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let UserGetter
|
||||
const mongojs = require('../../infrastructure/mongojs')
|
||||
const metrics = require('metrics-sharelatex')
|
||||
const logger = require('logger-sharelatex')
|
||||
const { db } = mongojs
|
||||
const { ObjectId } = mongojs
|
||||
const { promisifyAll } = require('../../util/promises')
|
||||
const { getUserAffiliations } = require('../Institutions/InstitutionsAPI')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const Features = require('../../infrastructure/Features')
|
||||
|
||||
module.exports = UserGetter = {
|
||||
const UserGetter = {
|
||||
getUser(query, projection, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, user) {}
|
||||
@@ -248,3 +248,6 @@ var decorateFullEmails = (defaultEmail, emailsData, affiliationsData) =>
|
||||
].map(method =>
|
||||
metrics.timeAsyncMethod(UserGetter, method, 'mongo.UserGetter', logger)
|
||||
)
|
||||
|
||||
UserGetter.promises = promisifyAll(UserGetter)
|
||||
module.exports = UserGetter
|
||||
|
||||
@@ -106,7 +106,9 @@ describe('ProjectDeleter', function() {
|
||||
}
|
||||
|
||||
this.ProjectDetailsHandler = {
|
||||
generateUniqueName: sinon.stub().yields(null, this.project.name)
|
||||
promises: {
|
||||
generateUniqueName: sinon.stub().resolves(this.project.name)
|
||||
}
|
||||
}
|
||||
|
||||
this.db = {
|
||||
@@ -533,7 +535,7 @@ describe('ProjectDeleter', function() {
|
||||
this.ProjectDeleter.undeleteProject(this.project._id, err => {
|
||||
expect(err).not.to.exist
|
||||
sinon.assert.calledWith(
|
||||
this.ProjectDetailsHandler.generateUniqueName,
|
||||
this.ProjectDetailsHandler.promises.generateUniqueName,
|
||||
this.project.owner_ref
|
||||
)
|
||||
done()
|
||||
@@ -544,7 +546,7 @@ describe('ProjectDeleter', function() {
|
||||
this.ProjectDeleter.undeleteProject(this.project._id, err => {
|
||||
expect(err).not.to.exist
|
||||
sinon.assert.calledWith(
|
||||
this.ProjectDetailsHandler.generateUniqueName,
|
||||
this.ProjectDetailsHandler.promises.generateUniqueName,
|
||||
this.project.owner_ref,
|
||||
this.project.name + ' (Restored)'
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,4 +135,4 @@ describe('ProjectHelper', function() {
|
||||
})
|
||||
|
||||
// describe "ensureNameIsUnique", ->
|
||||
// see tests for: ProjectDetailsHandler.ensureProjectNameIsUnique, which calls here.
|
||||
// see tests for: ProjectDetailsHandler.generateUniqueName, which calls here.
|
||||
|
||||
Reference in New Issue
Block a user