EditorHttpController.js 8.4 KB

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