CompileController.js 17 KB

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