EditorHttpController.js 10 KB

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