const settings = require('@overleaf/settings') const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex'] const MAX_TIMEOUT = 600 function parse(body, callback) { let resource const response = {} if (body.compile == null) { return callback( new Error('top level object should have a compile attribute') ) } const { compile } = body if (!compile.options) { compile.options = {} } try { response.metricsOpts = { path: _parseAttribute('metricsPath', compile.options.metricsPath, { default: '', type: 'string', }), method: _parseAttribute('metricsMethod', compile.options.metricsMethod, { default: '', type: 'string', }), } response.compiler = _parseAttribute('compiler', compile.options.compiler, { validValues: VALID_COMPILERS, default: 'pdflatex', type: 'string', }) response.enablePdfCaching = _parseAttribute( 'enablePdfCaching', compile.options.enablePdfCaching, { default: false, type: 'boolean', } ) response.timeout = _parseAttribute('timeout', compile.options.timeout, { default: MAX_TIMEOUT, type: 'number', }) response.imageName = _parseAttribute( 'imageName', compile.options.imageName, { type: 'string', validValues: settings.clsi && settings.clsi.docker && settings.clsi.docker.allowedImages, } ) response.draft = _parseAttribute('draft', compile.options.draft, { default: false, type: 'boolean', }) response.stopOnFirstError = _parseAttribute( 'stopOnFirstError', compile.options.stopOnFirstError, { default: false, type: 'boolean', } ) response.check = _parseAttribute('check', compile.options.check, { type: 'string', }) response.flags = _parseAttribute('flags', compile.options.flags, { default: [], type: 'object', }) if (settings.allowedCompileGroups) { response.compileGroup = _parseAttribute( 'compileGroup', compile.options.compileGroup, { validValues: settings.allowedCompileGroups, default: '', type: 'string', } ) } // The syncType specifies whether the request contains all // resources (full) or only those resources to be updated // in-place (incremental). response.syncType = _parseAttribute('syncType', compile.options.syncType, { validValues: ['full', 'incremental'], type: 'string', }) // The syncState is an identifier passed in with the request // which has the property that it changes when any resource is // added, deleted, moved or renamed. // // on syncType full the syncState identifier is passed in and // stored // // on syncType incremental the syncState identifier must match // the stored value response.syncState = _parseAttribute( 'syncState', compile.options.syncState, { type: 'string' } ) if (response.timeout > MAX_TIMEOUT) { response.timeout = MAX_TIMEOUT } response.timeout = response.timeout * 1000 // milliseconds response.resources = (compile.resources || []).map(resource => _parseResource(resource) ) const rootResourcePath = _parseAttribute( 'rootResourcePath', compile.rootResourcePath, { default: 'main.tex', type: 'string', } ) const originalRootResourcePath = rootResourcePath const sanitizedRootResourcePath = _sanitizePath(rootResourcePath) response.rootResourcePath = _checkPath(sanitizedRootResourcePath) for (resource of response.resources) { if (resource.path === originalRootResourcePath) { resource.path = sanitizedRootResourcePath } } } catch (error1) { const error = error1 return callback(error) } callback(null, response) } function _parseResource(resource) { let modified if (resource.path == null || typeof resource.path !== 'string') { throw new Error('all resources should have a path attribute') } if (resource.modified != null) { modified = new Date(resource.modified) if (isNaN(modified.getTime())) { throw new Error( `resource modified date could not be understood: ${resource.modified}` ) } } if (resource.url == null && resource.content == null) { throw new Error( 'all resources should have either a url or content attribute' ) } if (resource.content != null && typeof resource.content !== 'string') { throw new Error('content attribute should be a string') } if (resource.url != null && typeof resource.url !== 'string') { throw new Error('url attribute should be a string') } return { path: resource.path, modified, url: resource.url, content: resource.content, } } function _parseAttribute(name, attribute, options) { if (attribute != null) { if (options.validValues != null) { if (options.validValues.indexOf(attribute) === -1) { throw new Error( `${name} attribute should be one of: ${options.validValues.join( ', ' )}` ) } } if (options.type != null) { // eslint-disable-next-line valid-typeof if (typeof attribute !== options.type) { throw new Error(`${name} attribute should be a ${options.type}`) } } } else { if (options.default != null) { return options.default } } return attribute } function _sanitizePath(path) { // See http://php.net/manual/en/function.escapeshellcmd.php // eslint-disable-next-line no-control-regex return path.replace(/[#&;`|*?~<>^()[\]{}$\\\x0A\xFF\x00]/g, '') } function _checkPath(path) { // check that the request does not use a relative path for (const dir of Array.from(path.split('/'))) { if (dir === '..') { throw new Error('relative path in root resource') } } return path } module.exports = { parse, MAX_TIMEOUT }