RequestParser.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. /* eslint-disable
  2. no-control-regex,
  3. no-throw-literal,
  4. no-unused-vars,
  5. no-useless-escape,
  6. valid-typeof,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS101: Remove unnecessary use of Array.from
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * DS205: Consider reworking code to avoid use of IIFEs
  15. * DS207: Consider shorter variations of null checks
  16. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  17. */
  18. let RequestParser
  19. const settings = require('@overleaf/settings')
  20. module.exports = RequestParser = {
  21. VALID_COMPILERS: ['pdflatex', 'latex', 'xelatex', 'lualatex'],
  22. MAX_TIMEOUT: 600,
  23. parse(body, callback) {
  24. let resource
  25. if (callback == null) {
  26. callback = function () {}
  27. }
  28. const response = {}
  29. if (body.compile == null) {
  30. return callback(
  31. new Error('top level object should have a compile attribute')
  32. )
  33. }
  34. const { compile } = body
  35. if (!compile.options) {
  36. compile.options = {}
  37. }
  38. try {
  39. response.metricsOpts = {
  40. path: this._parseAttribute('metricsPath', compile.options.metricsPath, {
  41. default: '',
  42. type: 'string',
  43. }),
  44. method: this._parseAttribute(
  45. 'metricsMethod',
  46. compile.options.metricsMethod,
  47. {
  48. default: '',
  49. type: 'string',
  50. }
  51. ),
  52. }
  53. response.compiler = this._parseAttribute(
  54. 'compiler',
  55. compile.options.compiler,
  56. {
  57. validValues: this.VALID_COMPILERS,
  58. default: 'pdflatex',
  59. type: 'string',
  60. }
  61. )
  62. response.enablePdfCaching = this._parseAttribute(
  63. 'enablePdfCaching',
  64. compile.options.enablePdfCaching,
  65. {
  66. default: false,
  67. type: 'boolean',
  68. }
  69. )
  70. response.timeout = this._parseAttribute(
  71. 'timeout',
  72. compile.options.timeout,
  73. {
  74. default: RequestParser.MAX_TIMEOUT,
  75. type: 'number',
  76. }
  77. )
  78. response.imageName = this._parseAttribute(
  79. 'imageName',
  80. compile.options.imageName,
  81. {
  82. type: 'string',
  83. validValues:
  84. settings.clsi &&
  85. settings.clsi.docker &&
  86. settings.clsi.docker.allowedImages,
  87. }
  88. )
  89. response.draft = this._parseAttribute('draft', compile.options.draft, {
  90. default: false,
  91. type: 'boolean',
  92. })
  93. response.check = this._parseAttribute('check', compile.options.check, {
  94. type: 'string',
  95. })
  96. response.flags = this._parseAttribute('flags', compile.options.flags, {
  97. default: [],
  98. type: 'object',
  99. })
  100. if (settings.allowedCompileGroups) {
  101. response.compileGroup = this._parseAttribute(
  102. 'compileGroup',
  103. compile.options.compileGroup,
  104. {
  105. validValues: settings.allowedCompileGroups,
  106. default: '',
  107. type: 'string',
  108. }
  109. )
  110. }
  111. // The syncType specifies whether the request contains all
  112. // resources (full) or only those resources to be updated
  113. // in-place (incremental).
  114. response.syncType = this._parseAttribute(
  115. 'syncType',
  116. compile.options.syncType,
  117. {
  118. validValues: ['full', 'incremental'],
  119. type: 'string',
  120. }
  121. )
  122. // The syncState is an identifier passed in with the request
  123. // which has the property that it changes when any resource is
  124. // added, deleted, moved or renamed.
  125. //
  126. // on syncType full the syncState identifier is passed in and
  127. // stored
  128. //
  129. // on syncType incremental the syncState identifier must match
  130. // the stored value
  131. response.syncState = this._parseAttribute(
  132. 'syncState',
  133. compile.options.syncState,
  134. { type: 'string' }
  135. )
  136. if (response.timeout > RequestParser.MAX_TIMEOUT) {
  137. response.timeout = RequestParser.MAX_TIMEOUT
  138. }
  139. response.timeout = response.timeout * 1000 // milliseconds
  140. response.resources = (() => {
  141. const result = []
  142. for (resource of Array.from(compile.resources || [])) {
  143. result.push(this._parseResource(resource))
  144. }
  145. return result
  146. })()
  147. const rootResourcePath = this._parseAttribute(
  148. 'rootResourcePath',
  149. compile.rootResourcePath,
  150. {
  151. default: 'main.tex',
  152. type: 'string',
  153. }
  154. )
  155. const originalRootResourcePath = rootResourcePath
  156. const sanitizedRootResourcePath =
  157. RequestParser._sanitizePath(rootResourcePath)
  158. response.rootResourcePath = RequestParser._checkPath(
  159. sanitizedRootResourcePath
  160. )
  161. for (resource of Array.from(response.resources)) {
  162. if (resource.path === originalRootResourcePath) {
  163. resource.path = sanitizedRootResourcePath
  164. }
  165. }
  166. } catch (error1) {
  167. const error = error1
  168. return callback(error)
  169. }
  170. return callback(null, response)
  171. },
  172. _parseResource(resource) {
  173. let modified
  174. if (resource.path == null || typeof resource.path !== 'string') {
  175. throw 'all resources should have a path attribute'
  176. }
  177. if (resource.modified != null) {
  178. modified = new Date(resource.modified)
  179. if (isNaN(modified.getTime())) {
  180. throw `resource modified date could not be understood: ${resource.modified}`
  181. }
  182. }
  183. if (resource.url == null && resource.content == null) {
  184. throw 'all resources should have either a url or content attribute'
  185. }
  186. if (resource.content != null && typeof resource.content !== 'string') {
  187. throw 'content attribute should be a string'
  188. }
  189. if (resource.url != null && typeof resource.url !== 'string') {
  190. throw 'url attribute should be a string'
  191. }
  192. return {
  193. path: resource.path,
  194. modified,
  195. url: resource.url,
  196. content: resource.content,
  197. }
  198. },
  199. _parseAttribute(name, attribute, options) {
  200. if (attribute != null) {
  201. if (options.validValues != null) {
  202. if (options.validValues.indexOf(attribute) === -1) {
  203. throw `${name} attribute should be one of: ${options.validValues.join(
  204. ', '
  205. )}`
  206. }
  207. }
  208. if (options.type != null) {
  209. if (typeof attribute !== options.type) {
  210. throw `${name} attribute should be a ${options.type}`
  211. }
  212. }
  213. } else {
  214. if (options.default != null) {
  215. return options.default
  216. }
  217. }
  218. return attribute
  219. },
  220. _sanitizePath(path) {
  221. // See http://php.net/manual/en/function.escapeshellcmd.php
  222. return path.replace(
  223. /[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g,
  224. ''
  225. )
  226. },
  227. _checkPath(path) {
  228. // check that the request does not use a relative path
  229. for (const dir of Array.from(path.split('/'))) {
  230. if (dir === '..') {
  231. throw 'relative path in root resource'
  232. }
  233. }
  234. return path
  235. },
  236. }