CompileController.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. /* eslint-disable
  2. camelcase,
  3. node/handle-callback-err,
  4. max-len,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. let CompileController
  16. const OError = require('@overleaf/o-error')
  17. const Metrics = require('@overleaf/metrics')
  18. const ProjectGetter = require('../Project/ProjectGetter')
  19. const CompileManager = require('./CompileManager')
  20. const ClsiManager = require('./ClsiManager')
  21. const logger = require('logger-sharelatex')
  22. const request = require('request')
  23. const Settings = require('@overleaf/settings')
  24. const SessionManager = require('../Authentication/SessionManager')
  25. const RateLimiter = require('../../infrastructure/RateLimiter')
  26. const ClsiCookieManager = require('./ClsiCookieManager')(
  27. Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined
  28. )
  29. const Path = require('path')
  30. const COMPILE_TIMEOUT_MS = 10 * 60 * 1000
  31. function getImageNameForProject(projectId, callback) {
  32. ProjectGetter.getProject(projectId, { imageName: 1 }, (err, project) => {
  33. if (err) return callback(err)
  34. if (!project) return callback(new Error('project not found'))
  35. callback(null, project.imageName)
  36. })
  37. }
  38. module.exports = CompileController = {
  39. compile(req, res, next) {
  40. res.setTimeout(COMPILE_TIMEOUT_MS)
  41. const project_id = req.params.Project_id
  42. const isAutoCompile = !!req.query.auto_compile
  43. const enablePdfCaching = !!req.query.enable_pdf_caching
  44. const fileLineErrors = !!req.query.file_line_errors
  45. const user_id = SessionManager.getLoggedInUserId(req.session)
  46. const options = {
  47. isAutoCompile,
  48. enablePdfCaching,
  49. fileLineErrors,
  50. }
  51. if (req.body.rootDoc_id) {
  52. options.rootDoc_id = req.body.rootDoc_id
  53. } else if (
  54. req.body.settingsOverride &&
  55. req.body.settingsOverride.rootDoc_id
  56. ) {
  57. // Can be removed after deploy
  58. options.rootDoc_id = req.body.settingsOverride.rootDoc_id
  59. }
  60. if (req.body.compiler) {
  61. options.compiler = req.body.compiler
  62. }
  63. if (req.body.draft) {
  64. options.draft = req.body.draft
  65. }
  66. if (['validate', 'error', 'silent'].includes(req.body.check)) {
  67. options.check = req.body.check
  68. }
  69. if (req.body.incrementalCompilesEnabled) {
  70. options.incrementalCompilesEnabled = true
  71. }
  72. CompileManager.compile(
  73. project_id,
  74. user_id,
  75. options,
  76. (
  77. error,
  78. status,
  79. outputFiles,
  80. clsiServerId,
  81. limits,
  82. validationProblems,
  83. stats,
  84. timings
  85. ) => {
  86. if (error) {
  87. Metrics.inc('compile-error')
  88. return next(error)
  89. }
  90. Metrics.inc('compile-status', 1, { status: status })
  91. res.json({
  92. status,
  93. outputFiles,
  94. compileGroup: limits != null ? limits.compileGroup : undefined,
  95. clsiServerId,
  96. validationProblems,
  97. stats,
  98. timings,
  99. pdfDownloadDomain: Settings.pdfDownloadDomain,
  100. })
  101. }
  102. )
  103. },
  104. stopCompile(req, res, next) {
  105. if (next == null) {
  106. next = function () {}
  107. }
  108. const project_id = req.params.Project_id
  109. const user_id = SessionManager.getLoggedInUserId(req.session)
  110. return CompileManager.stopCompile(project_id, user_id, function (error) {
  111. if (error != null) {
  112. return next(error)
  113. }
  114. return res.status(200).send()
  115. })
  116. },
  117. // Used for submissions through the public API
  118. compileSubmission(req, res, next) {
  119. if (next == null) {
  120. next = function () {}
  121. }
  122. res.setTimeout(COMPILE_TIMEOUT_MS)
  123. const { submission_id } = req.params
  124. const options = {}
  125. if ((req.body != null ? req.body.rootResourcePath : undefined) != null) {
  126. options.rootResourcePath = req.body.rootResourcePath
  127. }
  128. if (req.body != null ? req.body.compiler : undefined) {
  129. options.compiler = req.body.compiler
  130. }
  131. if (req.body != null ? req.body.draft : undefined) {
  132. options.draft = req.body.draft
  133. }
  134. if (
  135. ['validate', 'error', 'silent'].includes(
  136. req.body != null ? req.body.check : undefined
  137. )
  138. ) {
  139. options.check = req.body.check
  140. }
  141. options.compileGroup =
  142. (req.body != null ? req.body.compileGroup : undefined) ||
  143. Settings.defaultFeatures.compileGroup
  144. options.timeout =
  145. (req.body != null ? req.body.timeout : undefined) ||
  146. Settings.defaultFeatures.compileTimeout
  147. return ClsiManager.sendExternalRequest(
  148. submission_id,
  149. req.body,
  150. options,
  151. function (error, status, outputFiles, clsiServerId, validationProblems) {
  152. if (error != null) {
  153. return next(error)
  154. }
  155. res.contentType('application/json')
  156. return res.status(200).send(
  157. JSON.stringify({
  158. status,
  159. outputFiles,
  160. clsiServerId,
  161. validationProblems,
  162. })
  163. )
  164. }
  165. )
  166. },
  167. _compileAsUser(req, callback) {
  168. // callback with user_id if per-user, undefined otherwise
  169. if (!Settings.disablePerUserCompiles) {
  170. const user_id = SessionManager.getLoggedInUserId(req.session)
  171. return callback(null, user_id)
  172. } else {
  173. return callback()
  174. }
  175. }, // do a per-project compile, not per-user
  176. _downloadAsUser(req, callback) {
  177. // callback with user_id if per-user, undefined otherwise
  178. if (!Settings.disablePerUserCompiles) {
  179. const user_id = SessionManager.getLoggedInUserId(req.session)
  180. return callback(null, user_id)
  181. } else {
  182. return callback()
  183. }
  184. }, // do a per-project compile, not per-user
  185. downloadPdf(req, res, next) {
  186. if (next == null) {
  187. next = function () {}
  188. }
  189. Metrics.inc('pdf-downloads')
  190. const project_id = req.params.Project_id
  191. const isPdfjsPartialDownload =
  192. req.query != null ? req.query.pdfng : undefined
  193. const rateLimit = function (callback) {
  194. if (isPdfjsPartialDownload) {
  195. return callback(null, true)
  196. } else {
  197. const rateLimitOpts = {
  198. endpointName: 'full-pdf-download',
  199. throttle: 1000,
  200. subjectName: req.ip,
  201. timeInterval: 60 * 60,
  202. }
  203. return RateLimiter.addCount(rateLimitOpts, callback)
  204. }
  205. }
  206. return ProjectGetter.getProject(
  207. project_id,
  208. { name: 1 },
  209. function (err, project) {
  210. res.contentType('application/pdf')
  211. const filename = `${CompileController._getSafeProjectName(project)}.pdf`
  212. if (req.query.popupDownload) {
  213. res.setContentDisposition('attachment', { filename })
  214. } else {
  215. res.setContentDisposition('', { filename })
  216. }
  217. return rateLimit(function (err, canContinue) {
  218. if (err != null) {
  219. logger.err({ err }, 'error checking rate limit for pdf download')
  220. return res.sendStatus(500)
  221. } else if (!canContinue) {
  222. logger.log(
  223. { project_id, ip: req.ip },
  224. 'rate limit hit downloading pdf'
  225. )
  226. return res.sendStatus(500)
  227. } else {
  228. return CompileController._downloadAsUser(
  229. req,
  230. function (error, user_id) {
  231. const url = CompileController._getFileUrl(
  232. project_id,
  233. user_id,
  234. req.params.build_id,
  235. 'output.pdf'
  236. )
  237. return CompileController.proxyToClsi(
  238. project_id,
  239. url,
  240. req,
  241. res,
  242. next
  243. )
  244. }
  245. )
  246. }
  247. })
  248. }
  249. )
  250. },
  251. _getSafeProjectName(project) {
  252. const wordRegExp = /\W/g
  253. const safeProjectName = project.name.replace(wordRegExp, '_')
  254. return safeProjectName
  255. },
  256. deleteAuxFiles(req, res, next) {
  257. const project_id = req.params.Project_id
  258. const { clsiserverid } = req.query
  259. return CompileController._compileAsUser(req, function (error, user_id) {
  260. if (error != null) {
  261. return next(error)
  262. }
  263. CompileManager.deleteAuxFiles(
  264. project_id,
  265. user_id,
  266. clsiserverid,
  267. function (error) {
  268. if (error != null) {
  269. return next(error)
  270. }
  271. return res.sendStatus(200)
  272. }
  273. )
  274. })
  275. },
  276. // this is only used by templates, so is not called with a user_id
  277. compileAndDownloadPdf(req, res, next) {
  278. const { project_id } = req.params
  279. // pass user_id as null, since templates are an "anonymous" compile
  280. return CompileManager.compile(project_id, null, {}, function (err) {
  281. if (err != null) {
  282. logger.err(
  283. { err, project_id },
  284. 'something went wrong compile and downloading pdf'
  285. )
  286. res.sendStatus(500)
  287. }
  288. const url = `/project/${project_id}/output/output.pdf`
  289. return CompileController.proxyToClsi(project_id, url, req, res, next)
  290. })
  291. },
  292. getFileFromClsi(req, res, next) {
  293. if (next == null) {
  294. next = function () {}
  295. }
  296. const project_id = req.params.Project_id
  297. return CompileController._downloadAsUser(req, function (error, user_id) {
  298. if (error != null) {
  299. return next(error)
  300. }
  301. const url = CompileController._getFileUrl(
  302. project_id,
  303. user_id,
  304. req.params.build_id,
  305. req.params.file
  306. )
  307. return CompileController.proxyToClsi(project_id, url, req, res, next)
  308. })
  309. },
  310. getFileFromClsiWithoutUser(req, res, next) {
  311. if (next == null) {
  312. next = function () {}
  313. }
  314. const { submission_id } = req.params
  315. const url = CompileController._getFileUrl(
  316. submission_id,
  317. null,
  318. req.params.build_id,
  319. req.params.file
  320. )
  321. const limits = {
  322. compileGroup:
  323. (req.body != null ? req.body.compileGroup : undefined) ||
  324. Settings.defaultFeatures.compileGroup,
  325. }
  326. return CompileController.proxyToClsiWithLimits(
  327. submission_id,
  328. url,
  329. limits,
  330. req,
  331. res,
  332. next
  333. )
  334. },
  335. // compute a GET file url for a given project, user (optional), build (optional) and file
  336. _getFileUrl(project_id, user_id, build_id, file) {
  337. let url
  338. if (user_id != null && build_id != null) {
  339. url = `/project/${project_id}/user/${user_id}/build/${build_id}/output/${file}`
  340. } else if (user_id != null) {
  341. url = `/project/${project_id}/user/${user_id}/output/${file}`
  342. } else if (build_id != null) {
  343. url = `/project/${project_id}/build/${build_id}/output/${file}`
  344. } else {
  345. url = `/project/${project_id}/output/${file}`
  346. }
  347. return url
  348. },
  349. // compute a POST url for a project, user (optional) and action
  350. _getUrl(project_id, user_id, action) {
  351. let path = `/project/${project_id}`
  352. if (user_id != null) {
  353. path += `/user/${user_id}`
  354. }
  355. return `${path}/${action}`
  356. },
  357. proxySyncPdf(req, res, next) {
  358. if (next == null) {
  359. next = function () {}
  360. }
  361. const project_id = req.params.Project_id
  362. const { page, h, v } = req.query
  363. if (!(page != null ? page.match(/^\d+$/) : undefined)) {
  364. return next(new Error('invalid page parameter'))
  365. }
  366. if (!(h != null ? h.match(/^-?\d+\.\d+$/) : undefined)) {
  367. return next(new Error('invalid h parameter'))
  368. }
  369. if (!(v != null ? v.match(/^-?\d+\.\d+$/) : undefined)) {
  370. return next(new Error('invalid v parameter'))
  371. }
  372. // whether this request is going to a per-user container
  373. return CompileController._compileAsUser(req, function (error, user_id) {
  374. if (error != null) {
  375. return next(error)
  376. }
  377. getImageNameForProject(project_id, (error, imageName) => {
  378. if (error) return next(error)
  379. const url = CompileController._getUrl(project_id, user_id, 'sync/pdf')
  380. const destination = { url, qs: { page, h, v, imageName } }
  381. return CompileController.proxyToClsi(
  382. project_id,
  383. destination,
  384. req,
  385. res,
  386. next
  387. )
  388. })
  389. })
  390. },
  391. proxySyncCode(req, res, next) {
  392. if (next == null) {
  393. next = function () {}
  394. }
  395. const project_id = req.params.Project_id
  396. const { file, line, column } = req.query
  397. if (file == null) {
  398. return next(new Error('missing file parameter'))
  399. }
  400. // Check that we are dealing with a simple file path (this is not
  401. // strictly needed because synctex uses this parameter as a label
  402. // to look up in the synctex output, and does not open the file
  403. // itself). Since we have valid synctex paths like foo/./bar we
  404. // allow those by replacing /./ with /
  405. const testPath = file.replace('/./', '/')
  406. if (Path.resolve('/', testPath) !== `/${testPath}`) {
  407. return next(new Error('invalid file parameter'))
  408. }
  409. if (!(line != null ? line.match(/^\d+$/) : undefined)) {
  410. return next(new Error('invalid line parameter'))
  411. }
  412. if (!(column != null ? column.match(/^\d+$/) : undefined)) {
  413. return next(new Error('invalid column parameter'))
  414. }
  415. return CompileController._compileAsUser(req, function (error, user_id) {
  416. if (error != null) {
  417. return next(error)
  418. }
  419. getImageNameForProject(project_id, (error, imageName) => {
  420. if (error) return next(error)
  421. const url = CompileController._getUrl(project_id, user_id, 'sync/code')
  422. const destination = { url, qs: { file, line, column, imageName } }
  423. return CompileController.proxyToClsi(
  424. project_id,
  425. destination,
  426. req,
  427. res,
  428. next
  429. )
  430. })
  431. })
  432. },
  433. proxyToClsi(project_id, url, req, res, next) {
  434. if (next == null) {
  435. next = function () {}
  436. }
  437. if (req.query != null ? req.query.compileGroup : undefined) {
  438. return CompileController.proxyToClsiWithLimits(
  439. project_id,
  440. url,
  441. { compileGroup: req.query.compileGroup },
  442. req,
  443. res,
  444. next
  445. )
  446. } else {
  447. return CompileManager.getProjectCompileLimits(
  448. project_id,
  449. function (error, limits) {
  450. if (error != null) {
  451. return next(error)
  452. }
  453. return CompileController.proxyToClsiWithLimits(
  454. project_id,
  455. url,
  456. limits,
  457. req,
  458. res,
  459. next
  460. )
  461. }
  462. )
  463. }
  464. },
  465. proxyToClsiWithLimits(project_id, url, limits, req, res, next) {
  466. if (next == null) {
  467. next = function () {}
  468. }
  469. _getPersistenceOptions(req, project_id, (err, persistenceOptions) => {
  470. let qs
  471. if (err != null) {
  472. OError.tag(err, 'error getting cookie jar for clsi request')
  473. return next(err)
  474. }
  475. // expand any url parameter passed in as {url:..., qs:...}
  476. if (typeof url === 'object') {
  477. ;({ url, qs } = url)
  478. }
  479. const compilerUrl = Settings.apis.clsi.url
  480. url = `${compilerUrl}${url}`
  481. const oneMinute = 60 * 1000
  482. // the base request
  483. const options = {
  484. url,
  485. method: req.method,
  486. timeout: oneMinute,
  487. ...persistenceOptions,
  488. }
  489. // add any provided query string
  490. if (qs != null) {
  491. options.qs = Object.assign(options.qs || {}, qs)
  492. }
  493. // if we have a build parameter, pass it through to the clsi
  494. if (
  495. (req.query != null ? req.query.pdfng : undefined) &&
  496. (req.query != null ? req.query.build : undefined) != null
  497. ) {
  498. // only for new pdf viewer
  499. if (options.qs == null) {
  500. options.qs = {}
  501. }
  502. options.qs.build = req.query.build
  503. }
  504. // if we are byte serving pdfs, pass through If-* and Range headers
  505. // do not send any others, there's a proxying loop if Host: is passed!
  506. if (req.query != null ? req.query.pdfng : undefined) {
  507. const newHeaders = {}
  508. for (const h in req.headers) {
  509. const v = req.headers[h]
  510. if (/^(If-|Range)/i.test(h)) {
  511. newHeaders[h] = req.headers[h]
  512. }
  513. }
  514. options.headers = newHeaders
  515. }
  516. const proxy = request(options)
  517. proxy.pipe(res)
  518. return proxy.on('error', error =>
  519. logger.warn({ err: error, url }, 'CLSI proxy error')
  520. )
  521. })
  522. },
  523. wordCount(req, res, next) {
  524. const project_id = req.params.Project_id
  525. const file = req.query.file || false
  526. const { clsiserverid } = req.query
  527. return CompileController._compileAsUser(req, function (error, user_id) {
  528. if (error != null) {
  529. return next(error)
  530. }
  531. CompileManager.wordCount(
  532. project_id,
  533. user_id,
  534. file,
  535. clsiserverid,
  536. function (error, body) {
  537. if (error != null) {
  538. return next(error)
  539. }
  540. res.contentType('application/json')
  541. return res.send(body)
  542. }
  543. )
  544. })
  545. },
  546. }
  547. function _getPersistenceOptions(req, projectId, callback) {
  548. const { clsiserverid } = req.query
  549. const user_id = SessionManager.getLoggedInUserId(req)
  550. if (clsiserverid && typeof clsiserverid === 'string') {
  551. callback(null, { qs: { clsiserverid } })
  552. } else {
  553. ClsiCookieManager.getCookieJar(projectId, user_id, (err, jar) => {
  554. callback(err, { jar })
  555. })
  556. }
  557. }