ProjectHelper.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // ts-check
  2. const { ObjectId } = require('mongodb-legacy')
  3. const _ = require('lodash')
  4. const { promisify } = require('util')
  5. const Settings = require('@overleaf/settings')
  6. /**
  7. * @import { MongoProject } from "./types"
  8. */
  9. const ENGINE_TO_COMPILER_MAP = {
  10. latex_dvipdf: 'latex',
  11. pdflatex: 'pdflatex',
  12. xelatex: 'xelatex',
  13. lualatex: 'lualatex',
  14. }
  15. module.exports = {
  16. compilerFromV1Engine,
  17. isArchived,
  18. isTrashed,
  19. isArchivedOrTrashed,
  20. calculateArchivedArray,
  21. ensureNameIsUnique,
  22. getAllowedImagesForUser,
  23. promises: {
  24. ensureNameIsUnique: promisify(ensureNameIsUnique),
  25. },
  26. }
  27. function compilerFromV1Engine(engine) {
  28. return ENGINE_TO_COMPILER_MAP[engine]
  29. }
  30. /**
  31. @param {MongoProject} project
  32. @param {string} userId
  33. * @returns {boolean}
  34. */
  35. function isArchived(project, userId) {
  36. userId = new ObjectId(userId)
  37. return (project.archived || []).some(id => id.equals(userId))
  38. }
  39. /**
  40. * @param {MongoProject} project
  41. * @param {string} userId
  42. * @returns {boolean}
  43. */
  44. function isTrashed(project, userId) {
  45. userId = new ObjectId(userId)
  46. return (project.trashed || []).some(id => id.equals(userId))
  47. }
  48. /**
  49. * @param {MongoProject} project
  50. * @param {string} userId
  51. * @returns {boolean}
  52. */
  53. function isArchivedOrTrashed(project, userId) {
  54. return isArchived(project, userId) || isTrashed(project, userId)
  55. }
  56. function _allCollaborators(project) {
  57. return _.unionWith(
  58. [project.owner_ref],
  59. project.collaberator_refs,
  60. project.readOnly_refs,
  61. project.tokenAccessReadAndWrite_refs,
  62. project.tokenAccessReadOnly_refs,
  63. _objectIdEquals
  64. )
  65. }
  66. function calculateArchivedArray(project, userId, action) {
  67. let archived = project.archived
  68. userId = new ObjectId(userId)
  69. if (archived === true) {
  70. archived = _allCollaborators(project)
  71. } else if (!archived) {
  72. archived = []
  73. }
  74. if (action === 'ARCHIVE') {
  75. archived = _.unionWith(archived, [userId], _objectIdEquals)
  76. } else if (action === 'UNARCHIVE') {
  77. archived = archived.filter(id => !_objectIdEquals(id, userId))
  78. } else {
  79. throw new Error('Unrecognised action')
  80. }
  81. return archived
  82. }
  83. function ensureNameIsUnique(nameList, name, suffixes, maxLength, callback) {
  84. // create a set of all project names
  85. if (suffixes == null) {
  86. suffixes = []
  87. }
  88. const allNames = new Set(nameList)
  89. const isUnique = x => !allNames.has(x)
  90. // check if the supplied name is already unique
  91. if (isUnique(name)) {
  92. return callback(null, name)
  93. }
  94. // the name already exists, try adding the user-supplied suffixes to generate a unique name
  95. for (const suffix of suffixes) {
  96. const candidateName = _addSuffixToProjectName(name, suffix, maxLength)
  97. if (isUnique(candidateName)) {
  98. return callback(null, candidateName)
  99. }
  100. }
  101. // if there are no (more) suffixes, use a numeric one
  102. const uniqueName = _addNumericSuffixToProjectName(name, allNames, maxLength)
  103. if (uniqueName != null) {
  104. callback(null, uniqueName)
  105. } else {
  106. callback(new Error(`Failed to generate a unique name for: ${name}`))
  107. }
  108. }
  109. function _objectIdEquals(firstVal, secondVal) {
  110. // For use as a comparator for unionWith
  111. return firstVal.toString() === secondVal.toString()
  112. }
  113. function _addSuffixToProjectName(name, suffix, maxLength) {
  114. // append the suffix and truncate the project title if needed
  115. if (suffix == null) {
  116. suffix = ''
  117. }
  118. const truncatedLength = maxLength - suffix.length
  119. return name.substr(0, truncatedLength) + suffix
  120. }
  121. function _addNumericSuffixToProjectName(name, allProjectNames, maxLength) {
  122. const NUMERIC_SUFFIX_MATCH = / \((\d+)\)$/
  123. const suffixedName = function (basename, number) {
  124. const suffix = ` (${number})`
  125. return basename.substr(0, maxLength - suffix.length) + suffix
  126. }
  127. const match = name.match(NUMERIC_SUFFIX_MATCH)
  128. let basename = name
  129. let n = 1
  130. if (match != null) {
  131. basename = name.replace(NUMERIC_SUFFIX_MATCH, '')
  132. n = parseInt(match[1])
  133. }
  134. const prefixMatcher = new RegExp(`^${_.escapeRegExp(basename)} \\(\\d+\\)$`)
  135. const projectNamesWithSamePrefix = Array.from(allProjectNames).filter(name =>
  136. prefixMatcher.test(name)
  137. )
  138. const last = allProjectNames.size + n
  139. const nIsLikelyAYear = n > 1000 && projectNamesWithSamePrefix.length < n / 2
  140. if (nIsLikelyAYear) {
  141. basename = name
  142. n = 1
  143. }
  144. while (n <= last) {
  145. const candidate = suffixedName(basename, n)
  146. if (!allProjectNames.has(candidate)) {
  147. return candidate
  148. }
  149. n += 1
  150. }
  151. return null
  152. }
  153. function getAllowedImagesForUser(user) {
  154. const images = Settings.allowedImageNames || []
  155. if (user?.alphaProgram) {
  156. return images
  157. } else {
  158. return images.filter(image => !image.alphaOnly)
  159. }
  160. }