ProjectDetailsHandler.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. const _ = require('underscore')
  2. const ProjectGetter = require('./ProjectGetter')
  3. const UserGetter = require('../User/UserGetter')
  4. const { Project } = require('../../models/Project')
  5. const logger = require('@overleaf/logger')
  6. const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
  7. const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
  8. const Errors = require('../Errors/Errors')
  9. const TokenGenerator = require('../TokenGenerator/TokenGenerator')
  10. const ProjectHelper = require('./ProjectHelper')
  11. const settings = require('@overleaf/settings')
  12. const { callbackify } = require('util')
  13. const MAX_PROJECT_NAME_LENGTH = 150
  14. module.exports = {
  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. }
  135. // FIXME: we should put a lock around this to make it completely safe, but we would need to do that at
  136. // the point of project creation, rather than just checking the name at the start of the import.
  137. // If we later move this check into ProjectCreationHandler we can ensure all new projects are created
  138. // with a unique name. But that requires thinking through how we would handle incoming projects from
  139. // dropbox for example.
  140. async function generateUniqueName(userId, name, suffixes = []) {
  141. const allUsersProjectNames =
  142. await ProjectGetter.promises.findAllUsersProjects(userId, { name: 1 })
  143. // allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]}
  144. // collect all of the names and flatten them into a single array
  145. const projectNameList = _.pluck(
  146. _.flatten(_.values(allUsersProjectNames)),
  147. 'name'
  148. )
  149. const uniqueName = await ProjectHelper.promises.ensureNameIsUnique(
  150. projectNameList,
  151. name,
  152. suffixes,
  153. MAX_PROJECT_NAME_LENGTH
  154. )
  155. return uniqueName
  156. }
  157. function fixProjectName(name) {
  158. if (name === '' || !name) {
  159. name = 'Untitled'
  160. }
  161. if (name.indexOf('/') > -1) {
  162. // v2 does not allow / in a project name
  163. name = name.replace(/\//g, '-')
  164. }
  165. if (name.indexOf('\\') > -1) {
  166. // backslashes in project name will prevent syncing to dropbox
  167. name = name.replace(/\\/g, '')
  168. }
  169. if (name.length > MAX_PROJECT_NAME_LENGTH) {
  170. name = name.substr(0, MAX_PROJECT_NAME_LENGTH)
  171. }
  172. return name
  173. }
  174. async function setPublicAccessLevel(projectId, newAccessLevel) {
  175. // DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer
  176. // be passed here. Remove after token-based access has been live for a while
  177. if (
  178. projectId != null &&
  179. newAccessLevel != null &&
  180. _.include(
  181. [
  182. PublicAccessLevels.READ_ONLY,
  183. PublicAccessLevels.READ_AND_WRITE,
  184. PublicAccessLevels.PRIVATE,
  185. PublicAccessLevels.TOKEN_BASED,
  186. ],
  187. newAccessLevel
  188. )
  189. ) {
  190. await Project.updateOne(
  191. { _id: projectId },
  192. { publicAccesLevel: newAccessLevel }
  193. ).exec()
  194. }
  195. }
  196. async function ensureTokensArePresent(projectId) {
  197. const project = await ProjectGetter.promises.getProject(projectId, {
  198. tokens: 1,
  199. })
  200. if (
  201. project.tokens != null &&
  202. project.tokens.readOnly != null &&
  203. project.tokens.readAndWrite != null
  204. ) {
  205. return project.tokens
  206. }
  207. await _generateTokens(project)
  208. await Project.updateOne(
  209. { _id: projectId },
  210. { $set: { tokens: project.tokens } }
  211. ).exec()
  212. return project.tokens
  213. }
  214. async function clearTokens(projectId) {
  215. await Project.updateOne(
  216. { _id: projectId },
  217. { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
  218. ).exec()
  219. }
  220. async function _generateTokens(project, callback) {
  221. if (!project.tokens) {
  222. project.tokens = {}
  223. }
  224. const { tokens } = project
  225. if (tokens.readAndWrite == null) {
  226. const { token, numericPrefix } = TokenGenerator.readAndWriteToken()
  227. tokens.readAndWrite = token
  228. tokens.readAndWritePrefix = numericPrefix
  229. }
  230. if (tokens.readOnly == null) {
  231. tokens.readOnly =
  232. await TokenGenerator.promises.generateUniqueReadOnlyToken()
  233. }
  234. }