2019-11-25 10:41:12 -05:00
|
|
|
const { db, ObjectId } = require('../../infrastructure/mongojs')
|
2019-07-18 10:18:56 -04:00
|
|
|
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-10-02 10:06:57 -04:00
|
|
|
const ProjectHelper = require('./ProjectHelper')
|
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-10-07 04:30:51 -04:00
|
|
|
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
|
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
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
function logWarningOnError(msg) {
|
|
|
|
return function(err) {
|
|
|
|
if (err) {
|
|
|
|
logger.warn({ err }, msg)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-02-12 10:14:57 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const ProjectDeleter = {
|
|
|
|
markAsDeletedByExternalSource(projectId, callback) {
|
|
|
|
callback =
|
|
|
|
callback ||
|
|
|
|
logWarningOnError('error marking project as deleted by external source')
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.log(
|
2020-02-12 10:14:57 -05:00
|
|
|
{ project_id: projectId },
|
2019-05-29 05:21:06 -04:00
|
|
|
'marking project as deleted by external data source'
|
|
|
|
)
|
2020-02-12 10:14:57 -05:00
|
|
|
const conditions = { _id: projectId }
|
2019-05-29 05:21:06 -04:00
|
|
|
const update = { deletedByExternalDataSource: true }
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
Project.update(conditions, update, {}, err => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
require('../Editor/EditorController').notifyUsersProjectHasBeenDeletedOrRenamed(
|
2020-02-12 10:14:57 -05:00
|
|
|
projectId,
|
|
|
|
() => callback() // don't return error, as project has been updated
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
2020-02-12 10:14:57 -05:00
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
unmarkAsDeletedByExternalSource(projectId, callback) {
|
|
|
|
callback =
|
|
|
|
callback ||
|
|
|
|
logWarningOnError('error unmarking project as deleted by external source')
|
|
|
|
const conditions = { _id: projectId }
|
2019-05-29 05:21:06 -04:00
|
|
|
const update = { deletedByExternalDataSource: false }
|
2020-02-12 10:14:57 -05:00
|
|
|
Project.update(conditions, update, {}, callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
deleteUsersProjects(userId, callback) {
|
|
|
|
Project.find({ owner_ref: userId }, function(error, projects) {
|
|
|
|
if (error) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(error)
|
|
|
|
}
|
2020-02-12 10:14:57 -05:00
|
|
|
async.eachLimit(
|
2019-05-29 05:21:06 -04:00
|
|
|
projects,
|
2020-02-12 10:14:57 -05:00
|
|
|
5,
|
2019-05-29 05:21:06 -04:00
|
|
|
(project, cb) => ProjectDeleter.deleteProject(project._id, cb),
|
|
|
|
function(err) {
|
2020-02-12 10:14:57 -05:00
|
|
|
if (err) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(err)
|
|
|
|
}
|
2020-02-12 10:14:57 -05:00
|
|
|
CollaboratorsHandler.removeUserFromAllProjects(userId, callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
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) {
|
2020-02-12 10:14:57 -05:00
|
|
|
if (err) {
|
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
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
async.eachSeries(
|
|
|
|
deletedProjects,
|
|
|
|
function(deletedProject, cb) {
|
|
|
|
ProjectDeleter.expireDeletedProject(
|
|
|
|
deletedProject.deleterData.deletedProjectId,
|
|
|
|
cb
|
|
|
|
)
|
|
|
|
},
|
|
|
|
callback
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
restoreProject(projectId, callback) {
|
|
|
|
Project.update({ _id: projectId }, { $unset: { archived: true } }, callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
2019-06-27 06:10:46 -04:00
|
|
|
|
2019-07-18 10:18:56 -04:00
|
|
|
// Async methods
|
|
|
|
|
2019-11-25 10:41:12 -05:00
|
|
|
async function archiveProject(projectId, userId) {
|
2019-10-02 10:06:57 -04:00
|
|
|
try {
|
2019-11-25 10:41:12 -05:00
|
|
|
let project = await Project.findOne({ _id: projectId }).exec()
|
2019-10-02 10:06:57 -04:00
|
|
|
if (!project) {
|
|
|
|
throw new Errors.NotFoundError('project not found')
|
|
|
|
}
|
|
|
|
const archived = ProjectHelper.calculateArchivedArray(
|
|
|
|
project,
|
|
|
|
userId,
|
|
|
|
'ARCHIVE'
|
|
|
|
)
|
|
|
|
|
2019-11-25 10:41:12 -05:00
|
|
|
await Project.update(
|
|
|
|
{ _id: projectId },
|
|
|
|
{ $set: { archived: archived }, $pull: { trashed: ObjectId(userId) } }
|
|
|
|
)
|
2019-10-02 10:06:57 -04:00
|
|
|
} catch (err) {
|
|
|
|
logger.warn({ err }, 'problem archiving project')
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-25 10:41:12 -05:00
|
|
|
async function unarchiveProject(projectId, userId) {
|
2019-10-02 10:06:57 -04:00
|
|
|
try {
|
2019-11-25 10:41:12 -05:00
|
|
|
let project = await Project.findOne({ _id: projectId }).exec()
|
2019-10-02 10:06:57 -04:00
|
|
|
if (!project) {
|
|
|
|
throw new Errors.NotFoundError('project not found')
|
|
|
|
}
|
|
|
|
|
|
|
|
const archived = ProjectHelper.calculateArchivedArray(
|
|
|
|
project,
|
|
|
|
userId,
|
|
|
|
'UNARCHIVE'
|
|
|
|
)
|
|
|
|
|
2019-11-25 10:41:12 -05:00
|
|
|
await Project.update({ _id: projectId }, { $set: { archived: archived } })
|
2019-10-02 10:06:57 -04:00
|
|
|
} catch (err) {
|
|
|
|
logger.warn({ err }, 'problem unarchiving project')
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-25 10:41:12 -05:00
|
|
|
async function trashProject(projectId, userId) {
|
|
|
|
try {
|
|
|
|
let project = await Project.findOne({ _id: projectId }).exec()
|
|
|
|
if (!project) {
|
|
|
|
throw new Errors.NotFoundError('project not found')
|
|
|
|
}
|
|
|
|
|
|
|
|
await Project.update(
|
|
|
|
{ _id: projectId },
|
|
|
|
{
|
|
|
|
$addToSet: { trashed: ObjectId(userId) },
|
|
|
|
$pull: { archived: ObjectId(userId) }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} catch (err) {
|
|
|
|
logger.warn({ err }, 'problem trashing project')
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function untrashProject(projectId, userId) {
|
|
|
|
try {
|
|
|
|
let project = await Project.findOne({ _id: projectId }).exec()
|
|
|
|
if (!project) {
|
|
|
|
throw new Errors.NotFoundError('project not found')
|
|
|
|
}
|
|
|
|
|
|
|
|
await Project.update(
|
|
|
|
{ _id: projectId },
|
|
|
|
{ $pull: { trashed: ObjectId(userId) } }
|
|
|
|
)
|
|
|
|
} catch (err) {
|
|
|
|
logger.warn({ err }, 'problem untrashing project')
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
async function deleteProject(projectId, options = {}) {
|
2019-07-18 10:18:56 -04:00
|
|
|
try {
|
2020-02-12 10:14:57 -05:00
|
|
|
const project = await Project.findOne({ _id: projectId }).exec()
|
2019-07-18 10:18:56 -04:00
|
|
|
if (!project) {
|
|
|
|
throw new Errors.NotFoundError('project not found')
|
|
|
|
}
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
const deleterData = {
|
2019-07-26 11:27:28 -04:00
|
|
|
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,
|
|
|
|
deletedProjectOverleafId: project.overleaf
|
|
|
|
? project.overleaf.id
|
|
|
|
: undefined,
|
|
|
|
deletedProjectOverleafHistoryId:
|
|
|
|
project.overleaf && project.overleaf.history
|
|
|
|
? project.overleaf.history.id
|
|
|
|
: undefined,
|
|
|
|
deletedProjectReadOnlyTokenAccessIds: project.tokenAccessReadOnly_refs,
|
|
|
|
deletedProjectReadWriteToken: project.tokens.readAndWrite,
|
|
|
|
deletedProjectReadOnlyToken: project.tokens.readOnly,
|
|
|
|
deletedProjectLastUpdatedAt: project.lastUpdated
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys(deleterData).forEach(
|
|
|
|
key => (deleterData[key] === undefined ? delete deleterData[key] : '')
|
|
|
|
)
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
await DeletedProject.update(
|
|
|
|
{ 'deleterData.deletedProjectId': projectId },
|
|
|
|
{ project, deleterData },
|
|
|
|
{ upsert: true }
|
|
|
|
)
|
2019-07-18 10:18:56 -04:00
|
|
|
|
|
|
|
const flushProjectToMongoAndDelete = promisify(
|
|
|
|
DocumentUpdaterHandler.flushProjectToMongoAndDelete
|
|
|
|
)
|
2020-02-12 10:14:57 -05:00
|
|
|
await flushProjectToMongoAndDelete(projectId)
|
2019-07-18 10:18:56 -04:00
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
const memberIds = await CollaboratorsGetter.promises.getMemberIds(projectId)
|
2019-07-18 10:18:56 -04:00
|
|
|
|
|
|
|
// fire these jobs in the background
|
2020-02-12 10:14:57 -05:00
|
|
|
Array.from(memberIds).forEach(memberId =>
|
|
|
|
TagsHandler.removeProjectFromAllTags(memberId, projectId, () => {})
|
2019-07-18 10:18:56 -04:00
|
|
|
)
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
await Project.remove({ _id: projectId }).exec()
|
2019-07-18 10:18:56 -04:00
|
|
|
} catch (err) {
|
|
|
|
logger.warn({ err }, 'problem deleting project')
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
logger.log({ project_id: projectId }, 'successfully deleted project')
|
2019-07-18 10:18:56 -04:00
|
|
|
}
|
|
|
|
|
2020-02-12 10:14:57 -05:00
|
|
|
async function undeleteProject(projectId) {
|
2019-07-18 10:18:56 -04:00
|
|
|
let deletedProject = await DeletedProject.findOne({
|
2020-02-12 10:14:57 -05:00
|
|
|
'deleterData.deletedProjectId': projectId
|
2019-07-18 10:18:56 -04:00
|
|
|
}).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
|
2019-09-09 07:52:25 -04:00
|
|
|
restored.name = await ProjectDetailsHandler.promises.generateUniqueName(
|
2019-07-18 10:18:56 -04:00
|
|
|
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()
|
|
|
|
} 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-10-02 10:06:57 -04:00
|
|
|
archiveProject: archiveProject,
|
|
|
|
unarchiveProject: unarchiveProject,
|
2019-11-25 10:41:12 -05:00
|
|
|
trashProject: trashProject,
|
|
|
|
untrashProject: untrashProject,
|
2019-07-18 10:18:56 -04:00
|
|
|
deleteProject: deleteProject,
|
|
|
|
undeleteProject: undeleteProject,
|
|
|
|
expireDeletedProject: expireDeletedProject,
|
2020-02-27 07:50:29 -05:00
|
|
|
deleteUsersProjects: promisify(ProjectDeleter.deleteUsersProjects),
|
|
|
|
unmarkAsDeletedByExternalSource: promisify(
|
|
|
|
ProjectDeleter.unmarkAsDeletedByExternalSource
|
|
|
|
)
|
2019-06-27 06:10:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
ProjectDeleter.promises = promises
|
2019-10-02 10:06:57 -04:00
|
|
|
ProjectDeleter.archiveProject = callbackify(archiveProject)
|
|
|
|
ProjectDeleter.unarchiveProject = callbackify(unarchiveProject)
|
2019-11-25 10:41:12 -05:00
|
|
|
ProjectDeleter.trashProject = callbackify(trashProject)
|
|
|
|
ProjectDeleter.untrashProject = callbackify(untrashProject)
|
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
|