2019-05-29 05:21:06 -04:00
|
|
|
/* eslint-disable
|
|
|
|
camelcase,
|
|
|
|
handle-callback-err,
|
|
|
|
*/
|
|
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
|
|
// Fix any style issues and re-enable lint.
|
|
|
|
/*
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
* DS101: Remove unnecessary use of Array.from
|
|
|
|
* 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
|
|
|
|
*/
|
2019-07-18 10:18:56 -04:00
|
|
|
const { db } = require('../../infrastructure/mongojs')
|
|
|
|
const { promisify, callbackify } = require('util')
|
2019-05-29 05:21:06 -04:00
|
|
|
const { Project } = require('../../models/Project')
|
|
|
|
const { DeletedProject } = require('../../models/DeletedProject')
|
2019-07-18 10:18:56 -04:00
|
|
|
const Errors = require('../Errors/Errors')
|
2019-05-29 05:21:06 -04:00
|
|
|
const logger = require('logger-sharelatex')
|
2019-07-18 10:18:56 -04:00
|
|
|
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
|
|
|
const TagsHandler = require('../Tags/TagsHandler')
|
2019-05-29 05:21:06 -04:00
|
|
|
const async = require('async')
|
2019-07-18 10:18:56 -04:00
|
|
|
const ProjectDetailsHandler = require('./ProjectDetailsHandler')
|
2019-05-29 05:21:06 -04:00
|
|
|
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
2019-07-18 10:18:56 -04:00
|
|
|
const DocstoreManager = require('../Docstore/DocstoreManager')
|
|
|
|
const moment = require('moment')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-06-27 06:10:46 -04:00
|
|
|
const ProjectDeleter = {
|
2019-05-29 05:21:06 -04:00
|
|
|
markAsDeletedByExternalSource(project_id, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error) {}
|
|
|
|
}
|
|
|
|
logger.log(
|
|
|
|
{ project_id },
|
|
|
|
'marking project as deleted by external data source'
|
|
|
|
)
|
|
|
|
const conditions = { _id: project_id }
|
|
|
|
const update = { deletedByExternalDataSource: true }
|
|
|
|
|
|
|
|
return Project.update(conditions, update, {}, err =>
|
|
|
|
require('../Editor/EditorController').notifyUsersProjectHasBeenDeletedOrRenamed(
|
|
|
|
project_id,
|
|
|
|
() => callback()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
unmarkAsDeletedByExternalSource(project_id, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error) {}
|
|
|
|
}
|
|
|
|
logger.log(
|
|
|
|
{ project_id },
|
|
|
|
'removing flag marking project as deleted by external data source'
|
|
|
|
)
|
2019-07-18 10:18:56 -04:00
|
|
|
const conditions = { _id: project_id }
|
2019-05-29 05:21:06 -04:00
|
|
|
const update = { deletedByExternalDataSource: false }
|
|
|
|
return Project.update(conditions, update, {}, callback)
|
|
|
|
},
|
|
|
|
|
|
|
|
deleteUsersProjects(user_id, callback) {
|
|
|
|
logger.log({ user_id }, 'deleting users projects')
|
|
|
|
|
|
|
|
return Project.find({ owner_ref: user_id }, function(error, projects) {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
return async.each(
|
|
|
|
projects,
|
|
|
|
(project, cb) => ProjectDeleter.deleteProject(project._id, cb),
|
|
|
|
function(err) {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
return CollaboratorsHandler.removeUserFromAllProjets(
|
|
|
|
user_id,
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-07-18 10:18:56 -04:00
|
|
|
expireDeletedProjectsAfterDuration(callback) {
|
|
|
|
const DURATION = 90
|
|
|
|
DeletedProject.find(
|
|
|
|
{
|
|
|
|
'deleterData.deletedAt': {
|
|
|
|
$lt: new Date(moment().subtract(DURATION, 'days'))
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
2019-07-18 10:18:56 -04:00
|
|
|
project: {
|
|
|
|
$ne: null
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function(err, deletedProjects) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-07-18 10:18:56 -04:00
|
|
|
logger.err({ err }, 'Problem with finding deletedProject')
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(err)
|
|
|
|
}
|
2019-07-18 10:18:56 -04:00
|
|
|
|
|
|
|
if (deletedProjects.length) {
|
|
|
|
async.eachSeries(
|
|
|
|
deletedProjects,
|
|
|
|
function(deletedProject, cb) {
|
|
|
|
ProjectDeleter.expireDeletedProject(
|
|
|
|
deletedProject.deletedProjectId,
|
|
|
|
cb
|
|
|
|
)
|
|
|
|
},
|
|
|
|
function(err) {
|
|
|
|
if (err != null) {
|
|
|
|
logger.err({ err })
|
|
|
|
}
|
|
|
|
callback(err)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
logger.log({}, 'No deleted projects for duration were found')
|
|
|
|
callback(err)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
archiveProject(project_id, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error) {}
|
|
|
|
}
|
|
|
|
logger.log({ project_id }, 'archived project from user request')
|
|
|
|
return Project.update(
|
|
|
|
{ _id: project_id },
|
|
|
|
{ $set: { archived: true } },
|
|
|
|
function(err) {
|
|
|
|
if (err != null) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn({ err }, 'problem archived project')
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
logger.log(
|
|
|
|
{ project_id },
|
|
|
|
'successfully archived project from user request'
|
|
|
|
)
|
|
|
|
return callback()
|
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
restoreProject(project_id, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error) {}
|
|
|
|
}
|
|
|
|
return Project.update(
|
|
|
|
{ _id: project_id },
|
|
|
|
{ $unset: { archived: true } },
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2019-06-27 06:10:46 -04:00
|
|
|
|
2019-07-18 10:18:56 -04:00
|
|
|
// Async methods
|
|
|
|
|
|
|
|
async function deleteProject(project_id, options = {}) {
|
|
|
|
logger.log({ project_id }, 'deleting project')
|
|
|
|
|
|
|
|
try {
|
|
|
|
let project = await Project.findOne({ _id: project_id }).exec()
|
|
|
|
if (!project) {
|
|
|
|
throw new Errors.NotFoundError('project not found')
|
|
|
|
}
|
|
|
|
|
|
|
|
await DeletedProject.create({
|
|
|
|
project: project,
|
|
|
|
deleterData: {
|
|
|
|
deletedAt: new Date(),
|
|
|
|
deleterId:
|
|
|
|
options.deleterUser != null ? options.deleterUser._id : undefined,
|
|
|
|
deleterIpAddress: options.ipAddress,
|
|
|
|
deletedProjectId: project._id,
|
|
|
|
deletedProjectOwnerId: project.owner_ref,
|
|
|
|
deletedProjectCollaboratorIds: project.collaberator_refs,
|
|
|
|
deletedProjectReadOnlyIds: project.readOnly_refs,
|
|
|
|
deletedProjectReadWriteTokenAccessIds:
|
|
|
|
project.tokenAccessReadAndWrite_refs,
|
2019-07-26 10:26:25 -04:00
|
|
|
deletedProjectOverleafId: project.overleaf
|
|
|
|
? project.overleaf.id
|
|
|
|
: undefined,
|
|
|
|
deletedProjectOverleafHistoryId:
|
|
|
|
project.overleaf && project.overleaf.history
|
|
|
|
? project.overleaf.history.id
|
|
|
|
: undefined,
|
2019-07-18 10:18:56 -04:00
|
|
|
deletedProjectReadOnlyTokenAccessIds: project.tokenAccessReadOnly_refs,
|
|
|
|
deletedProjectReadWriteToken: project.tokens.readAndWrite,
|
|
|
|
deletedProjectReadOnlyToken: project.tokens.readOnly,
|
|
|
|
deletedProjectLastUpdatedAt: project.lastUpdated
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const flushProjectToMongoAndDelete = promisify(
|
|
|
|
DocumentUpdaterHandler.flushProjectToMongoAndDelete
|
|
|
|
)
|
|
|
|
await flushProjectToMongoAndDelete(project_id)
|
|
|
|
|
|
|
|
const getMemberIds = promisify(CollaboratorsHandler.getMemberIds)
|
|
|
|
let member_ids = await getMemberIds(project_id)
|
|
|
|
|
|
|
|
// fire these jobs in the background
|
|
|
|
Array.from(member_ids).forEach(member_id =>
|
|
|
|
TagsHandler.removeProjectFromAllTags(member_id, project_id, () => {})
|
|
|
|
)
|
|
|
|
|
|
|
|
await Project.remove({ _id: project_id }).exec()
|
|
|
|
} catch (err) {
|
|
|
|
logger.warn({ err }, 'problem deleting project')
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.log({ project_id }, 'successfully deleted project')
|
|
|
|
}
|
|
|
|
|
|
|
|
async function undeleteProject(project_id) {
|
|
|
|
let deletedProject = await DeletedProject.findOne({
|
|
|
|
'deleterData.deletedProjectId': project_id
|
|
|
|
}).exec()
|
|
|
|
|
|
|
|
if (!deletedProject) {
|
2019-07-19 05:39:58 -04:00
|
|
|
throw new Errors.NotFoundError('project_not_found')
|
2019-07-18 10:18:56 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!deletedProject.project) {
|
2019-07-19 05:39:58 -04:00
|
|
|
throw new Errors.NotFoundError('project_too_old_to_restore')
|
2019-07-18 10:18:56 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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(
|
|
|
|
deletedProject.deleterData.deletedProjectOwnerId,
|
|
|
|
restored.name + ' (Restored)'
|
|
|
|
)
|
|
|
|
restored.archived = undefined
|
|
|
|
|
|
|
|
// we can't use Mongoose to re-insert the project, as it won't
|
|
|
|
// create a new document with an _id already specified. We need to
|
|
|
|
// insert it directly into the collection
|
|
|
|
|
|
|
|
// db.projects.insert doesn't work with promisify
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
db.projects.insert(restored, err => {
|
|
|
|
if (err) {
|
|
|
|
reject(err)
|
|
|
|
} else {
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
await DeletedProject.deleteOne({ _id: deletedProject._id }).exec()
|
|
|
|
}
|
|
|
|
|
|
|
|
async function expireDeletedProject(projectId) {
|
|
|
|
try {
|
|
|
|
const deletedProject = await DeletedProject.findOne({
|
|
|
|
'deleterData.deletedProjectId': projectId
|
|
|
|
}).exec()
|
|
|
|
if (!deletedProject) {
|
|
|
|
throw new Errors.NotFoundError(
|
|
|
|
`No deleted project found for project id ${projectId}`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (!deletedProject.project) {
|
|
|
|
logger.warn(
|
|
|
|
{ projectId },
|
|
|
|
`Attempted to expire already-expired deletedProject`
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const destroyProject = promisify(DocstoreManager.destroyProject)
|
|
|
|
await destroyProject(deletedProject.project._id)
|
|
|
|
|
|
|
|
await DeletedProject.update(
|
|
|
|
{
|
|
|
|
_id: deletedProject._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$set: {
|
|
|
|
'deleterData.deleterIpAddress': null,
|
|
|
|
project: null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
).exec()
|
|
|
|
|
|
|
|
logger.log({ projectId }, 'Successfully expired deleted project')
|
|
|
|
} catch (error) {
|
|
|
|
logger.warn({ projectId, error }, 'error expiring deleted project')
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Exported class
|
|
|
|
|
2019-06-27 06:10:46 -04:00
|
|
|
const promises = {
|
2019-07-18 10:18:56 -04:00
|
|
|
deleteProject: deleteProject,
|
|
|
|
undeleteProject: undeleteProject,
|
|
|
|
expireDeletedProject: expireDeletedProject,
|
|
|
|
deleteUsersProjects: promisify(ProjectDeleter.deleteUsersProjects)
|
2019-06-27 06:10:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
ProjectDeleter.promises = promises
|
2019-07-18 10:18:56 -04:00
|
|
|
ProjectDeleter.deleteProject = callbackify(deleteProject)
|
|
|
|
ProjectDeleter.undeleteProject = callbackify(undeleteProject)
|
|
|
|
ProjectDeleter.expireDeletedProject = callbackify(expireDeletedProject)
|
2019-06-27 06:10:46 -04:00
|
|
|
|
|
|
|
module.exports = ProjectDeleter
|