EditorHttpController.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. const ProjectDeleter = require('../Project/ProjectDeleter')
  2. const EditorController = require('./EditorController')
  3. const ProjectGetter = require('../Project/ProjectGetter')
  4. const AuthorizationManager = require('../Authorization/AuthorizationManager')
  5. const ProjectEditorHandler = require('../Project/ProjectEditorHandler')
  6. const Metrics = require('@overleaf/metrics')
  7. const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
  8. const CollaboratorsInviteHandler = require('../Collaborators/CollaboratorsInviteHandler')
  9. const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
  10. const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
  11. const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
  12. const SessionManager = require('../Authentication/SessionManager')
  13. const Errors = require('../Errors/Errors')
  14. const HttpErrorHandler = require('../Errors/HttpErrorHandler')
  15. const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
  16. const DocstoreManager = require('../Docstore/DocstoreManager')
  17. const logger = require('logger-sharelatex')
  18. const { expressify } = require('../../util/promises')
  19. module.exports = {
  20. joinProject: expressify(joinProject),
  21. addDoc: expressify(addDoc),
  22. addFolder: expressify(addFolder),
  23. renameEntity: expressify(renameEntity),
  24. moveEntity: expressify(moveEntity),
  25. deleteDoc: expressify(deleteDoc),
  26. deleteFile: expressify(deleteFile),
  27. deleteFolder: expressify(deleteFolder),
  28. deleteEntity: expressify(deleteEntity),
  29. convertDocToFile: expressify(convertDocToFile),
  30. _nameIsAcceptableLength,
  31. }
  32. const unsupportedSpellcheckLanguages = [
  33. 'am',
  34. 'hy',
  35. 'bn',
  36. 'gu',
  37. 'he',
  38. 'hi',
  39. 'hu',
  40. 'is',
  41. 'kn',
  42. 'ml',
  43. 'mr',
  44. 'or',
  45. 'ss',
  46. 'ta',
  47. 'te',
  48. 'uk',
  49. 'uz',
  50. 'zu',
  51. 'fi',
  52. ]
  53. async function joinProject(req, res, next) {
  54. const projectId = req.params.Project_id
  55. let userId = req.query.user_id
  56. if (userId === 'anonymous-user') {
  57. userId = null
  58. }
  59. Metrics.inc('editor.join-project')
  60. const {
  61. project,
  62. privilegeLevel,
  63. isRestrictedUser,
  64. } = await _buildJoinProjectView(req, projectId, userId)
  65. if (!project) {
  66. return res.sendStatus(403)
  67. }
  68. // Hide access tokens if this is not the project owner
  69. TokenAccessHandler.protectTokens(project, privilegeLevel)
  70. // Hide sensitive data if the user is restricted
  71. if (isRestrictedUser) {
  72. project.owner = { _id: project.owner._id }
  73. project.members = []
  74. }
  75. // Only show the 'renamed or deleted' message once
  76. if (project.deletedByExternalDataSource) {
  77. await ProjectDeleter.promises.unmarkAsDeletedByExternalSource(projectId)
  78. }
  79. // disable spellchecking for currently unsupported spell check languages
  80. // preserve the value in the db so they can use it again once we add back
  81. // support.
  82. if (
  83. unsupportedSpellcheckLanguages.indexOf(project.spellCheckLanguage) !== -1
  84. ) {
  85. project.spellCheckLanguage = ''
  86. }
  87. res.json({
  88. project,
  89. privilegeLevel,
  90. isRestrictedUser,
  91. })
  92. }
  93. async function _buildJoinProjectView(req, projectId, userId) {
  94. const project = await ProjectGetter.promises.getProjectWithoutDocLines(
  95. projectId
  96. )
  97. if (project == null) {
  98. throw new Errors.NotFoundError('project not found')
  99. }
  100. let deletedDocsFromDocstore = []
  101. try {
  102. deletedDocsFromDocstore = await DocstoreManager.promises.getAllDeletedDocs(
  103. projectId
  104. )
  105. } catch (err) {
  106. // The query in docstore is not optimized at this time and fails for
  107. // projects with many very large, deleted documents.
  108. // Not serving the user with deletedDocs from docstore may cause a minor
  109. // UI issue with deleted files that are no longer available for restore.
  110. logger.warn(
  111. { err, projectId },
  112. 'soft-failure when fetching deletedDocs from docstore'
  113. )
  114. }
  115. const members = await CollaboratorsGetter.promises.getInvitedMembersWithPrivilegeLevels(
  116. projectId
  117. )
  118. const token = TokenAccessHandler.getRequestToken(req, projectId)
  119. const privilegeLevel = await AuthorizationManager.promises.getPrivilegeLevelForProject(
  120. userId,
  121. projectId,
  122. token
  123. )
  124. if (privilegeLevel == null || privilegeLevel === PrivilegeLevels.NONE) {
  125. return { project: null, privilegeLevel: null, isRestrictedUser: false }
  126. }
  127. const invites = await CollaboratorsInviteHandler.promises.getAllInvites(
  128. projectId
  129. )
  130. const isTokenMember = await CollaboratorsHandler.promises.userIsTokenMember(
  131. userId,
  132. projectId
  133. )
  134. const isRestrictedUser = AuthorizationManager.isRestrictedUser(
  135. userId,
  136. privilegeLevel,
  137. isTokenMember
  138. )
  139. return {
  140. project: ProjectEditorHandler.buildProjectModelView(
  141. project,
  142. members,
  143. invites,
  144. deletedDocsFromDocstore
  145. ),
  146. privilegeLevel,
  147. isRestrictedUser,
  148. }
  149. }
  150. function _nameIsAcceptableLength(name) {
  151. return name != null && name.length < 150 && name.length !== 0
  152. }
  153. async function addDoc(req, res, next) {
  154. const projectId = req.params.Project_id
  155. const { name } = req.body
  156. const parentFolderId = req.body.parent_folder_id
  157. const userId = SessionManager.getLoggedInUserId(req.session)
  158. if (!_nameIsAcceptableLength(name)) {
  159. return res.sendStatus(400)
  160. }
  161. try {
  162. const doc = await EditorController.promises.addDoc(
  163. projectId,
  164. parentFolderId,
  165. name,
  166. [],
  167. 'editor',
  168. userId
  169. )
  170. res.json(doc)
  171. } catch (err) {
  172. if (err.message === 'project_has_too_many_files') {
  173. res.status(400).json(req.i18n.translate('project_has_too_many_files'))
  174. } else {
  175. next(err)
  176. }
  177. }
  178. }
  179. async function addFolder(req, res, next) {
  180. const projectId = req.params.Project_id
  181. const { name } = req.body
  182. const parentFolderId = req.body.parent_folder_id
  183. const userId = SessionManager.getLoggedInUserId(req.session)
  184. if (!_nameIsAcceptableLength(name)) {
  185. return res.sendStatus(400)
  186. }
  187. try {
  188. const doc = await EditorController.promises.addFolder(
  189. projectId,
  190. parentFolderId,
  191. name,
  192. 'editor',
  193. userId
  194. )
  195. res.json(doc)
  196. } catch (err) {
  197. if (err.message === 'project_has_too_many_files') {
  198. res.status(400).json(req.i18n.translate('project_has_too_many_files'))
  199. } else if (err.message === 'invalid element name') {
  200. res.status(400).json(req.i18n.translate('invalid_file_name'))
  201. } else {
  202. next(err)
  203. }
  204. }
  205. }
  206. async function renameEntity(req, res, next) {
  207. const projectId = req.params.Project_id
  208. const entityId = req.params.entity_id
  209. const entityType = req.params.entity_type
  210. const { name } = req.body
  211. if (!_nameIsAcceptableLength(name)) {
  212. return res.sendStatus(400)
  213. }
  214. const userId = SessionManager.getLoggedInUserId(req.session)
  215. await EditorController.promises.renameEntity(
  216. projectId,
  217. entityId,
  218. entityType,
  219. name,
  220. userId
  221. )
  222. res.sendStatus(204)
  223. }
  224. async function moveEntity(req, res, next) {
  225. const projectId = req.params.Project_id
  226. const entityId = req.params.entity_id
  227. const entityType = req.params.entity_type
  228. const folderId = req.body.folder_id
  229. const userId = SessionManager.getLoggedInUserId(req.session)
  230. await EditorController.promises.moveEntity(
  231. projectId,
  232. entityId,
  233. folderId,
  234. entityType,
  235. userId
  236. )
  237. res.sendStatus(204)
  238. }
  239. async function deleteDoc(req, res, next) {
  240. req.params.entity_type = 'doc'
  241. await deleteEntity(req, res, next)
  242. }
  243. async function deleteFile(req, res, next) {
  244. req.params.entity_type = 'file'
  245. await deleteEntity(req, res, next)
  246. }
  247. async function deleteFolder(req, res, next) {
  248. req.params.entity_type = 'folder'
  249. await deleteEntity(req, res, next)
  250. }
  251. async function deleteEntity(req, res, next) {
  252. const projectId = req.params.Project_id
  253. const entityId = req.params.entity_id
  254. const entityType = req.params.entity_type
  255. const userId = SessionManager.getLoggedInUserId(req.session)
  256. await EditorController.promises.deleteEntity(
  257. projectId,
  258. entityId,
  259. entityType,
  260. 'editor',
  261. userId
  262. )
  263. res.sendStatus(204)
  264. }
  265. async function convertDocToFile(req, res, next) {
  266. const projectId = req.params.Project_id
  267. const docId = req.params.entity_id
  268. const { userId } = req.body
  269. try {
  270. const fileRef = await ProjectEntityUpdateHandler.promises.convertDocToFile(
  271. projectId,
  272. docId,
  273. userId
  274. )
  275. res.json({ fileId: fileRef._id.toString() })
  276. } catch (err) {
  277. if (err instanceof Errors.NotFoundError) {
  278. return HttpErrorHandler.notFound(req, res, 'Document not found')
  279. } else if (err instanceof Errors.DocHasRangesError) {
  280. return HttpErrorHandler.unprocessableEntity(
  281. req,
  282. res,
  283. 'Document has comments or tracked changes'
  284. )
  285. } else {
  286. throw err
  287. }
  288. }
  289. }