HistoryController.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. let HistoryController
  2. const OError = require('@overleaf/o-error')
  3. const async = require('async')
  4. const logger = require('@overleaf/logger')
  5. const request = require('request')
  6. const settings = require('@overleaf/settings')
  7. const SessionManager = require('../Authentication/SessionManager')
  8. const UserGetter = require('../User/UserGetter')
  9. const ProjectGetter = require('../Project/ProjectGetter')
  10. const Errors = require('../Errors/Errors')
  11. const HistoryManager = require('./HistoryManager')
  12. const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
  13. const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
  14. const RestoreManager = require('./RestoreManager')
  15. const { pipeline } = require('stream')
  16. const { prepareZipAttachment } = require('../../infrastructure/Response')
  17. const Features = require('../../infrastructure/Features')
  18. module.exports = HistoryController = {
  19. proxyToHistoryApi(req, res, next) {
  20. const userId = SessionManager.getLoggedInUserId(req.session)
  21. const url = settings.apis.project_history.url + req.url
  22. const getReq = request({
  23. url,
  24. method: req.method,
  25. headers: {
  26. 'X-User-Id': userId,
  27. },
  28. })
  29. pipeline(getReq, res, function (err) {
  30. // If the downstream request is cancelled, we get an
  31. // ERR_STREAM_PREMATURE_CLOSE.
  32. if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
  33. logger.warn({ url, err }, 'history API error')
  34. next(err)
  35. }
  36. })
  37. },
  38. proxyToHistoryApiAndInjectUserDetails(req, res, next) {
  39. const userId = SessionManager.getLoggedInUserId(req.session)
  40. const url = settings.apis.project_history.url + req.url
  41. HistoryController._makeRequest(
  42. {
  43. url,
  44. method: req.method,
  45. json: true,
  46. headers: {
  47. 'X-User-Id': userId,
  48. },
  49. },
  50. function (err, body) {
  51. if (err) {
  52. return next(err)
  53. }
  54. HistoryManager.injectUserDetails(body, function (err, data) {
  55. if (err) {
  56. return next(err)
  57. }
  58. res.json(data)
  59. })
  60. }
  61. )
  62. },
  63. resyncProjectHistory(req, res, next) {
  64. // increase timeout to 6 minutes
  65. res.setTimeout(6 * 60 * 1000)
  66. const projectId = req.params.Project_id
  67. const opts = {}
  68. const historyRangesMigration = req.body.historyRangesMigration
  69. if (historyRangesMigration) {
  70. opts.historyRangesMigration = historyRangesMigration
  71. }
  72. ProjectEntityUpdateHandler.resyncProjectHistory(
  73. projectId,
  74. opts,
  75. function (err) {
  76. if (err instanceof Errors.ProjectHistoryDisabledError) {
  77. return res.sendStatus(404)
  78. }
  79. if (err) {
  80. return next(err)
  81. }
  82. res.sendStatus(204)
  83. }
  84. )
  85. },
  86. restoreFileFromV2(req, res, next) {
  87. const { project_id: projectId } = req.params
  88. const { version, pathname } = req.body
  89. const userId = SessionManager.getLoggedInUserId(req.session)
  90. RestoreManager.restoreFileFromV2(
  91. userId,
  92. projectId,
  93. version,
  94. pathname,
  95. function (err, entity) {
  96. if (err) {
  97. return next(err)
  98. }
  99. res.json({
  100. type: entity.type,
  101. id: entity._id,
  102. })
  103. }
  104. )
  105. },
  106. revertFile(req, res, next) {
  107. const { project_id: projectId } = req.params
  108. const { version, pathname } = req.body
  109. const userId = SessionManager.getLoggedInUserId(req.session)
  110. RestoreManager.revertFile(
  111. userId,
  112. projectId,
  113. version,
  114. pathname,
  115. {},
  116. function (err, entity) {
  117. if (err) {
  118. return next(err)
  119. }
  120. res.json({
  121. type: entity.type,
  122. id: entity._id,
  123. })
  124. }
  125. )
  126. },
  127. revertProject(req, res, next) {
  128. const { project_id: projectId } = req.params
  129. const { version } = req.body
  130. const userId = SessionManager.getLoggedInUserId(req.session)
  131. RestoreManager.revertProject(userId, projectId, version, function (err) {
  132. if (err) {
  133. return next(err)
  134. }
  135. res.sendStatus(200)
  136. })
  137. },
  138. getLabels(req, res, next) {
  139. const projectId = req.params.Project_id
  140. HistoryController._makeRequest(
  141. {
  142. method: 'GET',
  143. url: `${settings.apis.project_history.url}/project/${projectId}/labels`,
  144. json: true,
  145. },
  146. function (err, labels) {
  147. if (err) {
  148. return next(err)
  149. }
  150. HistoryController._enrichLabels(labels, (err, labels) => {
  151. if (err) {
  152. return next(err)
  153. }
  154. res.json(labels)
  155. })
  156. }
  157. )
  158. },
  159. createLabel(req, res, next) {
  160. const projectId = req.params.Project_id
  161. const { comment, version } = req.body
  162. const userId = SessionManager.getLoggedInUserId(req.session)
  163. HistoryController._makeRequest(
  164. {
  165. method: 'POST',
  166. url: `${settings.apis.project_history.url}/project/${projectId}/user/${userId}/labels`,
  167. json: { comment, version },
  168. },
  169. function (err, label) {
  170. if (err) {
  171. return next(err)
  172. }
  173. HistoryController._enrichLabel(label, (err, label) => {
  174. if (err) {
  175. return next(err)
  176. }
  177. res.json(label)
  178. })
  179. }
  180. )
  181. },
  182. _enrichLabel(label, callback) {
  183. if (!label.user_id) {
  184. return callback(null, label)
  185. }
  186. UserGetter.getUser(
  187. label.user_id,
  188. { first_name: 1, last_name: 1, email: 1 },
  189. (err, user) => {
  190. if (err) {
  191. return callback(err)
  192. }
  193. const newLabel = Object.assign({}, label)
  194. newLabel.user_display_name = HistoryController._displayNameForUser(user)
  195. callback(null, newLabel)
  196. }
  197. )
  198. },
  199. _enrichLabels(labels, callback) {
  200. if (!labels || !labels.length) {
  201. return callback(null, [])
  202. }
  203. const uniqueUsers = new Set(labels.map(label => label.user_id))
  204. // For backwards compatibility expect missing user_id fields
  205. uniqueUsers.delete(undefined)
  206. if (!uniqueUsers.size) {
  207. return callback(null, labels)
  208. }
  209. UserGetter.getUsers(
  210. Array.from(uniqueUsers),
  211. { first_name: 1, last_name: 1, email: 1 },
  212. function (err, rawUsers) {
  213. if (err) {
  214. return callback(err)
  215. }
  216. const users = new Map(rawUsers.map(user => [String(user._id), user]))
  217. labels.forEach(label => {
  218. const user = users.get(label.user_id)
  219. if (!user) return
  220. label.user_display_name = HistoryController._displayNameForUser(user)
  221. })
  222. callback(null, labels)
  223. }
  224. )
  225. },
  226. _displayNameForUser(user) {
  227. if (user == null) {
  228. return 'Anonymous'
  229. }
  230. if (user.name) {
  231. return user.name
  232. }
  233. let name = [user.first_name, user.last_name]
  234. .filter(n => n != null)
  235. .join(' ')
  236. .trim()
  237. if (name === '') {
  238. name = user.email.split('@')[0]
  239. }
  240. if (!name) {
  241. return '?'
  242. }
  243. return name
  244. },
  245. deleteLabel(req, res, next) {
  246. const { Project_id: projectId, label_id: labelId } = req.params
  247. const userId = SessionManager.getLoggedInUserId(req.session)
  248. ProjectGetter.getProject(
  249. projectId,
  250. {
  251. owner_ref: true,
  252. },
  253. (err, project) => {
  254. if (err) {
  255. return next(err)
  256. }
  257. // If the current user is the project owner, we can use the non-user-specific delete label endpoint.
  258. // Otherwise, we have to use the user-specific version (which only deletes the label if it is owned by the user)
  259. const deleteEndpointUrl = project.owner_ref.equals(userId)
  260. ? `${settings.apis.project_history.url}/project/${projectId}/labels/${labelId}`
  261. : `${settings.apis.project_history.url}/project/${projectId}/user/${userId}/labels/${labelId}`
  262. HistoryController._makeRequest(
  263. {
  264. method: 'DELETE',
  265. url: deleteEndpointUrl,
  266. },
  267. function (err) {
  268. if (err) {
  269. return next(err)
  270. }
  271. res.sendStatus(204)
  272. }
  273. )
  274. }
  275. )
  276. },
  277. _makeRequest(options, callback) {
  278. return request(options, function (err, response, body) {
  279. if (err) {
  280. return callback(err)
  281. }
  282. if (response.statusCode >= 200 && response.statusCode < 300) {
  283. callback(null, body)
  284. } else {
  285. err = new Error(
  286. `history api responded with non-success code: ${response.statusCode}`
  287. )
  288. callback(err)
  289. }
  290. })
  291. },
  292. downloadZipOfVersion(req, res, next) {
  293. const { project_id: projectId, version } = req.params
  294. ProjectDetailsHandler.getDetails(projectId, function (err, project) {
  295. if (err) {
  296. return next(err)
  297. }
  298. const v1Id =
  299. project.overleaf &&
  300. project.overleaf.history &&
  301. project.overleaf.history.id
  302. if (v1Id == null) {
  303. logger.error(
  304. { projectId, version },
  305. 'got request for zip version of non-v1 history project'
  306. )
  307. return res.sendStatus(402)
  308. }
  309. HistoryController._pipeHistoryZipToResponse(
  310. v1Id,
  311. version,
  312. `${project.name} (Version ${version})`,
  313. req,
  314. res,
  315. next
  316. )
  317. })
  318. },
  319. _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res, next) {
  320. if (req.destroyed) {
  321. // client has disconnected -- skip project history api call and download
  322. return
  323. }
  324. // increase timeout to 6 minutes
  325. res.setTimeout(6 * 60 * 1000)
  326. const url = `${settings.apis.v1_history.url}/projects/${v1ProjectId}/version/${version}/zip`
  327. const options = {
  328. auth: {
  329. user: settings.apis.v1_history.user,
  330. pass: settings.apis.v1_history.pass,
  331. },
  332. json: true,
  333. url,
  334. }
  335. if (!Features.hasFeature('saas')) {
  336. const getReq = request({ ...options, method: 'get' })
  337. getReq.on('error', function (err) {
  338. logger.warn({ err, v1ProjectId, version }, 'history zip download error')
  339. res.sendStatus(500)
  340. })
  341. getReq.on('response', function (response) {
  342. const statusCode = response.statusCode
  343. if (statusCode !== 200) {
  344. logger.warn(
  345. { v1ProjectId, version, statusCode },
  346. 'history zip download failed'
  347. )
  348. if (statusCode === 404) {
  349. res.sendStatus(404)
  350. } else {
  351. res.sendStatus(500)
  352. }
  353. return
  354. }
  355. prepareZipAttachment(res, `${name}.zip`)
  356. pipeline(response, res, function (err) {
  357. // If the downstream request is cancelled, we get an
  358. // ERR_STREAM_PREMATURE_CLOSE.
  359. if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
  360. logger.error({ err, v1ProjectId, version }, 'history API error')
  361. next(err)
  362. }
  363. })
  364. })
  365. return
  366. }
  367. request({ ...options, method: 'post' }, function (err, response, body) {
  368. if (err) {
  369. OError.tag(err, 'history API error', {
  370. v1ProjectId,
  371. version,
  372. })
  373. return next(err)
  374. }
  375. if (response.statusCode !== 200) {
  376. if (response.statusCode === 404) {
  377. return next(new Errors.NotFoundError('zip not found'))
  378. } else {
  379. return next(
  380. new OError('Error while getting zip for download', {
  381. v1ProjectId,
  382. statusCode: response.statusCode,
  383. })
  384. )
  385. }
  386. }
  387. if (req.destroyed) {
  388. // client has disconnected -- skip delayed s3 download
  389. return
  390. }
  391. if (!body.zipUrl) {
  392. return next(
  393. new OError('Missing zipUrl, cannot fetch zip file', {
  394. v1ProjectId,
  395. body,
  396. statusCode: response.statusCode,
  397. })
  398. )
  399. }
  400. let retryAttempt = 0
  401. let retryDelay = 2000
  402. // retry for about 6 minutes starting with short delay
  403. async.retry(
  404. 40,
  405. callback =>
  406. setTimeout(function () {
  407. if (req.destroyed) {
  408. // client has disconnected -- skip s3 download
  409. return callback() // stop async.retry loop
  410. }
  411. // increase delay by 1 second up to 10
  412. if (retryDelay < 10000) {
  413. retryDelay += 1000
  414. }
  415. retryAttempt++
  416. const getReq = request({
  417. url: body.zipUrl,
  418. sendImmediately: true,
  419. })
  420. const abortS3Request = () => getReq.abort()
  421. req.on('close', abortS3Request)
  422. res.on('timeout', abortS3Request)
  423. function cleanupAbortTrigger() {
  424. req.off('close', abortS3Request)
  425. res.off('timeout', abortS3Request)
  426. }
  427. getReq.on('response', function (response) {
  428. if (response.statusCode !== 200) {
  429. cleanupAbortTrigger()
  430. return callback(new Error('invalid response'))
  431. }
  432. // pipe also proxies the headers, but we want to customize these ones
  433. delete response.headers['content-disposition']
  434. delete response.headers['content-type']
  435. res.status(response.statusCode)
  436. prepareZipAttachment(res, `${name}.zip`)
  437. pipeline(response, res, err => {
  438. // If the downstream request is cancelled, we get an
  439. // ERR_STREAM_PREMATURE_CLOSE.
  440. if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
  441. logger.warn(
  442. { err, v1ProjectId, version, retryAttempt },
  443. 'history s3 proxying error'
  444. )
  445. }
  446. })
  447. callback()
  448. })
  449. getReq.on('error', function (err) {
  450. logger.warn(
  451. { err, v1ProjectId, version, retryAttempt },
  452. 'history s3 download error'
  453. )
  454. cleanupAbortTrigger()
  455. callback(err)
  456. })
  457. }, retryDelay),
  458. function (err) {
  459. if (err) {
  460. OError.tag(err, 'history s3 download failed', {
  461. v1ProjectId,
  462. version,
  463. retryAttempt,
  464. })
  465. next(err)
  466. }
  467. }
  468. )
  469. })
  470. },
  471. }