ProjectController.js 48 KB

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