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('logger-sharelatex')
  6. const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
  7. const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
  8. const Errors = require('../Errors/Errors')
  9. const ProjectTokenGenerator = require('./ProjectTokenGenerator')
  10. const ProjectHelper = require('./ProjectHelper')
  11. const settings = require('settings-sharelatex')
  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.update(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.update({ _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 = await ProjectGetter.promises.findAllUsersProjects(
  141. userId,
  142. { name: 1 }
  143. )
  144. // allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]}
  145. // collect all of the names and flatten them into a single array
  146. const projectNameList = _.pluck(
  147. _.flatten(_.values(allUsersProjectNames)),
  148. 'name'
  149. )
  150. const uniqueName = await ProjectHelper.promises.ensureNameIsUnique(
  151. projectNameList,
  152. name,
  153. suffixes,
  154. MAX_PROJECT_NAME_LENGTH
  155. )
  156. return uniqueName
  157. }
  158. function fixProjectName(name) {
  159. if (name === '' || !name) {
  160. name = 'Untitled'
  161. }
  162. if (name.indexOf('/') > -1) {
  163. // v2 does not allow / in a project name
  164. name = name.replace(/\//g, '-')
  165. }
  166. if (name.indexOf('\\') > -1) {
  167. // backslashes in project name will prevent syncing to dropbox
  168. name = name.replace(/\\/g, '')
  169. }
  170. if (name.length > MAX_PROJECT_NAME_LENGTH) {
  171. name = name.substr(0, MAX_PROJECT_NAME_LENGTH)
  172. }
  173. return name
  174. }
  175. async function setPublicAccessLevel(projectId, newAccessLevel) {
  176. // DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer
  177. // be passed here. Remove after token-based access has been live for a while
  178. if (
  179. projectId != null &&
  180. newAccessLevel != null &&
  181. _.include(
  182. [
  183. PublicAccessLevels.READ_ONLY,
  184. PublicAccessLevels.READ_AND_WRITE,
  185. PublicAccessLevels.PRIVATE,
  186. PublicAccessLevels.TOKEN_BASED
  187. ],
  188. newAccessLevel
  189. )
  190. ) {
  191. await Project.update(
  192. { _id: projectId },
  193. { publicAccesLevel: newAccessLevel }
  194. ).exec()
  195. }
  196. }
  197. async function ensureTokensArePresent(projectId) {
  198. const project = await ProjectGetter.promises.getProject(projectId, {
  199. tokens: 1
  200. })
  201. if (
  202. project.tokens != null &&
  203. project.tokens.readOnly != null &&
  204. project.tokens.readAndWrite != null
  205. ) {
  206. return project.tokens
  207. }
  208. await _generateTokens(project)
  209. await Project.update(
  210. { _id: projectId },
  211. { $set: { tokens: project.tokens } }
  212. ).exec()
  213. return project.tokens
  214. }
  215. async function clearTokens(projectId) {
  216. await Project.update(
  217. { _id: projectId },
  218. { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
  219. ).exec()
  220. }
  221. async function _generateTokens(project, callback) {
  222. if (!project.tokens) {
  223. project.tokens = {}
  224. }
  225. const { tokens } = project
  226. if (tokens.readAndWrite == null) {
  227. const { token, numericPrefix } = ProjectTokenGenerator.readAndWriteToken()
  228. tokens.readAndWrite = token
  229. tokens.readAndWritePrefix = numericPrefix
  230. }
  231. if (tokens.readOnly == null) {
  232. tokens.readOnly = await ProjectTokenGenerator.promises.generateUniqueReadOnlyToken()
  233. }
  234. }