ProjectDetailsHandler.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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.log(
  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. await validateProjectName(newName)
  96. logger.log({ projectId, newName }, 'renaming project')
  97. let project
  98. try {
  99. project = await ProjectGetter.promises.getProject(projectId, { name: true })
  100. } catch (err) {
  101. logger.warn({ err, projectId }, 'error getting project')
  102. throw err
  103. }
  104. if (project == null) {
  105. logger.warn({ projectId }, 'could not find project to rename')
  106. return
  107. }
  108. const oldProjectName = project.name
  109. await Project.updateOne({ _id: projectId }, { name: newName }).exec()
  110. await TpdsUpdateSender.promises.moveEntity({
  111. project_id: projectId,
  112. project_name: oldProjectName,
  113. newProjectName: newName,
  114. })
  115. }
  116. async function validateProjectName(name) {
  117. if (name == null || name.length === 0) {
  118. throw new Errors.InvalidNameError('Project name cannot be blank')
  119. }
  120. if (name.length > MAX_PROJECT_NAME_LENGTH) {
  121. throw new Errors.InvalidNameError('Project name is too long')
  122. }
  123. if (name.indexOf('/') > -1) {
  124. throw new Errors.InvalidNameError(
  125. 'Project name cannot contain / characters'
  126. )
  127. }
  128. if (name.indexOf('\\') > -1) {
  129. throw new Errors.InvalidNameError(
  130. 'Project name cannot contain \\ characters'
  131. )
  132. }
  133. }
  134. // FIXME: we should put a lock around this to make it completely safe, but we would need to do that at
  135. // the point of project creation, rather than just checking the name at the start of the import.
  136. // If we later move this check into ProjectCreationHandler we can ensure all new projects are created
  137. // with a unique name. But that requires thinking through how we would handle incoming projects from
  138. // dropbox for example.
  139. async function generateUniqueName(userId, name, suffixes = []) {
  140. const allUsersProjectNames =
  141. await ProjectGetter.promises.findAllUsersProjects(userId, { name: 1 })
  142. // allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]}
  143. // collect all of the names and flatten them into a single array
  144. const projectNameList = _.pluck(
  145. _.flatten(_.values(allUsersProjectNames)),
  146. 'name'
  147. )
  148. const uniqueName = await ProjectHelper.promises.ensureNameIsUnique(
  149. projectNameList,
  150. name,
  151. suffixes,
  152. MAX_PROJECT_NAME_LENGTH
  153. )
  154. return uniqueName
  155. }
  156. function fixProjectName(name) {
  157. if (name === '' || !name) {
  158. name = 'Untitled'
  159. }
  160. if (name.indexOf('/') > -1) {
  161. // v2 does not allow / in a project name
  162. name = name.replace(/\//g, '-')
  163. }
  164. if (name.indexOf('\\') > -1) {
  165. // backslashes in project name will prevent syncing to dropbox
  166. name = name.replace(/\\/g, '')
  167. }
  168. if (name.length > MAX_PROJECT_NAME_LENGTH) {
  169. name = name.substr(0, MAX_PROJECT_NAME_LENGTH)
  170. }
  171. return name
  172. }
  173. async function setPublicAccessLevel(projectId, newAccessLevel) {
  174. // DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer
  175. // be passed here. Remove after token-based access has been live for a while
  176. if (
  177. projectId != null &&
  178. newAccessLevel != null &&
  179. _.include(
  180. [
  181. PublicAccessLevels.READ_ONLY,
  182. PublicAccessLevels.READ_AND_WRITE,
  183. PublicAccessLevels.PRIVATE,
  184. PublicAccessLevels.TOKEN_BASED,
  185. ],
  186. newAccessLevel
  187. )
  188. ) {
  189. await Project.updateOne(
  190. { _id: projectId },
  191. { publicAccesLevel: newAccessLevel }
  192. ).exec()
  193. }
  194. }
  195. async function ensureTokensArePresent(projectId) {
  196. const project = await ProjectGetter.promises.getProject(projectId, {
  197. tokens: 1,
  198. })
  199. if (
  200. project.tokens != null &&
  201. project.tokens.readOnly != null &&
  202. project.tokens.readAndWrite != null
  203. ) {
  204. return project.tokens
  205. }
  206. await _generateTokens(project)
  207. await Project.updateOne(
  208. { _id: projectId },
  209. { $set: { tokens: project.tokens } }
  210. ).exec()
  211. return project.tokens
  212. }
  213. async function clearTokens(projectId) {
  214. await Project.updateOne(
  215. { _id: projectId },
  216. { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
  217. ).exec()
  218. }
  219. async function _generateTokens(project, callback) {
  220. if (!project.tokens) {
  221. project.tokens = {}
  222. }
  223. const { tokens } = project
  224. if (tokens.readAndWrite == null) {
  225. const { token, numericPrefix } = TokenGenerator.readAndWriteToken()
  226. tokens.readAndWrite = token
  227. tokens.readAndWritePrefix = numericPrefix
  228. }
  229. if (tokens.readOnly == null) {
  230. tokens.readOnly =
  231. await TokenGenerator.promises.generateUniqueReadOnlyToken()
  232. }
  233. }