RequestParser.js 6.7 KB

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