ProjectController.js 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114
  1. const Path = require('path')
  2. const fs = require('fs')
  3. const crypto = require('crypto')
  4. const async = require('async')
  5. const logger = require('logger-sharelatex')
  6. const ProjectDeleter = require('./ProjectDeleter')
  7. const ProjectDuplicator = require('./ProjectDuplicator')
  8. const ProjectCreationHandler = require('./ProjectCreationHandler')
  9. const EditorController = require('../Editor/EditorController')
  10. const ProjectHelper = require('./ProjectHelper')
  11. const metrics = require('metrics-sharelatex')
  12. const { User } = require('../../models/User')
  13. const TagsHandler = require('../Tags/TagsHandler')
  14. const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
  15. const NotificationsHandler = require('../Notifications/NotificationsHandler')
  16. const LimitationsManager = require('../Subscription/LimitationsManager')
  17. const Settings = require('settings-sharelatex')
  18. const AuthorizationManager = require('../Authorization/AuthorizationManager')
  19. const InactiveProjectManager = require('../InactiveData/InactiveProjectManager')
  20. const ProjectUpdateHandler = require('./ProjectUpdateHandler')
  21. const ProjectGetter = require('./ProjectGetter')
  22. const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
  23. const AuthenticationController = require('../Authentication/AuthenticationController')
  24. const PackageVersions = require('../../infrastructure/PackageVersions')
  25. const Sources = require('../Authorization/Sources')
  26. const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
  27. const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
  28. const Modules = require('../../infrastructure/Modules')
  29. const ProjectEntityHandler = require('./ProjectEntityHandler')
  30. const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
  31. const UserGetter = require('../User/UserGetter')
  32. const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
  33. const { V1ConnectionError } = require('../Errors/Errors')
  34. const Features = require('../../infrastructure/Features')
  35. const BrandVariationsHandler = require('../BrandVariations/BrandVariationsHandler')
  36. const { getUserAffiliations } = require('../Institutions/InstitutionsAPI')
  37. const V1Handler = require('../V1/V1Handler')
  38. const UserController = require('../User/UserController')
  39. const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
  40. if (!affiliation.institution) return false
  41. // Could have multiple emails at the same institution, and if any are
  42. // linked to the institution then do not show notification for others
  43. if (
  44. linkedInstitutionIds.indexOf(affiliation.institution.id.toString()) === -1
  45. ) {
  46. if (affiliation.institution.ssoEnabled) return true
  47. if (affiliation.institution.ssoBeta && session.samlBeta) return true
  48. return false
  49. }
  50. return false
  51. }
  52. const ProjectController = {
  53. _isInPercentageRollout(rolloutName, objectId, percentage) {
  54. if (Settings.bypassPercentageRollouts === true) {
  55. return true
  56. }
  57. const data = `${rolloutName}:${objectId.toString()}`
  58. const md5hash = crypto
  59. .createHash('md5')
  60. .update(data)
  61. .digest('hex')
  62. const counter = parseInt(md5hash.slice(26, 32), 16)
  63. return counter % 100 < percentage
  64. },
  65. updateProjectSettings(req, res, next) {
  66. const projectId = req.params.Project_id
  67. const jobs = []
  68. if (req.body.compiler != null) {
  69. jobs.push(callback =>
  70. EditorController.setCompiler(projectId, req.body.compiler, callback)
  71. )
  72. }
  73. if (req.body.imageName != null) {
  74. jobs.push(callback =>
  75. EditorController.setImageName(projectId, req.body.imageName, callback)
  76. )
  77. }
  78. if (req.body.name != null) {
  79. jobs.push(callback =>
  80. EditorController.renameProject(projectId, req.body.name, callback)
  81. )
  82. }
  83. if (req.body.spellCheckLanguage != null) {
  84. jobs.push(callback =>
  85. EditorController.setSpellCheckLanguage(
  86. projectId,
  87. req.body.spellCheckLanguage,
  88. callback
  89. )
  90. )
  91. }
  92. if (req.body.rootDocId != null) {
  93. jobs.push(callback =>
  94. EditorController.setRootDoc(projectId, req.body.rootDocId, callback)
  95. )
  96. }
  97. async.series(jobs, error => {
  98. if (error != null) {
  99. return next(error)
  100. }
  101. res.sendStatus(204)
  102. })
  103. },
  104. updateProjectAdminSettings(req, res, next) {
  105. const projectId = req.params.Project_id
  106. const jobs = []
  107. if (req.body.publicAccessLevel != null) {
  108. jobs.push(callback =>
  109. EditorController.setPublicAccessLevel(
  110. projectId,
  111. req.body.publicAccessLevel,
  112. callback
  113. )
  114. )
  115. }
  116. async.series(jobs, error => {
  117. if (error != null) {
  118. return next(error)
  119. }
  120. res.sendStatus(204)
  121. })
  122. },
  123. deleteProject(req, res) {
  124. const projectId = req.params.Project_id
  125. const user = AuthenticationController.getSessionUser(req)
  126. const cb = err => {
  127. if (err != null) {
  128. res.sendStatus(500)
  129. } else {
  130. res.sendStatus(200)
  131. }
  132. }
  133. ProjectDeleter.deleteProject(
  134. projectId,
  135. { deleterUser: user, ipAddress: req.ip },
  136. cb
  137. )
  138. },
  139. archiveProject(req, res, next) {
  140. const projectId = req.params.Project_id
  141. const userId = AuthenticationController.getLoggedInUserId(req)
  142. ProjectDeleter.archiveProject(projectId, userId, function(err) {
  143. if (err != null) {
  144. return next(err)
  145. } else {
  146. return res.sendStatus(200)
  147. }
  148. })
  149. },
  150. unarchiveProject(req, res, next) {
  151. const projectId = req.params.Project_id
  152. const userId = AuthenticationController.getLoggedInUserId(req)
  153. ProjectDeleter.unarchiveProject(projectId, userId, function(err) {
  154. if (err != null) {
  155. return next(err)
  156. } else {
  157. return res.sendStatus(200)
  158. }
  159. })
  160. },
  161. trashProject(req, res, next) {
  162. const projectId = req.params.project_id
  163. const userId = AuthenticationController.getLoggedInUserId(req)
  164. ProjectDeleter.trashProject(projectId, userId, function(err) {
  165. if (err != null) {
  166. return next(err)
  167. } else {
  168. return res.sendStatus(200)
  169. }
  170. })
  171. },
  172. untrashProject(req, res, next) {
  173. const projectId = req.params.project_id
  174. const userId = AuthenticationController.getLoggedInUserId(req)
  175. ProjectDeleter.untrashProject(projectId, userId, function(err) {
  176. if (err != null) {
  177. return next(err)
  178. } else {
  179. return res.sendStatus(200)
  180. }
  181. })
  182. },
  183. expireDeletedProjectsAfterDuration(req, res) {
  184. ProjectDeleter.expireDeletedProjectsAfterDuration(err => {
  185. if (err != null) {
  186. res.sendStatus(500)
  187. } else {
  188. res.sendStatus(200)
  189. }
  190. })
  191. },
  192. expireDeletedProject(req, res, next) {
  193. const { projectId } = req.params
  194. ProjectDeleter.expireDeletedProject(projectId, err => {
  195. if (err != null) {
  196. next(err)
  197. } else {
  198. res.sendStatus(200)
  199. }
  200. })
  201. },
  202. restoreProject(req, res) {
  203. const projectId = req.params.Project_id
  204. ProjectDeleter.restoreProject(projectId, err => {
  205. if (err != null) {
  206. res.sendStatus(500)
  207. } else {
  208. res.sendStatus(200)
  209. }
  210. })
  211. },
  212. cloneProject(req, res, next) {
  213. res.setTimeout(5 * 60 * 1000) // allow extra time for the copy to complete
  214. metrics.inc('cloned-project')
  215. const projectId = req.params.Project_id
  216. const { projectName } = req.body
  217. logger.log({ projectId, projectName }, 'cloning project')
  218. if (!AuthenticationController.isUserLoggedIn(req)) {
  219. return res.send({ redir: '/register' })
  220. }
  221. const currentUser = AuthenticationController.getSessionUser(req)
  222. ProjectDuplicator.duplicate(
  223. currentUser,
  224. projectId,
  225. projectName,
  226. (err, project) => {
  227. if (err != null) {
  228. logger.warn(
  229. { err, projectId, userId: currentUser._id },
  230. 'error cloning project'
  231. )
  232. return next(err)
  233. }
  234. res.send({
  235. name: project.name,
  236. project_id: project._id,
  237. owner_ref: project.owner_ref
  238. })
  239. }
  240. )
  241. },
  242. newProject(req, res, next) {
  243. const userId = AuthenticationController.getLoggedInUserId(req)
  244. const projectName =
  245. req.body.projectName != null ? req.body.projectName.trim() : undefined
  246. const { template } = req.body
  247. async.waterfall(
  248. [
  249. cb => {
  250. if (template === 'example') {
  251. ProjectCreationHandler.createExampleProject(userId, projectName, cb)
  252. } else {
  253. ProjectCreationHandler.createBasicProject(userId, projectName, cb)
  254. }
  255. }
  256. ],
  257. (err, project) => {
  258. if (err != null) {
  259. return next(err)
  260. }
  261. res.send({ project_id: project._id })
  262. }
  263. )
  264. },
  265. renameProject(req, res, next) {
  266. const projectId = req.params.Project_id
  267. const newName = req.body.newProjectName
  268. EditorController.renameProject(projectId, newName, err => {
  269. if (err != null) {
  270. return next(err)
  271. }
  272. res.sendStatus(200)
  273. })
  274. },
  275. userProjectsJson(req, res, next) {
  276. const userId = AuthenticationController.getLoggedInUserId(req)
  277. ProjectGetter.findAllUsersProjects(
  278. userId,
  279. 'name lastUpdated publicAccesLevel archived trashed owner_ref tokens',
  280. (err, projects) => {
  281. if (err != null) {
  282. return next(err)
  283. }
  284. // _buildProjectList already converts archived/trashed to booleans so isArchivedOrTrashed should not be used here
  285. projects = ProjectController._buildProjectList(projects, userId)
  286. .filter(p => !(p.archived || p.trashed))
  287. .filter(p => !p.isV1Project)
  288. .map(p => ({ _id: p.id, name: p.name, accessLevel: p.accessLevel }))
  289. res.json({ projects })
  290. }
  291. )
  292. },
  293. projectEntitiesJson(req, res, next) {
  294. const projectId = req.params.Project_id
  295. ProjectGetter.getProject(projectId, (err, project) => {
  296. if (err != null) {
  297. return next(err)
  298. }
  299. ProjectEntityHandler.getAllEntitiesFromProject(
  300. project,
  301. (err, docs, files) => {
  302. if (err != null) {
  303. return next(err)
  304. }
  305. const entities = docs
  306. .concat(files)
  307. .sort((a, b) => a.path > b.path) // Sort by path ascending
  308. .map(e => ({
  309. path: e.path,
  310. type: e.doc != null ? 'doc' : 'file'
  311. }))
  312. res.json({ project_id: projectId, entities })
  313. }
  314. )
  315. })
  316. },
  317. projectListPage(req, res, next) {
  318. const timer = new metrics.Timer('project-list')
  319. const userId = AuthenticationController.getLoggedInUserId(req)
  320. const currentUser = AuthenticationController.getSessionUser(req)
  321. let noV1Connection = false
  322. let institutionLinkingError
  323. async.parallel(
  324. {
  325. tags(cb) {
  326. TagsHandler.getAllTags(userId, cb)
  327. },
  328. notifications(cb) {
  329. NotificationsHandler.getUserNotifications(userId, cb)
  330. },
  331. projects(cb) {
  332. ProjectGetter.findAllUsersProjects(
  333. userId,
  334. 'name lastUpdated lastUpdatedBy publicAccesLevel archived trashed owner_ref tokens',
  335. cb
  336. )
  337. },
  338. v1Projects(cb) {
  339. if (!Features.hasFeature('overleaf-integration')) {
  340. return cb(null, null)
  341. }
  342. Modules.hooks.fire('findAllV1Projects', userId, (error, projects) => {
  343. if (projects == null) {
  344. projects = []
  345. }
  346. if (error != null && error instanceof V1ConnectionError) {
  347. noV1Connection = true
  348. return cb(null, null)
  349. }
  350. cb(error, projects[0])
  351. })
  352. }, // hooks.fire returns an array of results, only need first
  353. hasSubscription(cb) {
  354. LimitationsManager.hasPaidSubscription(
  355. currentUser,
  356. (error, hasPaidSubscription) => {
  357. if (error != null && error instanceof V1ConnectionError) {
  358. noV1Connection = true
  359. return cb(null, true)
  360. }
  361. cb(error, hasPaidSubscription)
  362. }
  363. )
  364. },
  365. user(cb) {
  366. User.findById(
  367. userId,
  368. 'emails featureSwitches overleaf awareOfV2 features lastLoginIp',
  369. cb
  370. )
  371. },
  372. userAffiliations(cb) {
  373. if (!Features.hasFeature('affiliations')) {
  374. return cb(null, null)
  375. }
  376. getUserAffiliations(userId, (error, affiliations) => {
  377. if (error && error instanceof V1ConnectionError) {
  378. noV1Connection = true
  379. return cb(null, [])
  380. }
  381. cb(error, affiliations)
  382. })
  383. }
  384. },
  385. (err, results) => {
  386. if (err != null) {
  387. logger.warn({ err }, 'error getting data for project list page')
  388. return next(err)
  389. }
  390. if (noV1Connection) {
  391. results.v1Projects = results.v1Projects || { projects: [], tags: [] }
  392. results.v1Projects.noConnection = true
  393. }
  394. const { notifications, user, userAffiliations } = results
  395. // Handle case of deleted user
  396. if (user == null) {
  397. UserController.logout(req, res, next)
  398. return
  399. }
  400. const v1Tags =
  401. (results.v1Projects != null ? results.v1Projects.tags : undefined) ||
  402. []
  403. const tags = results.tags.concat(v1Tags)
  404. const notificationsInstitution = []
  405. for (const notification of notifications) {
  406. notification.html = req.i18n.translate(
  407. notification.templateKey,
  408. notification.messageOpts
  409. )
  410. }
  411. // Institution SSO Notifications
  412. if (Features.hasFeature('saml')) {
  413. const samlSession = req.session.saml
  414. // Notification: SSO Available
  415. const linkedInstitutionIds = []
  416. user.emails.forEach(email => {
  417. if (email.samlProviderId) {
  418. linkedInstitutionIds.push(email.samlProviderId)
  419. }
  420. })
  421. if (Array.isArray(userAffiliations)) {
  422. userAffiliations.forEach(affiliation => {
  423. if (
  424. _ssoAvailable(affiliation, req.session, linkedInstitutionIds)
  425. ) {
  426. notificationsInstitution.push({
  427. email: affiliation.email,
  428. institutionId: affiliation.institution.id,
  429. institutionName: affiliation.institution.name,
  430. templateKey: 'notification_institution_sso_available'
  431. })
  432. }
  433. })
  434. }
  435. if (samlSession) {
  436. // Notification: After SSO Linked
  437. if (samlSession.linked) {
  438. notificationsInstitution.push({
  439. email: samlSession.institutionEmail,
  440. institutionName: samlSession.linked.universityName,
  441. templateKey: 'notification_institution_sso_linked'
  442. })
  443. }
  444. // Notification: After SSO Linked or Logging in
  445. // The requested email does not match primary email returned from
  446. // the institution
  447. if (
  448. samlSession.requestedEmail &&
  449. samlSession.emailNonCanonical &&
  450. !samlSession.linkedToAnother
  451. ) {
  452. notificationsInstitution.push({
  453. institutionEmail: samlSession.emailNonCanonical,
  454. requestedEmail: samlSession.requestedEmail,
  455. templateKey: 'notification_institution_sso_non_canonical'
  456. })
  457. }
  458. // Notification: Tried to register, but account already existed
  459. // registerIntercept is set before the institution callback.
  460. // institutionEmail is set after institution callback.
  461. // Check for both in case SSO flow was abandoned
  462. if (
  463. samlSession.registerIntercept &&
  464. samlSession.institutionEmail &&
  465. !samlSession.linkedToAnother
  466. ) {
  467. notificationsInstitution.push({
  468. email: samlSession.institutionEmail,
  469. templateKey: 'notification_institution_sso_already_registered'
  470. })
  471. }
  472. // Notification: Already linked to another account
  473. if (samlSession.linkedToAnother) {
  474. notificationsInstitution.push({
  475. templateKey: 'notification_institution_sso_linked_by_another'
  476. })
  477. }
  478. // Notification: When there is a session error
  479. if (samlSession.error) {
  480. institutionLinkingError = samlSession.error
  481. notificationsInstitution.push({
  482. message: samlSession.error.message,
  483. templateKey: 'notification_institution_sso_error',
  484. tryAgain: samlSession.error.tryAgain
  485. })
  486. }
  487. }
  488. delete req.session.saml
  489. }
  490. const portalTemplates = ProjectController._buildPortalTemplatesList(
  491. userAffiliations
  492. )
  493. const projects = ProjectController._buildProjectList(
  494. results.projects,
  495. userId,
  496. results.v1Projects != null ? results.v1Projects.projects : undefined
  497. )
  498. const warnings = ProjectController._buildWarningsList(
  499. results.v1Projects
  500. )
  501. // in v2 add notifications for matching university IPs
  502. if (Settings.overleaf != null && req.ip !== user.lastLoginIp) {
  503. NotificationsBuilder.ipMatcherAffiliation(user._id).create(req.ip)
  504. }
  505. ProjectController._injectProjectUsers(projects, (error, projects) => {
  506. if (error != null) {
  507. return next(error)
  508. }
  509. const viewModel = {
  510. title: 'your_projects',
  511. priority_title: true,
  512. projects,
  513. tags,
  514. notifications: notifications || [],
  515. notificationsInstitution,
  516. portalTemplates,
  517. user,
  518. userAffiliations,
  519. hasSubscription: results.hasSubscription,
  520. institutionLinkingError,
  521. isShowingV1Projects:
  522. results.v1Projects != null &&
  523. results.v1Projects.projects.length > 0,
  524. warnings,
  525. zipFileSizeLimit: Settings.maxUploadSize
  526. }
  527. if (
  528. Settings.algolia &&
  529. Settings.algolia.app_id &&
  530. Settings.algolia.read_only_api_key
  531. ) {
  532. viewModel.showUserDetailsArea = true
  533. viewModel.algolia_api_key = Settings.algolia.read_only_api_key
  534. viewModel.algolia_app_id = Settings.algolia.app_id
  535. } else {
  536. viewModel.showUserDetailsArea = false
  537. }
  538. const paidUser =
  539. (user.features != null ? user.features.github : undefined) &&
  540. (user.features != null ? user.features.dropbox : undefined) // use a heuristic for paid account
  541. const freeUserProportion = 0.1
  542. const sampleFreeUser =
  543. parseInt(user._id.toString().slice(-2), 16) <
  544. freeUserProportion * 255
  545. const showFrontWidget = paidUser || sampleFreeUser
  546. if (showFrontWidget) {
  547. viewModel.frontChatWidgetRoomId =
  548. Settings.overleaf != null
  549. ? Settings.overleaf.front_chat_widget_room_id
  550. : undefined
  551. }
  552. res.render('project/list', viewModel)
  553. timer.done()
  554. })
  555. }
  556. )
  557. },
  558. loadEditor(req, res, next) {
  559. let anonymous, userId
  560. const timer = new metrics.Timer('load-editor')
  561. if (!Settings.editorIsOpen) {
  562. return res.render('general/closed', { title: 'updating_site' })
  563. }
  564. if (AuthenticationController.isUserLoggedIn(req)) {
  565. userId = AuthenticationController.getLoggedInUserId(req)
  566. anonymous = false
  567. } else {
  568. anonymous = true
  569. userId = null
  570. }
  571. const projectId = req.params.Project_id
  572. // record failures to load the custom websocket
  573. if ((req.query != null ? req.query.ws : undefined) === 'fallback') {
  574. metrics.inc('load-editor-ws-fallback')
  575. }
  576. async.auto(
  577. {
  578. project(cb) {
  579. ProjectGetter.getProject(
  580. projectId,
  581. {
  582. name: 1,
  583. lastUpdated: 1,
  584. track_changes: 1,
  585. owner_ref: 1,
  586. brandVariationId: 1,
  587. overleaf: 1,
  588. tokens: 1
  589. },
  590. (err, project) => {
  591. if (err != null) {
  592. return cb(err)
  593. }
  594. if (
  595. (project.overleaf != null ? project.overleaf.id : undefined) ==
  596. null ||
  597. (project.tokens != null
  598. ? project.tokens.readAndWrite
  599. : undefined) == null ||
  600. Settings.projectImportingCheckMaxCreateDelta == null
  601. ) {
  602. return cb(null, project)
  603. }
  604. const createDelta =
  605. (new Date().getTime() -
  606. new Date(project._id.getTimestamp()).getTime()) /
  607. 1000
  608. if (
  609. !(createDelta < Settings.projectImportingCheckMaxCreateDelta)
  610. ) {
  611. return cb(null, project)
  612. }
  613. V1Handler.getDocExported(
  614. project.tokens.readAndWrite,
  615. (err, docExported) => {
  616. if (err != null) {
  617. return next(err)
  618. }
  619. project.exporting = docExported.exporting
  620. cb(null, project)
  621. }
  622. )
  623. }
  624. )
  625. },
  626. user(cb) {
  627. if (userId == null) {
  628. cb(null, defaultSettingsForAnonymousUser(userId))
  629. } else {
  630. User.findById(userId, (err, user) => {
  631. // Handle case of deleted user
  632. if (user == null) {
  633. UserController.logout(req, res, next)
  634. return
  635. }
  636. logger.log({ projectId, userId }, 'got user')
  637. cb(err, user)
  638. })
  639. }
  640. },
  641. subscription(cb) {
  642. if (userId == null) {
  643. return cb()
  644. }
  645. SubscriptionLocator.getUsersSubscription(userId, cb)
  646. },
  647. activate(cb) {
  648. InactiveProjectManager.reactivateProjectIfRequired(projectId, cb)
  649. },
  650. markAsOpened(cb) {
  651. // don't need to wait for this to complete
  652. ProjectUpdateHandler.markAsOpened(projectId, () => {})
  653. cb()
  654. },
  655. isTokenMember(cb) {
  656. if (userId == null) {
  657. return cb()
  658. }
  659. CollaboratorsGetter.userIsTokenMember(userId, projectId, cb)
  660. },
  661. brandVariation: [
  662. 'project',
  663. (cb, results) => {
  664. if (
  665. (results.project != null
  666. ? results.project.brandVariationId
  667. : undefined) == null
  668. ) {
  669. return cb()
  670. }
  671. BrandVariationsHandler.getBrandVariationById(
  672. results.project.brandVariationId,
  673. (error, brandVariationDetails) => cb(error, brandVariationDetails)
  674. )
  675. }
  676. ],
  677. flushToTpds: cb => {
  678. TpdsProjectFlusher.flushProjectToTpdsIfNeeded(projectId, cb)
  679. }
  680. },
  681. (err, results) => {
  682. if (err != null) {
  683. logger.warn({ err }, 'error getting details for project page')
  684. return next(err)
  685. }
  686. const { project } = results
  687. const { user } = results
  688. const { subscription } = results
  689. const { brandVariation } = results
  690. const anonRequestToken = TokenAccessHandler.getRequestToken(
  691. req,
  692. projectId
  693. )
  694. const { isTokenMember } = results
  695. AuthorizationManager.getPrivilegeLevelForProject(
  696. userId,
  697. projectId,
  698. anonRequestToken,
  699. (error, privilegeLevel) => {
  700. let allowedFreeTrial
  701. if (error != null) {
  702. return next(error)
  703. }
  704. if (
  705. privilegeLevel == null ||
  706. privilegeLevel === PrivilegeLevels.NONE
  707. ) {
  708. return res.sendStatus(401)
  709. }
  710. if (project.exporting) {
  711. res.render('project/importing', { bodyClasses: ['editor'] })
  712. return
  713. }
  714. if (
  715. subscription != null &&
  716. subscription.freeTrial != null &&
  717. subscription.freeTrial.expiresAt != null
  718. ) {
  719. allowedFreeTrial = !!subscription.freeTrial.allowed || true
  720. }
  721. res.render('project/editor', {
  722. title: project.name,
  723. priority_title: true,
  724. bodyClasses: ['editor'],
  725. project_id: project._id,
  726. user: {
  727. id: userId,
  728. email: user.email,
  729. first_name: user.first_name,
  730. last_name: user.last_name,
  731. referal_id: user.referal_id,
  732. signUpDate: user.signUpDate,
  733. subscription: {
  734. freeTrial: { allowed: allowedFreeTrial }
  735. },
  736. featureSwitches: user.featureSwitches,
  737. features: user.features,
  738. refProviders: user.refProviders,
  739. betaProgram: user.betaProgram,
  740. isAdmin: user.isAdmin
  741. },
  742. userSettings: {
  743. mode: user.ace.mode,
  744. editorTheme: user.ace.theme,
  745. fontSize: user.ace.fontSize,
  746. autoComplete: user.ace.autoComplete,
  747. autoPairDelimiters: user.ace.autoPairDelimiters,
  748. pdfViewer: user.ace.pdfViewer,
  749. syntaxValidation: user.ace.syntaxValidation,
  750. fontFamily: user.ace.fontFamily || 'lucida',
  751. lineHeight: user.ace.lineHeight || 'normal',
  752. overallTheme: user.ace.overallTheme
  753. },
  754. trackChangesState: project.track_changes,
  755. privilegeLevel,
  756. chatUrl: Settings.apis.chat.url,
  757. anonymous,
  758. anonymousAccessToken: anonymous ? anonRequestToken : null,
  759. isTokenMember,
  760. isRestrictedTokenMember: AuthorizationManager.isRestrictedUser(
  761. userId,
  762. privilegeLevel,
  763. isTokenMember
  764. ),
  765. languages: Settings.languages,
  766. editorThemes: THEME_LIST,
  767. maxDocLength: Settings.max_doc_length,
  768. useV2History:
  769. project.overleaf &&
  770. project.overleaf.history &&
  771. Boolean(project.overleaf.history.display),
  772. brandVariation,
  773. allowedImageNames: Settings.allowedImageNames || [],
  774. gitBridgePublicBaseUrl: Settings.gitBridgePublicBaseUrl,
  775. showSupport: Features.hasFeature('support')
  776. })
  777. timer.done()
  778. }
  779. )
  780. }
  781. )
  782. },
  783. _buildProjectList(allProjects, userId, v1Projects) {
  784. let project
  785. if (v1Projects == null) {
  786. v1Projects = []
  787. }
  788. const {
  789. owned,
  790. readAndWrite,
  791. readOnly,
  792. tokenReadAndWrite,
  793. tokenReadOnly
  794. } = allProjects
  795. const projects = []
  796. for (project of owned) {
  797. projects.push(
  798. ProjectController._buildProjectViewModel(
  799. project,
  800. 'owner',
  801. Sources.OWNER,
  802. userId
  803. )
  804. )
  805. }
  806. // Invite-access
  807. for (project of readAndWrite) {
  808. projects.push(
  809. ProjectController._buildProjectViewModel(
  810. project,
  811. 'readWrite',
  812. Sources.INVITE,
  813. userId
  814. )
  815. )
  816. }
  817. for (project of readOnly) {
  818. projects.push(
  819. ProjectController._buildProjectViewModel(
  820. project,
  821. 'readOnly',
  822. Sources.INVITE,
  823. userId
  824. )
  825. )
  826. }
  827. for (project of v1Projects) {
  828. projects.push(ProjectController._buildV1ProjectViewModel(project))
  829. }
  830. // Token-access
  831. // Only add these projects if they're not already present, this gives us cascading access
  832. // from 'owner' => 'token-read-only'
  833. for (project of tokenReadAndWrite) {
  834. if (
  835. projects.filter(p => p.id.toString() === project._id.toString())
  836. .length === 0
  837. ) {
  838. projects.push(
  839. ProjectController._buildProjectViewModel(
  840. project,
  841. 'readAndWrite',
  842. Sources.TOKEN,
  843. userId
  844. )
  845. )
  846. }
  847. }
  848. for (project of tokenReadOnly) {
  849. if (
  850. projects.filter(p => p.id.toString() === project._id.toString())
  851. .length === 0
  852. ) {
  853. projects.push(
  854. ProjectController._buildProjectViewModel(
  855. project,
  856. 'readOnly',
  857. Sources.TOKEN,
  858. userId
  859. )
  860. )
  861. }
  862. }
  863. return projects
  864. },
  865. _buildProjectViewModel(project, accessLevel, source, userId) {
  866. const archived = ProjectHelper.isArchived(project, userId)
  867. // If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
  868. const trashed = ProjectHelper.isTrashed(project, userId) && !archived
  869. TokenAccessHandler.protectTokens(project, accessLevel)
  870. const model = {
  871. id: project._id,
  872. name: project.name,
  873. lastUpdated: project.lastUpdated,
  874. lastUpdatedBy: project.lastUpdatedBy,
  875. publicAccessLevel: project.publicAccesLevel,
  876. accessLevel,
  877. source,
  878. archived,
  879. trashed,
  880. owner_ref: project.owner_ref,
  881. isV1Project: false
  882. }
  883. if (accessLevel === PrivilegeLevels.READ_ONLY && source === Sources.TOKEN) {
  884. model.owner_ref = null
  885. model.lastUpdatedBy = null
  886. }
  887. return model
  888. },
  889. _buildV1ProjectViewModel(project) {
  890. const archived = project.archived
  891. // If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
  892. const trashed = project.removed && !archived
  893. const projectViewModel = {
  894. id: project.id,
  895. name: project.title,
  896. lastUpdated: new Date(project.updated_at * 1000), // Convert from epoch
  897. archived: archived,
  898. trashed: trashed,
  899. isV1Project: true
  900. }
  901. if (
  902. (project.owner != null && project.owner.user_is_owner) ||
  903. (project.creator != null && project.creator.user_is_creator)
  904. ) {
  905. projectViewModel.accessLevel = 'owner'
  906. } else {
  907. projectViewModel.accessLevel = 'readOnly'
  908. }
  909. if (project.owner != null) {
  910. projectViewModel.owner = {
  911. first_name: project.owner.name
  912. }
  913. } else if (project.creator != null) {
  914. projectViewModel.owner = {
  915. first_name: project.creator.name
  916. }
  917. }
  918. return projectViewModel
  919. },
  920. _injectProjectUsers(projects, callback) {
  921. const users = {}
  922. for (const project of projects) {
  923. if (project.owner_ref != null) {
  924. users[project.owner_ref.toString()] = true
  925. }
  926. if (project.lastUpdatedBy != null) {
  927. users[project.lastUpdatedBy.toString()] = true
  928. }
  929. }
  930. const userIds = Object.keys(users)
  931. async.eachSeries(
  932. userIds,
  933. (userId, cb) => {
  934. UserGetter.getUser(
  935. userId,
  936. { first_name: 1, last_name: 1, email: 1 },
  937. (error, user) => {
  938. if (error != null) {
  939. return cb(error)
  940. }
  941. users[userId] = user
  942. cb()
  943. }
  944. )
  945. },
  946. error => {
  947. if (error != null) {
  948. return callback(error)
  949. }
  950. for (const project of projects) {
  951. if (project.owner_ref != null) {
  952. project.owner = users[project.owner_ref.toString()]
  953. }
  954. if (project.lastUpdatedBy != null) {
  955. project.lastUpdatedBy =
  956. users[project.lastUpdatedBy.toString()] || null
  957. }
  958. }
  959. callback(null, projects)
  960. }
  961. )
  962. },
  963. _buildWarningsList(v1ProjectData) {
  964. if (v1ProjectData == null) {
  965. v1ProjectData = {}
  966. }
  967. const warnings = []
  968. if (v1ProjectData.noConnection) {
  969. warnings.push(
  970. 'Error accessing Overleaf V1. Some of your projects or features may be missing.'
  971. )
  972. }
  973. if (v1ProjectData.hasHiddenV1Projects) {
  974. warnings.push(
  975. "Looks like you've got a lot of V1 projects! Some of them may be hidden on V2. To view them all, use the V1 dashboard."
  976. )
  977. }
  978. return warnings
  979. },
  980. _buildPortalTemplatesList(affiliations) {
  981. if (affiliations == null) {
  982. affiliations = []
  983. }
  984. const portalTemplates = []
  985. for (let aff of affiliations) {
  986. if (
  987. aff.portal &&
  988. aff.portal.slug &&
  989. aff.portal.templates_count &&
  990. aff.portal.templates_count > 0
  991. ) {
  992. const portalPath = aff.institution.isUniversity ? '/edu/' : '/org/'
  993. portalTemplates.push({
  994. name: aff.institution.name,
  995. url: Settings.siteUrl + portalPath + aff.portal.slug
  996. })
  997. }
  998. }
  999. return portalTemplates
  1000. }
  1001. }
  1002. var defaultSettingsForAnonymousUser = userId => ({
  1003. id: userId,
  1004. ace: {
  1005. mode: 'none',
  1006. theme: 'textmate',
  1007. fontSize: '12',
  1008. autoComplete: true,
  1009. spellCheckLanguage: '',
  1010. pdfViewer: '',
  1011. syntaxValidation: true
  1012. },
  1013. subscription: {
  1014. freeTrial: {
  1015. allowed: true
  1016. }
  1017. },
  1018. featureSwitches: {
  1019. github: false
  1020. }
  1021. })
  1022. var THEME_LIST = []
  1023. function generateThemeList() {
  1024. const files = fs.readdirSync(
  1025. Path.join(
  1026. __dirname,
  1027. '/../../../../frontend/js/vendor/',
  1028. PackageVersions.lib('ace')
  1029. )
  1030. )
  1031. const result = []
  1032. for (let file of files) {
  1033. if (file.slice(-2) === 'js' && /^theme-/.test(file)) {
  1034. const cleanName = file.slice(0, -3).slice(6)
  1035. result.push(THEME_LIST.push(cleanName))
  1036. } else {
  1037. result.push(undefined)
  1038. }
  1039. }
  1040. }
  1041. generateThemeList()
  1042. module.exports = ProjectController