ProjectDetailsHandler.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. max-len,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS103: Rewrite code to no longer use __guard__
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. const ProjectGetter = require('./ProjectGetter')
  17. const UserGetter = require('../User/UserGetter')
  18. const { Project } = require('../../models/Project')
  19. const { ObjectId } = require('mongojs')
  20. const logger = require('logger-sharelatex')
  21. const tpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
  22. const _ = require('underscore')
  23. const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
  24. const Errors = require('../Errors/Errors')
  25. const ProjectTokenGenerator = require('./ProjectTokenGenerator')
  26. const ProjectEntityHandler = require('./ProjectEntityHandler')
  27. const ProjectHelper = require('./ProjectHelper')
  28. const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
  29. const settings = require('settings-sharelatex')
  30. const { promisify } = require('util')
  31. const ProjectDetailsHandler = {
  32. getDetails(project_id, callback) {
  33. return ProjectGetter.getProject(
  34. project_id,
  35. {
  36. name: true,
  37. description: true,
  38. compiler: true,
  39. features: true,
  40. owner_ref: true,
  41. overleaf: true
  42. },
  43. function(err, project) {
  44. if (err != null) {
  45. logger.err({ err, project_id }, 'error getting project')
  46. return callback(err)
  47. }
  48. if (project == null) {
  49. return callback(new Errors.NotFoundError('project not found'))
  50. }
  51. return UserGetter.getUser(project.owner_ref, function(err, user) {
  52. if (err != null) {
  53. return callback(err)
  54. }
  55. const details = {
  56. name: project.name,
  57. description: project.description,
  58. compiler: project.compiler,
  59. features:
  60. (user != null ? user.features : undefined) ||
  61. settings.defaultFeatures
  62. }
  63. if (project.overleaf != null) {
  64. details.overleaf = project.overleaf
  65. }
  66. logger.log({ project_id, details }, 'getting project details')
  67. return callback(err, details)
  68. })
  69. }
  70. )
  71. },
  72. getProjectDescription(project_id, callback) {
  73. return ProjectGetter.getProject(
  74. project_id,
  75. { description: true },
  76. (err, project) =>
  77. callback(err, project != null ? project.description : undefined)
  78. )
  79. },
  80. setProjectDescription(project_id, description, callback) {
  81. const conditions = { _id: project_id }
  82. const update = { description }
  83. logger.log(
  84. { conditions, update, project_id, description },
  85. 'setting project description'
  86. )
  87. return Project.update(conditions, update, function(err) {
  88. if (err != null) {
  89. logger.err({ err }, 'something went wrong setting project description')
  90. }
  91. return callback(err)
  92. })
  93. },
  94. transferOwnership(project_id, user_id, suffix, callback) {
  95. if (suffix == null) {
  96. suffix = ''
  97. }
  98. if (typeof suffix === 'function') {
  99. callback = suffix
  100. suffix = ''
  101. }
  102. return ProjectGetter.getProject(
  103. project_id,
  104. { owner_ref: true, name: true },
  105. function(err, project) {
  106. if (err != null) {
  107. return callback(err)
  108. }
  109. if (project == null) {
  110. return callback(new Errors.NotFoundError('project not found'))
  111. }
  112. if (project.owner_ref === user_id) {
  113. return callback()
  114. }
  115. return UserGetter.getUser(user_id, function(err, user) {
  116. if (err != null) {
  117. return callback(err)
  118. }
  119. if (user == null) {
  120. return callback(new Errors.NotFoundError('user not found'))
  121. }
  122. // we make sure the user to which the project is transferred is not a collaborator for the project,
  123. // this prevents any conflict during unique name generation
  124. return CollaboratorsHandler.removeUserFromProject(
  125. project_id,
  126. user_id,
  127. function(err) {
  128. if (err != null) {
  129. return callback(err)
  130. }
  131. return ProjectDetailsHandler.generateUniqueName(
  132. user_id,
  133. project.name + suffix,
  134. function(err, name) {
  135. if (err != null) {
  136. return callback(err)
  137. }
  138. return Project.update(
  139. { _id: project_id },
  140. {
  141. $set: {
  142. owner_ref: user_id,
  143. name
  144. }
  145. },
  146. function(err) {
  147. if (err != null) {
  148. return callback(err)
  149. }
  150. return ProjectEntityHandler.flushProjectToThirdPartyDataStore(
  151. project_id,
  152. callback
  153. )
  154. }
  155. )
  156. }
  157. )
  158. }
  159. )
  160. })
  161. }
  162. )
  163. },
  164. renameProject(project_id, newName, callback) {
  165. if (callback == null) {
  166. callback = function() {}
  167. }
  168. return ProjectDetailsHandler.validateProjectName(newName, function(error) {
  169. if (error != null) {
  170. return callback(error)
  171. }
  172. logger.log({ project_id, newName }, 'renaming project')
  173. return ProjectGetter.getProject(project_id, { name: true }, function(
  174. err,
  175. project
  176. ) {
  177. if (err != null || project == null) {
  178. logger.err(
  179. { err, project_id },
  180. 'error getting project or could not find it todo project rename'
  181. )
  182. return callback(err)
  183. }
  184. const oldProjectName = project.name
  185. return Project.update(
  186. { _id: project_id },
  187. { name: newName },
  188. (err, project) => {
  189. if (err != null) {
  190. return callback(err)
  191. }
  192. return tpdsUpdateSender.moveEntity(
  193. {
  194. project_id,
  195. project_name: oldProjectName,
  196. newProjectName: newName
  197. },
  198. callback
  199. )
  200. }
  201. )
  202. })
  203. })
  204. },
  205. MAX_PROJECT_NAME_LENGTH: 150,
  206. validateProjectName(name, callback) {
  207. if (callback == null) {
  208. callback = function(error) {}
  209. }
  210. if (name == null || name.length === 0) {
  211. return callback(
  212. new Errors.InvalidNameError('Project name cannot be blank')
  213. )
  214. } else if (name.length > ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH) {
  215. return callback(new Errors.InvalidNameError('Project name is too long'))
  216. } else if (name.indexOf('/') > -1) {
  217. return callback(
  218. new Errors.InvalidNameError('Project name cannot contain / characters')
  219. )
  220. } else if (name.indexOf('\\') > -1) {
  221. return callback(
  222. new Errors.InvalidNameError('Project name cannot contain \\ characters')
  223. )
  224. } else {
  225. return callback()
  226. }
  227. },
  228. generateUniqueName(user_id, name, suffixes, callback) {
  229. if (suffixes == null) {
  230. suffixes = []
  231. }
  232. if (callback == null) {
  233. callback = function(error, newName) {}
  234. }
  235. if (arguments.length === 3 && typeof suffixes === 'function') {
  236. // make suffixes an optional argument
  237. callback = suffixes
  238. suffixes = []
  239. }
  240. return ProjectDetailsHandler.ensureProjectNameIsUnique(
  241. user_id,
  242. name,
  243. suffixes,
  244. callback
  245. )
  246. },
  247. // FIXME: we should put a lock around this to make it completely safe, but we would need to do that at
  248. // the point of project creation, rather than just checking the name at the start of the import.
  249. // If we later move this check into ProjectCreationHandler we can ensure all new projects are created
  250. // with a unique name. But that requires thinking through how we would handle incoming projects from
  251. // dropbox for example.
  252. ensureProjectNameIsUnique(user_id, name, suffixes, callback) {
  253. if (suffixes == null) {
  254. suffixes = []
  255. }
  256. if (callback == null) {
  257. callback = function(error, name, changed) {}
  258. }
  259. return ProjectGetter.findAllUsersProjects(user_id, { name: 1 }, function(
  260. error,
  261. allUsersProjectNames
  262. ) {
  263. if (error != null) {
  264. return callback(error)
  265. }
  266. // allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]}
  267. // collect all of the names and flatten them into a single array
  268. const projectNameList = _.pluck(
  269. _.flatten(_.values(allUsersProjectNames)),
  270. 'name'
  271. )
  272. return ProjectHelper.ensureNameIsUnique(
  273. projectNameList,
  274. name,
  275. suffixes,
  276. ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH,
  277. callback
  278. )
  279. })
  280. },
  281. fixProjectName(name) {
  282. if (name === '' || !name) {
  283. name = 'Untitled'
  284. }
  285. if (name.indexOf('/') > -1) {
  286. // v2 does not allow / in a project name
  287. name = name.replace(/\//g, '-')
  288. }
  289. if (name.indexOf('\\') > -1) {
  290. // backslashes in project name will prevent syncing to dropbox
  291. name = name.replace(/\\/g, '')
  292. }
  293. if (name.length > ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH) {
  294. name = name.substr(0, ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH)
  295. }
  296. return name
  297. },
  298. setPublicAccessLevel(project_id, newAccessLevel, callback) {
  299. if (callback == null) {
  300. callback = function() {}
  301. }
  302. logger.log({ project_id, level: newAccessLevel }, 'set public access level')
  303. // DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer
  304. // be passed here. Remove after token-based access has been live for a while
  305. if (
  306. project_id != null &&
  307. newAccessLevel != null &&
  308. _.include(
  309. [
  310. PublicAccessLevels.READ_ONLY,
  311. PublicAccessLevels.READ_AND_WRITE,
  312. PublicAccessLevels.PRIVATE,
  313. PublicAccessLevels.TOKEN_BASED
  314. ],
  315. newAccessLevel
  316. )
  317. ) {
  318. return Project.update(
  319. { _id: project_id },
  320. { publicAccesLevel: newAccessLevel },
  321. err => callback(err)
  322. )
  323. }
  324. },
  325. ensureTokensArePresent(project_id, callback) {
  326. if (callback == null) {
  327. callback = function(err, tokens) {}
  328. }
  329. return ProjectGetter.getProject(project_id, { tokens: 1 }, function(
  330. err,
  331. project
  332. ) {
  333. if (err != null) {
  334. return callback(err)
  335. }
  336. if (
  337. project.tokens != null &&
  338. project.tokens.readOnly != null &&
  339. project.tokens.readAndWrite != null
  340. ) {
  341. logger.log({ project_id }, 'project already has tokens')
  342. return callback(null, project.tokens)
  343. } else {
  344. logger.log(
  345. {
  346. project_id,
  347. has_tokens: project.tokens != null,
  348. has_readOnly:
  349. __guard__(
  350. project != null ? project.tokens : undefined,
  351. x => x.readOnly
  352. ) != null,
  353. has_readAndWrite:
  354. __guard__(
  355. project != null ? project.tokens : undefined,
  356. x1 => x1.readAndWrite
  357. ) != null
  358. },
  359. 'generating tokens for project'
  360. )
  361. return ProjectDetailsHandler._generateTokens(project, function(err) {
  362. if (err != null) {
  363. return callback(err)
  364. }
  365. return Project.update(
  366. { _id: project_id },
  367. { $set: { tokens: project.tokens } },
  368. function(err) {
  369. if (err != null) {
  370. return callback(err)
  371. }
  372. return callback(null, project.tokens)
  373. }
  374. )
  375. })
  376. }
  377. })
  378. },
  379. _generateTokens(project, callback) {
  380. if (callback == null) {
  381. callback = function(err) {}
  382. }
  383. if (!project.tokens) {
  384. project.tokens = {}
  385. }
  386. const { tokens } = project
  387. if (tokens.readAndWrite == null) {
  388. const { token, numericPrefix } = ProjectTokenGenerator.readAndWriteToken()
  389. tokens.readAndWrite = token
  390. tokens.readAndWritePrefix = numericPrefix
  391. }
  392. if (tokens.readOnly == null) {
  393. return ProjectTokenGenerator.generateUniqueReadOnlyToken(function(
  394. err,
  395. token
  396. ) {
  397. if (err != null) {
  398. return callback(err)
  399. }
  400. tokens.readOnly = token
  401. return callback()
  402. })
  403. } else {
  404. return callback()
  405. }
  406. }
  407. }
  408. function __guard__(value, transform) {
  409. return typeof value !== 'undefined' && value !== null
  410. ? transform(value)
  411. : undefined
  412. }
  413. const promises = {
  414. generateUniqueName: promisify(ProjectDetailsHandler.generateUniqueName)
  415. }
  416. ProjectDetailsHandler.promises = promises
  417. module.exports = ProjectDetailsHandler