@jsreport/jsreport-core 3.11.4 → 4.0.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 +12 -0
- package/lib/main/optionsSchema.js +263 -270
- package/lib/main/reporter.js +12 -16
- package/lib/main/settings.js +14 -8
- package/lib/worker/render/executeEngine.js +23 -1
- package/lib/worker/reporter.js +45 -0
- package/lib/worker/sandbox/createSandbox.js +164 -709
- package/lib/worker/sandbox/isolatedRequire.js +442 -0
- package/lib/worker/sandbox/propertiesSandbox.js +521 -0
- package/lib/worker/sandbox/requireSandbox.js +117 -0
- package/lib/worker/sandbox/runInSandbox.js +270 -255
- package/package.json +7 -6
- package/test/extensions/validExtensions/listeners/main.js +62 -51
- package/test/extensions/validExtensions/listeners/worker.js +81 -68
- package/lib/main/migration/resourcesToAssets.js +0 -230
- package/lib/main/migration/xlsxTemplatesToAssets.js +0 -128
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
const os = require('os')
|
|
2
1
|
const util = require('util')
|
|
3
|
-
const
|
|
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
|
-
const { VM, VMScript } = require('vm2')
|
|
11
|
-
const originalVM = require('vm')
|
|
2
|
+
const vm = require('vm')
|
|
12
3
|
const stackTrace = require('stack-trace')
|
|
13
4
|
const { codeFrameColumns } = require('@babel/code-frame')
|
|
5
|
+
const createPropertiesManager = require('./propertiesSandbox')
|
|
6
|
+
const createSandboxRequire = require('./requireSandbox')
|
|
14
7
|
|
|
15
|
-
module.exports = (_sandbox, options = {})
|
|
8
|
+
module.exports = async function createSandbox (_sandbox, options = {}) {
|
|
16
9
|
const {
|
|
10
|
+
rootDirectory,
|
|
17
11
|
onLog,
|
|
18
12
|
formatError,
|
|
19
13
|
propertiesConfig = {},
|
|
20
14
|
globalModules = [],
|
|
21
15
|
allowedModules = [],
|
|
22
16
|
safeExecution,
|
|
17
|
+
isolateModules,
|
|
23
18
|
requireMap
|
|
24
19
|
} = options
|
|
25
20
|
|
|
@@ -39,150 +34,109 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
39
34
|
// remove duplicates in paths
|
|
40
35
|
requirePaths = requirePaths.filter((v, i) => requirePaths.indexOf(v) === i)
|
|
41
36
|
|
|
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')
|
|
37
|
+
addConsoleMethod(_console, 'log', 'debug', onLog)
|
|
38
|
+
addConsoleMethod(_console, 'warn', 'warn', onLog)
|
|
39
|
+
addConsoleMethod(_console, 'error', 'error', onLog)
|
|
59
40
|
|
|
60
|
-
const
|
|
61
|
-
if (requireMap) {
|
|
62
|
-
const mapResult = requireMap(moduleName, { context })
|
|
41
|
+
const propsManager = createPropertiesManager(propertiesConfig)
|
|
63
42
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!safeExecution || allowAllModules || allowedModules === '*') {
|
|
70
|
-
return doRequire(moduleName, requirePaths, modulesCache)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const m = allowedModules.find(mod => (mod.id || mod) === moduleName)
|
|
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
|
-
)
|
|
43
|
+
// we copy the object based on config to avoid sharing same context
|
|
44
|
+
// (with getters/setters) in the rest of request pipeline
|
|
45
|
+
const sandbox = propsManager.copyPropertyValuesFrom(_sandbox)
|
|
82
46
|
|
|
83
|
-
|
|
84
|
-
formatError(error, moduleName)
|
|
85
|
-
}
|
|
47
|
+
propsManager.applyPropertiesConfigTo(sandbox)
|
|
86
48
|
|
|
87
|
-
|
|
88
|
-
|
|
49
|
+
const sourceFilesInfo = new Map()
|
|
50
|
+
// eslint-disable-next-line
|
|
51
|
+
let compartment
|
|
89
52
|
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
_sandbox[info.globalVariableName] = doRequire(info.module, requirePaths, modulesCache)
|
|
53
|
+
if (safeExecution) {
|
|
54
|
+
// eslint-disable-next-line
|
|
55
|
+
compartment = new Compartment()
|
|
94
56
|
}
|
|
95
57
|
|
|
96
|
-
|
|
97
|
-
const originalValues = {}
|
|
98
|
-
const proxiesInVM = new WeakMap()
|
|
99
|
-
const customProxies = new WeakMap()
|
|
58
|
+
let vmSandbox
|
|
100
59
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
60
|
+
if (safeExecution) {
|
|
61
|
+
vmSandbox = compartment.globalThis
|
|
62
|
+
|
|
63
|
+
vmSandbox = Object.assign(vmSandbox, {
|
|
64
|
+
// SES does not expose the Buffer, Intl by default, we expose it because it is handy for users,
|
|
65
|
+
// it is exposed as it is, because we already harden() it on reporter init
|
|
66
|
+
Buffer,
|
|
67
|
+
Intl,
|
|
68
|
+
// we need to expose Date, and Math to allow Date.now(), Math.random()
|
|
69
|
+
// these objects are already hardened by lockdown()
|
|
70
|
+
Date,
|
|
71
|
+
Math
|
|
72
|
+
})
|
|
73
|
+
} else {
|
|
74
|
+
vmSandbox = vm.createContext(undefined)
|
|
75
|
+
vmSandbox.Buffer = Buffer
|
|
76
|
+
}
|
|
104
77
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
78
|
+
const doSandboxRequire = createSandboxRequire(safeExecution, isolateModules, modulesCache, {
|
|
79
|
+
rootDirectory,
|
|
80
|
+
requirePaths,
|
|
81
|
+
requireMap,
|
|
82
|
+
allowedModules,
|
|
83
|
+
compileScript: doCompileScript,
|
|
84
|
+
formatError
|
|
108
85
|
})
|
|
109
86
|
|
|
110
87
|
Object.assign(sandbox, {
|
|
111
88
|
console: _console,
|
|
112
|
-
require: (m) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (safeExecution) {
|
|
119
|
-
safeVM = new VM()
|
|
120
|
-
|
|
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])
|
|
89
|
+
require: (m) => { return doSandboxRequire(m, { context: vmSandbox }) },
|
|
90
|
+
setTimeout: (...args) => {
|
|
91
|
+
return setTimeout(...args)
|
|
92
|
+
},
|
|
93
|
+
clearTimeout: (...args) => {
|
|
94
|
+
return clearTimeout(...args)
|
|
127
95
|
}
|
|
96
|
+
})
|
|
128
97
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
vmSandbox = originalVM.createContext(undefined)
|
|
132
|
-
vmSandbox.Buffer = Buffer
|
|
133
|
-
|
|
134
|
-
for (const name in sandbox) {
|
|
135
|
-
vmSandbox[name] = sandbox[name]
|
|
136
|
-
}
|
|
98
|
+
for (const name in sandbox) {
|
|
99
|
+
vmSandbox[name] = sandbox[name]
|
|
137
100
|
}
|
|
138
101
|
|
|
139
|
-
// processing top level props because getter/setter descriptors
|
|
102
|
+
// processing top level props here because getter/setter descriptors
|
|
140
103
|
// for top level properties will only work after VM instantiation
|
|
141
|
-
|
|
142
|
-
const currentConfig = propsConfig[key]
|
|
104
|
+
propsManager.applyRootPropertiesConfigTo(vmSandbox)
|
|
143
105
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const sourceFilesInfo = new Map()
|
|
106
|
+
for (const info of globalModules) {
|
|
107
|
+
// it is important to use _sandboxRequire function with allowAllModules: true here to avoid
|
|
108
|
+
// getting hit by the allowed modules restriction
|
|
109
|
+
vmSandbox[info.globalVariableName] = doSandboxRequire(info.module, { context: vmSandbox, useMap: false, allowAllModules: true })
|
|
110
|
+
}
|
|
150
111
|
|
|
151
112
|
return {
|
|
152
113
|
sandbox: vmSandbox,
|
|
153
114
|
console: _console,
|
|
154
115
|
sourceFilesInfo,
|
|
155
|
-
compileScript
|
|
116
|
+
compileScript (code, filename) {
|
|
156
117
|
return doCompileScript(code, filename, safeExecution)
|
|
157
118
|
},
|
|
158
|
-
restore
|
|
159
|
-
return
|
|
119
|
+
restore () {
|
|
120
|
+
return propsManager.restorePropertiesFrom(vmSandbox)
|
|
160
121
|
},
|
|
161
|
-
sandboxRequire
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
122
|
+
sandboxRequire (modulePath) {
|
|
123
|
+
return doSandboxRequire(modulePath, { context: vmSandbox, allowAllModules: true })
|
|
124
|
+
},
|
|
125
|
+
async run (codeOrScript, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) {
|
|
165
126
|
if (filename != null && source != null) {
|
|
166
127
|
sourceFilesInfo.set(filename, { filename, source, entity, entitySet, errorLineNumberOffset })
|
|
167
128
|
}
|
|
168
129
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
run = async () => {
|
|
173
|
-
return safeVM.run(script)
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
run = async () => {
|
|
177
|
-
return script.runInContext(vmSandbox, {
|
|
178
|
-
displayErrors: true
|
|
179
|
-
})
|
|
130
|
+
try {
|
|
131
|
+
if (safeExecution) {
|
|
132
|
+
return await compartment.evaluate(codeOrScript + `\n//# sourceURL=${filename}`)
|
|
180
133
|
}
|
|
181
|
-
}
|
|
182
134
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return
|
|
135
|
+
const script = typeof codeOrScript !== 'string' ? codeOrScript : doCompileScript(codeOrScript, filename, safeExecution)
|
|
136
|
+
|
|
137
|
+
return await script.runInContext(vmSandbox, {
|
|
138
|
+
displayErrors: true
|
|
139
|
+
})
|
|
186
140
|
} catch (e) {
|
|
187
141
|
decorateErrorMessage(e, sourceFilesInfo)
|
|
188
142
|
|
|
@@ -193,122 +147,19 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
193
147
|
}
|
|
194
148
|
|
|
195
149
|
function doCompileScript (code, filename, safeExecution) {
|
|
196
|
-
let script
|
|
197
|
-
|
|
198
150
|
if (safeExecution) {
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
151
|
+
return code
|
|
294
152
|
}
|
|
295
153
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
pathsSearched++
|
|
154
|
+
return new vm.Script(code, {
|
|
155
|
+
filename,
|
|
156
|
+
displayErrors: true,
|
|
157
|
+
importModuleDynamically: () => {
|
|
158
|
+
// 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.
|
|
159
|
+
// eslint-disable-next-line no-throw-literal
|
|
160
|
+
throw 'Dynamic imports are not allowed.'
|
|
304
161
|
}
|
|
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
|
|
162
|
+
})
|
|
312
163
|
}
|
|
313
164
|
|
|
314
165
|
function decorateErrorMessage (e, sourceFilesInfo) {
|
|
@@ -383,484 +234,88 @@ function decorateErrorMessage (e, sourceFilesInfo) {
|
|
|
383
234
|
e.message = `${e.message}`
|
|
384
235
|
}
|
|
385
236
|
|
|
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
|
|
237
|
+
function addConsoleMethod (target, consoleMethod, level, onLog) {
|
|
238
|
+
target[consoleMethod] = function () {
|
|
239
|
+
if (onLog == null) {
|
|
240
|
+
return
|
|
520
241
|
}
|
|
521
|
-
}
|
|
522
242
|
|
|
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)
|
|
243
|
+
onLog({
|
|
244
|
+
timestamp: new Date().getTime(),
|
|
245
|
+
level: level,
|
|
246
|
+
message: util.format.apply(util, arguments)
|
|
536
247
|
})
|
|
537
248
|
}
|
|
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
|
-
}
|
|
739
|
-
|
|
740
|
-
function sortPropertiesByLevel (a, b) {
|
|
741
|
-
const parts = a.split('.')
|
|
742
|
-
const parts2 = b.split('.')
|
|
743
|
-
|
|
744
|
-
return parts.length - parts2.length
|
|
745
249
|
}
|
|
746
250
|
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
}
|
|
251
|
+
/**
|
|
252
|
+
* NOTE: In the past (<= 3.11.3) the code sandbox in jsreport have worked like this:
|
|
253
|
+
* User code (helpers, scripts, etc) are evaluated in a context we create dedicated to it
|
|
254
|
+
* (`vmSandbox` variable in this file), Modules (code you import using `require`) are evaluated
|
|
255
|
+
* with normal node.js require mechanism, which means such code is evaluated in the main context.
|
|
256
|
+
* One of our requirements is to have isolated modules in the sandbox, this means that
|
|
257
|
+
* imported modules in a render are not re-used in other completely different renders.
|
|
258
|
+
* This requirement differs in the way the node.js require works because it caches the modules,
|
|
259
|
+
* so if you require the same module in different places you get the same module instance,
|
|
260
|
+
* we don't want that, we want to have isolated modules in each render.
|
|
261
|
+
*
|
|
262
|
+
* To fullfil this requirement the approach we took was to make all the require calls inside the sandbox
|
|
263
|
+
* to not cache its resolved modules, to achieve that after a require call is done
|
|
264
|
+
* we proceed to restore the require.cache to its original state, the state that was there before
|
|
265
|
+
* a require call happens, which means we would have to save the current require.cache (before a require) then
|
|
266
|
+
* delete all entries in that object (using delete require.cache[entry]),
|
|
267
|
+
* so the require can re-evaluate the module code and give a fresh module instance.
|
|
268
|
+
* The problem we discovered later was that this approach leads to memory leaks,
|
|
269
|
+
* using the normal require and deleting something from require.cache is not a good idea,
|
|
270
|
+
* it makes memory leaks happen, we didn't dig deeper but it seems node.js internals rely
|
|
271
|
+
* on the presence of the entries to be there in the require.cache in order to execute
|
|
272
|
+
* cleanup during the require execution, handling this improperly make the memory leaks happens,
|
|
273
|
+
* unfortunately doing this properly seems to require access to node.js internals,
|
|
274
|
+
* so nothing else we can do here.
|
|
275
|
+
*
|
|
276
|
+
* NOTE: Currently (>= 3.12.0) the code sandbox works like this:
|
|
277
|
+
* User code (helpers, scripts, etc) are evaluated just like before, in a context we create dedicated to it,
|
|
278
|
+
* however Modules (code you import using `require`) are evaluated with a custom version of require (requireSandbox) we've created.
|
|
279
|
+
* This version of require does not cache resolved modules into the require.cache, so it does not suffer
|
|
280
|
+
* from the memory leaks described above in the past version.
|
|
281
|
+
* The required modules are still cached but in our own managed cache, which only lives per render,
|
|
282
|
+
* so different requires to same modules in same render will return the same module instance (as expected)
|
|
283
|
+
*
|
|
284
|
+
* The main problem we found initially with the custom require was that creating a new
|
|
285
|
+
* instance of vm.Script per module to be evaluated allocated a lot of memory when doing a test case with lot of renders,
|
|
286
|
+
* no matter how many times the same module is required across renders, it was going to be compiled again.
|
|
287
|
+
* the problem was that GC (Garbage Collector) becomes really lazy to claim back the memory used
|
|
288
|
+
* for these scripts, in the end after some idle time the GC claims back the memory but it takes time,
|
|
289
|
+
* this is problem when you do a test case like doing 200 to 1000 renders to a template,
|
|
290
|
+
* it makes memory to be allocated a lot and not released just after some time of being idle, if you don't
|
|
291
|
+
* give it idle time it will eventually just choke and break with heap of out memory errors
|
|
292
|
+
* https://github.com/nodejs/node/issues/40014, https://github.com/nodejs/node/issues/3113,
|
|
293
|
+
* https://github.com/jestjs/jest/issues/11956
|
|
294
|
+
* A workaround to alleviate the issue was to cache scripts, so module is evaluated only once
|
|
295
|
+
* across renders, this is not a problem because we are not caching the module itself, only the compile part
|
|
296
|
+
* which makes sense. with this workaround the test case of 200 - 1000 renders allocates less memory
|
|
297
|
+
* but still the GC continue to be lazy, just that it will hold longer until it breaks
|
|
298
|
+
* with the heap out of memory error.
|
|
299
|
+
* A possible fix to this problem of the lazy GC we found was to use manually call the GC
|
|
300
|
+
* (when running node with --expose-gc), using something like this after render `setTimeout(() => { gc() }, 500)`,
|
|
301
|
+
* this makes the case to release memory better and more faster, however we did not added this because
|
|
302
|
+
* we don't want to deal with running node with exposed gc
|
|
303
|
+
*
|
|
304
|
+
* Another problem we found during the custom require implementation was that all render requests was
|
|
305
|
+
* just going to one worker (the rest were not used and idle), we fixed this and rotated the requests
|
|
306
|
+
* across the workers and the memory release was better (it seems it gives the GC of each worker a bit more of idle time)
|
|
307
|
+
*
|
|
308
|
+
* Last problem we found, was to decide in which context the Modules are going to be run,
|
|
309
|
+
* this affects the results of the user code will get when running constructor comparison,
|
|
310
|
+
* instanceof checks, or when errors originate from modules and are catch in the user code.
|
|
311
|
+
* There were alternatives to run in it in main context (just like the the normal node.js require does it),
|
|
312
|
+
* run it in the same context than sandbox (the context in which the user code is evaluated),
|
|
313
|
+
* or run it in a new context (managed by us).
|
|
314
|
+
* Something that affected our decision was to check how the constructors and instanceof checks
|
|
315
|
+
* were already working in past versions, how it behaves when using trustUserCode: true or not,
|
|
316
|
+
* the results were that when trustUserCode: true is used it produces different results
|
|
317
|
+
* to the results of trustUserCode: false, there was already some inconsistency,
|
|
318
|
+
* so we decided to keep the same behavior and not introduce more inconsistencies,
|
|
319
|
+
* which means we evaluate Modules with main context, one benefit or using the main context
|
|
320
|
+
* was that we did not have to care about re-exposing node.js globals (like Buffer, etc)
|
|
321
|
+
*/
|