@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.
@@ -1,25 +1,20 @@
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
- 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
- function addConsoleMethod (consoleMethod, level) {
43
- _console[consoleMethod] = function () {
44
- if (onLog == null) {
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 _require = function (moduleName, { context, allowAllModules = false } = {}) {
61
- if (requireMap) {
62
- const mapResult = requireMap(moduleName, { context })
41
+ const propsManager = createPropertiesManager(propertiesConfig)
63
42
 
64
- if (mapResult != null) {
65
- return mapResult
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
- if (formatError) {
84
- formatError(error, moduleName)
85
- }
47
+ propsManager.applyPropertiesConfigTo(sandbox)
86
48
 
87
- throw error
88
- }
49
+ const sourceFilesInfo = new Map()
50
+ // eslint-disable-next-line
51
+ let compartment
89
52
 
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)
53
+ if (safeExecution) {
54
+ // eslint-disable-next-line
55
+ compartment = new Compartment()
94
56
  }
95
57
 
96
- const propsConfig = normalizePropertiesConfigInHierarchy(propertiesConfig)
97
- const originalValues = {}
98
- const proxiesInVM = new WeakMap()
99
- const customProxies = new WeakMap()
58
+ let vmSandbox
100
59
 
101
- // we copy the object based on config to avoid sharing same context
102
- // (with getters/setters) in the rest of request pipeline
103
- const sandbox = copyBasedOnPropertiesConfig(_sandbox, propertiesConfig)
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
- applyPropertiesConfig(sandbox, propsConfig, {
106
- original: originalValues,
107
- customProxies
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) => _require(m, { context: _sandbox })
113
- })
114
-
115
- let safeVM
116
- let vmSandbox
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
- 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
- }
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
- Object.keys(propsConfig).forEach((key) => {
142
- const currentConfig = propsConfig[key]
104
+ propsManager.applyRootPropertiesConfigTo(vmSandbox)
143
105
 
144
- if (currentConfig.root && currentConfig.root.sandboxReadOnly) {
145
- readOnlyProp(vmSandbox, key, [], customProxies, { onlyTopLevel: true })
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: (code, filename) => {
116
+ compileScript (code, filename) {
156
117
  return doCompileScript(code, filename, safeExecution)
157
118
  },
158
- restore: () => {
159
- return restoreProperties(vmSandbox, originalValues, proxiesInVM, customProxies)
119
+ restore () {
120
+ return propsManager.restorePropertiesFrom(vmSandbox)
160
121
  },
161
- sandboxRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
162
- run: async (codeOrScript, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) => {
163
- let run
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
- 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
- })
130
+ try {
131
+ if (safeExecution) {
132
+ return await compartment.evaluate(codeOrScript + `\n//# sourceURL=${filename}`)
180
133
  }
181
- }
182
134
 
183
- try {
184
- const result = await run()
185
- return result
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
- 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
-
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
- 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++
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 getOriginalFromProxy (proxiesInVM, customProxies, value) {
387
- let newValue
388
-
389
- if (customProxies.has(value)) {
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
- const processStandAloneProperties = (c) => {
524
- Object.keys(c.standalone).forEach((skey) => {
525
- const sconfig = c.standalone[skey]
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
- function normalizePropertiesConfigInHierarchy (configMap) {
748
- const configMapKeys = Object.keys(configMap)
749
-
750
- const groupedKeys = groupBy(configMapKeys, (key) => {
751
- const parts = key.split('.')
752
-
753
- if (parts.length === 1) {
754
- return ''
755
- }
756
-
757
- return parts.slice(0, -1).join('.')
758
- })
759
-
760
- const hierarchy = []
761
- const hierarchyLevels = {}
762
-
763
- // we sort to ensure that top level properties names are processed first
764
- Object.keys(groupedKeys).sort(sortPropertiesByLevel).forEach((key) => {
765
- if (key === '') {
766
- hierarchy.push('')
767
- return
768
- }
769
-
770
- const parts = key.split('.')
771
- const lastIndexParts = parts.length - 1
772
-
773
- if (parts.length === 1) {
774
- hierarchy.push(parts[0])
775
- hierarchyLevels[key] = {}
776
- return
777
- }
778
-
779
- for (let i = 0; i < parts.length; i++) {
780
- const currentKey = parts.slice(0, i + 1).join('.')
781
- const indexInHierarchy = hierarchy.indexOf(currentKey)
782
- let parentHierarchy = hierarchyLevels
783
-
784
- if (indexInHierarchy === -1 && i === lastIndexParts) {
785
- let parentExistsInTopLevel = false
786
-
787
- for (let j = 0; j < i; j++) {
788
- const segmentedKey = parts.slice(0, j + 1).join('.')
789
-
790
- if (parentExistsInTopLevel !== true) {
791
- parentExistsInTopLevel = hierarchy.indexOf(segmentedKey) !== -1
792
- }
793
-
794
- if (parentHierarchy[segmentedKey] != null) {
795
- parentHierarchy = parentHierarchy[segmentedKey]
796
- }
797
- }
798
-
799
- if (!parentExistsInTopLevel) {
800
- hierarchy.push(key)
801
- }
802
-
803
- parentHierarchy[key] = {}
804
- }
805
- }
806
- })
807
-
808
- const toHierarchyConfigMap = (parentLevels) => {
809
- return (acu, key) => {
810
- if (key === '') {
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
- }
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
+ */