ProjectDetailsHandler.mjs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import _ from 'lodash'
  2. import ProjectGetter from './ProjectGetter.mjs'
  3. import UserGetter from '../User/UserGetter.mjs'
  4. import { Project } from '../../models/Project.js'
  5. import logger from '@overleaf/logger'
  6. import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.mjs'
  7. import PublicAccessLevels from '../Authorization/PublicAccessLevels.mjs'
  8. import Errors from '../Errors/Errors.js'
  9. import TokenGenerator from '../TokenGenerator/TokenGenerator.js'
  10. import ProjectHelper from './ProjectHelper.mjs'
  11. import settings from '@overleaf/settings'
  12. import { callbackify } from 'node:util'
  13. const MAX_PROJECT_NAME_LENGTH = 150
  14. export default {
  15. MAX_PROJECT_NAME_LENGTH,
  16. getDetails: callbackify(getDetails),
  17. getProjectDescription: callbackify(getProjectDescription),
  18. setProjectDescription: callbackify(setProjectDescription),
  19. renameProject: callbackify(renameProject),
  20. validateProjectName: callbackify(validateProjectName),
  21. generateUniqueName: callbackify(generateUniqueName),
  22. setPublicAccessLevel: callbackify(setPublicAccessLevel),
  23. ensureTokensArePresent: callbackify(ensureTokensArePresent),
  24. clearTokens: callbackify(clearTokens),
  25. fixProjectName,
  26. promises: {
  27. getDetails,
  28. getProjectDescription,
  29. setProjectDescription,
  30. renameProject,
  31. validateProjectName,
  32. generateUniqueName,
  33. setPublicAccessLevel,
  34. ensureTokensArePresent,
  35. clearTokens,
  36. },
  37. }
  38. async function getDetails(projectId) {
  39. let project
  40. try {
  41. project = await ProjectGetter.promises.getProject(projectId, {
  42. name: true,
  43. description: true,
  44. compiler: true,
  45. features: true,
  46. owner_ref: true,
  47. overleaf: true,
  48. })
  49. } catch (err) {
  50. logger.warn({ err, projectId }, 'error getting project')
  51. throw err
  52. }
  53. if (project == null) {
  54. throw new Errors.NotFoundError('project not found')
  55. }
  56. const user = await UserGetter.promises.getUser(project.owner_ref)
  57. const details = {
  58. name: project.name,
  59. description: project.description,
  60. compiler: project.compiler,
  61. features:
  62. user != null && user.features != null
  63. ? user.features
  64. : settings.defaultFeatures,
  65. }
  66. if (project.overleaf != null) {
  67. details.overleaf = project.overleaf
  68. }
  69. return details
  70. }
  71. async function getProjectDescription(projectId) {
  72. const project = await ProjectGetter.promises.getProject(projectId, {
  73. description: true,
  74. })
  75. if (project == null) {
  76. return undefined
  77. }
  78. return project.description
  79. }
  80. async function setProjectDescription(projectId, description) {
  81. const conditions = { _id: projectId }
  82. const update = { description }
  83. logger.debug(
  84. { conditions, update, projectId, description },
  85. 'setting project description'
  86. )
  87. try {
  88. await Project.updateOne(conditions, update).exec()
  89. } catch (err) {
  90. logger.warn({ err }, 'something went wrong setting project description')
  91. throw err
  92. }
  93. }
  94. async function renameProject(projectId, newName) {
  95. newName = newName.trim()
  96. await validateProjectName(newName)
  97. logger.debug({ projectId, newName }, 'renaming project')
  98. let project
  99. try {
  100. project = await ProjectGetter.promises.getProject(projectId, { name: true })
  101. } catch (err) {
  102. logger.warn({ err, projectId }, 'error getting project')
  103. throw err
  104. }
  105. if (project == null) {
  106. logger.warn({ projectId }, 'could not find project to rename')
  107. return
  108. }
  109. const oldProjectName = project.name
  110. await Project.updateOne({ _id: projectId }, { name: newName }).exec()
  111. await TpdsUpdateSender.promises.moveEntity({
  112. projectId,
  113. projectName: oldProjectName,
  114. newProjectName: newName,
  115. })
  116. }
  117. async function validateProjectName(name) {
  118. if (name == null || name.length === 0) {
  119. throw new Errors.InvalidNameError('Project name cannot be blank')
  120. }
  121. if (name.length > MAX_PROJECT_NAME_LENGTH) {
  122. throw new Errors.InvalidNameError('Project name is too long')
  123. }
  124. if (name.indexOf('/') > -1) {
  125. throw new Errors.InvalidNameError(
  126. 'Project name cannot contain / characters'
  127. )
  128. }
  129. if (name.indexOf('\\') > -1) {
  130. throw new Errors.InvalidNameError(
  131. 'Project name cannot contain \\ characters'
  132. )
  133. }
  134. if (name !== name.trim()) {
  135. throw new Errors.InvalidNameError(
  136. 'Project name cannot start or end with whitespace'
  137. )
  138. }
  139. }
  140. // FIXME: we should put a lock around this to make it completely safe, but we would need to do that at
  141. // the point of project creation, rather than just checking the name at the start of the import.
  142. // If we later move this check into ProjectCreationHandler we can ensure all new projects are created
  143. // with a unique name. But that requires thinking through how we would handle incoming projects from
  144. // dropbox for example.
  145. async function generateUniqueName(userId, name, suffixes = []) {
  146. const allUsersProjectNames =
  147. await ProjectGetter.promises.findAllUsersProjects(userId, { name: 1 })
  148. // allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]}
  149. // collect all of the names and flatten them into a single array
  150. const projectNameList = _.map(
  151. _.flattenDeep(_.values(allUsersProjectNames)),
  152. 'name'
  153. )
  154. const uniqueName = ProjectHelper.ensureNameIsUnique(
  155. projectNameList,
  156. name,
  157. suffixes,
  158. MAX_PROJECT_NAME_LENGTH
  159. )
  160. return uniqueName
  161. }
  162. function fixProjectName(name) {
  163. // Remove any leading or trailing whitespace
  164. name = typeof name === 'string' ? name.trim() : ''
  165. // Apply a default name if the name is empty
  166. if (name === '') {
  167. name = 'Untitled'
  168. }
  169. if (name.indexOf('/') > -1) {
  170. // v2 does not allow / in a project name
  171. name = name.replace(/\//g, '-')
  172. }
  173. if (name.indexOf('\\') > -1) {
  174. // backslashes in project name will prevent syncing to dropbox
  175. name = name.replace(/\\/g, '')
  176. }
  177. if (name.length > MAX_PROJECT_NAME_LENGTH) {
  178. name = name.substr(0, MAX_PROJECT_NAME_LENGTH)
  179. }
  180. // Remove any leading or trailing whitespace after fixing
  181. name = name.trim()
  182. return name
  183. }
  184. async function setPublicAccessLevel(projectId, newAccessLevel) {
  185. if (
  186. projectId != null &&
  187. newAccessLevel != null &&
  188. _.includes(
  189. [PublicAccessLevels.PRIVATE, PublicAccessLevels.TOKEN_BASED],
  190. newAccessLevel
  191. )
  192. ) {
  193. await Project.updateOne(
  194. { _id: projectId },
  195. { publicAccesLevel: newAccessLevel }
  196. ).exec()
  197. } else {
  198. throw new Errors.InvalidError('unexpected access level')
  199. }
  200. }
  201. async function ensureTokensArePresent(projectId) {
  202. const project = await ProjectGetter.promises.getProject(projectId, {
  203. tokens: 1,
  204. })
  205. if (
  206. project.tokens != null &&
  207. project.tokens.readOnly != null &&
  208. project.tokens.readAndWrite != null
  209. ) {
  210. return
  211. }
  212. await _generateTokens(project)
  213. await Project.updateOne(
  214. { _id: projectId },
  215. { $set: { tokens: project.tokens } }
  216. ).exec()
  217. }
  218. async function clearTokens(projectId) {
  219. await Project.updateOne(
  220. { _id: projectId },
  221. { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
  222. ).exec()
  223. }
  224. async function _generateTokens(project) {
  225. if (!project.tokens) {
  226. project.tokens = {}
  227. }
  228. const { tokens } = project
  229. if (tokens.readAndWrite == null) {
  230. const { token, numericPrefix } = TokenGenerator.readAndWriteToken()
  231. tokens.readAndWrite = token
  232. tokens.readAndWritePrefix = numericPrefix
  233. }
  234. if (tokens.readOnly == null) {
  235. tokens.readOnly =
  236. await TokenGenerator.promises.generateUniqueReadOnlyToken()
  237. }
  238. }