@jsreport/jsreport-core 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -166,6 +166,9 @@ function configureLogger (logger, _transports) {
166
166
  }
167
167
 
168
168
  for (const { TransportClass, options } of transportsToAdd) {
169
+ if (options.silent) {
170
+ continue
171
+ }
169
172
  const transportInstance = new TransportClass(options)
170
173
 
171
174
  const existingTransport = logger.transports.find((t) => t.name === transportInstance.name)
@@ -39,6 +39,7 @@ async function optionsLoad ({
39
39
  loadConfigResult = await loadConfig(defaults, options, false)
40
40
  }
41
41
 
42
+ const explicitOptions = loadConfigResult[0]
42
43
  const appliedConfigFile = loadConfigResult[1]
43
44
 
44
45
  options.loadConfig = shouldLoadExternalConfig
@@ -79,9 +80,16 @@ async function optionsLoad ({
79
80
  options.store = options.store || { provider: 'memory' }
80
81
 
81
82
  options.sandbox = options.sandbox || {}
82
- if (options.allowLocalFilesAccess === true) {
83
+
84
+ // NOTE: handling back-compatible introduction of "trustUserCode" option, "allowLocalFilesAccess" is deprecated
85
+ if (explicitOptions.allowLocalFilesAccess === true && explicitOptions.trustUserCode == null) {
86
+ options.trustUserCode = true
87
+ }
88
+
89
+ if (options.trustUserCode === true) {
83
90
  options.sandbox.allowedModules = '*'
84
91
  }
92
+
85
93
  options.sandbox.nativeModules = options.sandbox.nativeModules || []
86
94
  options.sandbox.modules = options.sandbox.modules || []
87
95
  options.sandbox.allowedModules = options.sandbox.allowedModules || []
@@ -98,7 +106,7 @@ async function optionsLoad ({
98
106
  fs.mkdirSync(options.tempCoreDirectory, { recursive: true })
99
107
  }
100
108
 
101
- return appliedConfigFile
109
+ return [explicitOptions, appliedConfigFile]
102
110
  }
103
111
 
104
112
  /**
@@ -59,6 +59,7 @@ module.exports.getRootSchemaOptions = () => ({
59
59
  default: '2s'
60
60
  },
61
61
  enableRequestReportTimeout: { type: 'boolean', default: false, description: 'option that enables passing a custom report timeout per request using req.options.timeout. this enables that the caller of the report generation control the report timeout so enable it only when you trust the caller' },
62
+ trustUserCode: { type: 'boolean', default: false, description: 'option that control whether code sandboxing is enabled or not, code sandboxing has an impact on performance when rendering large reports. when true code sandboxing will be disabled meaning that users can potentially penetrate the local system if you allow code from external users to be part of your reports' },
62
63
  allowLocalFilesAccess: { type: 'boolean', default: false },
63
64
  encryption: {
64
65
  type: 'object',
@@ -77,6 +78,7 @@ module.exports.getRootSchemaOptions = () => ({
77
78
  },
78
79
  sandbox: {
79
80
  type: 'object',
81
+ default: {},
80
82
  properties: {
81
83
  allowedModules: {
82
84
  anyOf: [{
@@ -89,9 +91,10 @@ module.exports.getRootSchemaOptions = () => ({
89
91
  },
90
92
  cache: {
91
93
  type: 'object',
94
+ default: {},
92
95
  properties: {
93
- max: { type: 'number' },
94
- enabled: { type: 'boolean' }
96
+ max: { type: 'number', default: 100 },
97
+ enabled: { type: 'boolean', default: true }
95
98
  }
96
99
  }
97
100
  }
@@ -182,7 +185,7 @@ module.exports.getRootSchemaOptions = () => ({
182
185
  '$jsreport-acceptsDuration': true,
183
186
  default: '1m'
184
187
  },
185
- maxResponseSize: {
188
+ maxDiffSize: {
186
189
  type: ['string', 'number'],
187
190
  '$jsreport-acceptsSize': true,
188
191
  default: '50mb'
@@ -5,7 +5,7 @@
5
5
  */
6
6
  const path = require('path')
7
7
  const { Readable } = require('stream')
8
- const Reaper = require('reap2')
8
+ const Reaper = require('@jsreport/reap')
9
9
  const optionsLoad = require('./optionsLoad')
10
10
  const { createLogger, configureLogger, silentLogs } = require('./logger')
11
11
  const checkEntityName = require('./validateEntityName')
@@ -90,8 +90,8 @@ class MainReporter extends Reporter {
90
90
  return this
91
91
  }
92
92
 
93
- async extensionsLoad (opts) {
94
- const appliedConfigFile = await optionsLoad({
93
+ async extensionsLoad (_opts = {}) {
94
+ const [explicitOptions, appliedConfigFile] = await optionsLoad({
95
95
  defaults: this.defaults,
96
96
  options: this.options,
97
97
  validator: this.optionsValidator,
@@ -106,6 +106,8 @@ class MainReporter extends Reporter {
106
106
  silentLogs(this.logger)
107
107
  }
108
108
 
109
+ const { onConfigDetails, ...opts } = _opts
110
+
109
111
  this.logger.info(`Initializing jsreport (version: ${this.version}, configuration file: ${appliedConfigFile || 'none'}, nodejs: ${process.versions.node})`)
110
112
 
111
113
  await this.extensionsManager.load(opts)
@@ -130,6 +132,10 @@ class MainReporter extends Reporter {
130
132
  throw new Error(`options contain values that does not match the defined full root schema. ${rootOptionsValidation.fullErrorMessage}`)
131
133
  }
132
134
 
135
+ if (typeof onConfigDetails === 'function') {
136
+ onConfigDetails(explicitOptions)
137
+ }
138
+
133
139
  return this
134
140
  }
135
141
 
@@ -157,6 +163,7 @@ class MainReporter extends Reporter {
157
163
  */
158
164
  async init () {
159
165
  this.closing = this.closed = false
166
+
160
167
  if (this._initialized || this._initializing) {
161
168
  throw new Error('jsreport already initialized or just initializing. Make sure init is called only once')
162
169
  }
@@ -175,7 +182,14 @@ class MainReporter extends Reporter {
175
182
 
176
183
  try {
177
184
  this._registerLogMainAction()
178
- await this.extensionsLoad()
185
+
186
+ let explicitOptions
187
+
188
+ await this.extensionsLoad({
189
+ onConfigDetails: (_explicitOptions) => {
190
+ explicitOptions = _explicitOptions
191
+ }
192
+ })
179
193
 
180
194
  this.documentStore = DocumentStore(Object.assign({}, this.options, { logger: this.logger }), this.entityTypeValidator, this.encryption)
181
195
  documentStoreActions(this)
@@ -200,6 +214,14 @@ class MainReporter extends Reporter {
200
214
 
201
215
  await this.extensionsManager.init()
202
216
 
217
+ if (this.options.trustUserCode) {
218
+ this.logger.info('Code sandboxing is disabled, users can potentially penetrate the local system if you allow code from external users to be part of your reports')
219
+ }
220
+
221
+ if (explicitOptions.trustUserCode == null && explicitOptions.allowLocalFilesAccess != null) {
222
+ this.logger.warn('options.allowLocalFilesAccess is deprecated, use options.trustUserCode instead')
223
+ }
224
+
203
225
  this.logger.info(`Using general timeout for rendering (reportTimeout: ${this.options.reportTimeout})`)
204
226
 
205
227
  if (this.options.store.provider === 'memory') {
@@ -9,9 +9,10 @@ const LRU = require('lru-cache')
9
9
  const { nanoid } = require('nanoid')
10
10
 
11
11
  module.exports = (reporter) => {
12
- const cache = LRU(reporter.options.sandbox.cache || { max: 100 })
12
+ const templatesCache = LRU(reporter.options.sandbox.cache)
13
+ let systemHelpersCache
13
14
 
14
- reporter.templatingEngines = { cache }
15
+ reporter.templatingEngines = { cache: templatesCache }
15
16
 
16
17
  const executionFnParsedParamsMap = new Map()
17
18
  const executionAsyncResultsMap = new Map()
@@ -60,6 +61,33 @@ module.exports = (reporter) => {
60
61
  evaluate: async (executionInfo, entityInfo) => {
61
62
  return templatingEnginesEvaluate(false, executionInfo, entityInfo, req)
62
63
  },
64
+ waitForAsyncHelper: async (maybeAsyncContent) => {
65
+ if (
66
+ context.__executionId == null ||
67
+ !executionAsyncResultsMap.has(context.__executionId) ||
68
+ typeof maybeAsyncContent !== 'string'
69
+ ) {
70
+ return maybeAsyncContent
71
+ }
72
+
73
+ const asyncResultMap = executionAsyncResultsMap.get(context.__executionId)
74
+ const asyncHelperResultRegExp = /{#asyncHelperResult ([^{}]+)}/
75
+ let content = maybeAsyncContent
76
+ let matchResult
77
+
78
+ do {
79
+ if (matchResult != null) {
80
+ const matchedPart = matchResult[0]
81
+ const asyncResultId = matchResult[1]
82
+ const result = await asyncResultMap.get(asyncResultId)
83
+ content = `${content.slice(0, matchResult.index)}${result}${content.slice(matchResult.index + matchedPart.length)}`
84
+ }
85
+
86
+ matchResult = content.match(asyncHelperResultRegExp)
87
+ } while (matchResult != null)
88
+
89
+ return content
90
+ },
63
91
  waitForAsyncHelpers: async () => {
64
92
  if (context.__executionId != null && executionAsyncResultsMap.has(context.__executionId)) {
65
93
  const asyncResultMap = executionAsyncResultsMap.get(context.__executionId)
@@ -102,22 +130,49 @@ module.exports = (reporter) => {
102
130
  entityPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
103
131
  }
104
132
 
105
- const registerResults = await reporter.registerHelpersListeners.fire(req)
106
- const systemHelpers = []
133
+ const normalizedHelpers = `${helpers || ''}`
134
+ const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${normalizedHelpers}`
107
135
 
108
- for (const result of registerResults) {
109
- if (result == null) {
110
- continue
136
+ const initFn = async (getTopLevelFunctions, compileScript) => {
137
+ if (systemHelpersCache != null) {
138
+ return systemHelpersCache
111
139
  }
112
140
 
113
- if (typeof result === 'string') {
114
- systemHelpers.push(result)
141
+ const registerResults = await reporter.registerHelpersListeners.fire()
142
+ const systemHelpers = []
143
+
144
+ for (const result of registerResults) {
145
+ if (result == null) {
146
+ continue
147
+ }
148
+
149
+ if (typeof result === 'string') {
150
+ systemHelpers.push(result)
151
+ }
115
152
  }
116
- }
117
153
 
118
- const systemHelpersStr = systemHelpers.join('\n')
119
- const joinedHelpers = systemHelpersStr + '\n' + (helpers || '')
120
- const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${joinedHelpers}`
154
+ const systemHelpersStr = systemHelpers.join('\n')
155
+
156
+ const functionNames = getTopLevelFunctions(systemHelpersStr)
157
+
158
+ const exposeSystemHelpersCode = `for (const fName of ${JSON.stringify(functionNames)}) { this[fName] = __topLevelFunctions[fName] }`
159
+
160
+ // we sync the __topLevelFunctions with system helpers and expose it immediately to the global context
161
+ const userCode = `(async () => { ${systemHelpersStr};
162
+ __topLevelFunctions = {...__topLevelFunctions, ${functionNames.map(h => `"${h}": ${h}`).join(',')}}; ${exposeSystemHelpersCode}
163
+ })()`
164
+
165
+ const filename = 'system-helpers.js'
166
+ const script = compileScript(userCode, filename)
167
+
168
+ systemHelpersCache = {
169
+ filename,
170
+ source: systemHelpersStr,
171
+ script
172
+ }
173
+
174
+ return systemHelpersCache
175
+ }
121
176
 
122
177
  const executionFn = async ({ require, console, topLevelFunctions, context }) => {
123
178
  const asyncResultMap = new Map()
@@ -129,17 +184,16 @@ module.exports = (reporter) => {
129
184
 
130
185
  const key = `template:${content}:${engine.name}`
131
186
 
132
- if (!cache.has(key)) {
187
+ if (!templatesCache.has(key)) {
133
188
  try {
134
- cache.set(key, engine.compile(content, { require }))
189
+ templatesCache.set(key, engine.compile(content, { require }))
135
190
  } catch (e) {
136
191
  e.property = 'content'
137
192
  throw e
138
193
  }
139
194
  }
140
195
 
141
- const compiledTemplate = cache.get(key)
142
-
196
+ const compiledTemplate = templatesCache.get(key)
143
197
  const wrappedTopLevelFunctions = {}
144
198
 
145
199
  for (const h of Object.keys(topLevelFunctions)) {
@@ -147,7 +201,9 @@ module.exports = (reporter) => {
147
201
  }
148
202
 
149
203
  let contentResult = await engine.execute(compiledTemplate, wrappedTopLevelFunctions, data, { require })
204
+
150
205
  const resolvedResultsMap = new Map()
206
+
151
207
  while (asyncResultMap.size > 0) {
152
208
  await Promise.all([...asyncResultMap.keys()].map(async (k) => {
153
209
  resolvedResultsMap.set(k, `${await asyncResultMap.get(k)}`)
@@ -178,14 +234,16 @@ module.exports = (reporter) => {
178
234
  return executionFn({ require, console, topLevelFunctions, context })
179
235
  } else {
180
236
  const awaiter = {}
237
+
181
238
  awaiter.promise = new Promise((resolve) => {
182
239
  awaiter.resolve = resolve
183
240
  })
241
+
184
242
  executionFnParsedParamsMap.get(req.context.id).set(executionFnParsedParamsKey, awaiter)
185
243
  }
186
244
 
187
245
  if (reporter.options.sandbox.cache && reporter.options.sandbox.cache.enabled === false) {
188
- cache.reset()
246
+ templatesCache.reset()
189
247
  }
190
248
 
191
249
  try {
@@ -193,10 +251,10 @@ module.exports = (reporter) => {
193
251
  context: {
194
252
  ...(engine.createContext ? engine.createContext() : {})
195
253
  },
196
- userCode: joinedHelpers,
254
+ userCode: normalizedHelpers,
255
+ initFn,
197
256
  executionFn,
198
257
  currentPath: entityPath,
199
- errorLineNumberOffset: systemHelpersStr.split('\n').length,
200
258
  onRequire: (moduleName, { context }) => {
201
259
  if (engine.onRequire) {
202
260
  return engine.onRequire(moduleName, { context })
@@ -4,7 +4,7 @@ const path = require('path')
4
4
  module.exports = (reporter) => {
5
5
  let helpersScript
6
6
 
7
- reporter.registerHelpersListeners.add('core-helpers', (req) => {
7
+ reporter.registerHelpersListeners.add('core-helpers', () => {
8
8
  return helpersScript
9
9
  })
10
10
 
@@ -12,11 +12,11 @@ module.exports = (reporter) => {
12
12
  helpersScript = await fs.readFile(path.join(__dirname, '../../static/helpers.js'), 'utf8')
13
13
  })
14
14
 
15
- reporter.extendProxy((proxy, req, { safeRequire }) => {
15
+ reporter.extendProxy((proxy, req, { sandboxRequire }) => {
16
16
  proxy.module = async (module) => {
17
- if (!reporter.options.allowLocalFilesAccess && reporter.options.sandbox.allowedModules !== '*') {
17
+ if (!reporter.options.trustUserCode && reporter.options.sandbox.allowedModules !== '*') {
18
18
  if (reporter.options.sandbox.allowedModules.indexOf(module) === -1) {
19
- throw reporter.createError(`require of module ${module} was rejected. Either set allowLocalFilesAccess=true or sandbox.allowLocalModules='*' or sandbox.allowLocalModules=['${module}'] `, { status: 400 })
19
+ throw reporter.createError(`require of module ${module} was rejected. Either set trustUserCode=true or sandbox.allowLocalModules='*' or sandbox.allowLocalModules=['${module}'] `, { status: 400 })
20
20
  }
21
21
  }
22
22
 
@@ -72,7 +72,7 @@ class Profiler {
72
72
  let content = res.content
73
73
 
74
74
  if (content != null) {
75
- if (content.length > this.reporter.options.profiler.maxResponseSize) {
75
+ if (content.length > this.reporter.options.profiler.maxDiffSize) {
76
76
  content = {
77
77
  tooLarge: true
78
78
  }
@@ -97,7 +97,12 @@ class Profiler {
97
97
 
98
98
  const stringifiedReq = JSON.stringify({ template: req.template, data: req.data }, null, 2)
99
99
 
100
- m.req = { diff: createPatch('req', req.context.profiling.reqLastVal || '', stringifiedReq, 0) }
100
+ m.req = { }
101
+ if (stringifiedReq.length * 4 > this.reporter.options.profiler.maxDiffSize) {
102
+ m.req.tooLarge = true
103
+ } else {
104
+ m.req.diff = createPatch('req', req.context.profiling.reqLastVal || '', stringifiedReq, 0)
105
+ }
101
106
 
102
107
  req.context.profiling.resLastVal = (res.content == null || isbinaryfile(res.content) || content.tooLarge) ? null : res.content.toString()
103
108
  req.context.profiling.resMetaLastVal = stringifiedResMeta
@@ -111,14 +111,14 @@ class WorkerReporter extends Reporter {
111
111
  this._proxyRegistrationFns.push(registrationFn)
112
112
  }
113
113
 
114
- createProxy ({ req, runInSandbox, context, getTopLevelFunctions, safeRequire }) {
114
+ createProxy ({ req, runInSandbox, context, getTopLevelFunctions, sandboxRequire }) {
115
115
  const proxyInstance = {}
116
116
  for (const fn of this._proxyRegistrationFns) {
117
117
  fn(proxyInstance, req, {
118
118
  runInSandbox,
119
119
  context,
120
120
  getTopLevelFunctions,
121
- safeRequire
121
+ sandboxRequire
122
122
  })
123
123
  }
124
124
  return proxyInstance
@@ -137,10 +137,11 @@ class WorkerReporter extends Reporter {
137
137
  manager,
138
138
  context,
139
139
  userCode,
140
+ initFn,
140
141
  executionFn,
142
+ currentPath,
141
143
  onRequire,
142
144
  propertiesConfig,
143
- currentPath,
144
145
  errorLineNumberOffset
145
146
  }, req) {
146
147
  // we flush before running code in sandbox because it can potentially
@@ -152,10 +153,11 @@ class WorkerReporter extends Reporter {
152
153
  manager,
153
154
  context,
154
155
  userCode,
156
+ initFn,
155
157
  executionFn,
158
+ currentPath,
156
159
  onRequire,
157
160
  propertiesConfig,
158
- currentPath,
159
161
  errorLineNumberOffset
160
162
  }, req)
161
163
  }
@@ -19,6 +19,7 @@ module.exports = (_sandbox, options = {}) => {
19
19
  propertiesConfig = {},
20
20
  globalModules = [],
21
21
  allowedModules = [],
22
+ safeExecution,
22
23
  requireMap
23
24
  } = options
24
25
 
@@ -65,7 +66,7 @@ module.exports = (_sandbox, options = {}) => {
65
66
  }
66
67
  }
67
68
 
68
- if (allowAllModules || allowedModules === '*') {
69
+ if (!safeExecution || allowAllModules || allowedModules === '*') {
69
70
  return doRequire(moduleName, requirePaths, modulesCache)
70
71
  }
71
72
 
@@ -111,14 +112,28 @@ module.exports = (_sandbox, options = {}) => {
111
112
  require: (m) => _require(m, { context: _sandbox })
112
113
  })
113
114
 
114
- const vm = new VM()
115
+ let safeVM
116
+ let vmSandbox
115
117
 
116
- // delete the vm.sandbox.global because it introduces json stringify issues
117
- // and we don't need such global in context
118
- delete vm.sandbox.global
118
+ if (safeExecution) {
119
+ safeVM = new VM()
119
120
 
120
- for (const name in sandbox) {
121
- vm.setGlobal(name, sandbox[name])
121
+ // delete the vm.sandbox.global because it introduces json stringify issues
122
+ // and we don't need such global in context
123
+ delete safeVM.sandbox.global
124
+
125
+ for (const name in sandbox) {
126
+ safeVM.setGlobal(name, sandbox[name])
127
+ }
128
+
129
+ vmSandbox = safeVM.sandbox
130
+ } else {
131
+ vmSandbox = originalVM.createContext(undefined)
132
+ vmSandbox.Buffer = Buffer
133
+
134
+ for (const name in sandbox) {
135
+ vmSandbox[name] = sandbox[name]
136
+ }
122
137
  }
123
138
 
124
139
  // processing top level props because getter/setter descriptors
@@ -127,49 +142,46 @@ module.exports = (_sandbox, options = {}) => {
127
142
  const currentConfig = propsConfig[key]
128
143
 
129
144
  if (currentConfig.root && currentConfig.root.sandboxReadOnly) {
130
- readOnlyProp(vm.sandbox, key, [], customProxies, { onlyTopLevel: true })
145
+ readOnlyProp(vmSandbox, key, [], customProxies, { onlyTopLevel: true })
131
146
  }
132
147
  })
133
148
 
134
149
  const sourceFilesInfo = new Map()
135
150
 
136
151
  return {
137
- sandbox: vm.sandbox,
152
+ sandbox: vmSandbox,
138
153
  console: _console,
139
154
  sourceFilesInfo,
155
+ compileScript: (code, filename) => {
156
+ return doCompileScript(code, filename, safeExecution)
157
+ },
140
158
  restore: () => {
141
- return restoreProperties(vm.sandbox, originalValues, proxiesInVM, customProxies)
159
+ return restoreProperties(vmSandbox, originalValues, proxiesInVM, customProxies)
142
160
  },
143
- safeRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
144
- run: async (code, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) => {
145
- const script = new VMScript(code, filename)
161
+ sandboxRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
162
+ run: async (codeOrScript, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) => {
163
+ let run
146
164
 
147
165
  if (filename != null && source != null) {
148
166
  sourceFilesInfo.set(filename, { filename, source, entity, entitySet, errorLineNumberOffset })
149
167
  }
150
168
 
151
- // NOTE: if we need to upgrade vm2 we will need to check the source of this function
152
- // in vm2 repo and see if we need to change this,
153
- // we needed to override this method because we want "displayErrors" to be true in order
154
- // to show nice error when the compile of a script fails
155
- script._compile = function (prefix, suffix) {
156
- return new originalVM.Script(prefix + this.getCompiledCode() + suffix, {
157
- __proto__: null,
158
- filename: this.filename,
159
- displayErrors: true,
160
- lineOffset: this.lineOffset,
161
- columnOffset: this.columnOffset,
162
- // THIS FN WAS TAKEN FROM vm2 source, nothing special here
163
- importModuleDynamically: () => {
164
- // We can't throw an error object here because since vm.Script doesn't store a context, we can't properly contextify that error object.
165
- // eslint-disable-next-line no-throw-literal
166
- throw 'Dynamic imports are not allowed.'
167
- }
168
- })
169
+ const script = typeof codeOrScript !== 'string' ? codeOrScript : doCompileScript(codeOrScript, filename, safeExecution)
170
+
171
+ if (safeExecution) {
172
+ run = async () => {
173
+ return safeVM.run(script)
174
+ }
175
+ } else {
176
+ run = async () => {
177
+ return script.runInContext(vmSandbox, {
178
+ displayErrors: true
179
+ })
180
+ }
169
181
  }
170
182
 
171
183
  try {
172
- const result = await vm.run(script)
184
+ const result = await run()
173
185
  return result
174
186
  } catch (e) {
175
187
  decorateErrorMessage(e, sourceFilesInfo)
@@ -180,10 +192,53 @@ module.exports = (_sandbox, options = {}) => {
180
192
  }
181
193
  }
182
194
 
195
+ function doCompileScript (code, filename, safeExecution) {
196
+ let script
197
+
198
+ if (safeExecution) {
199
+ script = new VMScript(code, filename)
200
+
201
+ // NOTE: if we need to upgrade vm2 we will need to check the source of this function
202
+ // in vm2 repo and see if we need to change this,
203
+ // we needed to override this method because we want "displayErrors" to be true in order
204
+ // to show nice error when the compile of a script fails
205
+ script._compile = function (prefix, suffix) {
206
+ return new originalVM.Script(prefix + this.getCompiledCode() + suffix, {
207
+ __proto__: null,
208
+ filename: this.filename,
209
+ displayErrors: true,
210
+ lineOffset: this.lineOffset,
211
+ columnOffset: this.columnOffset,
212
+ // THIS FN WAS TAKEN FROM vm2 source, nothing special here
213
+ importModuleDynamically: () => {
214
+ // We can't throw an error object here because since vm.Script doesn't store a context, we can't properly contextify that error object.
215
+ // eslint-disable-next-line no-throw-literal
216
+ throw 'Dynamic imports are not allowed.'
217
+ }
218
+ })
219
+ }
220
+
221
+ // do the compilation
222
+ script._compileVM()
223
+ } else {
224
+ script = new originalVM.Script(code, {
225
+ filename,
226
+ displayErrors: true,
227
+ importModuleDynamically: () => {
228
+ // We can't throw an error object here because since vm.Script doesn't store a context, we can't properly contextify that error object.
229
+ // eslint-disable-next-line no-throw-literal
230
+ throw 'Dynamic imports are not allowed.'
231
+ }
232
+ })
233
+ }
234
+
235
+ return script
236
+ }
237
+
183
238
  function doRequire (moduleName, requirePaths = [], modulesCache) {
184
239
  const searchedPaths = []
185
240
 
186
- function safeRequire (require, modulePath) {
241
+ function optimizedRequire (require, modulePath) {
187
242
  // save the current module cache, we will use this to restore the cache to the
188
243
  // original values after the require finish
189
244
  const originalModuleCache = Object.assign(Object.create(null), require.cache)
@@ -238,13 +293,13 @@ function doRequire (moduleName, requirePaths = [], modulesCache) {
238
293
  }
239
294
  }
240
295
 
241
- let result = safeRequire(require, moduleName)
296
+ let result = optimizedRequire(require, moduleName)
242
297
 
243
298
  if (!result) {
244
299
  let pathsSearched = 0
245
300
 
246
301
  while (!result && pathsSearched < requirePaths.length) {
247
- result = safeRequire(require, path.join(requirePaths[pathsSearched], moduleName))
302
+ result = optimizedRequire(require, path.join(requirePaths[pathsSearched], moduleName))
248
303
  pathsSearched++
249
304
  }
250
305
  }
@@ -1,14 +1,17 @@
1
1
  const LRU = require('lru-cache')
2
2
  const stackTrace = require('stack-trace')
3
3
  const { customAlphabet } = require('nanoid')
4
- const safeSandbox = require('./safeSandbox')
4
+ const createSandbox = require('./createSandbox')
5
5
  const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
6
6
 
7
7
  module.exports = (reporter) => {
8
- return ({
8
+ const functionsCache = LRU(reporter.options.sandbox.cache)
9
+
10
+ return async ({
9
11
  manager = {},
10
12
  context,
11
13
  userCode,
14
+ initFn,
12
15
  executionFn,
13
16
  currentPath,
14
17
  onRequire,
@@ -19,7 +22,8 @@ module.exports = (reporter) => {
19
22
 
20
23
  // we use dynamic name because of the potential nested vm2 execution in the jsreportProxy.assets.require
21
24
  // it may turn out it is a bad approach in assets so we gonna delete it here
22
- const executionFnName = nanoid() + '_executionFn'
25
+ const executionFnName = `${nanoid()}_executionFn`
26
+
23
27
  context[executionFnName] = executionFn
24
28
  context.__appDirectory = reporter.options.appDirectory
25
29
  context.__rootDirectory = reporter.options.rootDirectory
@@ -28,13 +32,14 @@ module.exports = (reporter) => {
28
32
  context.__topLevelFunctions = {}
29
33
  context.__handleError = (err) => handleError(reporter, err)
30
34
 
31
- const { sourceFilesInfo, run, restore, sandbox, safeRequire } = safeSandbox(context, {
35
+ const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = createSandbox(context, {
32
36
  onLog: (log) => {
33
37
  reporter.logger[log.level](log.message, { ...req, timestamp: log.timestamp })
34
38
  },
35
39
  formatError: (error, moduleName) => {
36
- error.message += ` To be able to require custom modules you need to add to configuration { "allowLocalFilesAccess": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }`
40
+ error.message += ` To be able to require custom modules you need to add to configuration { "trustUserCode": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }`
37
41
  },
42
+ safeExecution: reporter.options.trustUserCode === false,
38
43
  modulesCache: reporter.requestModulesCache.get(req.context.rootId),
39
44
  globalModules: reporter.options.sandbox.nativeModules || [],
40
45
  allowedModules: reporter.options.sandbox.allowedModules,
@@ -61,7 +66,17 @@ module.exports = (reporter) => {
61
66
  }
62
67
  })
63
68
 
64
- jsreportProxy = reporter.createProxy({ req, runInSandbox: run, context: sandbox, getTopLevelFunctions, safeRequire })
69
+ const _getTopLevelFunctions = (code) => {
70
+ return getTopLevelFunctions(functionsCache, code)
71
+ }
72
+
73
+ jsreportProxy = reporter.createProxy({
74
+ req,
75
+ runInSandbox: run,
76
+ context: sandbox,
77
+ getTopLevelFunctions: _getTopLevelFunctions,
78
+ sandboxRequire
79
+ })
65
80
 
66
81
  jsreportProxy.currentPath = async () => {
67
82
  // we get the current path by throwing an error, which give us a stack trace
@@ -114,7 +129,18 @@ module.exports = (reporter) => {
114
129
  // be passed in options
115
130
  manager.restore = restore
116
131
 
117
- const functionNames = getTopLevelFunctions(userCode)
132
+ if (typeof initFn === 'function') {
133
+ const initScriptInfo = await initFn(_getTopLevelFunctions, compileScript)
134
+
135
+ if (initScriptInfo) {
136
+ await run(initScriptInfo.script, {
137
+ filename: initScriptInfo.filename || 'sandbox-init.js',
138
+ source: initScriptInfo.source
139
+ })
140
+ }
141
+ }
142
+
143
+ const functionNames = getTopLevelFunctions(functionsCache, userCode)
118
144
  const functionsCode = `return {${functionNames.map(h => `"${h}": ${h}`).join(',')}}`
119
145
  const executionCode = `;(async () => { ${userCode} \n\n;${functionsCode} })()
120
146
  .then((topLevelFunctions) => {
@@ -180,12 +206,11 @@ function handleError (reporter, errValue) {
180
206
  })
181
207
  }
182
208
 
183
- const functionsCache = LRU({ max: 100 })
184
- function getTopLevelFunctions (code) {
209
+ function getTopLevelFunctions (cache, code) {
185
210
  const key = `functions:${code}`
186
211
 
187
- if (functionsCache.has(key)) {
188
- return functionsCache.get(key)
212
+ if (cache.has(key)) {
213
+ return cache.get(key)
189
214
  }
190
215
 
191
216
  // lazy load to speed up boot
@@ -223,6 +248,6 @@ function getTopLevelFunctions (code) {
223
248
  return []
224
249
  }
225
250
 
226
- functionsCache.set(key, names)
251
+ cache.set(key, names)
227
252
  return names
228
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -32,10 +32,10 @@
32
32
  "@babel/code-frame": "7.12.13",
33
33
  "@babel/parser": "7.14.4",
34
34
  "@babel/traverse": "7.12.9",
35
- "@jsreport/advanced-workers": "1.2.1",
35
+ "@jsreport/advanced-workers": "1.2.2",
36
36
  "@jsreport/mingo": "2.4.1",
37
37
  "ajv": "6.12.6",
38
- "app-root-path": "2.0.1",
38
+ "app-root-path": "3.0.0",
39
39
  "bytes": "3.1.2",
40
40
  "camelcase": "5.0.0",
41
41
  "debug": "4.3.2",
@@ -56,14 +56,14 @@
56
56
  "nanoid": "3.2.0",
57
57
  "nconf": "0.12.0",
58
58
  "node.extend.without.arrays": "1.1.6",
59
- "reap2": "1.0.1",
59
+ "@jsreport/reap": "0.1.0",
60
60
  "semver": "7.3.5",
61
61
  "serializator": "1.0.2",
62
62
  "stack-trace": "0.0.10",
63
63
  "triple-beam": "1.3.0",
64
64
  "unset-value": "1.0.0",
65
65
  "uuid": "8.3.2",
66
- "vm2": "3.9.7",
66
+ "vm2": "3.9.9",
67
67
  "winston": "3.3.3",
68
68
  "winston-transport": "4.4.0"
69
69
  },