mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 10:23:46 -05:00
b456ea726d
* wip generate directory for hash content * cleanup, remove console logging * add content caching module * Return PDF stream ranges with compile response * Return the PDF file size in the compile response * PDF range endpoint * [misc] WIP: pdf caching: preserve the m-time on static content files * [misc] WIP: pdf caching: improve browser caching, emit caching headers * [misc] WIP: pdf caching: do not emit very small chunks <1kB * [misc] keep up with moving output files into a separate directory * [OutputCacheManager] add global feature flag for enabling pdf caching * [misc] add contentId into the URL for protecting PDF stream contents * [misc] support PDF stream caching for anonymous users * [misc] add per-request feature flag for enabling PDF stream caching * [misc] enable pdf caching in CI and emit metrics at the end of run * [misc] expose compile stats and timings to the frontend * [misc] log an error in case saving output files fails * [misc] add metrics for pdf bandwidth and pdf caching performance * [misc] add a dark mode to the pdf caching for computing ranges only * [misc] move pdf caching metrics into ContentCacheMetrics * [misc] add a config option for the min chunk size of pdf ranges Co-authored-by: Brian Gough <brian.gough@overleaf.com> Co-authored-by: Eric Mc Sween <eric.mcsween@overleaf.com>
241 lines
6.7 KiB
JavaScript
241 lines
6.7 KiB
JavaScript
/* eslint-disable
|
|
handle-callback-err,
|
|
no-control-regex,
|
|
no-throw-literal,
|
|
no-unused-vars,
|
|
no-useless-escape,
|
|
standard/no-callback-literal,
|
|
valid-typeof,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS101: Remove unnecessary use of Array.from
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS205: Consider reworking code to avoid use of IIFEs
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
let RequestParser
|
|
const settings = require('settings-sharelatex')
|
|
|
|
module.exports = RequestParser = {
|
|
VALID_COMPILERS: ['pdflatex', 'latex', 'xelatex', 'lualatex'],
|
|
MAX_TIMEOUT: 600,
|
|
|
|
parse(body, callback) {
|
|
let resource
|
|
if (callback == null) {
|
|
callback = function (error, data) {}
|
|
}
|
|
const response = {}
|
|
|
|
if (body.compile == null) {
|
|
return callback('top level object should have a compile attribute')
|
|
}
|
|
|
|
const { compile } = body
|
|
if (!compile.options) {
|
|
compile.options = {}
|
|
}
|
|
|
|
try {
|
|
response.compiler = this._parseAttribute(
|
|
'compiler',
|
|
compile.options.compiler,
|
|
{
|
|
validValues: this.VALID_COMPILERS,
|
|
default: 'pdflatex',
|
|
type: 'string'
|
|
}
|
|
)
|
|
response.enablePdfCaching = this._parseAttribute(
|
|
'enablePdfCaching',
|
|
compile.options.enablePdfCaching,
|
|
{
|
|
default: false,
|
|
type: 'boolean'
|
|
}
|
|
)
|
|
response.timeout = this._parseAttribute(
|
|
'timeout',
|
|
compile.options.timeout,
|
|
{
|
|
default: RequestParser.MAX_TIMEOUT,
|
|
type: 'number'
|
|
}
|
|
)
|
|
response.imageName = this._parseAttribute(
|
|
'imageName',
|
|
compile.options.imageName,
|
|
{
|
|
type: 'string',
|
|
validValues:
|
|
settings.clsi &&
|
|
settings.clsi.docker &&
|
|
settings.clsi.docker.allowedImages
|
|
}
|
|
)
|
|
response.draft = this._parseAttribute('draft', compile.options.draft, {
|
|
default: false,
|
|
type: 'boolean'
|
|
})
|
|
response.check = this._parseAttribute('check', compile.options.check, {
|
|
type: 'string'
|
|
})
|
|
response.flags = this._parseAttribute('flags', compile.options.flags, {
|
|
default: [],
|
|
type: 'object'
|
|
})
|
|
if (settings.allowedCompileGroups) {
|
|
response.compileGroup = this._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 = this._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 = this._parseAttribute(
|
|
'syncState',
|
|
compile.options.syncState,
|
|
{ type: 'string' }
|
|
)
|
|
|
|
if (response.timeout > RequestParser.MAX_TIMEOUT) {
|
|
response.timeout = RequestParser.MAX_TIMEOUT
|
|
}
|
|
response.timeout = response.timeout * 1000 // milliseconds
|
|
|
|
response.resources = (() => {
|
|
const result = []
|
|
for (resource of Array.from(compile.resources || [])) {
|
|
result.push(this._parseResource(resource))
|
|
}
|
|
return result
|
|
})()
|
|
|
|
const rootResourcePath = this._parseAttribute(
|
|
'rootResourcePath',
|
|
compile.rootResourcePath,
|
|
{
|
|
default: 'main.tex',
|
|
type: 'string'
|
|
}
|
|
)
|
|
const originalRootResourcePath = rootResourcePath
|
|
const sanitizedRootResourcePath = RequestParser._sanitizePath(
|
|
rootResourcePath
|
|
)
|
|
response.rootResourcePath = RequestParser._checkPath(
|
|
sanitizedRootResourcePath
|
|
)
|
|
|
|
for (resource of Array.from(response.resources)) {
|
|
if (resource.path === originalRootResourcePath) {
|
|
resource.path = sanitizedRootResourcePath
|
|
}
|
|
}
|
|
} catch (error1) {
|
|
const error = error1
|
|
return callback(error)
|
|
}
|
|
|
|
return callback(null, response)
|
|
},
|
|
|
|
_parseResource(resource) {
|
|
let modified
|
|
if (resource.path == null || typeof resource.path !== 'string') {
|
|
throw 'all resources should have a path attribute'
|
|
}
|
|
|
|
if (resource.modified != null) {
|
|
modified = new Date(resource.modified)
|
|
if (isNaN(modified.getTime())) {
|
|
throw `resource modified date could not be understood: ${resource.modified}`
|
|
}
|
|
}
|
|
|
|
if (resource.url == null && resource.content == null) {
|
|
throw 'all resources should have either a url or content attribute'
|
|
}
|
|
if (resource.content != null && typeof resource.content !== 'string') {
|
|
throw 'content attribute should be a string'
|
|
}
|
|
if (resource.url != null && typeof resource.url !== 'string') {
|
|
throw 'url attribute should be a string'
|
|
}
|
|
|
|
return {
|
|
path: resource.path,
|
|
modified,
|
|
url: resource.url,
|
|
content: resource.content
|
|
}
|
|
},
|
|
|
|
_parseAttribute(name, attribute, options) {
|
|
if (attribute != null) {
|
|
if (options.validValues != null) {
|
|
if (options.validValues.indexOf(attribute) === -1) {
|
|
throw `${name} attribute should be one of: ${options.validValues.join(
|
|
', '
|
|
)}`
|
|
}
|
|
}
|
|
if (options.type != null) {
|
|
if (typeof attribute !== options.type) {
|
|
throw `${name} attribute should be a ${options.type}`
|
|
}
|
|
}
|
|
} else {
|
|
if (options.default != null) {
|
|
return options.default
|
|
}
|
|
}
|
|
return attribute
|
|
},
|
|
|
|
_sanitizePath(path) {
|
|
// See http://php.net/manual/en/function.escapeshellcmd.php
|
|
return path.replace(
|
|
/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g,
|
|
''
|
|
)
|
|
},
|
|
|
|
_checkPath(path) {
|
|
// check that the request does not use a relative path
|
|
for (const dir of Array.from(path.split('/'))) {
|
|
if (dir === '..') {
|
|
throw 'relative path in root resource'
|
|
}
|
|
}
|
|
return path
|
|
}
|
|
}
|