overleaf/services/web/app/src/Features/Project/ProjectDetailsHandler.js

430 lines
13 KiB
JavaScript
Raw Normal View History

/* 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
*/
let ProjectDetailsHandler
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 PublicAccessLevels = require('../Authorization/PublicAccessLevels')
const Errors = require('../Errors/Errors')
const ProjectTokenGenerator = require('./ProjectTokenGenerator')
const ProjectEntityHandler = require('./ProjectEntityHandler')
const ProjectHelper = require('./ProjectHelper')
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
const settings = require('settings-sharelatex')
module.exports = 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.err({ 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
}
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.err({ 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.err(
{ 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 > this.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 > this.MAX_PROJECT_NAME_LENGTH) {
name = name.substr(0, this.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()
}
}
}
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}