ProjectController.js 33 KB

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