ProjectController.js 30 KB

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