@jsreport/jsreport-core 3.11.4 → 3.12.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.
- package/README.md +6 -0
- package/lib/main/optionsSchema.js +1 -0
- package/lib/main/reporter.js +4 -0
- package/lib/worker/sandbox/createSandbox.js +138 -641
- package/lib/worker/sandbox/isolatedRequire.js +462 -0
- package/lib/worker/sandbox/propertiesSandbox.js +521 -0
- package/lib/worker/sandbox/requireSandbox.js +117 -0
- package/lib/worker/sandbox/runInSandbox.js +257 -255
- package/package.json +4 -3
- package/test/extensions/validExtensions/listeners/worker.js +6 -0
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
const os = require('os')
|
|
2
1
|
const util = require('util')
|
|
3
|
-
const path = require('path')
|
|
4
|
-
const extend = require('node.extend.without.arrays')
|
|
5
|
-
const get = require('lodash.get')
|
|
6
|
-
const set = require('lodash.set')
|
|
7
|
-
const hasOwn = require('has-own-deep')
|
|
8
|
-
const unsetValue = require('unset-value')
|
|
9
|
-
const groupBy = require('lodash.groupby')
|
|
10
2
|
const { VM, VMScript } = require('vm2')
|
|
11
3
|
const originalVM = require('vm')
|
|
12
4
|
const stackTrace = require('stack-trace')
|
|
13
5
|
const { codeFrameColumns } = require('@babel/code-frame')
|
|
6
|
+
const createPropertiesManager = require('./propertiesSandbox')
|
|
7
|
+
const createSandboxRequire = require('./requireSandbox')
|
|
14
8
|
|
|
15
|
-
module.exports = (_sandbox, options = {})
|
|
9
|
+
module.exports = function createSandbox (_sandbox, options = {}) {
|
|
16
10
|
const {
|
|
11
|
+
rootDirectory,
|
|
17
12
|
onLog,
|
|
18
13
|
formatError,
|
|
19
14
|
propertiesConfig = {},
|
|
20
15
|
globalModules = [],
|
|
21
16
|
allowedModules = [],
|
|
22
17
|
safeExecution,
|
|
18
|
+
isolateModules,
|
|
23
19
|
requireMap
|
|
24
20
|
} = options
|
|
25
21
|
|
|
@@ -39,82 +35,37 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
39
35
|
// remove duplicates in paths
|
|
40
36
|
requirePaths = requirePaths.filter((v, i) => requirePaths.indexOf(v) === i)
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
onLog({
|
|
49
|
-
timestamp: new Date().getTime(),
|
|
50
|
-
level: level,
|
|
51
|
-
message: util.format.apply(util, arguments)
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
addConsoleMethod('log', 'debug')
|
|
57
|
-
addConsoleMethod('warn', 'warn')
|
|
58
|
-
addConsoleMethod('error', 'error')
|
|
59
|
-
|
|
60
|
-
const _require = function (moduleName, { context, allowAllModules = false } = {}) {
|
|
61
|
-
if (requireMap) {
|
|
62
|
-
const mapResult = requireMap(moduleName, { context })
|
|
63
|
-
|
|
64
|
-
if (mapResult != null) {
|
|
65
|
-
return mapResult
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!safeExecution || allowAllModules || allowedModules === '*') {
|
|
70
|
-
return doRequire(moduleName, requirePaths, modulesCache)
|
|
71
|
-
}
|
|
38
|
+
addConsoleMethod(_console, 'log', 'debug', onLog)
|
|
39
|
+
addConsoleMethod(_console, 'warn', 'warn', onLog)
|
|
40
|
+
addConsoleMethod(_console, 'error', 'error', onLog)
|
|
72
41
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (m) {
|
|
76
|
-
return doRequire(m.path || moduleName, requirePaths, modulesCache)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const error = new Error(
|
|
80
|
-
`require of "${moduleName}" module has been blocked.`
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
if (formatError) {
|
|
84
|
-
formatError(error, moduleName)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
throw error
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
for (const info of globalModules) {
|
|
91
|
-
// it is important to use "doRequire" function here to avoid
|
|
92
|
-
// getting hit by the allowed modules restriction
|
|
93
|
-
_sandbox[info.globalVariableName] = doRequire(info.module, requirePaths, modulesCache)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const propsConfig = normalizePropertiesConfigInHierarchy(propertiesConfig)
|
|
97
|
-
const originalValues = {}
|
|
98
|
-
const proxiesInVM = new WeakMap()
|
|
99
|
-
const customProxies = new WeakMap()
|
|
42
|
+
const propsManager = createPropertiesManager(propertiesConfig)
|
|
100
43
|
|
|
101
44
|
// we copy the object based on config to avoid sharing same context
|
|
102
45
|
// (with getters/setters) in the rest of request pipeline
|
|
103
|
-
const sandbox =
|
|
46
|
+
const sandbox = propsManager.copyPropertyValuesFrom(_sandbox)
|
|
104
47
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
48
|
+
propsManager.applyPropertiesConfigTo(sandbox)
|
|
49
|
+
|
|
50
|
+
let safeVM
|
|
51
|
+
// with standard vm this variable is the same as context, with vm2 it is a proxy of context
|
|
52
|
+
// (which is not the real internal context)
|
|
53
|
+
let vmSandbox
|
|
54
|
+
|
|
55
|
+
const doSandboxRequire = createSandboxRequire(safeExecution, isolateModules, modulesCache, {
|
|
56
|
+
rootDirectory,
|
|
57
|
+
requirePaths,
|
|
58
|
+
requireMap,
|
|
59
|
+
allowedModules,
|
|
60
|
+
compileScript: doCompileScript,
|
|
61
|
+
formatError
|
|
108
62
|
})
|
|
109
63
|
|
|
110
64
|
Object.assign(sandbox, {
|
|
111
65
|
console: _console,
|
|
112
|
-
require
|
|
66
|
+
require (m) { return doSandboxRequire(m, { context: vmSandbox }) }
|
|
113
67
|
})
|
|
114
68
|
|
|
115
|
-
let safeVM
|
|
116
|
-
let vmSandbox
|
|
117
|
-
|
|
118
69
|
if (safeExecution) {
|
|
119
70
|
safeVM = new VM()
|
|
120
71
|
|
|
@@ -126,6 +77,21 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
126
77
|
safeVM.setGlobal(name, sandbox[name])
|
|
127
78
|
}
|
|
128
79
|
|
|
80
|
+
// so far we don't have the need to have access to real vm context inside vm2,
|
|
81
|
+
// but if we need it, we should use the code bellow to get it.
|
|
82
|
+
// NOTE: if we need to upgrade vm2 we will need to check the source of this function
|
|
83
|
+
// in vm2 repo and see if we need to change this,
|
|
84
|
+
// we just execute this to get access to the internal context, so we can use it later
|
|
85
|
+
// with the our require function, in newer versions of vm2 we may need to change how to
|
|
86
|
+
// get access to it
|
|
87
|
+
// https://github.com/patriksimek/vm2/blob/3.9.17/lib/vm.js#L281
|
|
88
|
+
// safeVM._runScript({
|
|
89
|
+
// runInContext: (_context) => {
|
|
90
|
+
// vmContext = _context
|
|
91
|
+
// return ''
|
|
92
|
+
// }
|
|
93
|
+
// })
|
|
94
|
+
|
|
129
95
|
vmSandbox = safeVM.sandbox
|
|
130
96
|
} else {
|
|
131
97
|
vmSandbox = originalVM.createContext(undefined)
|
|
@@ -136,15 +102,15 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
136
102
|
}
|
|
137
103
|
}
|
|
138
104
|
|
|
139
|
-
// processing top level props because getter/setter descriptors
|
|
105
|
+
// processing top level props here because getter/setter descriptors
|
|
140
106
|
// for top level properties will only work after VM instantiation
|
|
141
|
-
|
|
142
|
-
const currentConfig = propsConfig[key]
|
|
107
|
+
propsManager.applyRootPropertiesConfigTo(vmSandbox)
|
|
143
108
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
109
|
+
for (const info of globalModules) {
|
|
110
|
+
// it is important to use _sandboxRequire function with allowAllModules: true here to avoid
|
|
111
|
+
// getting hit by the allowed modules restriction
|
|
112
|
+
vmSandbox[info.globalVariableName] = doSandboxRequire(info.module, { context: vmSandbox, useMap: false, allowAllModules: true })
|
|
113
|
+
}
|
|
148
114
|
|
|
149
115
|
const sourceFilesInfo = new Map()
|
|
150
116
|
|
|
@@ -152,15 +118,17 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
152
118
|
sandbox: vmSandbox,
|
|
153
119
|
console: _console,
|
|
154
120
|
sourceFilesInfo,
|
|
155
|
-
compileScript
|
|
121
|
+
compileScript (code, filename) {
|
|
156
122
|
return doCompileScript(code, filename, safeExecution)
|
|
157
123
|
},
|
|
158
|
-
restore
|
|
159
|
-
return
|
|
124
|
+
restore () {
|
|
125
|
+
return propsManager.restorePropertiesFrom(vmSandbox)
|
|
160
126
|
},
|
|
161
|
-
sandboxRequire
|
|
162
|
-
|
|
163
|
-
|
|
127
|
+
sandboxRequire (modulePath) {
|
|
128
|
+
return doSandboxRequire(modulePath, { context: vmSandbox, allowAllModules: true })
|
|
129
|
+
},
|
|
130
|
+
async run (codeOrScript, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) {
|
|
131
|
+
let runScript
|
|
164
132
|
|
|
165
133
|
if (filename != null && source != null) {
|
|
166
134
|
sourceFilesInfo.set(filename, { filename, source, entity, entitySet, errorLineNumberOffset })
|
|
@@ -169,11 +137,11 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
169
137
|
const script = typeof codeOrScript !== 'string' ? codeOrScript : doCompileScript(codeOrScript, filename, safeExecution)
|
|
170
138
|
|
|
171
139
|
if (safeExecution) {
|
|
172
|
-
|
|
140
|
+
runScript = async function runScript () {
|
|
173
141
|
return safeVM.run(script)
|
|
174
142
|
}
|
|
175
143
|
} else {
|
|
176
|
-
|
|
144
|
+
runScript = async function runScript () {
|
|
177
145
|
return script.runInContext(vmSandbox, {
|
|
178
146
|
displayErrors: true
|
|
179
147
|
})
|
|
@@ -181,7 +149,7 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
181
149
|
}
|
|
182
150
|
|
|
183
151
|
try {
|
|
184
|
-
const result = await
|
|
152
|
+
const result = await runScript()
|
|
185
153
|
return result
|
|
186
154
|
} catch (e) {
|
|
187
155
|
decorateErrorMessage(e, sourceFilesInfo)
|
|
@@ -202,6 +170,7 @@ function doCompileScript (code, filename, safeExecution) {
|
|
|
202
170
|
// in vm2 repo and see if we need to change this,
|
|
203
171
|
// we needed to override this method because we want "displayErrors" to be true in order
|
|
204
172
|
// to show nice error when the compile of a script fails
|
|
173
|
+
// https://github.com/patriksimek/vm2/blob/3.9.17/lib/script.js#L329
|
|
205
174
|
script._compile = function (prefix, suffix) {
|
|
206
175
|
return new originalVM.Script(prefix + this.getCompiledCode() + suffix, {
|
|
207
176
|
__proto__: null,
|
|
@@ -235,82 +204,6 @@ function doCompileScript (code, filename, safeExecution) {
|
|
|
235
204
|
return script
|
|
236
205
|
}
|
|
237
206
|
|
|
238
|
-
function doRequire (moduleName, requirePaths = [], modulesCache) {
|
|
239
|
-
const searchedPaths = []
|
|
240
|
-
|
|
241
|
-
function optimizedRequire (require, modulePath) {
|
|
242
|
-
// save the current module cache, we will use this to restore the cache to the
|
|
243
|
-
// original values after the require finish
|
|
244
|
-
const originalModuleCache = Object.assign(Object.create(null), require.cache)
|
|
245
|
-
|
|
246
|
-
// clean/empty the current module cache
|
|
247
|
-
for (const cacheKey of Object.keys(require.cache)) {
|
|
248
|
-
delete require.cache[cacheKey]
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// restore any previous cache generated in the sandbox
|
|
252
|
-
for (const cacheKey of Object.keys(modulesCache)) {
|
|
253
|
-
require.cache[cacheKey] = modulesCache[cacheKey]
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
const moduleExport = require.main ? require.main.require(modulePath) : require(modulePath)
|
|
258
|
-
require.main.children.splice(require.main.children.indexOf(m => m.id === require.resolve(modulePath)), 1)
|
|
259
|
-
|
|
260
|
-
// save the current module cache generated after the require into the internal cache,
|
|
261
|
-
// and clean the current module cache again
|
|
262
|
-
for (const cacheKey of Object.keys(require.cache)) {
|
|
263
|
-
modulesCache[cacheKey] = require.cache[cacheKey]
|
|
264
|
-
delete require.cache[cacheKey]
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// restore the current module cache to the original cache values
|
|
268
|
-
for (const [oldCacheKey, value] of Object.entries(originalModuleCache)) {
|
|
269
|
-
require.cache[oldCacheKey] = value
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return moduleExport
|
|
273
|
-
} catch (e) {
|
|
274
|
-
// clean the current module cache again
|
|
275
|
-
for (const cacheKey of Object.keys(require.cache)) {
|
|
276
|
-
delete require.cache[cacheKey]
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// restore the current module cache to the original cache values
|
|
280
|
-
for (const [oldCacheKey, value] of Object.entries(originalModuleCache)) {
|
|
281
|
-
require.cache[oldCacheKey] = value
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (e.code && e.code === 'MODULE_NOT_FOUND') {
|
|
285
|
-
if (!searchedPaths.includes(modulePath)) {
|
|
286
|
-
searchedPaths.push(modulePath)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return false
|
|
290
|
-
} else {
|
|
291
|
-
throw new Error(`Unable to require module ${moduleName}. ${e.message}${os.EOL}${e.stack}`)
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let result = optimizedRequire(require, moduleName)
|
|
297
|
-
|
|
298
|
-
if (!result) {
|
|
299
|
-
let pathsSearched = 0
|
|
300
|
-
|
|
301
|
-
while (!result && pathsSearched < requirePaths.length) {
|
|
302
|
-
result = optimizedRequire(require, path.join(requirePaths[pathsSearched], moduleName))
|
|
303
|
-
pathsSearched++
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (!result) {
|
|
308
|
-
throw new Error(`Unable to find module ${moduleName}${os.EOL}The require calls:${os.EOL}${searchedPaths.map(p => `require('${p}')`).join(os.EOL)}${os.EOL}`)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return result
|
|
312
|
-
}
|
|
313
|
-
|
|
314
207
|
function decorateErrorMessage (e, sourceFilesInfo) {
|
|
315
208
|
const filesCount = sourceFilesInfo.size
|
|
316
209
|
|
|
@@ -383,484 +276,88 @@ function decorateErrorMessage (e, sourceFilesInfo) {
|
|
|
383
276
|
e.message = `${e.message}`
|
|
384
277
|
}
|
|
385
278
|
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
newValue = getOriginalFromProxy(proxiesInVM, customProxies, customProxies.get(value))
|
|
391
|
-
} else if (proxiesInVM.has(value)) {
|
|
392
|
-
newValue = getOriginalFromProxy(proxiesInVM, customProxies, proxiesInVM.get(value))
|
|
393
|
-
} else {
|
|
394
|
-
newValue = value
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return newValue
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function copyBasedOnPropertiesConfig (context, propertiesMap) {
|
|
401
|
-
const copied = []
|
|
402
|
-
const newContext = Object.assign({}, context)
|
|
403
|
-
|
|
404
|
-
Object.keys(propertiesMap).sort(sortPropertiesByLevel).forEach((prop) => {
|
|
405
|
-
const parts = prop.split('.')
|
|
406
|
-
const lastPartsIndex = parts.length - 1
|
|
407
|
-
|
|
408
|
-
for (let i = 0; i <= lastPartsIndex; i++) {
|
|
409
|
-
let currentContext = newContext
|
|
410
|
-
const propName = parts[i]
|
|
411
|
-
const parentPath = parts.slice(0, i).join('.')
|
|
412
|
-
const fullPropName = parts.slice(0, i + 1).join('.')
|
|
413
|
-
let value
|
|
414
|
-
|
|
415
|
-
if (copied.indexOf(fullPropName) !== -1) {
|
|
416
|
-
continue
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (parentPath !== '') {
|
|
420
|
-
currentContext = get(newContext, parentPath)
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (currentContext) {
|
|
424
|
-
value = currentContext[propName]
|
|
425
|
-
|
|
426
|
-
if (typeof value === 'object') {
|
|
427
|
-
if (value === null) {
|
|
428
|
-
value = null
|
|
429
|
-
} else if (Array.isArray(value)) {
|
|
430
|
-
value = Object.assign([], value)
|
|
431
|
-
} else {
|
|
432
|
-
value = Object.assign({}, value)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
currentContext[propName] = value
|
|
436
|
-
copied.push(fullPropName)
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
return newContext
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function applyPropertiesConfig (context, config, {
|
|
446
|
-
original,
|
|
447
|
-
customProxies,
|
|
448
|
-
isRoot = true,
|
|
449
|
-
isGrouped = true,
|
|
450
|
-
onlyReadOnlyTopLevel = false,
|
|
451
|
-
parentOpts,
|
|
452
|
-
prop
|
|
453
|
-
} = {}, readOnlyConfigured = []) {
|
|
454
|
-
let isHidden
|
|
455
|
-
let isReadOnly
|
|
456
|
-
let standalonePropertiesHandled = false
|
|
457
|
-
let innerPropertiesHandled = false
|
|
458
|
-
|
|
459
|
-
if (isRoot) {
|
|
460
|
-
return Object.keys(config).forEach((key) => {
|
|
461
|
-
applyPropertiesConfig(context, config[key], {
|
|
462
|
-
original,
|
|
463
|
-
customProxies,
|
|
464
|
-
prop: key,
|
|
465
|
-
isRoot: false,
|
|
466
|
-
isGrouped: true,
|
|
467
|
-
onlyReadOnlyTopLevel,
|
|
468
|
-
parentOpts
|
|
469
|
-
}, readOnlyConfigured)
|
|
470
|
-
})
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (parentOpts && parentOpts.sandboxHidden === true) {
|
|
474
|
-
return
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (isGrouped) {
|
|
478
|
-
isHidden = config.root ? config.root.sandboxHidden === true : false
|
|
479
|
-
isReadOnly = config.root ? config.root.sandboxReadOnly === true : false
|
|
480
|
-
} else {
|
|
481
|
-
isHidden = config ? config.sandboxHidden === true : false
|
|
482
|
-
isReadOnly = config ? config.sandboxReadOnly === true : false
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
let shouldStoreOriginal = isHidden || isReadOnly
|
|
486
|
-
|
|
487
|
-
// prevent storing original value if there is config some child prop
|
|
488
|
-
if (
|
|
489
|
-
shouldStoreOriginal &&
|
|
490
|
-
isGrouped &&
|
|
491
|
-
(config.inner != null || config.standalone != null)
|
|
492
|
-
) {
|
|
493
|
-
shouldStoreOriginal = false
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// saving original value
|
|
497
|
-
if (shouldStoreOriginal) {
|
|
498
|
-
let exists = true
|
|
499
|
-
let newValue
|
|
500
|
-
|
|
501
|
-
if (hasOwn(context, prop)) {
|
|
502
|
-
const originalPropValue = get(context, prop)
|
|
503
|
-
|
|
504
|
-
if (typeof originalPropValue === 'object' && originalPropValue != null) {
|
|
505
|
-
if (Array.isArray(originalPropValue)) {
|
|
506
|
-
newValue = extend(true, [], originalPropValue)
|
|
507
|
-
} else {
|
|
508
|
-
newValue = extend(true, {}, originalPropValue)
|
|
509
|
-
}
|
|
510
|
-
} else {
|
|
511
|
-
newValue = originalPropValue
|
|
512
|
-
}
|
|
513
|
-
} else {
|
|
514
|
-
exists = false
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
original[prop] = {
|
|
518
|
-
exists,
|
|
519
|
-
value: newValue
|
|
279
|
+
function addConsoleMethod (target, consoleMethod, level, onLog) {
|
|
280
|
+
target[consoleMethod] = function () {
|
|
281
|
+
if (onLog == null) {
|
|
282
|
+
return
|
|
520
283
|
}
|
|
521
|
-
}
|
|
522
284
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
applyPropertiesConfig(context, sconfig, {
|
|
528
|
-
original,
|
|
529
|
-
customProxies,
|
|
530
|
-
prop: skey,
|
|
531
|
-
isRoot: false,
|
|
532
|
-
isGrouped: false,
|
|
533
|
-
onlyReadOnlyTopLevel,
|
|
534
|
-
parentOpts: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
|
|
535
|
-
}, readOnlyConfigured)
|
|
285
|
+
onLog({
|
|
286
|
+
timestamp: new Date().getTime(),
|
|
287
|
+
level: level,
|
|
288
|
+
message: util.format.apply(util, arguments)
|
|
536
289
|
})
|
|
537
290
|
}
|
|
538
|
-
|
|
539
|
-
const processInnerProperties = (c) => {
|
|
540
|
-
Object.keys(c.inner).forEach((ikey) => {
|
|
541
|
-
const iconfig = c.inner[ikey]
|
|
542
|
-
|
|
543
|
-
applyPropertiesConfig(context, iconfig, {
|
|
544
|
-
original,
|
|
545
|
-
customProxies,
|
|
546
|
-
prop: ikey,
|
|
547
|
-
isRoot: false,
|
|
548
|
-
isGrouped: true,
|
|
549
|
-
parentOpts: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
|
|
550
|
-
}, readOnlyConfigured)
|
|
551
|
-
})
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (isHidden) {
|
|
555
|
-
omitProp(context, prop)
|
|
556
|
-
} else if (isReadOnly) {
|
|
557
|
-
readOnlyProp(context, prop, readOnlyConfigured, customProxies, {
|
|
558
|
-
onlyTopLevel: false,
|
|
559
|
-
onBeforeProxy: () => {
|
|
560
|
-
if (isGrouped && config.standalone != null) {
|
|
561
|
-
processStandAloneProperties(config)
|
|
562
|
-
standalonePropertiesHandled = true
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (isGrouped && config.inner != null) {
|
|
566
|
-
processInnerProperties(config)
|
|
567
|
-
innerPropertiesHandled = true
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
})
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (!isGrouped) {
|
|
574
|
-
return
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// don't process inner config when the value in context is empty
|
|
578
|
-
if (get(context, prop) == null) {
|
|
579
|
-
return
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (!standalonePropertiesHandled && config.standalone != null) {
|
|
583
|
-
processStandAloneProperties(config)
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (!innerPropertiesHandled && config.inner != null) {
|
|
587
|
-
processInnerProperties(config)
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function restoreProperties (context, originalValues, proxiesInVM, customProxies) {
|
|
592
|
-
const restored = []
|
|
593
|
-
const newContext = Object.assign({}, context)
|
|
594
|
-
|
|
595
|
-
Object.keys(originalValues).sort(sortPropertiesByLevel).forEach((prop) => {
|
|
596
|
-
const confValue = originalValues[prop]
|
|
597
|
-
const parts = prop.split('.')
|
|
598
|
-
const lastPartsIndex = parts.length - 1
|
|
599
|
-
|
|
600
|
-
for (let i = 0; i <= lastPartsIndex; i++) {
|
|
601
|
-
let currentContext = newContext
|
|
602
|
-
const propName = parts[i]
|
|
603
|
-
const parentPath = parts.slice(0, i).join('.')
|
|
604
|
-
const fullPropName = parts.slice(0, i + 1).join('.')
|
|
605
|
-
let value
|
|
606
|
-
|
|
607
|
-
if (restored.indexOf(fullPropName) !== -1) {
|
|
608
|
-
continue
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (parentPath !== '') {
|
|
612
|
-
currentContext = get(newContext, parentPath)
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (currentContext) {
|
|
616
|
-
value = currentContext[propName]
|
|
617
|
-
|
|
618
|
-
// unwrapping proxies
|
|
619
|
-
value = getOriginalFromProxy(proxiesInVM, customProxies, value)
|
|
620
|
-
|
|
621
|
-
if (typeof value === 'object') {
|
|
622
|
-
// we call object assign to be able to get rid of
|
|
623
|
-
// previous properties descriptors (hide/readOnly) configured
|
|
624
|
-
if (value === null) {
|
|
625
|
-
value = null
|
|
626
|
-
} else if (Array.isArray(value)) {
|
|
627
|
-
value = Object.assign([], value)
|
|
628
|
-
} else {
|
|
629
|
-
value = Object.assign({}, value)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
currentContext[propName] = value
|
|
633
|
-
restored.push(fullPropName)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (i === lastPartsIndex) {
|
|
637
|
-
if (confValue.exists) {
|
|
638
|
-
currentContext[propName] = confValue.value
|
|
639
|
-
} else {
|
|
640
|
-
delete currentContext[propName]
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
// unwrapping proxies for top level properties
|
|
648
|
-
Object.keys(newContext).forEach((prop) => {
|
|
649
|
-
newContext[prop] = getOriginalFromProxy(proxiesInVM, customProxies, newContext[prop])
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
return newContext
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function omitProp (context, prop) {
|
|
656
|
-
// if property has value, then set it to undefined first,
|
|
657
|
-
// unsetValue expects that property has some non empty value to remove the property
|
|
658
|
-
// so we set to "true" to ensure it works for all cases,
|
|
659
|
-
// we use unsetValue instead of lodash.omit because
|
|
660
|
-
// it supports object paths x.y.z and does not copy the object for each call
|
|
661
|
-
if (hasOwn(context, prop)) {
|
|
662
|
-
set(context, prop, true)
|
|
663
|
-
unsetValue(context, prop)
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function readOnlyProp (context, prop, configured, customProxies, { onlyTopLevel = false, onBeforeProxy } = {}) {
|
|
668
|
-
const parts = prop.split('.')
|
|
669
|
-
const lastPartsIndex = parts.length - 1
|
|
670
|
-
|
|
671
|
-
const throwError = (fullPropName) => {
|
|
672
|
-
throw new Error(`Can't modify read only property "${fullPropName}" inside sandbox`)
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
for (let i = 0; i <= lastPartsIndex; i++) {
|
|
676
|
-
let currentContext = context
|
|
677
|
-
const isTopLevelProp = i === 0
|
|
678
|
-
const propName = parts[i]
|
|
679
|
-
const parentPath = parts.slice(0, i).join('.')
|
|
680
|
-
const fullPropName = parts.slice(0, i + 1).join('.')
|
|
681
|
-
let value
|
|
682
|
-
|
|
683
|
-
if (configured.indexOf(fullPropName) !== -1) {
|
|
684
|
-
continue
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (parentPath !== '') {
|
|
688
|
-
currentContext = get(context, parentPath)
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (currentContext) {
|
|
692
|
-
value = currentContext[propName]
|
|
693
|
-
|
|
694
|
-
if (
|
|
695
|
-
i === lastPartsIndex &&
|
|
696
|
-
typeof value === 'object' &&
|
|
697
|
-
value != null
|
|
698
|
-
) {
|
|
699
|
-
const valueType = Array.isArray(value) ? 'array' : 'object'
|
|
700
|
-
const rawValue = value
|
|
701
|
-
|
|
702
|
-
if (onBeforeProxy) {
|
|
703
|
-
onBeforeProxy()
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
value = new Proxy(rawValue, {
|
|
707
|
-
set: (target, prop) => {
|
|
708
|
-
throw new Error(`Can't add or modify property "${prop}" to read only ${valueType} "${fullPropName}" inside sandbox`)
|
|
709
|
-
},
|
|
710
|
-
deleteProperty: (target, prop) => {
|
|
711
|
-
throw new Error(`Can't delete property "${prop}" in read only ${valueType} "${fullPropName}" inside sandbox`)
|
|
712
|
-
}
|
|
713
|
-
})
|
|
714
|
-
|
|
715
|
-
customProxies.set(value, rawValue)
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// only create the getter/setter wrapper if the property is defined,
|
|
719
|
-
// this prevents getting errors about proxy traps and descriptors differences
|
|
720
|
-
// when calling `JSON.stringify(req.context)` from a script
|
|
721
|
-
if (Object.prototype.hasOwnProperty.call(currentContext, propName)) {
|
|
722
|
-
if (!configured.includes(fullPropName)) {
|
|
723
|
-
configured.push(fullPropName)
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
Object.defineProperty(currentContext, propName, {
|
|
727
|
-
get: () => value,
|
|
728
|
-
set: () => { throwError(fullPropName) },
|
|
729
|
-
enumerable: true
|
|
730
|
-
})
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (isTopLevelProp && onlyTopLevel) {
|
|
734
|
-
break
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
291
|
}
|
|
739
292
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
groupedKeys[key].forEach((g) => {
|
|
812
|
-
acu[g] = {}
|
|
813
|
-
|
|
814
|
-
if (configMap[g] != null) {
|
|
815
|
-
acu[g].root = configMap[g]
|
|
816
|
-
}
|
|
817
|
-
})
|
|
818
|
-
|
|
819
|
-
return acu
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const currentLevel = parentLevels[key]
|
|
823
|
-
|
|
824
|
-
if (acu[key] == null) {
|
|
825
|
-
acu[key] = {}
|
|
826
|
-
|
|
827
|
-
if (configMap[key] != null) {
|
|
828
|
-
// root is config that was defined in the same property
|
|
829
|
-
// that it is grouped
|
|
830
|
-
acu[key].root = configMap[key]
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// standalone are properties that are direct, no groups
|
|
835
|
-
acu[key].standalone = groupedKeys[key].reduce((obj, stdProp) => {
|
|
836
|
-
// only add the property is not already grouped
|
|
837
|
-
if (groupedKeys[stdProp] == null) {
|
|
838
|
-
obj[stdProp] = configMap[stdProp]
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
return obj
|
|
842
|
-
}, {})
|
|
843
|
-
|
|
844
|
-
if (Object.keys(acu[key].standalone).length === 0) {
|
|
845
|
-
delete acu[key].standalone
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
const levelKeys = Object.keys(currentLevel)
|
|
849
|
-
|
|
850
|
-
if (levelKeys.length === 0) {
|
|
851
|
-
return acu
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// inner are properties which contains other properties, groups
|
|
855
|
-
acu[key].inner = levelKeys.reduce(toHierarchyConfigMap(currentLevel), {})
|
|
856
|
-
|
|
857
|
-
if (Object.keys(acu[key].inner).length === 0) {
|
|
858
|
-
delete acu[key].inner
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
return acu
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return hierarchy.reduce(toHierarchyConfigMap(hierarchyLevels), {})
|
|
866
|
-
}
|
|
293
|
+
/**
|
|
294
|
+
* NOTE: In the past (<= 3.11.3) the code sandbox in jsreport have worked like this:
|
|
295
|
+
* User code (helpers, scripts, etc) are evaluated in a context we create dedicated to it
|
|
296
|
+
* (`vmSandbox` variable in this file), Modules (code you import using `require`) are evaluated
|
|
297
|
+
* with normal node.js require mechanism, which means such code is evaluated in the main context.
|
|
298
|
+
* One of our requirements is to have isolated modules in the sandbox, this means that
|
|
299
|
+
* imported modules in a render are not re-used in other completely different renders.
|
|
300
|
+
* This requirement differs in the way the node.js require works because it caches the modules,
|
|
301
|
+
* so if you require the same module in different places you get the same module instance,
|
|
302
|
+
* we don't want that, we want to have isolated modules in each render.
|
|
303
|
+
*
|
|
304
|
+
* To fullfil this requirement the approach we took was to make all the require calls inside the sandbox
|
|
305
|
+
* to not cache its resolved modules, to achieve that after a require call is done
|
|
306
|
+
* we proceed to restore the require.cache to its original state, the state that was there before
|
|
307
|
+
* a require call happens, which means we would have to save the current require.cache (before a require) then
|
|
308
|
+
* delete all entries in that object (using delete require.cache[entry]),
|
|
309
|
+
* so the require can re-evaluate the module code and give a fresh module instance.
|
|
310
|
+
* The problem we discovered later was that this approach leads to memory leaks,
|
|
311
|
+
* using the normal require and deleting something from require.cache is not a good idea,
|
|
312
|
+
* it makes memory leaks happen, we didn't dig deeper but it seems node.js internals rely
|
|
313
|
+
* on the presence of the entries to be there in the require.cache in order to execute
|
|
314
|
+
* cleanup during the require execution, handling this improperly make the memory leaks happens,
|
|
315
|
+
* unfortunately doing this properly seems to require access to node.js internals,
|
|
316
|
+
* so nothing else we can do here.
|
|
317
|
+
*
|
|
318
|
+
* NOTE: Currently (>= 3.12.0) the code sandbox works like this:
|
|
319
|
+
* User code (helpers, scripts, etc) are evaluated just like before, in a context we create dedicated to it,
|
|
320
|
+
* however Modules (code you import using `require`) are evaluated with a custom version of require (requireSandbox) we've created.
|
|
321
|
+
* This version of require does not cache resolved modules into the require.cache, so it does not suffer
|
|
322
|
+
* from the memory leaks described above in the past version.
|
|
323
|
+
* The required modules are still cached but in our own managed cache, which only lives per render,
|
|
324
|
+
* so different requires to same modules in same render will return the same module instance (as expected)
|
|
325
|
+
*
|
|
326
|
+
* The main problem we found initially with the custom require was that creating a new
|
|
327
|
+
* instance of vm.Script per module to be evaluated allocated a lot of memory when doing a test case with lot of renders,
|
|
328
|
+
* no matter how many times the same module is required across renders, it was going to be compiled again.
|
|
329
|
+
* the problem was that GC (Garbage Collector) becomes really lazy to claim back the memory used
|
|
330
|
+
* for these scripts, in the end after some idle time the GC claims back the memory but it takes time,
|
|
331
|
+
* this is problem when you do a test case like doing 200 to 1000 renders to a template,
|
|
332
|
+
* it makes memory to be allocated a lot and not released just after some time of being idle, if you don't
|
|
333
|
+
* give it idle time it will eventually just choke and break with heap of out memory errors
|
|
334
|
+
* https://github.com/nodejs/node/issues/40014, https://github.com/nodejs/node/issues/3113,
|
|
335
|
+
* https://github.com/jestjs/jest/issues/11956
|
|
336
|
+
* A workaround to alleviate the issue was to cache scripts, so module is evaluated only once
|
|
337
|
+
* across renders, this is not a problem because we are not caching the module itself, only the compile part
|
|
338
|
+
* which makes sense. with this workaround the test case of 200 - 1000 renders allocates less memory
|
|
339
|
+
* but still the GC continue to be lazy, just that it will hold longer until it breaks
|
|
340
|
+
* with the heap out of memory error.
|
|
341
|
+
* A possible fix to this problem of the lazy GC we found was to use manually call the GC
|
|
342
|
+
* (when running node with --expose-gc), using something like this after render `setTimeout(() => { gc() }, 500)`,
|
|
343
|
+
* this makes the case to release memory better and more faster, however we did not added this because
|
|
344
|
+
* we don't want to deal with running node with exposed gc
|
|
345
|
+
*
|
|
346
|
+
* Another problem we found during the custom require implementation was that all render requests was
|
|
347
|
+
* just going to one worker (the rest were not used and idle), we fixed this and rotated the requests
|
|
348
|
+
* across the workers and the memory release was better (it seems it gives the GC of each worker a bit more of idle time)
|
|
349
|
+
*
|
|
350
|
+
* Last problem we found, was to decide in which context the Modules are going to be run,
|
|
351
|
+
* this affects the results of the user code will get when running constructor comparison,
|
|
352
|
+
* instanceof checks, or when errors originate from modules and are catch in the user code.
|
|
353
|
+
* There were alternatives to run in it in main context (just like the the normal node.js require does it),
|
|
354
|
+
* run it in the same context than sandbox (the context in which the user code is evaluated),
|
|
355
|
+
* or run it in a new context (managed by us).
|
|
356
|
+
* Something that affected our decision was to check how the constructors and instanceof checks
|
|
357
|
+
* were already working in past versions, how it behaves when using trustUserCode: true or not,
|
|
358
|
+
* the results were that when trustUserCode: true is used it produces different results
|
|
359
|
+
* to the results of trustUserCode: false, there was already some inconsistency,
|
|
360
|
+
* so we decided to keep the same behavior and not introduce more inconsistencies,
|
|
361
|
+
* which means we evaluate Modules with main context, one benefit or using the main context
|
|
362
|
+
* was that we did not have to care about re-exposing node.js globals (like Buffer, etc)
|
|
363
|
+
*/
|