EditorHttpController.mjs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import ProjectDeleter from '../Project/ProjectDeleter.mjs'
  2. import EditorController from './EditorController.mjs'
  3. import ProjectGetter from '../Project/ProjectGetter.mjs'
  4. import AuthorizationManager from '../Authorization/AuthorizationManager.mjs'
  5. import ProjectEditorHandler from '../Project/ProjectEditorHandler.mjs'
  6. import Metrics from '@overleaf/metrics'
  7. import CollaboratorsInviteGetter from '../Collaborators/CollaboratorsInviteGetter.mjs'
  8. import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
  9. import SessionManager from '../Authentication/SessionManager.mjs'
  10. import Errors from '../Errors/Errors.js'
  11. import { expressify } from '@overleaf/promise-utils'
  12. import Settings from '@overleaf/settings'
  13. import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.mjs'
  14. import { z, zz, validateReq } from '../../infrastructure/Validation.js'
  15. const ProjectAccess = CollaboratorsGetter.ProjectAccess
  16. export default {
  17. joinProject: expressify(joinProject),
  18. addDoc: expressify(addDoc),
  19. addFolder: expressify(addFolder),
  20. renameEntity: expressify(renameEntity),
  21. moveEntity: expressify(moveEntity),
  22. deleteDoc: expressify(deleteDoc),
  23. deleteFile: expressify(deleteFile),
  24. deleteFolder: expressify(deleteFolder),
  25. deleteEntity: expressify(deleteEntity),
  26. _nameIsAcceptableLength,
  27. }
  28. const joinProjectSchema = z.object({
  29. params: z.object({
  30. Project_id: zz.objectId(),
  31. }),
  32. body: z.object({
  33. userId: z.string(),
  34. anonymousAccessToken: z.string().optional(),
  35. }),
  36. })
  37. async function joinProject(req, res, next) {
  38. const { params, body } = validateReq(req, joinProjectSchema)
  39. const projectId = params.Project_id
  40. let userId = body.userId
  41. if (userId === 'anonymous-user') {
  42. userId = null
  43. }
  44. Metrics.inc('editor.join-project')
  45. const {
  46. project,
  47. privilegeLevel,
  48. isRestrictedUser,
  49. isTokenMember,
  50. isInvitedMember,
  51. } = await _buildJoinProjectView(req, projectId, userId)
  52. if (!project) {
  53. return res.sendStatus(403)
  54. }
  55. // Only show the 'renamed or deleted' message once
  56. if (project.deletedByExternalDataSource) {
  57. await ProjectDeleter.promises.unmarkAsDeletedByExternalSource(projectId)
  58. }
  59. if (project.spellCheckLanguage) {
  60. project.spellCheckLanguage = await chooseSpellCheckLanguage(
  61. project.spellCheckLanguage
  62. )
  63. }
  64. res.json({
  65. project,
  66. privilegeLevel,
  67. isRestrictedUser,
  68. isTokenMember,
  69. isInvitedMember,
  70. })
  71. }
  72. async function _buildJoinProjectView(req, projectId, userId) {
  73. const project =
  74. await ProjectGetter.promises.getProjectWithoutDocLines(projectId)
  75. if (project == null) {
  76. throw new Errors.NotFoundError('project not found')
  77. }
  78. const projectAccess = new ProjectAccess(project)
  79. const token = req.body.anonymousAccessToken
  80. const privilegeLevel =
  81. await AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess(
  82. userId,
  83. projectId,
  84. token,
  85. projectAccess
  86. )
  87. if (privilegeLevel == null || privilegeLevel === PrivilegeLevels.NONE) {
  88. return { project: null, privilegeLevel: null, isRestrictedUser: false }
  89. }
  90. const isTokenMember = projectAccess.isUserTokenMember(userId)
  91. const isInvitedMember = projectAccess.isUserInvitedMember(userId)
  92. const isRestrictedUser = AuthorizationManager.isRestrictedUser(
  93. userId,
  94. privilegeLevel,
  95. isTokenMember,
  96. isInvitedMember
  97. )
  98. let ownerMember
  99. let members = []
  100. let invites = []
  101. if (isRestrictedUser) {
  102. ownerMember = await projectAccess.loadOwner()
  103. } else {
  104. ;({ ownerMember, members } =
  105. await projectAccess.loadOwnerAndInvitedMembers())
  106. invites = await CollaboratorsInviteGetter.promises.getAllInvites(projectId)
  107. }
  108. return {
  109. project: ProjectEditorHandler.buildProjectModelView(
  110. project,
  111. ownerMember,
  112. members,
  113. invites,
  114. isRestrictedUser
  115. ),
  116. privilegeLevel,
  117. isTokenMember,
  118. isInvitedMember,
  119. isRestrictedUser,
  120. }
  121. }
  122. function _nameIsAcceptableLength(name) {
  123. return name != null && name.length < 150 && name.length !== 0
  124. }
  125. async function addDoc(req, res, next) {
  126. const projectId = req.params.Project_id
  127. const { name } = req.body
  128. const parentFolderId = req.body.parent_folder_id
  129. const userId = SessionManager.getLoggedInUserId(req.session)
  130. if (!_nameIsAcceptableLength(name)) {
  131. return res.sendStatus(400)
  132. }
  133. try {
  134. const doc = await EditorController.promises.addDoc(
  135. projectId,
  136. parentFolderId,
  137. name,
  138. [],
  139. 'editor',
  140. userId
  141. )
  142. res.json(doc)
  143. } catch (err) {
  144. if (err.message === 'project_has_too_many_files') {
  145. res.status(400).json(
  146. req.i18n.translate('project_has_too_many_files_limit', {
  147. limit: Settings.maxEntitiesPerProject,
  148. })
  149. )
  150. } else {
  151. next(err)
  152. }
  153. }
  154. }
  155. async function addFolder(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.addFolder(
  165. projectId,
  166. parentFolderId,
  167. name,
  168. 'editor',
  169. userId
  170. )
  171. res.json(doc)
  172. } catch (err) {
  173. if (err.message === 'project_has_too_many_files') {
  174. res.status(400).json(
  175. req.i18n.translate('project_has_too_many_files_limit', {
  176. limit: Settings.maxEntitiesPerProject,
  177. })
  178. )
  179. } else if (err.message === 'invalid element name') {
  180. res.status(400).json(req.i18n.translate('invalid_file_name'))
  181. } else {
  182. next(err)
  183. }
  184. }
  185. }
  186. async function renameEntity(req, res, next) {
  187. const projectId = req.params.Project_id
  188. const entityId = req.params.entity_id
  189. const entityType = req.params.entity_type
  190. const { name, source = 'editor' } = req.body
  191. if (!_nameIsAcceptableLength(name)) {
  192. return res.sendStatus(400)
  193. }
  194. const userId = SessionManager.getLoggedInUserId(req.session)
  195. await EditorController.promises.renameEntity(
  196. projectId,
  197. entityId,
  198. entityType,
  199. name,
  200. userId,
  201. source
  202. )
  203. res.sendStatus(204)
  204. }
  205. async function moveEntity(req, res, next) {
  206. const projectId = req.params.Project_id
  207. const entityId = req.params.entity_id
  208. const entityType = req.params.entity_type
  209. const folderId = req.body.folder_id
  210. const source = req.body.source ?? 'editor'
  211. const userId = SessionManager.getLoggedInUserId(req.session)
  212. await EditorController.promises.moveEntity(
  213. projectId,
  214. entityId,
  215. folderId,
  216. entityType,
  217. userId,
  218. source
  219. )
  220. res.sendStatus(204)
  221. }
  222. async function deleteDoc(req, res, next) {
  223. req.params.entity_type = 'doc'
  224. await deleteEntity(req, res, next)
  225. }
  226. async function deleteFile(req, res, next) {
  227. req.params.entity_type = 'file'
  228. await deleteEntity(req, res, next)
  229. }
  230. async function deleteFolder(req, res, next) {
  231. req.params.entity_type = 'folder'
  232. await deleteEntity(req, res, next)
  233. }
  234. async function deleteEntity(req, res, next) {
  235. const projectId = req.params.Project_id
  236. const entityId = req.params.entity_id
  237. const entityType = req.params.entity_type
  238. const userId = SessionManager.getLoggedInUserId(req.session)
  239. await EditorController.promises.deleteEntity(
  240. projectId,
  241. entityId,
  242. entityType,
  243. 'editor',
  244. userId
  245. )
  246. res.sendStatus(204)
  247. }
  248. const supportedSpellCheckLanguages = new Set(
  249. Settings.languages
  250. // only include spell-check languages that are available in the client
  251. .filter(language => language.dic !== undefined)
  252. .map(language => language.code)
  253. )
  254. async function chooseSpellCheckLanguage(spellCheckLanguage) {
  255. if (supportedSpellCheckLanguages.has(spellCheckLanguage)) {
  256. return spellCheckLanguage
  257. }
  258. // Preserve the value in the database so they can use it again once we add back support.
  259. // Map some server-only languages to a specific variant, or disable spell checking for currently unsupported spell check languages.
  260. switch (spellCheckLanguage) {
  261. case 'en':
  262. // map "English" to "English (American)"
  263. return 'en_US'
  264. case 'no':
  265. // map "Norwegian" to "Norwegian (Bokmål)"
  266. return 'nb_NO'
  267. default:
  268. // map anything else to "off"
  269. return ''
  270. }
  271. }