ProjectController.js 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540
  1. const _ = require('lodash')
  2. const OError = require('@overleaf/o-error')
  3. const crypto = require('crypto')
  4. const async = require('async')
  5. const logger = require('@overleaf/logger')
  6. const { ObjectId } = require('mongodb')
  7. const ProjectDeleter = require('./ProjectDeleter')
  8. const ProjectDuplicator = require('./ProjectDuplicator')
  9. const ProjectCreationHandler = require('./ProjectCreationHandler')
  10. const EditorController = require('../Editor/EditorController')
  11. const ProjectHelper = require('./ProjectHelper')
  12. const metrics = require('@overleaf/metrics')
  13. const { User } = require('../../models/User')
  14. const TagsHandler = require('../Tags/TagsHandler')
  15. const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
  16. const NotificationsHandler = require('../Notifications/NotificationsHandler')
  17. const LimitationsManager = require('../Subscription/LimitationsManager')
  18. const Settings = require('@overleaf/settings')
  19. const AuthorizationManager = require('../Authorization/AuthorizationManager')
  20. const InactiveProjectManager = require('../InactiveData/InactiveProjectManager')
  21. const ProjectUpdateHandler = require('./ProjectUpdateHandler')
  22. const ProjectGetter = require('./ProjectGetter')
  23. const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
  24. const SessionManager = require('../Authentication/SessionManager')
  25. const Sources = require('../Authorization/Sources')
  26. const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
  27. const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
  28. const ProjectEntityHandler = require('./ProjectEntityHandler')
  29. const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
  30. const UserGetter = require('../User/UserGetter')
  31. const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
  32. const { V1ConnectionError } = require('../Errors/Errors')
  33. const Features = require('../../infrastructure/Features')
  34. const BrandVariationsHandler = require('../BrandVariations/BrandVariationsHandler')
  35. const UserController = require('../User/UserController')
  36. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  37. const Modules = require('../../infrastructure/Modules')
  38. const SplitTestHandler = require('../SplitTests/SplitTestHandler')
  39. const FeaturesUpdater = require('../Subscription/FeaturesUpdater')
  40. const SpellingHandler = require('../Spelling/SpellingHandler')
  41. const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
  42. const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
  43. const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
  44. const SubscriptionViewModelBuilder = require('../Subscription/SubscriptionViewModelBuilder')
  45. const SurveyHandler = require('../Survey/SurveyHandler')
  46. const { expressify } = require('../../util/promises')
  47. const ProjectListController = require('./ProjectListController')
  48. /**
  49. * @typedef {import("./types").GetProjectsRequest} GetProjectsRequest
  50. * @typedef {import("./types").GetProjectsResponse} GetProjectsResponse
  51. * @typedef {import("./types").Project} Project
  52. */
  53. const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
  54. if (!affiliation.institution) return false
  55. // institution.confirmed is for the domain being confirmed, not the email
  56. // Do not show SSO UI for unconfirmed domains
  57. if (!affiliation.institution.confirmed) return false
  58. // Could have multiple emails at the same institution, and if any are
  59. // linked to the institution then do not show notification for others
  60. if (
  61. linkedInstitutionIds.indexOf(affiliation.institution.id.toString()) === -1
  62. ) {
  63. if (affiliation.institution.ssoEnabled) return true
  64. if (affiliation.institution.ssoBeta && session.samlBeta) return true
  65. return false
  66. }
  67. return false
  68. }
  69. const ProjectController = {
  70. _isInPercentageRollout(rolloutName, objectId, percentage) {
  71. if (Settings.bypassPercentageRollouts === true) {
  72. return true
  73. }
  74. const data = `${rolloutName}:${objectId.toString()}`
  75. const md5hash = crypto.createHash('md5').update(data).digest('hex')
  76. const counter = parseInt(md5hash.slice(26, 32), 16)
  77. return counter % 100 < percentage
  78. },
  79. updateProjectSettings(req, res, next) {
  80. const projectId = req.params.Project_id
  81. const jobs = []
  82. if (req.body.compiler != null) {
  83. jobs.push(callback =>
  84. EditorController.setCompiler(projectId, req.body.compiler, callback)
  85. )
  86. }
  87. if (req.body.imageName != null) {
  88. jobs.push(callback =>
  89. EditorController.setImageName(projectId, req.body.imageName, callback)
  90. )
  91. }
  92. if (req.body.name != null) {
  93. jobs.push(callback =>
  94. EditorController.renameProject(projectId, req.body.name, callback)
  95. )
  96. }
  97. if (req.body.spellCheckLanguage != null) {
  98. jobs.push(callback =>
  99. EditorController.setSpellCheckLanguage(
  100. projectId,
  101. req.body.spellCheckLanguage,
  102. callback
  103. )
  104. )
  105. }
  106. if (req.body.rootDocId != null) {
  107. jobs.push(callback =>
  108. EditorController.setRootDoc(projectId, req.body.rootDocId, callback)
  109. )
  110. }
  111. async.series(jobs, error => {
  112. if (error != null) {
  113. return next(error)
  114. }
  115. res.sendStatus(204)
  116. })
  117. },
  118. updateProjectAdminSettings(req, res, next) {
  119. const projectId = req.params.Project_id
  120. const jobs = []
  121. if (req.body.publicAccessLevel != null) {
  122. jobs.push(callback =>
  123. EditorController.setPublicAccessLevel(
  124. projectId,
  125. req.body.publicAccessLevel,
  126. callback
  127. )
  128. )
  129. }
  130. async.series(jobs, error => {
  131. if (error != null) {
  132. return next(error)
  133. }
  134. res.sendStatus(204)
  135. })
  136. },
  137. deleteProject(req, res) {
  138. const projectId = req.params.Project_id
  139. const user = SessionManager.getSessionUser(req.session)
  140. const cb = err => {
  141. if (err != null) {
  142. res.sendStatus(500)
  143. } else {
  144. res.sendStatus(200)
  145. }
  146. }
  147. ProjectDeleter.deleteProject(
  148. projectId,
  149. { deleterUser: user, ipAddress: req.ip },
  150. cb
  151. )
  152. },
  153. archiveProject(req, res, next) {
  154. const projectId = req.params.Project_id
  155. const userId = SessionManager.getLoggedInUserId(req.session)
  156. ProjectDeleter.archiveProject(projectId, userId, function (err) {
  157. if (err != null) {
  158. return next(err)
  159. } else {
  160. return res.sendStatus(200)
  161. }
  162. })
  163. },
  164. unarchiveProject(req, res, next) {
  165. const projectId = req.params.Project_id
  166. const userId = SessionManager.getLoggedInUserId(req.session)
  167. ProjectDeleter.unarchiveProject(projectId, userId, function (err) {
  168. if (err != null) {
  169. return next(err)
  170. } else {
  171. return res.sendStatus(200)
  172. }
  173. })
  174. },
  175. trashProject(req, res, next) {
  176. const projectId = req.params.project_id
  177. const userId = SessionManager.getLoggedInUserId(req.session)
  178. ProjectDeleter.trashProject(projectId, userId, function (err) {
  179. if (err != null) {
  180. return next(err)
  181. } else {
  182. return res.sendStatus(200)
  183. }
  184. })
  185. },
  186. untrashProject(req, res, next) {
  187. const projectId = req.params.project_id
  188. const userId = SessionManager.getLoggedInUserId(req.session)
  189. ProjectDeleter.untrashProject(projectId, userId, function (err) {
  190. if (err != null) {
  191. return next(err)
  192. } else {
  193. return res.sendStatus(200)
  194. }
  195. })
  196. },
  197. expireDeletedProjectsAfterDuration(req, res) {
  198. ProjectDeleter.expireDeletedProjectsAfterDuration(err => {
  199. if (err != null) {
  200. res.sendStatus(500)
  201. } else {
  202. res.sendStatus(200)
  203. }
  204. })
  205. },
  206. expireDeletedProject(req, res, next) {
  207. const { projectId } = req.params
  208. ProjectDeleter.expireDeletedProject(projectId, err => {
  209. if (err != null) {
  210. next(err)
  211. } else {
  212. res.sendStatus(200)
  213. }
  214. })
  215. },
  216. restoreProject(req, res) {
  217. const projectId = req.params.Project_id
  218. ProjectDeleter.restoreProject(projectId, err => {
  219. if (err != null) {
  220. res.sendStatus(500)
  221. } else {
  222. res.sendStatus(200)
  223. }
  224. })
  225. },
  226. cloneProject(req, res, next) {
  227. res.setTimeout(5 * 60 * 1000) // allow extra time for the copy to complete
  228. metrics.inc('cloned-project')
  229. const projectId = req.params.Project_id
  230. const { projectName } = req.body
  231. logger.debug({ projectId, projectName }, 'cloning project')
  232. if (!SessionManager.isUserLoggedIn(req.session)) {
  233. return res.json({ redir: '/register' })
  234. }
  235. const currentUser = SessionManager.getSessionUser(req.session)
  236. const { first_name: firstName, last_name: lastName, email } = currentUser
  237. ProjectDuplicator.duplicate(
  238. currentUser,
  239. projectId,
  240. projectName,
  241. (err, project) => {
  242. if (err != null) {
  243. OError.tag(err, 'error cloning project', {
  244. projectId,
  245. userId: currentUser._id,
  246. })
  247. return next(err)
  248. }
  249. res.json({
  250. name: project.name,
  251. lastUpdated: project.lastUpdated,
  252. project_id: project._id,
  253. owner_ref: project.owner_ref,
  254. owner: {
  255. first_name: firstName,
  256. last_name: lastName,
  257. email,
  258. _id: currentUser._id,
  259. },
  260. })
  261. }
  262. )
  263. },
  264. newProject(req, res, next) {
  265. const currentUser = SessionManager.getSessionUser(req.session)
  266. const {
  267. first_name: firstName,
  268. last_name: lastName,
  269. email,
  270. _id: userId,
  271. } = currentUser
  272. const projectName =
  273. req.body.projectName != null ? req.body.projectName.trim() : undefined
  274. const { template } = req.body
  275. async.waterfall(
  276. [
  277. cb => {
  278. if (template === 'example') {
  279. ProjectCreationHandler.createExampleProject(userId, projectName, cb)
  280. } else {
  281. ProjectCreationHandler.createBasicProject(userId, projectName, cb)
  282. }
  283. },
  284. ],
  285. (err, project) => {
  286. if (err != null) {
  287. return next(err)
  288. }
  289. res.json({
  290. project_id: project._id,
  291. owner_ref: project.owner_ref,
  292. owner: {
  293. first_name: firstName,
  294. last_name: lastName,
  295. email,
  296. _id: userId,
  297. },
  298. })
  299. }
  300. )
  301. },
  302. renameProject(req, res, next) {
  303. const projectId = req.params.Project_id
  304. const newName = req.body.newProjectName
  305. EditorController.renameProject(projectId, newName, err => {
  306. if (err != null) {
  307. return next(err)
  308. }
  309. res.sendStatus(200)
  310. })
  311. },
  312. userProjectsJson(req, res, next) {
  313. const userId = SessionManager.getLoggedInUserId(req.session)
  314. ProjectGetter.findAllUsersProjects(
  315. userId,
  316. 'name lastUpdated publicAccesLevel archived trashed owner_ref tokens',
  317. (err, projects) => {
  318. if (err != null) {
  319. return next(err)
  320. }
  321. // _buildProjectList already converts archived/trashed to booleans so isArchivedOrTrashed should not be used here
  322. projects = ProjectController._buildProjectList(projects, userId)
  323. .filter(p => !(p.archived || p.trashed))
  324. .map(p => ({ _id: p.id, name: p.name, accessLevel: p.accessLevel }))
  325. res.json({ projects })
  326. }
  327. )
  328. },
  329. projectEntitiesJson(req, res, next) {
  330. const projectId = req.params.Project_id
  331. ProjectGetter.getProject(projectId, (err, project) => {
  332. if (err != null) {
  333. return next(err)
  334. }
  335. const { docs, files } =
  336. ProjectEntityHandler.getAllEntitiesFromProject(project)
  337. const entities = docs
  338. .concat(files)
  339. // Sort by path ascending
  340. .sort((a, b) => (a.path > b.path ? 1 : a.path < b.path ? -1 : 0))
  341. .map(e => ({
  342. path: e.path,
  343. type: e.doc != null ? 'doc' : 'file',
  344. }))
  345. res.json({ project_id: projectId, entities })
  346. })
  347. },
  348. async projectListPage(req, res, next) {
  349. try {
  350. const assignment = await SplitTestHandler.promises.getAssignment(
  351. req,
  352. res,
  353. 'project-dashboard-react'
  354. )
  355. if (assignment.variant === 'enabled') {
  356. ProjectListController.projectListReactPage(req, res, next)
  357. } else {
  358. ProjectController._projectListAngularPage(req, res, next)
  359. }
  360. } catch (error) {
  361. logger.warn(
  362. { err: error },
  363. 'failed to get "project-dashboard-react" split test assignment'
  364. )
  365. ProjectController._projectListAngularPage(req, res, next)
  366. }
  367. },
  368. _projectListAngularPage(req, res, next) {
  369. const timer = new metrics.Timer('project-list')
  370. const userId = SessionManager.getLoggedInUserId(req.session)
  371. const currentUser = SessionManager.getSessionUser(req.session)
  372. async.parallel(
  373. {
  374. tags(cb) {
  375. TagsHandler.getAllTags(userId, cb)
  376. },
  377. notifications(cb) {
  378. NotificationsHandler.getUserNotifications(userId, cb)
  379. },
  380. projects(cb) {
  381. ProjectGetter.findAllUsersProjects(
  382. userId,
  383. 'name lastUpdated lastUpdatedBy publicAccesLevel archived trashed owner_ref tokens',
  384. cb
  385. )
  386. },
  387. hasSubscription(cb) {
  388. LimitationsManager.hasPaidSubscription(
  389. currentUser,
  390. (error, hasPaidSubscription) => {
  391. if (error != null && error instanceof V1ConnectionError) {
  392. return cb(null, true)
  393. }
  394. cb(error, hasPaidSubscription)
  395. }
  396. )
  397. },
  398. user(cb) {
  399. User.findById(
  400. userId,
  401. 'email emails featureSwitches overleaf awareOfV2 features lastLoginIp lastPrimaryEmailCheck signUpDate',
  402. cb
  403. )
  404. },
  405. userEmailsData(cb) {
  406. const result = { list: [], allInReconfirmNotificationPeriods: [] }
  407. UserGetter.getUserFullEmails(userId, (error, fullEmails) => {
  408. if (error && error instanceof V1ConnectionError) {
  409. return cb(null, result)
  410. }
  411. if (!Features.hasFeature('affiliations')) {
  412. result.list = fullEmails
  413. return cb(null, result)
  414. }
  415. Modules.hooks.fire(
  416. 'allInReconfirmNotificationPeriodsForUser',
  417. fullEmails,
  418. (error, results) => {
  419. if (error != null) {
  420. return cb(error)
  421. }
  422. // Module.hooks.fire accepts multiple methods
  423. // and does async.series
  424. const allInReconfirmNotificationPeriods =
  425. (results && results[0]) || []
  426. return cb(null, {
  427. list: fullEmails,
  428. allInReconfirmNotificationPeriods,
  429. })
  430. }
  431. )
  432. })
  433. },
  434. usersBestSubscription(cb) {
  435. if (!Features.hasFeature('saas')) {
  436. return cb()
  437. }
  438. SubscriptionViewModelBuilder.getBestSubscription(
  439. { _id: userId },
  440. (err, subscription) => {
  441. if (err) {
  442. // do not fail loading the project list when fetching the best subscription fails
  443. logger.error(
  444. { userId, err },
  445. 'Could not get usersBestSubscription'
  446. )
  447. return cb(null, { type: 'error' })
  448. }
  449. cb(null, subscription)
  450. }
  451. )
  452. },
  453. primaryEmailCheckActive(cb) {
  454. SplitTestHandler.getAssignment(
  455. req,
  456. res,
  457. 'primary-email-check',
  458. (err, assignment) => {
  459. if (err) {
  460. logger.warn(
  461. { err },
  462. 'failed to get "primary-email-check" split test assignment'
  463. )
  464. cb(null, false)
  465. } else {
  466. cb(null, assignment.variant === 'active')
  467. }
  468. }
  469. )
  470. },
  471. survey(cb) {
  472. SurveyHandler.getSurvey(userId, (err, survey) => {
  473. if (err) {
  474. logger.warn({ err }, 'failed to get survey')
  475. // do not fail loading the project list if we fail to load the survey
  476. cb(null, null)
  477. } else {
  478. cb(null, survey)
  479. }
  480. })
  481. },
  482. },
  483. (err, results) => {
  484. if (err != null) {
  485. OError.tag(err, 'error getting data for project list page')
  486. return next(err)
  487. }
  488. const { notifications, user, userEmailsData, primaryEmailCheckActive } =
  489. results
  490. if (
  491. user &&
  492. primaryEmailCheckActive &&
  493. UserPrimaryEmailCheckHandler.requiresPrimaryEmailCheck(user)
  494. ) {
  495. return res.redirect('/user/emails/primary-email-check')
  496. }
  497. const userEmails = userEmailsData.list || []
  498. const userAffiliations = userEmails
  499. .filter(emailData => !!emailData.affiliation)
  500. .map(emailData => {
  501. const result = emailData.affiliation
  502. result.email = emailData.email
  503. return result
  504. })
  505. const { allInReconfirmNotificationPeriods } = userEmailsData
  506. // Handle case of deleted user
  507. if (user == null) {
  508. UserController.logout(req, res, next)
  509. return
  510. }
  511. const tags = results.tags
  512. const notificationsInstitution = []
  513. for (const notification of notifications) {
  514. notification.html = req.i18n.translate(
  515. notification.templateKey,
  516. notification.messageOpts
  517. )
  518. }
  519. // Institution SSO Notifications
  520. let reconfirmedViaSAML
  521. if (Features.hasFeature('saml')) {
  522. reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed'])
  523. const samlSession = req.session.saml
  524. // Notification: SSO Available
  525. const linkedInstitutionIds = []
  526. user.emails.forEach(email => {
  527. if (email.samlProviderId) {
  528. linkedInstitutionIds.push(email.samlProviderId)
  529. }
  530. })
  531. if (Array.isArray(userAffiliations)) {
  532. userAffiliations.forEach(affiliation => {
  533. if (
  534. _ssoAvailable(affiliation, req.session, linkedInstitutionIds)
  535. ) {
  536. notificationsInstitution.push({
  537. email: affiliation.email,
  538. institutionId: affiliation.institution.id,
  539. institutionName: affiliation.institution.name,
  540. templateKey: 'notification_institution_sso_available',
  541. })
  542. }
  543. })
  544. }
  545. if (samlSession) {
  546. // Notification: After SSO Linked
  547. if (samlSession.linked) {
  548. notificationsInstitution.push({
  549. email: samlSession.institutionEmail,
  550. institutionName: samlSession.linked.universityName,
  551. templateKey: 'notification_institution_sso_linked',
  552. })
  553. }
  554. // Notification: After SSO Linked or Logging in
  555. // The requested email does not match primary email returned from
  556. // the institution
  557. if (
  558. samlSession.requestedEmail &&
  559. samlSession.emailNonCanonical &&
  560. !samlSession.error
  561. ) {
  562. notificationsInstitution.push({
  563. institutionEmail: samlSession.emailNonCanonical,
  564. requestedEmail: samlSession.requestedEmail,
  565. templateKey: 'notification_institution_sso_non_canonical',
  566. })
  567. }
  568. // Notification: Tried to register, but account already existed
  569. // registerIntercept is set before the institution callback.
  570. // institutionEmail is set after institution callback.
  571. // Check for both in case SSO flow was abandoned
  572. if (
  573. samlSession.registerIntercept &&
  574. samlSession.institutionEmail &&
  575. !samlSession.error
  576. ) {
  577. notificationsInstitution.push({
  578. email: samlSession.institutionEmail,
  579. templateKey: 'notification_institution_sso_already_registered',
  580. })
  581. }
  582. // Notification: When there is a session error
  583. if (samlSession.error) {
  584. notificationsInstitution.push({
  585. templateKey: 'notification_institution_sso_error',
  586. error: samlSession.error,
  587. })
  588. }
  589. }
  590. delete req.session.saml
  591. }
  592. const portalTemplates =
  593. ProjectController._buildPortalTemplatesList(userAffiliations)
  594. const projects = ProjectController._buildProjectList(
  595. results.projects,
  596. userId
  597. )
  598. // in v2 add notifications for matching university IPs
  599. if (Settings.overleaf != null && req.ip !== user.lastLoginIp) {
  600. NotificationsBuilder.ipMatcherAffiliation(user._id).create(
  601. req.ip,
  602. () => {}
  603. )
  604. }
  605. ProjectController._injectProjectUsers(projects, (error, projects) => {
  606. if (error != null) {
  607. return next(error)
  608. }
  609. const viewModel = {
  610. title: 'your_projects',
  611. priority_title: true,
  612. projects,
  613. tags,
  614. notifications: notifications || [],
  615. notificationsInstitution,
  616. allInReconfirmNotificationPeriods,
  617. portalTemplates,
  618. user,
  619. userAffiliations,
  620. userEmails,
  621. hasSubscription: results.hasSubscription,
  622. reconfirmedViaSAML,
  623. zipFileSizeLimit: Settings.maxUploadSize,
  624. isOverleaf: !!Settings.overleaf,
  625. metadata: { viewport: false },
  626. showThinFooter: true, // don't show the fat footer on the projects dashboard, as there's a fixed space available
  627. usersBestSubscription: results.usersBestSubscription,
  628. survey: results.survey,
  629. }
  630. const paidUser =
  631. (user.features != null ? user.features.github : undefined) &&
  632. (user.features != null ? user.features.dropbox : undefined) // use a heuristic for paid account
  633. const freeUserProportion = 0.1
  634. const sampleFreeUser =
  635. parseInt(user._id.toString().slice(-2), 16) <
  636. freeUserProportion * 255
  637. const showFrontWidget = paidUser || sampleFreeUser
  638. if (showFrontWidget) {
  639. viewModel.frontChatWidgetRoomId =
  640. Settings.overleaf != null
  641. ? Settings.overleaf.front_chat_widget_room_id
  642. : undefined
  643. }
  644. // null test targeting logged in users
  645. SplitTestHandler.promises.getAssignment(
  646. req,
  647. res,
  648. 'null-test-dashboard'
  649. )
  650. res.render('project/list', viewModel)
  651. timer.done()
  652. })
  653. }
  654. )
  655. },
  656. loadEditor(req, res, next) {
  657. const timer = new metrics.Timer('load-editor')
  658. if (!Settings.editorIsOpen) {
  659. return res.render('general/closed', { title: 'updating_site' })
  660. }
  661. let anonymous, userId, sessionUser
  662. if (SessionManager.isUserLoggedIn(req.session)) {
  663. sessionUser = SessionManager.getSessionUser(req.session)
  664. userId = SessionManager.getLoggedInUserId(req.session)
  665. anonymous = false
  666. } else {
  667. sessionUser = null
  668. anonymous = true
  669. userId = null
  670. }
  671. const projectId = req.params.Project_id
  672. async.auto(
  673. {
  674. project(cb) {
  675. ProjectGetter.getProject(
  676. projectId,
  677. {
  678. name: 1,
  679. lastUpdated: 1,
  680. track_changes: 1,
  681. owner_ref: 1,
  682. brandVariationId: 1,
  683. overleaf: 1,
  684. tokens: 1,
  685. },
  686. (err, project) => {
  687. if (err != null) {
  688. return cb(err)
  689. }
  690. cb(null, project)
  691. }
  692. )
  693. },
  694. user(cb) {
  695. if (userId == null) {
  696. cb(null, defaultSettingsForAnonymousUser(userId))
  697. } else {
  698. User.updateOne(
  699. { _id: ObjectId(userId) },
  700. { $set: { lastActive: new Date() } },
  701. {},
  702. () => {}
  703. )
  704. User.findById(
  705. userId,
  706. 'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace labsProgram labsProgramGalileo',
  707. (err, user) => {
  708. // Handle case of deleted user
  709. if (user == null) {
  710. UserController.logout(req, res, next)
  711. return
  712. }
  713. if (err) {
  714. return cb(err)
  715. }
  716. logger.debug({ projectId, userId }, 'got user')
  717. if (FeaturesUpdater.featuresEpochIsCurrent(user)) {
  718. return cb(null, user)
  719. }
  720. ProjectController._refreshFeatures(req, user, cb)
  721. }
  722. )
  723. }
  724. },
  725. userHasInstitutionLicence(cb) {
  726. if (!userId) {
  727. return cb(null, false)
  728. }
  729. InstitutionsFeatures.hasLicence(userId, (error, hasLicence) => {
  730. if (error) {
  731. // Don't fail if we can't get affiliation licences
  732. return cb(null, false)
  733. }
  734. cb(null, hasLicence)
  735. })
  736. },
  737. learnedWords(cb) {
  738. if (!userId) {
  739. return cb(null, [])
  740. }
  741. SpellingHandler.getUserDictionary(userId, cb)
  742. },
  743. subscription(cb) {
  744. if (userId == null) {
  745. return cb()
  746. }
  747. SubscriptionLocator.getUsersSubscription(userId, cb)
  748. },
  749. userIsMemberOfGroupSubscription(cb) {
  750. if (sessionUser == null) {
  751. return cb(null, false)
  752. }
  753. LimitationsManager.userIsMemberOfGroupSubscription(
  754. sessionUser,
  755. (error, isMember) => {
  756. cb(error, isMember)
  757. }
  758. )
  759. },
  760. activate(cb) {
  761. InactiveProjectManager.reactivateProjectIfRequired(projectId, cb)
  762. },
  763. markAsOpened(cb) {
  764. // don't need to wait for this to complete
  765. ProjectUpdateHandler.markAsOpened(projectId, () => {})
  766. cb()
  767. },
  768. isTokenMember(cb) {
  769. if (userId == null) {
  770. return cb()
  771. }
  772. CollaboratorsGetter.userIsTokenMember(userId, projectId, cb)
  773. },
  774. isInvitedMember(cb) {
  775. CollaboratorsGetter.isUserInvitedMemberOfProject(
  776. userId,
  777. projectId,
  778. cb
  779. )
  780. },
  781. brandVariation: [
  782. 'project',
  783. (results, cb) => {
  784. if (
  785. (results.project != null
  786. ? results.project.brandVariationId
  787. : undefined) == null
  788. ) {
  789. return cb()
  790. }
  791. BrandVariationsHandler.getBrandVariationById(
  792. results.project.brandVariationId,
  793. (error, brandVariationDetails) => cb(error, brandVariationDetails)
  794. )
  795. },
  796. ],
  797. flushToTpds: cb => {
  798. TpdsProjectFlusher.flushProjectToTpdsIfNeeded(projectId, cb)
  799. },
  800. sharingModalSplitTest(cb) {
  801. SplitTestHandler.getAssignment(
  802. req,
  803. res,
  804. 'project-share-modal-paywall',
  805. {},
  806. () => {
  807. // do not fail editor load if assignment fails
  808. cb()
  809. }
  810. )
  811. },
  812. sharingModalNullTest(cb) {
  813. // null test targeting logged in users, for front-end side
  814. SplitTestHandler.getAssignment(
  815. req,
  816. res,
  817. 'null-test-share-modal',
  818. {},
  819. () => {
  820. // do not fail editor load if assignment fails
  821. cb()
  822. }
  823. )
  824. },
  825. newSourceEditorAssignment(cb) {
  826. SplitTestHandler.getAssignment(
  827. req,
  828. res,
  829. 'source-editor',
  830. {},
  831. (error, assignment) => {
  832. // do not fail editor load if assignment fails
  833. if (error) {
  834. cb(null)
  835. } else {
  836. cb(null, assignment)
  837. }
  838. }
  839. )
  840. },
  841. pdfjsAssignment(cb) {
  842. SplitTestHandler.getAssignment(
  843. req,
  844. res,
  845. 'pdfjs',
  846. {},
  847. (error, assignment) => {
  848. // do not fail editor load if assignment fails
  849. if (error) {
  850. cb(null, { variant: 'default' })
  851. } else {
  852. cb(null, assignment)
  853. }
  854. }
  855. )
  856. },
  857. dictionaryEditorAssignment(cb) {
  858. SplitTestHandler.getAssignment(
  859. req,
  860. res,
  861. 'dictionary-editor',
  862. {},
  863. (error, assignment) => {
  864. // do not fail editor load if assignment fails
  865. if (error) {
  866. cb(null, { variant: 'default' })
  867. } else {
  868. cb(null, assignment)
  869. }
  870. }
  871. )
  872. },
  873. interstitialPaymentFromPaywallAssignment(cb) {
  874. SplitTestHandler.getAssignment(
  875. req,
  876. res,
  877. 'interstitial-payment-from-paywall',
  878. (error, assignment) => {
  879. // do not fail editor load if assignment fails
  880. if (error) {
  881. cb(null, { variant: 'default' })
  882. } else {
  883. cb(null, assignment)
  884. }
  885. }
  886. )
  887. },
  888. latexLogParserAssignment(cb) {
  889. SplitTestHandler.getAssignment(
  890. req,
  891. res,
  892. 'latex-log-parser',
  893. (error, assignment) => {
  894. // do not fail editor load if assignment fails
  895. if (error) {
  896. cb(null, { variant: 'default' })
  897. } else {
  898. cb(null, assignment)
  899. }
  900. }
  901. )
  902. },
  903. compileTimeWarningAssignment: [
  904. 'user',
  905. (results, cb) => {
  906. if (results.user?.features?.compileTimeout <= 60) {
  907. SplitTestHandler.getAssignment(
  908. req,
  909. res,
  910. 'compile-time-warning',
  911. {},
  912. () => {
  913. // do not fail editor load if assignment fails
  914. cb()
  915. }
  916. )
  917. } else {
  918. cb()
  919. }
  920. },
  921. ],
  922. linkSharingUpgradePromptAssignment(cb) {
  923. SplitTestHandler.getAssignment(
  924. req,
  925. res,
  926. 'link-sharing-upgrade-prompt',
  927. (error, assignment) => {
  928. // do not fail editor load if assignment fails
  929. if (error) {
  930. cb(null, { variant: 'default' })
  931. } else {
  932. cb(null, assignment)
  933. }
  934. }
  935. )
  936. },
  937. trackPdfDownloadAssignment(cb) {
  938. SplitTestHandler.getAssignment(req, res, 'track-pdf-download', () => {
  939. // We'll pick up the assignment from the res.locals assignment.
  940. cb()
  941. })
  942. },
  943. pdfCachingModeAssignment(cb) {
  944. SplitTestHandler.getAssignment(req, res, 'pdf-caching-mode', () => {
  945. // We'll pick up the assignment from the res.locals assignment.
  946. cb()
  947. })
  948. },
  949. pdfCachingPrefetchingAssignment(cb) {
  950. SplitTestHandler.getAssignment(
  951. req,
  952. res,
  953. 'pdf-caching-prefetching',
  954. () => {
  955. // We'll pick up the assignment from the res.locals assignment.
  956. cb()
  957. }
  958. )
  959. },
  960. pdfCachingPrefetchLargeAssignment(cb) {
  961. SplitTestHandler.getAssignment(
  962. req,
  963. res,
  964. 'pdf-caching-prefetch-large',
  965. () => {
  966. // We'll pick up the assignment from the res.locals assignment.
  967. cb()
  968. }
  969. )
  970. },
  971. pdfCachingCachedUrlLookupAssignment(cb) {
  972. SplitTestHandler.getAssignment(
  973. req,
  974. res,
  975. 'pdf-caching-cached-url-lookup',
  976. () => {
  977. // We'll pick up the assignment from the res.locals assignment.
  978. cb()
  979. }
  980. )
  981. },
  982. },
  983. (
  984. err,
  985. {
  986. project,
  987. user,
  988. userHasInstitutionLicence,
  989. learnedWords,
  990. subscription,
  991. userIsMemberOfGroupSubscription,
  992. isTokenMember,
  993. isInvitedMember,
  994. brandVariation,
  995. newSourceEditorAssignment,
  996. pdfjsAssignment,
  997. dictionaryEditorAssignment,
  998. }
  999. ) => {
  1000. if (err != null) {
  1001. OError.tag(err, 'error getting details for project page')
  1002. return next(err)
  1003. }
  1004. const anonRequestToken = TokenAccessHandler.getRequestToken(
  1005. req,
  1006. projectId
  1007. )
  1008. const allowedImageNames = ProjectHelper.getAllowedImagesForUser(user)
  1009. AuthorizationManager.getPrivilegeLevelForProject(
  1010. userId,
  1011. projectId,
  1012. anonRequestToken,
  1013. (error, privilegeLevel) => {
  1014. let allowedFreeTrial = true
  1015. if (error != null) {
  1016. return next(error)
  1017. }
  1018. if (
  1019. privilegeLevel == null ||
  1020. privilegeLevel === PrivilegeLevels.NONE
  1021. ) {
  1022. return res.sendStatus(401)
  1023. }
  1024. if (subscription != null) {
  1025. allowedFreeTrial = false
  1026. }
  1027. let wsUrl = Settings.wsUrl
  1028. let metricName = 'load-editor-ws'
  1029. if (user.betaProgram && Settings.wsUrlBeta !== undefined) {
  1030. wsUrl = Settings.wsUrlBeta
  1031. metricName += '-beta'
  1032. } else if (
  1033. Settings.wsUrlV2 &&
  1034. Settings.wsUrlV2Percentage > 0 &&
  1035. (ObjectId(projectId).getTimestamp() / 1000) % 100 <
  1036. Settings.wsUrlV2Percentage
  1037. ) {
  1038. wsUrl = Settings.wsUrlV2
  1039. metricName += '-v2'
  1040. }
  1041. if (req.query && req.query.ws === 'fallback') {
  1042. // `?ws=fallback` will connect to the bare origin, and ignore
  1043. // the custom wsUrl. Hence it must load the client side
  1044. // javascript from there too.
  1045. // Not resetting it here would possibly load a socket.io v2
  1046. // client and connect to a v0 endpoint.
  1047. wsUrl = undefined
  1048. metricName += '-fallback'
  1049. }
  1050. metrics.inc(metricName)
  1051. if (userId) {
  1052. AnalyticsManager.recordEventForUser(userId, 'project-opened', {
  1053. projectId: project._id,
  1054. })
  1055. }
  1056. // should not be used in place of split tests query param overrides (?my-split-test-name=my-variant)
  1057. function shouldDisplayFeature(name, variantFlag) {
  1058. if (req.query && req.query[name]) {
  1059. return req.query[name] === 'true'
  1060. } else {
  1061. return variantFlag === true
  1062. }
  1063. }
  1064. const debugPdfDetach = shouldDisplayFeature('debug_pdf_detach')
  1065. const detachRole = req.params.detachRole
  1066. const showNewSourceEditorOption =
  1067. newSourceEditorAssignment?.variant === 'codemirror' ||
  1068. user.betaProgram ||
  1069. shouldDisplayFeature('new_source_editor', false) // also allow override via ?new_source_editor=true
  1070. const showSymbolPalette =
  1071. !Features.hasFeature('saas') ||
  1072. (user.features && user.features.symbolPalette)
  1073. // It would be nice if this could go in the Galileo module but
  1074. // nothing else does that
  1075. const galileoEnabled = req.query?.galileo || ''
  1076. const galileoFeatures =
  1077. req.query && 'galileoFeatures' in req.query
  1078. ? req.query.galileoFeatures.split(',').map(f => f.trim())
  1079. : ['all']
  1080. const dictionaryEditorEnabled =
  1081. !Features.hasFeature('saas') ||
  1082. dictionaryEditorAssignment?.variant === 'enabled'
  1083. // Persistent upgrade prompts
  1084. // in header & in share project modal
  1085. const showUpgradePrompt =
  1086. Features.hasFeature('saas') &&
  1087. userId &&
  1088. !subscription &&
  1089. !userIsMemberOfGroupSubscription &&
  1090. !userHasInstitutionLicence
  1091. const template =
  1092. detachRole === 'detached'
  1093. ? 'project/editor_detached'
  1094. : 'project/editor'
  1095. res.render(template, {
  1096. title: project.name,
  1097. priority_title: true,
  1098. bodyClasses: ['editor'],
  1099. project_id: project._id,
  1100. projectName: project.name,
  1101. user: {
  1102. id: userId,
  1103. email: user.email,
  1104. first_name: user.first_name,
  1105. last_name: user.last_name,
  1106. referal_id: user.referal_id,
  1107. signUpDate: user.signUpDate,
  1108. allowedFreeTrial,
  1109. featureSwitches: user.featureSwitches,
  1110. features: user.features,
  1111. refProviders: _.mapValues(user.refProviders, Boolean),
  1112. alphaProgram: user.alphaProgram,
  1113. betaProgram: user.betaProgram,
  1114. labsProgram: user.labsProgram,
  1115. labsProgramGalileo: user.labsProgramGalileo,
  1116. isAdmin: hasAdminAccess(user),
  1117. },
  1118. userSettings: {
  1119. mode: user.ace.mode,
  1120. editorTheme: user.ace.theme,
  1121. fontSize: user.ace.fontSize,
  1122. autoComplete: user.ace.autoComplete,
  1123. autoPairDelimiters: user.ace.autoPairDelimiters,
  1124. pdfViewer: user.ace.pdfViewer,
  1125. syntaxValidation: user.ace.syntaxValidation,
  1126. fontFamily: user.ace.fontFamily || 'lucida',
  1127. lineHeight: user.ace.lineHeight || 'normal',
  1128. overallTheme: user.ace.overallTheme,
  1129. },
  1130. privilegeLevel,
  1131. anonymous,
  1132. anonymousAccessToken: anonymous ? anonRequestToken : null,
  1133. isTokenMember,
  1134. isRestrictedTokenMember: AuthorizationManager.isRestrictedUser(
  1135. userId,
  1136. privilegeLevel,
  1137. isTokenMember,
  1138. isInvitedMember
  1139. ),
  1140. languages: Settings.languages,
  1141. learnedWords,
  1142. editorThemes: THEME_LIST,
  1143. legacyEditorThemes: LEGACY_THEME_LIST,
  1144. maxDocLength: Settings.max_doc_length,
  1145. useV2History:
  1146. project.overleaf &&
  1147. project.overleaf.history &&
  1148. Boolean(project.overleaf.history.display),
  1149. brandVariation,
  1150. allowedImageNames,
  1151. gitBridgePublicBaseUrl: Settings.gitBridgePublicBaseUrl,
  1152. wsUrl,
  1153. showSupport: Features.hasFeature('support'),
  1154. pdfjsVariant: pdfjsAssignment.variant,
  1155. dictionaryEditorEnabled,
  1156. debugPdfDetach,
  1157. showNewSourceEditorOption,
  1158. showSymbolPalette,
  1159. galileoEnabled,
  1160. galileoFeatures,
  1161. detachRole,
  1162. metadata: { viewport: false },
  1163. showUpgradePrompt,
  1164. fixedSizeDocument: true,
  1165. })
  1166. timer.done()
  1167. }
  1168. )
  1169. }
  1170. )
  1171. },
  1172. _refreshFeatures(req, user, callback) {
  1173. // If the feature refresh has failed in this session, don't retry
  1174. // it - require the user to log in again.
  1175. if (req.session.feature_refresh_failed) {
  1176. metrics.inc('features-refresh', 1, {
  1177. path: 'load-editor',
  1178. status: 'skipped',
  1179. })
  1180. return callback(null, user)
  1181. }
  1182. // If the refresh takes too long then return the current
  1183. // features. Note that the user.features property may still be
  1184. // updated in the background after the callback is called.
  1185. callback = _.once(callback)
  1186. const refreshTimeoutHandler = setTimeout(() => {
  1187. req.session.feature_refresh_failed = { reason: 'timeout', at: new Date() }
  1188. metrics.inc('features-refresh', 1, {
  1189. path: 'load-editor',
  1190. status: 'timeout',
  1191. })
  1192. callback(null, user)
  1193. }, 5000)
  1194. // try to refresh user features now
  1195. const timer = new metrics.Timer('features-refresh-on-load-editor')
  1196. FeaturesUpdater.refreshFeatures(
  1197. user._id,
  1198. 'load-editor',
  1199. (err, features) => {
  1200. clearTimeout(refreshTimeoutHandler)
  1201. timer.done()
  1202. if (err) {
  1203. // keep a record to prevent unneceary retries and leave
  1204. // the original features unmodified if the refresh failed
  1205. req.session.feature_refresh_failed = {
  1206. reason: 'error',
  1207. at: new Date(),
  1208. }
  1209. metrics.inc('features-refresh', 1, {
  1210. path: 'load-editor',
  1211. status: 'error',
  1212. })
  1213. } else {
  1214. user.features = features
  1215. metrics.inc('features-refresh', 1, {
  1216. path: 'load-editor',
  1217. status: 'success',
  1218. })
  1219. }
  1220. return callback(null, user)
  1221. }
  1222. )
  1223. },
  1224. _buildProjectList(allProjects, userId) {
  1225. let project
  1226. const { owned, readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly } =
  1227. allProjects
  1228. const projects = []
  1229. for (project of owned) {
  1230. projects.push(
  1231. ProjectController._buildProjectViewModel(
  1232. project,
  1233. 'owner',
  1234. Sources.OWNER,
  1235. userId
  1236. )
  1237. )
  1238. }
  1239. // Invite-access
  1240. for (project of readAndWrite) {
  1241. projects.push(
  1242. ProjectController._buildProjectViewModel(
  1243. project,
  1244. 'readWrite',
  1245. Sources.INVITE,
  1246. userId
  1247. )
  1248. )
  1249. }
  1250. for (project of readOnly) {
  1251. projects.push(
  1252. ProjectController._buildProjectViewModel(
  1253. project,
  1254. 'readOnly',
  1255. Sources.INVITE,
  1256. userId
  1257. )
  1258. )
  1259. }
  1260. // Token-access
  1261. // Only add these projects if they're not already present, this gives us cascading access
  1262. // from 'owner' => 'token-read-only'
  1263. for (project of tokenReadAndWrite) {
  1264. if (
  1265. projects.filter(p => p.id.toString() === project._id.toString())
  1266. .length === 0
  1267. ) {
  1268. projects.push(
  1269. ProjectController._buildProjectViewModel(
  1270. project,
  1271. 'readAndWrite',
  1272. Sources.TOKEN,
  1273. userId
  1274. )
  1275. )
  1276. }
  1277. }
  1278. for (project of tokenReadOnly) {
  1279. if (
  1280. projects.filter(p => p.id.toString() === project._id.toString())
  1281. .length === 0
  1282. ) {
  1283. projects.push(
  1284. ProjectController._buildProjectViewModel(
  1285. project,
  1286. 'readOnly',
  1287. Sources.TOKEN,
  1288. userId
  1289. )
  1290. )
  1291. }
  1292. }
  1293. return projects
  1294. },
  1295. _buildProjectViewModel(project, accessLevel, source, userId) {
  1296. const archived = ProjectHelper.isArchived(project, userId)
  1297. // If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
  1298. const trashed = ProjectHelper.isTrashed(project, userId) && !archived
  1299. TokenAccessHandler.protectTokens(project, accessLevel)
  1300. const model = {
  1301. id: project._id,
  1302. name: project.name,
  1303. lastUpdated: project.lastUpdated,
  1304. lastUpdatedBy: project.lastUpdatedBy,
  1305. publicAccessLevel: project.publicAccesLevel,
  1306. accessLevel,
  1307. source,
  1308. archived,
  1309. trashed,
  1310. owner_ref: project.owner_ref,
  1311. isV1Project: false,
  1312. }
  1313. if (accessLevel === PrivilegeLevels.READ_ONLY && source === Sources.TOKEN) {
  1314. model.owner_ref = null
  1315. model.lastUpdatedBy = null
  1316. }
  1317. return model
  1318. },
  1319. _injectProjectUsers(projects, callback) {
  1320. const users = {}
  1321. for (const project of projects) {
  1322. if (project.owner_ref != null) {
  1323. users[project.owner_ref.toString()] = true
  1324. }
  1325. if (project.lastUpdatedBy != null) {
  1326. users[project.lastUpdatedBy.toString()] = true
  1327. }
  1328. }
  1329. const userIds = Object.keys(users)
  1330. async.eachSeries(
  1331. userIds,
  1332. (userId, cb) => {
  1333. UserGetter.getUser(
  1334. userId,
  1335. { first_name: 1, last_name: 1, email: 1 },
  1336. (error, user) => {
  1337. if (error != null) {
  1338. return cb(error)
  1339. }
  1340. users[userId] = user
  1341. cb()
  1342. }
  1343. )
  1344. },
  1345. error => {
  1346. if (error != null) {
  1347. return callback(error)
  1348. }
  1349. for (const project of projects) {
  1350. if (project.owner_ref != null) {
  1351. project.owner = users[project.owner_ref.toString()]
  1352. }
  1353. if (project.lastUpdatedBy != null) {
  1354. project.lastUpdatedBy =
  1355. users[project.lastUpdatedBy.toString()] || null
  1356. }
  1357. }
  1358. callback(null, projects)
  1359. }
  1360. )
  1361. },
  1362. _buildPortalTemplatesList(affiliations) {
  1363. if (affiliations == null) {
  1364. affiliations = []
  1365. }
  1366. const portalTemplates = []
  1367. for (const aff of affiliations) {
  1368. if (
  1369. aff.portal &&
  1370. aff.portal.slug &&
  1371. aff.portal.templates_count &&
  1372. aff.portal.templates_count > 0
  1373. ) {
  1374. const portalPath = aff.institution.isUniversity ? '/edu/' : '/org/'
  1375. portalTemplates.push({
  1376. name: aff.institution.name,
  1377. url: Settings.siteUrl + portalPath + aff.portal.slug,
  1378. })
  1379. }
  1380. }
  1381. return portalTemplates
  1382. },
  1383. }
  1384. const defaultSettingsForAnonymousUser = userId => ({
  1385. id: userId,
  1386. ace: {
  1387. mode: 'none',
  1388. theme: 'textmate',
  1389. fontSize: '12',
  1390. autoComplete: true,
  1391. spellCheckLanguage: '',
  1392. pdfViewer: '',
  1393. syntaxValidation: true,
  1394. },
  1395. subscription: {
  1396. freeTrial: {
  1397. allowed: true,
  1398. },
  1399. },
  1400. featureSwitches: {
  1401. github: false,
  1402. },
  1403. alphaProgram: false,
  1404. betaProgram: false,
  1405. })
  1406. const THEME_LIST = [
  1407. 'cobalt',
  1408. 'dracula',
  1409. 'eclipse',
  1410. 'monokai',
  1411. 'overleaf',
  1412. 'textmate',
  1413. ]
  1414. const LEGACY_THEME_LIST = [
  1415. 'ambiance',
  1416. 'chaos',
  1417. 'chrome',
  1418. 'clouds',
  1419. 'clouds_midnight',
  1420. 'crimson_editor',
  1421. 'dawn',
  1422. 'dreamweaver',
  1423. 'github',
  1424. 'gob',
  1425. 'gruvbox',
  1426. 'idle_fingers',
  1427. 'iplastic',
  1428. 'katzenmilch',
  1429. 'kr_theme',
  1430. 'kuroir',
  1431. 'merbivore',
  1432. 'merbivore_soft',
  1433. 'mono_industrial',
  1434. 'nord_dark',
  1435. 'pastel_on_dark',
  1436. 'solarized_dark',
  1437. 'solarized_light',
  1438. 'sqlserver',
  1439. 'terminal',
  1440. 'tomorrow',
  1441. 'tomorrow_night',
  1442. 'tomorrow_night_blue',
  1443. 'tomorrow_night_bright',
  1444. 'tomorrow_night_eighties',
  1445. 'twilight',
  1446. 'vibrant_ink',
  1447. 'xcode',
  1448. ]
  1449. ProjectController.projectListPage = expressify(
  1450. ProjectController.projectListPage
  1451. )
  1452. module.exports = ProjectController