RequestParser.js 6.6 KB

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