ProjectController.js 34 KB

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