EditorHttpController.js 9.0 KB

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