HistoryController.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. max-len,
  5. no-undef,
  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. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS103: Rewrite code to no longer use __guard__
  14. * DS207: Consider shorter variations of null checks
  15. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  16. */
  17. let HistoryController
  18. const _ = require('lodash')
  19. const async = require('async')
  20. const logger = require('logger-sharelatex')
  21. const request = require('request')
  22. const settings = require('settings-sharelatex')
  23. const AuthenticationController = require('../Authentication/AuthenticationController')
  24. const Errors = require('../Errors/Errors')
  25. const HistoryManager = require('./HistoryManager')
  26. const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
  27. const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
  28. const RestoreManager = require('./RestoreManager')
  29. const { pipeline } = require('stream')
  30. module.exports = HistoryController = {
  31. selectHistoryApi(req, res, next) {
  32. if (next == null) {
  33. next = function(error) {}
  34. }
  35. const project_id = req.params != null ? req.params.Project_id : undefined
  36. // find out which type of history service this project uses
  37. return ProjectDetailsHandler.getDetails(project_id, function(err, project) {
  38. if (err != null) {
  39. return next(err)
  40. }
  41. const history =
  42. project.overleaf != null ? project.overleaf.history : undefined
  43. if (
  44. (history != null ? history.id : undefined) != null &&
  45. (history != null ? history.display : undefined)
  46. ) {
  47. req.useProjectHistory = true
  48. } else {
  49. req.useProjectHistory = false
  50. }
  51. return next()
  52. })
  53. },
  54. ensureProjectHistoryEnabled(req, res, next) {
  55. if (next == null) {
  56. next = function(error) {}
  57. }
  58. if (req.useProjectHistory != null) {
  59. return next()
  60. } else {
  61. return res.sendStatus(404)
  62. }
  63. },
  64. proxyToHistoryApi(req, res, next) {
  65. if (next == null) {
  66. next = function(error) {}
  67. }
  68. const user_id = AuthenticationController.getLoggedInUserId(req)
  69. const url =
  70. HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
  71. const getReq = request({
  72. url,
  73. method: req.method,
  74. headers: {
  75. 'X-User-Id': user_id
  76. }
  77. })
  78. getReq.pipe(res)
  79. return getReq.on('error', function(error) {
  80. logger.warn({ url, err: error }, 'history API error')
  81. return next(error)
  82. })
  83. },
  84. proxyToHistoryApiAndInjectUserDetails(req, res, next) {
  85. if (next == null) {
  86. next = function(error) {}
  87. }
  88. const user_id = AuthenticationController.getLoggedInUserId(req)
  89. const url =
  90. HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
  91. return HistoryController._makeRequest(
  92. {
  93. url,
  94. method: req.method,
  95. json: true,
  96. headers: {
  97. 'X-User-Id': user_id
  98. }
  99. },
  100. function(error, body) {
  101. if (error != null) {
  102. return next(error)
  103. }
  104. return HistoryManager.injectUserDetails(body, function(error, data) {
  105. if (error != null) {
  106. return next(error)
  107. }
  108. return res.json(data)
  109. })
  110. }
  111. )
  112. },
  113. buildHistoryServiceUrl(useProjectHistory) {
  114. // choose a history service, either document-level (trackchanges)
  115. // or project-level (project_history)
  116. if (useProjectHistory) {
  117. return settings.apis.project_history.url
  118. } else {
  119. return settings.apis.trackchanges.url
  120. }
  121. },
  122. resyncProjectHistory(req, res, next) {
  123. if (next == null) {
  124. next = function(error) {}
  125. }
  126. const project_id = req.params.Project_id
  127. return ProjectEntityUpdateHandler.resyncProjectHistory(project_id, function(
  128. error
  129. ) {
  130. if (error instanceof Errors.ProjectHistoryDisabledError) {
  131. return res.sendStatus(404)
  132. }
  133. if (error != null) {
  134. return next(error)
  135. }
  136. return res.sendStatus(204)
  137. })
  138. },
  139. restoreFileFromV2(req, res, next) {
  140. const { project_id } = req.params
  141. const { version, pathname } = req.body
  142. const user_id = AuthenticationController.getLoggedInUserId(req)
  143. return RestoreManager.restoreFileFromV2(
  144. user_id,
  145. project_id,
  146. version,
  147. pathname,
  148. function(error, entity) {
  149. if (error != null) {
  150. return next(error)
  151. }
  152. return res.json({
  153. type: entity.type,
  154. id: entity._id
  155. })
  156. }
  157. )
  158. },
  159. restoreDocFromDeletedDoc(req, res, next) {
  160. const { project_id, doc_id } = req.params
  161. const { name } = req.body
  162. const user_id = AuthenticationController.getLoggedInUserId(req)
  163. if (name == null) {
  164. return res.sendStatus(400) // Malformed request
  165. }
  166. return RestoreManager.restoreDocFromDeletedDoc(
  167. user_id,
  168. project_id,
  169. doc_id,
  170. name,
  171. (error, doc) => {
  172. if (error != null) return next(error)
  173. res.json({
  174. doc_id: doc._id
  175. })
  176. }
  177. )
  178. },
  179. getLabels(req, res, next) {
  180. const project_id = req.params.Project_id
  181. const user_id = AuthenticationController.getLoggedInUserId(req)
  182. return HistoryController._makeRequest(
  183. {
  184. method: 'GET',
  185. url: `${
  186. settings.apis.project_history.url
  187. }/project/${project_id}/labels`,
  188. json: true
  189. },
  190. function(error, labels) {
  191. if (error != null) {
  192. return next(error)
  193. }
  194. return res.json(labels)
  195. }
  196. )
  197. },
  198. createLabel(req, res, next) {
  199. const project_id = req.params.Project_id
  200. const { comment, version } = req.body
  201. const user_id = AuthenticationController.getLoggedInUserId(req)
  202. return HistoryController._makeRequest(
  203. {
  204. method: 'POST',
  205. url: `${
  206. settings.apis.project_history.url
  207. }/project/${project_id}/user/${user_id}/labels`,
  208. json: { comment, version }
  209. },
  210. function(error, label) {
  211. if (error != null) {
  212. return next(error)
  213. }
  214. return res.json(label)
  215. }
  216. )
  217. },
  218. deleteLabel(req, res, next) {
  219. const project_id = req.params.Project_id
  220. const { label_id } = req.params
  221. const user_id = AuthenticationController.getLoggedInUserId(req)
  222. return HistoryController._makeRequest(
  223. {
  224. method: 'DELETE',
  225. url: `${
  226. settings.apis.project_history.url
  227. }/project/${project_id}/user/${user_id}/labels/${label_id}`
  228. },
  229. function(error) {
  230. if (error != null) {
  231. return next(error)
  232. }
  233. return res.sendStatus(204)
  234. }
  235. )
  236. },
  237. _makeRequest(options, callback) {
  238. return request(options, function(error, response, body) {
  239. if (error != null) {
  240. return callback(error)
  241. }
  242. if (response.statusCode >= 200 && response.statusCode < 300) {
  243. return callback(null, body)
  244. } else {
  245. error = new Error(
  246. `history api responded with non-success code: ${response.statusCode}`
  247. )
  248. logger.warn(
  249. { err: error },
  250. `project-history api responded with non-success code: ${
  251. response.statusCode
  252. }`
  253. )
  254. return callback(error)
  255. }
  256. })
  257. },
  258. downloadZipOfVersion(req, res, next) {
  259. const { project_id, version } = req.params
  260. return ProjectDetailsHandler.getDetails(project_id, function(err, project) {
  261. if (err != null) {
  262. return next(err)
  263. }
  264. const v1_id = __guard__(
  265. project.overleaf != null ? project.overleaf.history : undefined,
  266. x => x.id
  267. )
  268. if (v1_id == null) {
  269. logger.err(
  270. { project_id, version },
  271. 'got request for zip version of non-v1 history project'
  272. )
  273. return res.sendStatus(402)
  274. }
  275. return HistoryController._pipeHistoryZipToResponse(
  276. v1_id,
  277. version,
  278. `${project.name} (Version ${version})`,
  279. req,
  280. res,
  281. next
  282. )
  283. })
  284. },
  285. _pipeHistoryZipToResponse(v1_project_id, version, name, req, res, next) {
  286. if (req.aborted) {
  287. // client has disconnected -- skip project history api call and download
  288. return
  289. }
  290. // increase timeout to 6 minutes
  291. res.setTimeout(6 * 60 * 1000)
  292. const url = `${
  293. settings.apis.v1_history.url
  294. }/projects/${v1_project_id}/version/${version}/zip`
  295. const options = {
  296. auth: {
  297. user: settings.apis.v1_history.user,
  298. pass: settings.apis.v1_history.pass
  299. },
  300. json: true,
  301. method: 'post',
  302. url
  303. }
  304. return request(options, function(err, response, body) {
  305. if (err) {
  306. logger.warn({ err, v1_project_id, version }, 'history API error')
  307. return next(err)
  308. }
  309. if (req.aborted) {
  310. // client has disconnected -- skip delayed s3 download
  311. return
  312. }
  313. let retryAttempt = 0
  314. let retryDelay = 2000
  315. // retry for about 6 minutes starting with short delay
  316. return async.retry(
  317. 40,
  318. callback =>
  319. setTimeout(function() {
  320. if (req.aborted) {
  321. // client has disconnected -- skip s3 download
  322. return callback() // stop async.retry loop
  323. }
  324. // increase delay by 1 second up to 10
  325. if (retryDelay < 10000) {
  326. retryDelay += 1000
  327. }
  328. retryAttempt++
  329. const getReq = request({
  330. url: body.zipUrl,
  331. sendImmediately: true
  332. })
  333. const abortS3Request = () => getReq.abort()
  334. req.on('aborted', abortS3Request)
  335. res.on('timeout', abortS3Request)
  336. function cleanupAbortTrigger() {
  337. req.off('aborted', abortS3Request)
  338. res.off('timeout', abortS3Request)
  339. }
  340. getReq.on('response', function(response) {
  341. if (response.statusCode !== 200) {
  342. cleanupAbortTrigger()
  343. return callback(new Error('invalid response'))
  344. }
  345. // pipe also proxies the headers, but we want to customize these ones
  346. delete response.headers['content-disposition']
  347. delete response.headers['content-type']
  348. res.status(response.statusCode)
  349. res.setContentDisposition('attachment', {
  350. filename: `${name}.zip`
  351. })
  352. res.contentType('application/zip')
  353. pipeline(response, res, err => {
  354. if (err) {
  355. logger.warn(
  356. { err, v1_project_id, version, retryAttempt },
  357. 'history s3 proxying error'
  358. )
  359. }
  360. })
  361. callback()
  362. })
  363. return getReq.on('error', function(err) {
  364. logger.warn(
  365. { err, v1_project_id, version, retryAttempt },
  366. 'history s3 download error'
  367. )
  368. cleanupAbortTrigger()
  369. return callback(err)
  370. })
  371. }, retryDelay),
  372. function(err) {
  373. if (err) {
  374. logger.warn(
  375. { err, v1_project_id, version, retryAttempt },
  376. 'history s3 download failed'
  377. )
  378. return next(err)
  379. }
  380. }
  381. )
  382. })
  383. }
  384. }
  385. function __guard__(value, transform) {
  386. return typeof value !== 'undefined' && value !== null
  387. ? transform(value)
  388. : undefined
  389. }