EditorController.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. import logger from '@overleaf/logger'
  2. import OError from '@overleaf/o-error'
  3. import Metrics from '@overleaf/metrics'
  4. import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.mjs'
  5. import ProjectOptionsHandler from '../Project/ProjectOptionsHandler.js'
  6. import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
  7. import ProjectDeleter from '../Project/ProjectDeleter.mjs'
  8. import EditorRealTimeController from './EditorRealTimeController.js'
  9. import async from 'async'
  10. import PublicAccessLevels from '../Authorization/PublicAccessLevels.js'
  11. import { promisifyAll } from '@overleaf/promise-utils'
  12. const EditorController = {
  13. addDoc(projectId, folderId, docName, docLines, source, userId, callback) {
  14. EditorController.addDocWithRanges(
  15. projectId,
  16. folderId,
  17. docName,
  18. docLines,
  19. {},
  20. source,
  21. userId,
  22. callback
  23. )
  24. },
  25. addDocWithRanges(
  26. projectId,
  27. folderId,
  28. docName,
  29. docLines,
  30. docRanges,
  31. source,
  32. userId,
  33. callback
  34. ) {
  35. docName = docName.trim()
  36. Metrics.inc('editor.add-doc')
  37. ProjectEntityUpdateHandler.addDocWithRanges(
  38. projectId,
  39. folderId,
  40. docName,
  41. docLines,
  42. docRanges,
  43. userId,
  44. source,
  45. (err, doc, folderId) => {
  46. if (err) {
  47. OError.tag(err, 'error adding doc without lock', {
  48. projectId,
  49. docName,
  50. })
  51. return callback(err)
  52. }
  53. EditorRealTimeController.emitToRoom(
  54. projectId,
  55. 'reciveNewDoc',
  56. folderId,
  57. doc,
  58. source,
  59. userId
  60. )
  61. callback(err, doc)
  62. }
  63. )
  64. },
  65. addFile(
  66. projectId,
  67. folderId,
  68. fileName,
  69. fsPath,
  70. linkedFileData,
  71. source,
  72. userId,
  73. callback
  74. ) {
  75. fileName = fileName.trim()
  76. Metrics.inc('editor.add-file')
  77. ProjectEntityUpdateHandler.addFile(
  78. projectId,
  79. folderId,
  80. fileName,
  81. fsPath,
  82. linkedFileData,
  83. userId,
  84. source,
  85. (err, fileRef, folderId) => {
  86. if (err) {
  87. OError.tag(err, 'error adding file without lock', {
  88. projectId,
  89. folderId,
  90. fileName,
  91. })
  92. return callback(err)
  93. }
  94. EditorRealTimeController.emitToRoom(
  95. projectId,
  96. 'reciveNewFile',
  97. folderId,
  98. fileRef,
  99. source,
  100. linkedFileData,
  101. userId
  102. )
  103. callback(err, fileRef)
  104. }
  105. )
  106. },
  107. appendToDoc(projectId, docId, docLines, source, userId, callback) {
  108. ProjectEntityUpdateHandler.appendToDoc(
  109. projectId,
  110. docId,
  111. docLines,
  112. source,
  113. userId,
  114. function (err, doc) {
  115. if (err) {
  116. OError.tag(err, 'error appending to doc', {
  117. projectId,
  118. docId,
  119. })
  120. return callback(err)
  121. }
  122. callback(err, doc)
  123. }
  124. )
  125. },
  126. upsertDoc(projectId, folderId, docName, docLines, source, userId, callback) {
  127. ProjectEntityUpdateHandler.upsertDoc(
  128. projectId,
  129. folderId,
  130. docName,
  131. docLines,
  132. source,
  133. userId,
  134. function (err, doc, didAddNewDoc) {
  135. if (didAddNewDoc) {
  136. EditorRealTimeController.emitToRoom(
  137. projectId,
  138. 'reciveNewDoc',
  139. folderId,
  140. doc,
  141. source,
  142. userId
  143. )
  144. }
  145. callback(err, doc)
  146. }
  147. )
  148. },
  149. upsertFile(
  150. projectId,
  151. folderId,
  152. fileName,
  153. fsPath,
  154. linkedFileData,
  155. source,
  156. userId,
  157. callback
  158. ) {
  159. ProjectEntityUpdateHandler.upsertFile(
  160. projectId,
  161. folderId,
  162. fileName,
  163. fsPath,
  164. linkedFileData,
  165. userId,
  166. source,
  167. function (err, newFile, didAddFile, existingFile) {
  168. if (err) {
  169. return callback(err)
  170. }
  171. if (!didAddFile) {
  172. // replacement, so remove the existing file from the client
  173. EditorRealTimeController.emitToRoom(
  174. projectId,
  175. 'removeEntity',
  176. existingFile._id,
  177. source
  178. )
  179. }
  180. // now add the new file on the client
  181. EditorRealTimeController.emitToRoom(
  182. projectId,
  183. 'reciveNewFile',
  184. folderId,
  185. newFile,
  186. source,
  187. linkedFileData,
  188. userId
  189. )
  190. callback(null, newFile)
  191. }
  192. )
  193. },
  194. upsertDocWithPath(
  195. projectId,
  196. elementPath,
  197. docLines,
  198. source,
  199. userId,
  200. callback
  201. ) {
  202. ProjectEntityUpdateHandler.upsertDocWithPath(
  203. projectId,
  204. elementPath,
  205. docLines,
  206. source,
  207. userId,
  208. function (err, doc, didAddNewDoc, newFolders, lastFolder) {
  209. if (err) {
  210. return callback(err)
  211. }
  212. EditorController._notifyProjectUsersOfNewFolders(
  213. projectId,
  214. newFolders,
  215. function (err) {
  216. if (err) {
  217. return callback(err)
  218. }
  219. if (didAddNewDoc) {
  220. EditorRealTimeController.emitToRoom(
  221. projectId,
  222. 'reciveNewDoc',
  223. lastFolder._id,
  224. doc,
  225. source,
  226. userId
  227. )
  228. }
  229. callback(null, { doc, folder: lastFolder })
  230. }
  231. )
  232. }
  233. )
  234. },
  235. upsertFileWithPath(
  236. projectId,
  237. elementPath,
  238. fsPath,
  239. linkedFileData,
  240. source,
  241. userId,
  242. callback
  243. ) {
  244. ProjectEntityUpdateHandler.upsertFileWithPath(
  245. projectId,
  246. elementPath,
  247. fsPath,
  248. linkedFileData,
  249. userId,
  250. source,
  251. function (
  252. err,
  253. newFile,
  254. didAddFile,
  255. existingFile,
  256. newFolders,
  257. lastFolder
  258. ) {
  259. if (err) {
  260. return callback(err)
  261. }
  262. EditorController._notifyProjectUsersOfNewFolders(
  263. projectId,
  264. newFolders,
  265. function (err) {
  266. if (err) {
  267. return callback(err)
  268. }
  269. if (!didAddFile) {
  270. // replacement, so remove the existing file from the client
  271. EditorRealTimeController.emitToRoom(
  272. projectId,
  273. 'removeEntity',
  274. existingFile._id,
  275. source
  276. )
  277. }
  278. // now add the new file on the client
  279. EditorRealTimeController.emitToRoom(
  280. projectId,
  281. 'reciveNewFile',
  282. lastFolder._id,
  283. newFile,
  284. source,
  285. linkedFileData,
  286. userId
  287. )
  288. callback(null, { file: newFile, folder: lastFolder })
  289. }
  290. )
  291. }
  292. )
  293. },
  294. addFolder(projectId, folderId, folderName, source, userId, callback) {
  295. folderName = folderName.trim()
  296. Metrics.inc('editor.add-folder')
  297. ProjectEntityUpdateHandler.addFolder(
  298. projectId,
  299. folderId,
  300. folderName,
  301. userId,
  302. (err, folder, folderId) => {
  303. if (err) {
  304. OError.tag(err, 'could not add folder', {
  305. projectId,
  306. folderId,
  307. folderName,
  308. source,
  309. })
  310. return callback(err)
  311. }
  312. EditorController._notifyProjectUsersOfNewFolder(
  313. projectId,
  314. folderId,
  315. folder,
  316. userId,
  317. function (err) {
  318. if (err) {
  319. return callback(err)
  320. }
  321. callback(null, folder)
  322. }
  323. )
  324. }
  325. )
  326. },
  327. mkdirp(projectId, path, userId, callback) {
  328. logger.debug({ projectId, path }, "making directories if they don't exist")
  329. ProjectEntityUpdateHandler.mkdirp(
  330. projectId,
  331. path,
  332. userId,
  333. (err, newFolders, lastFolder) => {
  334. if (err) {
  335. OError.tag(err, 'could not mkdirp', {
  336. projectId,
  337. path,
  338. })
  339. return callback(err)
  340. }
  341. EditorController._notifyProjectUsersOfNewFolders(
  342. projectId,
  343. newFolders,
  344. function (err) {
  345. if (err) {
  346. return callback(err)
  347. }
  348. callback(null, newFolders, lastFolder)
  349. }
  350. )
  351. }
  352. )
  353. },
  354. deleteEntity(projectId, entityId, entityType, source, userId, callback) {
  355. Metrics.inc('editor.delete-entity')
  356. ProjectEntityUpdateHandler.deleteEntity(
  357. projectId,
  358. entityId,
  359. entityType,
  360. userId,
  361. source,
  362. function (err) {
  363. if (err) {
  364. OError.tag(err, 'could not delete entity', {
  365. projectId,
  366. entityId,
  367. entityType,
  368. })
  369. return callback(err)
  370. }
  371. logger.debug(
  372. { projectId, entityId, entityType },
  373. 'telling users entity has been deleted'
  374. )
  375. EditorRealTimeController.emitToRoom(
  376. projectId,
  377. 'removeEntity',
  378. entityId,
  379. source
  380. )
  381. callback()
  382. }
  383. )
  384. },
  385. deleteEntityWithPath(projectId, path, source, userId, callback) {
  386. ProjectEntityUpdateHandler.deleteEntityWithPath(
  387. projectId,
  388. path,
  389. userId,
  390. source,
  391. function (err, entityId) {
  392. if (err) {
  393. return callback(err)
  394. }
  395. EditorRealTimeController.emitToRoom(
  396. projectId,
  397. 'removeEntity',
  398. entityId,
  399. source
  400. )
  401. callback(null, entityId)
  402. }
  403. )
  404. },
  405. updateProjectDescription(projectId, description, callback) {
  406. logger.debug({ projectId, description }, 'updating project description')
  407. ProjectDetailsHandler.setProjectDescription(
  408. projectId,
  409. description,
  410. function (err) {
  411. if (err) {
  412. OError.tag(
  413. err,
  414. 'something went wrong setting the project description',
  415. {
  416. projectId,
  417. description,
  418. }
  419. )
  420. return callback(err)
  421. }
  422. EditorRealTimeController.emitToRoom(
  423. projectId,
  424. 'projectDescriptionUpdated',
  425. description
  426. )
  427. callback()
  428. }
  429. )
  430. },
  431. deleteProject(projectId, callback) {
  432. Metrics.inc('editor.delete-project')
  433. ProjectDeleter.deleteProject(projectId, callback)
  434. },
  435. renameEntity(
  436. projectId,
  437. entityId,
  438. entityType,
  439. newName,
  440. userId,
  441. source,
  442. callback
  443. ) {
  444. Metrics.inc('editor.rename-entity')
  445. ProjectEntityUpdateHandler.renameEntity(
  446. projectId,
  447. entityId,
  448. entityType,
  449. newName,
  450. userId,
  451. source,
  452. function (err) {
  453. if (err) {
  454. OError.tag(err, 'error renaming entity', {
  455. projectId,
  456. entityId,
  457. entityType,
  458. newName,
  459. })
  460. return callback(err)
  461. }
  462. if (newName.length > 0) {
  463. EditorRealTimeController.emitToRoom(
  464. projectId,
  465. 'reciveEntityRename',
  466. entityId,
  467. newName
  468. )
  469. }
  470. callback()
  471. }
  472. )
  473. },
  474. moveEntity(
  475. projectId,
  476. entityId,
  477. folderId,
  478. entityType,
  479. userId,
  480. source,
  481. callback
  482. ) {
  483. Metrics.inc('editor.move-entity')
  484. ProjectEntityUpdateHandler.moveEntity(
  485. projectId,
  486. entityId,
  487. folderId,
  488. entityType,
  489. userId,
  490. source,
  491. function (err) {
  492. if (err) {
  493. OError.tag(err, 'error moving entity', {
  494. projectId,
  495. entityId,
  496. folderId,
  497. })
  498. return callback(err)
  499. }
  500. EditorRealTimeController.emitToRoom(
  501. projectId,
  502. 'reciveEntityMove',
  503. entityId,
  504. folderId
  505. )
  506. callback()
  507. }
  508. )
  509. },
  510. renameProject(projectId, newName, callback) {
  511. ProjectDetailsHandler.renameProject(projectId, newName, function (err) {
  512. if (err) {
  513. OError.tag(err, 'error renaming project', {
  514. projectId,
  515. newName,
  516. })
  517. return callback(err)
  518. }
  519. EditorRealTimeController.emitToRoom(
  520. projectId,
  521. 'projectNameUpdated',
  522. newName
  523. )
  524. callback()
  525. })
  526. },
  527. setCompiler(projectId, compiler, callback) {
  528. ProjectOptionsHandler.setCompiler(projectId, compiler, function (err) {
  529. if (err) {
  530. return callback(err)
  531. }
  532. EditorRealTimeController.emitToRoom(
  533. projectId,
  534. 'compilerUpdated',
  535. compiler
  536. )
  537. callback()
  538. })
  539. },
  540. setImageName(projectId, imageName, callback) {
  541. ProjectOptionsHandler.setImageName(projectId, imageName, function (err) {
  542. if (err) {
  543. return callback(err)
  544. }
  545. EditorRealTimeController.emitToRoom(
  546. projectId,
  547. 'imageNameUpdated',
  548. imageName
  549. )
  550. callback()
  551. })
  552. },
  553. setSpellCheckLanguage(projectId, languageCode, callback) {
  554. ProjectOptionsHandler.setSpellCheckLanguage(
  555. projectId,
  556. languageCode,
  557. function (err) {
  558. if (err) {
  559. return callback(err)
  560. }
  561. EditorRealTimeController.emitToRoom(
  562. projectId,
  563. 'spellCheckLanguageUpdated',
  564. languageCode
  565. )
  566. callback()
  567. }
  568. )
  569. },
  570. setPublicAccessLevel(projectId, newAccessLevel, callback) {
  571. async.series(
  572. [
  573. cb => {
  574. if (newAccessLevel === PublicAccessLevels.TOKEN_BASED) {
  575. ProjectDetailsHandler.ensureTokensArePresent(projectId, cb)
  576. } else {
  577. cb()
  578. }
  579. },
  580. cb =>
  581. ProjectDetailsHandler.setPublicAccessLevel(
  582. projectId,
  583. newAccessLevel,
  584. cb
  585. ),
  586. cb => {
  587. EditorRealTimeController.emitToRoom(
  588. projectId,
  589. 'project:publicAccessLevel:changed',
  590. { newAccessLevel }
  591. )
  592. cb()
  593. },
  594. ],
  595. callback
  596. )
  597. },
  598. setRootDoc(projectId, newRootDocID, callback) {
  599. ProjectEntityUpdateHandler.setRootDoc(
  600. projectId,
  601. newRootDocID,
  602. function (err) {
  603. if (err) {
  604. return callback(err)
  605. }
  606. EditorRealTimeController.emitToRoom(
  607. projectId,
  608. 'rootDocUpdated',
  609. newRootDocID
  610. )
  611. callback()
  612. }
  613. )
  614. },
  615. setMainBibliographyDoc(projectId, newBibliographyDocId, callback) {
  616. ProjectEntityUpdateHandler.setMainBibliographyDoc(
  617. projectId,
  618. newBibliographyDocId,
  619. function (err) {
  620. if (err) {
  621. return callback(err)
  622. }
  623. EditorRealTimeController.emitToRoom(
  624. projectId,
  625. 'mainBibliographyDocUpdated',
  626. newBibliographyDocId
  627. )
  628. callback()
  629. }
  630. )
  631. },
  632. _notifyProjectUsersOfNewFolders(projectId, folders, callback) {
  633. async.eachSeries(
  634. folders,
  635. (folder, cb) =>
  636. EditorController._notifyProjectUsersOfNewFolder(
  637. projectId,
  638. folder.parentFolder_id,
  639. folder,
  640. null,
  641. cb
  642. ),
  643. callback
  644. )
  645. },
  646. _notifyProjectUsersOfNewFolder(
  647. projectId,
  648. folderId,
  649. folder,
  650. userId,
  651. callback
  652. ) {
  653. EditorRealTimeController.emitToRoom(
  654. projectId,
  655. 'reciveNewFolder',
  656. folderId,
  657. folder,
  658. userId
  659. )
  660. callback()
  661. },
  662. }
  663. EditorController.promises = promisifyAll(EditorController, {
  664. multiResult: {
  665. mkdirp: ['newFolders', 'lastFolder'],
  666. },
  667. })
  668. export default EditorController