ProjectDetailsHandler.coffee 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. ProjectGetter = require("./ProjectGetter")
  2. UserGetter = require("../User/UserGetter")
  3. Project = require('../../models/Project').Project
  4. ObjectId = require("mongojs").ObjectId
  5. logger = require("logger-sharelatex")
  6. tpdsUpdateSender = require '../ThirdPartyDataStore/TpdsUpdateSender'
  7. _ = require("underscore")
  8. PublicAccessLevels = require("../Authorization/PublicAccessLevels")
  9. Errors = require("../Errors/Errors")
  10. ProjectTokenGenerator = require('./ProjectTokenGenerator')
  11. module.exports = ProjectDetailsHandler =
  12. getDetails: (project_id, callback)->
  13. ProjectGetter.getProject project_id, {name:true, description:true, compiler:true, features:true, owner_ref:true, overleaf:true}, (err, project)->
  14. if err?
  15. logger.err err:err, project_id:project_id, "error getting project"
  16. return callback(err)
  17. return callback(new Errors.NotFoundError("project not found")) if !project?
  18. UserGetter.getUser project.owner_ref, (err, user) ->
  19. return callback(err) if err?
  20. details =
  21. name : project.name
  22. description: project.description
  23. compiler: project.compiler
  24. features: user.features
  25. if project.overleaf?
  26. details.overleaf = project.overleaf
  27. logger.log project_id:project_id, details: details, "getting project details"
  28. callback(err, details)
  29. getProjectDescription: (project_id, callback)->
  30. ProjectGetter.getProject project_id, description: true, (err, project)->
  31. callback(err, project?.description)
  32. setProjectDescription: (project_id, description, callback)->
  33. conditions = _id:project_id
  34. update = description:description
  35. logger.log conditions:conditions, update:update, project_id:project_id, description:description, "setting project description"
  36. Project.update conditions, update, (err)->
  37. if err?
  38. logger.err err:err, "something went wrong setting project description"
  39. callback(err)
  40. renameProject: (project_id, newName, callback = ->)->
  41. ProjectDetailsHandler.validateProjectName newName, (error) ->
  42. return callback(error) if error?
  43. logger.log project_id: project_id, newName:newName, "renaming project"
  44. ProjectGetter.getProject project_id, {name:true}, (err, project)->
  45. if err? or !project?
  46. logger.err err:err, project_id:project_id, "error getting project or could not find it todo project rename"
  47. return callback(err)
  48. oldProjectName = project.name
  49. Project.update _id:project_id, {name: newName}, (err, project)=>
  50. if err?
  51. return callback(err)
  52. tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}, callback
  53. MAX_PROJECT_NAME_LENGTH: 150
  54. validateProjectName: (name, callback = (error) ->) ->
  55. if !name? or name.length == 0
  56. return callback(new Errors.InvalidNameError("Project name cannot be blank"))
  57. else if name.length > @MAX_PROJECT_NAME_LENGTH
  58. return callback(new Errors.InvalidNameError("Project name is too long"))
  59. else if name.indexOf("/") > -1
  60. return callback(new Errors.InvalidNameError("Project name cannot contain / characters"))
  61. else if name.indexOf("\\") > -1
  62. return callback(new Errors.InvalidNameError("Project name cannot contain \\ characters"))
  63. else
  64. return callback()
  65. generateUniqueName: (user_id, name, suffixes = [], callback = (error, newName) -> ) ->
  66. if arguments.length is 3 && typeof suffixes is 'function' # make suffixes an optional argument
  67. callback = suffixes
  68. suffixes = []
  69. ProjectDetailsHandler.ensureProjectNameIsUnique user_id, name, suffixes, callback
  70. # FIXME: we should put a lock around this to make it completely safe, but we would need to do that at
  71. # the point of project creation, rather than just checking the name at the start of the import.
  72. # If we later move this check into ProjectCreationHandler we can ensure all new projects are created
  73. # with a unique name. But that requires thinking through how we would handle incoming projects from
  74. # dropbox for example.
  75. ensureProjectNameIsUnique: (user_id, name, suffixes = [], callback = (error, name, changed)->) ->
  76. ProjectGetter.findAllUsersProjects user_id, {name: 1}, (error, allUsersProjectNames) ->
  77. return callback(error) if error?
  78. # allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]}
  79. # collect all of the names and flatten them into a single array
  80. projectNameList = _.pluck(_.flatten(_.values(allUsersProjectNames)),'name')
  81. # create a set of all project names
  82. allProjectNames = new Set()
  83. for projectName in projectNameList
  84. allProjectNames.add(projectName)
  85. isUnique = (x) -> !allProjectNames.has(x)
  86. # check if the supplied name is already unique
  87. if isUnique(name)
  88. return callback(null, name, false)
  89. # the name already exists, try adding the user-supplied suffixes to generate a unique name
  90. for suffix in suffixes
  91. candidateName = ProjectDetailsHandler._addSuffixToProjectName(name, suffix)
  92. if isUnique(candidateName)
  93. return callback(null, candidateName, true)
  94. # if there are no (more) suffixes, use a numeric one
  95. uniqueName = ProjectDetailsHandler._addNumericSuffixToProjectName(name, allProjectNames)
  96. if uniqueName?
  97. callback(null, uniqueName, true)
  98. else
  99. callback(new Error("Failed to generate a unique name for file: #{name}"))
  100. fixProjectName: (name) ->
  101. if name == "" || !name
  102. name = "Untitled"
  103. if name.indexOf('/') > -1
  104. # v2 does not allow / in a project name
  105. name = name.replace(/\//g, '-')
  106. if name.indexOf('\\') > -1
  107. # backslashes in project name will prevent syncing to dropbox
  108. name = name.replace(/\\/g, '')
  109. if name.length > @MAX_PROJECT_NAME_LENGTH
  110. name = name.substr(0, @MAX_PROJECT_NAME_LENGTH)
  111. return name
  112. setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)->
  113. logger.log project_id: project_id, level: newAccessLevel, "set public access level"
  114. # DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer
  115. # be passed here. Remove after token-based access has been live for a while
  116. if project_id? && newAccessLevel? and _.include [
  117. PublicAccessLevels.READ_ONLY,
  118. PublicAccessLevels.READ_AND_WRITE,
  119. PublicAccessLevels.PRIVATE,
  120. PublicAccessLevels.TOKEN_BASED
  121. ], newAccessLevel
  122. Project.update {_id:project_id},{publicAccesLevel:newAccessLevel}, (err)->
  123. callback(err)
  124. ensureTokensArePresent: (project_id, callback=(err, tokens)->) ->
  125. ProjectGetter.getProject project_id, {tokens: 1}, (err, project) ->
  126. return callback(err) if err?
  127. if project.tokens? and project.tokens.readOnly? and project.tokens.readAndWrite?
  128. logger.log {project_id}, "project already has tokens"
  129. return callback(null, project.tokens)
  130. else
  131. logger.log {
  132. project_id,
  133. has_tokens: project.tokens?,
  134. has_readOnly: project?.tokens?.readOnly?,
  135. has_readAndWrite: project?.tokens?.readAndWrite?
  136. }, "generating tokens for project"
  137. tokens = project.tokens || {}
  138. if !tokens.readOnly?
  139. tokens.readOnly = ProjectTokenGenerator.readOnlyToken()
  140. if !tokens.readAndWrite?
  141. tokens.readAndWrite = ProjectTokenGenerator.readAndWriteToken()
  142. Project.update {_id: project_id}, {$set: {tokens: tokens}}, (err) ->
  143. return callback(err) if err?
  144. callback(null, tokens)
  145. _addSuffixToProjectName: (name, suffix = '') ->
  146. # append the suffix and truncate the project title if needed
  147. truncatedLength = ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH - suffix.length
  148. return name.substr(0, truncatedLength) + suffix
  149. _addNumericSuffixToProjectName: (name, allProjectNames) ->
  150. NUMERIC_SUFFIX_MATCH = / \((\d+)\)$/
  151. suffixedName = (basename, number) ->
  152. suffix = " (#{number})"
  153. return basename.substr(0, ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH - suffix.length) + suffix
  154. match = name.match(NUMERIC_SUFFIX_MATCH)
  155. basename = name
  156. n = 1
  157. last = allProjectNames.size + n
  158. if match?
  159. basename = name.replace(NUMERIC_SUFFIX_MATCH, '')
  160. n = parseInt(match[1])
  161. while n <= last
  162. candidate = suffixedName(basename, n)
  163. return candidate unless allProjectNames.has(candidate)
  164. n += 1
  165. return null