EditorHttpController.mjs 7.6 KB

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