EditorController.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. const logger = require('@overleaf/logger')
  2. const OError = require('@overleaf/o-error')
  3. const Metrics = require('@overleaf/metrics')
  4. const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
  5. const ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
  6. const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
  7. const ProjectDeleter = require('../Project/ProjectDeleter')
  8. const EditorRealTimeController = require('./EditorRealTimeController')
  9. const async = require('async')
  10. const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
  11. const { promisifyAll } = require('@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. upsertDoc(projectId, folderId, docName, docLines, source, userId, callback) {
  108. ProjectEntityUpdateHandler.upsertDoc(
  109. projectId,
  110. folderId,
  111. docName,
  112. docLines,
  113. source,
  114. userId,
  115. function (err, doc, didAddNewDoc) {
  116. if (didAddNewDoc) {
  117. EditorRealTimeController.emitToRoom(
  118. projectId,
  119. 'reciveNewDoc',
  120. folderId,
  121. doc,
  122. source,
  123. userId
  124. )
  125. }
  126. callback(err, doc)
  127. }
  128. )
  129. },
  130. upsertFile(
  131. projectId,
  132. folderId,
  133. fileName,
  134. fsPath,
  135. linkedFileData,
  136. source,
  137. userId,
  138. callback
  139. ) {
  140. ProjectEntityUpdateHandler.upsertFile(
  141. projectId,
  142. folderId,
  143. fileName,
  144. fsPath,
  145. linkedFileData,
  146. userId,
  147. source,
  148. function (err, newFile, didAddFile, existingFile) {
  149. if (err) {
  150. return callback(err)
  151. }
  152. if (!didAddFile) {
  153. // replacement, so remove the existing file from the client
  154. EditorRealTimeController.emitToRoom(
  155. projectId,
  156. 'removeEntity',
  157. existingFile._id,
  158. source
  159. )
  160. }
  161. // now add the new file on the client
  162. EditorRealTimeController.emitToRoom(
  163. projectId,
  164. 'reciveNewFile',
  165. folderId,
  166. newFile,
  167. source,
  168. linkedFileData,
  169. userId
  170. )
  171. callback(null, newFile)
  172. }
  173. )
  174. },
  175. upsertDocWithPath(
  176. projectId,
  177. elementPath,
  178. docLines,
  179. source,
  180. userId,
  181. callback
  182. ) {
  183. ProjectEntityUpdateHandler.upsertDocWithPath(
  184. projectId,
  185. elementPath,
  186. docLines,
  187. source,
  188. userId,
  189. function (err, doc, didAddNewDoc, newFolders, lastFolder) {
  190. if (err) {
  191. return callback(err)
  192. }
  193. EditorController._notifyProjectUsersOfNewFolders(
  194. projectId,
  195. newFolders,
  196. function (err) {
  197. if (err) {
  198. return callback(err)
  199. }
  200. if (didAddNewDoc) {
  201. EditorRealTimeController.emitToRoom(
  202. projectId,
  203. 'reciveNewDoc',
  204. lastFolder._id,
  205. doc,
  206. source,
  207. userId
  208. )
  209. }
  210. callback(null, { doc, folder: lastFolder })
  211. }
  212. )
  213. }
  214. )
  215. },
  216. upsertFileWithPath(
  217. projectId,
  218. elementPath,
  219. fsPath,
  220. linkedFileData,
  221. source,
  222. userId,
  223. callback
  224. ) {
  225. ProjectEntityUpdateHandler.upsertFileWithPath(
  226. projectId,
  227. elementPath,
  228. fsPath,
  229. linkedFileData,
  230. userId,
  231. source,
  232. function (
  233. err,
  234. newFile,
  235. didAddFile,
  236. existingFile,
  237. newFolders,
  238. lastFolder
  239. ) {
  240. if (err) {
  241. return callback(err)
  242. }
  243. EditorController._notifyProjectUsersOfNewFolders(
  244. projectId,
  245. newFolders,
  246. function (err) {
  247. if (err) {
  248. return callback(err)
  249. }
  250. if (!didAddFile) {
  251. // replacement, so remove the existing file from the client
  252. EditorRealTimeController.emitToRoom(
  253. projectId,
  254. 'removeEntity',
  255. existingFile._id,
  256. source
  257. )
  258. }
  259. // now add the new file on the client
  260. EditorRealTimeController.emitToRoom(
  261. projectId,
  262. 'reciveNewFile',
  263. lastFolder._id,
  264. newFile,
  265. source,
  266. linkedFileData,
  267. userId
  268. )
  269. callback(null, { file: newFile, folder: lastFolder })
  270. }
  271. )
  272. }
  273. )
  274. },
  275. addFolder(projectId, folderId, folderName, source, userId, callback) {
  276. folderName = folderName.trim()
  277. Metrics.inc('editor.add-folder')
  278. ProjectEntityUpdateHandler.addFolder(
  279. projectId,
  280. folderId,
  281. folderName,
  282. (err, folder, folderId) => {
  283. if (err) {
  284. OError.tag(err, 'could not add folder', {
  285. projectId,
  286. folderId,
  287. folderName,
  288. source,
  289. })
  290. return callback(err)
  291. }
  292. EditorController._notifyProjectUsersOfNewFolder(
  293. projectId,
  294. folderId,
  295. folder,
  296. userId,
  297. function (err) {
  298. if (err) {
  299. return callback(err)
  300. }
  301. callback(null, folder)
  302. }
  303. )
  304. }
  305. )
  306. },
  307. mkdirp(projectId, path, callback) {
  308. logger.debug({ projectId, path }, "making directories if they don't exist")
  309. ProjectEntityUpdateHandler.mkdirp(
  310. projectId,
  311. path,
  312. (err, newFolders, lastFolder) => {
  313. if (err) {
  314. OError.tag(err, 'could not mkdirp', {
  315. projectId,
  316. path,
  317. })
  318. return callback(err)
  319. }
  320. EditorController._notifyProjectUsersOfNewFolders(
  321. projectId,
  322. newFolders,
  323. function (err) {
  324. if (err) {
  325. return callback(err)
  326. }
  327. callback(null, newFolders, lastFolder)
  328. }
  329. )
  330. }
  331. )
  332. },
  333. deleteEntity(projectId, entityId, entityType, source, userId, callback) {
  334. Metrics.inc('editor.delete-entity')
  335. ProjectEntityUpdateHandler.deleteEntity(
  336. projectId,
  337. entityId,
  338. entityType,
  339. userId,
  340. source,
  341. function (err) {
  342. if (err) {
  343. OError.tag(err, 'could not delete entity', {
  344. projectId,
  345. entityId,
  346. entityType,
  347. })
  348. return callback(err)
  349. }
  350. logger.debug(
  351. { projectId, entityId, entityType },
  352. 'telling users entity has been deleted'
  353. )
  354. EditorRealTimeController.emitToRoom(
  355. projectId,
  356. 'removeEntity',
  357. entityId,
  358. source
  359. )
  360. callback()
  361. }
  362. )
  363. },
  364. deleteEntityWithPath(projectId, path, source, userId, callback) {
  365. ProjectEntityUpdateHandler.deleteEntityWithPath(
  366. projectId,
  367. path,
  368. userId,
  369. source,
  370. function (err, entityId) {
  371. if (err) {
  372. return callback(err)
  373. }
  374. EditorRealTimeController.emitToRoom(
  375. projectId,
  376. 'removeEntity',
  377. entityId,
  378. source
  379. )
  380. callback(null, entityId)
  381. }
  382. )
  383. },
  384. updateProjectDescription(projectId, description, callback) {
  385. logger.debug({ projectId, description }, 'updating project description')
  386. ProjectDetailsHandler.setProjectDescription(
  387. projectId,
  388. description,
  389. function (err) {
  390. if (err) {
  391. OError.tag(
  392. err,
  393. 'something went wrong setting the project description',
  394. {
  395. projectId,
  396. description,
  397. }
  398. )
  399. return callback(err)
  400. }
  401. EditorRealTimeController.emitToRoom(
  402. projectId,
  403. 'projectDescriptionUpdated',
  404. description
  405. )
  406. callback()
  407. }
  408. )
  409. },
  410. deleteProject(projectId, callback) {
  411. Metrics.inc('editor.delete-project')
  412. ProjectDeleter.deleteProject(projectId, callback)
  413. },
  414. renameEntity(
  415. projectId,
  416. entityId,
  417. entityType,
  418. newName,
  419. userId,
  420. source,
  421. callback
  422. ) {
  423. Metrics.inc('editor.rename-entity')
  424. ProjectEntityUpdateHandler.renameEntity(
  425. projectId,
  426. entityId,
  427. entityType,
  428. newName,
  429. userId,
  430. source,
  431. function (err) {
  432. if (err) {
  433. OError.tag(err, 'error renaming entity', {
  434. projectId,
  435. entityId,
  436. entityType,
  437. newName,
  438. })
  439. return callback(err)
  440. }
  441. if (newName.length > 0) {
  442. EditorRealTimeController.emitToRoom(
  443. projectId,
  444. 'reciveEntityRename',
  445. entityId,
  446. newName
  447. )
  448. }
  449. callback()
  450. }
  451. )
  452. },
  453. moveEntity(
  454. projectId,
  455. entityId,
  456. folderId,
  457. entityType,
  458. userId,
  459. source,
  460. callback
  461. ) {
  462. Metrics.inc('editor.move-entity')
  463. ProjectEntityUpdateHandler.moveEntity(
  464. projectId,
  465. entityId,
  466. folderId,
  467. entityType,
  468. userId,
  469. source,
  470. function (err) {
  471. if (err) {
  472. OError.tag(err, 'error moving entity', {
  473. projectId,
  474. entityId,
  475. folderId,
  476. })
  477. return callback(err)
  478. }
  479. EditorRealTimeController.emitToRoom(
  480. projectId,
  481. 'reciveEntityMove',
  482. entityId,
  483. folderId
  484. )
  485. callback()
  486. }
  487. )
  488. },
  489. renameProject(projectId, newName, callback) {
  490. ProjectDetailsHandler.renameProject(projectId, newName, function (err) {
  491. if (err) {
  492. OError.tag(err, 'error renaming project', {
  493. projectId,
  494. newName,
  495. })
  496. return callback(err)
  497. }
  498. EditorRealTimeController.emitToRoom(
  499. projectId,
  500. 'projectNameUpdated',
  501. newName
  502. )
  503. callback()
  504. })
  505. },
  506. setCompiler(projectId, compiler, callback) {
  507. ProjectOptionsHandler.setCompiler(projectId, compiler, function (err) {
  508. if (err) {
  509. return callback(err)
  510. }
  511. EditorRealTimeController.emitToRoom(
  512. projectId,
  513. 'compilerUpdated',
  514. compiler
  515. )
  516. callback()
  517. })
  518. },
  519. setImageName(projectId, imageName, callback) {
  520. ProjectOptionsHandler.setImageName(projectId, imageName, function (err) {
  521. if (err) {
  522. return callback(err)
  523. }
  524. EditorRealTimeController.emitToRoom(
  525. projectId,
  526. 'imageNameUpdated',
  527. imageName
  528. )
  529. callback()
  530. })
  531. },
  532. setSpellCheckLanguage(projectId, languageCode, callback) {
  533. ProjectOptionsHandler.setSpellCheckLanguage(
  534. projectId,
  535. languageCode,
  536. function (err) {
  537. if (err) {
  538. return callback(err)
  539. }
  540. EditorRealTimeController.emitToRoom(
  541. projectId,
  542. 'spellCheckLanguageUpdated',
  543. languageCode
  544. )
  545. callback()
  546. }
  547. )
  548. },
  549. setPublicAccessLevel(projectId, newAccessLevel, callback) {
  550. ProjectDetailsHandler.setPublicAccessLevel(
  551. projectId,
  552. newAccessLevel,
  553. function (err) {
  554. if (err) {
  555. return callback(err)
  556. }
  557. EditorRealTimeController.emitToRoom(
  558. projectId,
  559. 'project:publicAccessLevel:changed',
  560. { newAccessLevel }
  561. )
  562. if (newAccessLevel === PublicAccessLevels.TOKEN_BASED) {
  563. ProjectDetailsHandler.ensureTokensArePresent(projectId, callback)
  564. } else {
  565. callback()
  566. }
  567. }
  568. )
  569. },
  570. setRootDoc(projectId, newRootDocID, callback) {
  571. ProjectEntityUpdateHandler.setRootDoc(
  572. projectId,
  573. newRootDocID,
  574. function (err) {
  575. if (err) {
  576. return callback(err)
  577. }
  578. EditorRealTimeController.emitToRoom(
  579. projectId,
  580. 'rootDocUpdated',
  581. newRootDocID
  582. )
  583. callback()
  584. }
  585. )
  586. },
  587. _notifyProjectUsersOfNewFolders(projectId, folders, callback) {
  588. async.eachSeries(
  589. folders,
  590. (folder, cb) =>
  591. EditorController._notifyProjectUsersOfNewFolder(
  592. projectId,
  593. folder.parentFolder_id,
  594. folder,
  595. null,
  596. cb
  597. ),
  598. callback
  599. )
  600. },
  601. _notifyProjectUsersOfNewFolder(
  602. projectId,
  603. folderId,
  604. folder,
  605. userId,
  606. callback
  607. ) {
  608. EditorRealTimeController.emitToRoom(
  609. projectId,
  610. 'reciveNewFolder',
  611. folderId,
  612. folder,
  613. userId
  614. )
  615. callback()
  616. },
  617. }
  618. EditorController.promises = promisifyAll(EditorController, {
  619. multiResult: {
  620. mkdirp: ['newFolders', 'lastFolder'],
  621. },
  622. })
  623. module.exports = EditorController