EditorHttpController.js 7.9 KB

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