@jsreport/jsreport-core 3.11.3 → 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.
@@ -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
- 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')
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
- 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
- )
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 = copyBasedOnPropertiesConfig(_sandbox, propertiesConfig)
46
+ const sandbox = propsManager.copyPropertyValuesFrom(_sandbox)
104
47
 
105
- applyPropertiesConfig(sandbox, propsConfig, {
106
- original: originalValues,
107
- customProxies
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: (m) => _require(m, { context: _sandbox })
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
- Object.keys(propsConfig).forEach((key) => {
142
- const currentConfig = propsConfig[key]
107
+ propsManager.applyRootPropertiesConfigTo(vmSandbox)
143
108
 
144
- if (currentConfig.root && currentConfig.root.sandboxReadOnly) {
145
- readOnlyProp(vmSandbox, key, [], customProxies, { onlyTopLevel: true })
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: (code, filename) => {
121
+ compileScript (code, filename) {
156
122
  return doCompileScript(code, filename, safeExecution)
157
123
  },
158
- restore: () => {
159
- return restoreProperties(vmSandbox, originalValues, proxiesInVM, customProxies)
124
+ restore () {
125
+ return propsManager.restorePropertiesFrom(vmSandbox)
160
126
  },
161
- sandboxRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
162
- run: async (codeOrScript, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) => {
163
- let run
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
- run = async () => {
140
+ runScript = async function runScript () {
173
141
  return safeVM.run(script)
174
142
  }
175
143
  } else {
176
- run = async () => {
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 run()
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 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
279
+ function addConsoleMethod (target, consoleMethod, level, onLog) {
280
+ target[consoleMethod] = function () {
281
+ if (onLog == null) {
282
+ return
520
283
  }
521
- }
522
284
 
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)
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
- function sortPropertiesByLevel (a, b) {
741
- const parts = a.split('.')
742
- const parts2 = b.split('.')
743
-
744
- return parts.length - parts2.length
745
- }
746
-
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
- }
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
+ */