CompileController.js 16 KB

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