ProjectController.js 30 KB

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