CompileController.js 16 KB

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